【Pwn-长城杯2025 (php-pwn) simi-final php-master 详解】此文章归类为:Pwn。
我们拿到附件之后可以看到文件的结构大致如下

由于之前也没有接触过phppwn,所以比赛现场看到这个文件结构还是很懵逼的
赛后拿去问问gpt
==以下是GPT的回答:==
这个文件的结构看起来非常像是一个 OCI(Open Container Initiative)镜像,也就是说,它很可能是一个用来打包容器镜像的标准格式结构。下面是每个文件或目录的作用解析:
这个 .tar 文件不是普通的 PHP 项目源码压缩包,而是一个 容器镜像文件(OCI 镜像),它可能是使用类似 buildah、skopeo、docker(带有 --format oci 参数)等工具打包生成的。
经过gpt分析,这是一个docker打包好的容器镜像文件,所以可以通过docker启动,并进入docker容器查看具体的文件结构
通过命令可以直接docker 加载这个镜像,然后运行进入,就可以查看这个容器内大致的文件结构了
同kernel那类题目有些相似,一般php pwn都是会在一个extensions的文件夹下放入一个.so的动态链接库文件,pwn手分析的目标一般都是这些.so文件,出题人留下的漏洞一般都写在这些动态链接库当中
本题也不例外,我们可以用
查找extensions目录所在的位置,可以在这个目录下找到我们需要分析的.so动态链接库文件

找到了需要分析的文件,我们如何从镜像中将之提取出来方便我们本机分析呢?
我们用到docker-compose 组件,创建一个yml启动配置文件,建立本机与容器之间的映射关系,就可以在本机与docker容器之间建立一个类似共享文件夹的东西,在容器中将这些.so文件复制到 这个共享文件夹中,就可以拉出来正常分析了
docker-compose.yml
我们只需要在容器中将这些.so文件 cp 到 /var/www/html/exp文件夹下,就可以在本机的data文件下看到了,也就可以拿去ida分析了
index.php
我们的题目是一个apache启动的index.php,可以做一个任意文件上传,我们应该像web题一样上传一个可以执行的.php文件并访问他执行,这里也就顺便把 fix 说了加个后缀过滤,不让上传php文件就完事了。。。
所以exp也得用php写了
题目这里给的信息已经很明显了,直接取名vuln.so明显有问题
打开看到的信息大致如下,出题人还是很友好的,符号表全部都有保留方便逆向分析

在解析具体函数之前介绍一下
???? 函数原型
参数说明:
常见类型字符

还原结构体,大致如下



同时汇编观察发现,zend_parse_parameters 函数的传入参数其实很多,修正一下

这下对劲了

基本的需要利用的漏洞函数都已解析完毕
注意到,本质是堆的UAF 任意地址写,同时overwrite函数没有检查init
从题目的docker容器中可以查找到对应的php版本
php --version
PHP 8.1.20 (cli) (built: Jun 13 2023 12:02:18) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.1.20, Copyright (c) Zend Technologies
下载它的源码
heap相关结构体
_zend_mm_heap
_zend_mm_chunk
注释写的还算比较好理解
emalloc
zend_mm_alloc_heap
对于php的堆利用大多数都采用小堆块,我们直接看小堆块的申请逻辑
zend_mm_alloc_small
zend_mm_alloc_small_slow
至此emalloc的流程大致的解析完毕
efree
和emalloc很像,我们直接跟到最后那个函数,看看他如何处理free掉的堆块
zend_mm_free_small
总体是十分简洁好看的,就是简单的链表头插
那么我们不难得出一个大体的结论和逻辑
emalloc

efree
直接头插链入freelist
而且好像没有做很多检查和限制,有点像tcache 的逻辑
而事实上,我们完全可以把它当作tcache 来攻击
在4. 中提到的端口映射,实际上我们只用将gdbserver传入类似共享文件夹中,然后在容器中启动监听,再让主机启动gdb target remote ip:port 即可调试上
用我们上文写好的启动配置 启动
可以清晰看到我们的端口映射关系和启动信息
在主机中访问本地ip,可以看到docker容器中的80端口已经被映射了出来,我们可以访问到这个服务了
我们在docker容器中传入gdbserver 并运行

直接 target remote 本机已经映射好的端口就可以远程调试上了
嫌麻烦的话可以写一个启动脚本

在写之前我们可以看看vuln.so的保护
[*] '/root/Desktop/pwn/php_master/data/vuln.so'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
got表可写
首先是套板子的介绍,我们打pwn都需要直到各种基址,这样题目中的vuln的got表偏移,对于我们而言才有的意义
只要没有禁用我们的读写权限,就可以读出基址
否则还是走老路,找一些show的函数,泄露基址
由yaml文件,我们的
用上文所讲的方法就可以连接到容器的gdbserver

接下来我们要先进行调试,看看我们的思路是否正确
由于做好了映射,我们直接本机访问127.0.0.1:1111 端口就可以触发exp.php了
但由于很多模块还未被加载到内存,所以让本机gdb先直接跑起来,c
此时会加载一堆符号表,同时将.so文件加载到内存

CTRL + c 打断
此时我们就可以对着vuln.so中的函数直接下断了
然后本机浏览器访问localhost:1111端口,即可触发exp.php
这里我直接下断allocate过程,如果一切顺利的话,第四次执行到该函数,会申请到efree@got
其他的如果读者还想验证也是同样的道理
第四次 allocate

返回值 可以观察到就是efree@got

将一个堆块内容改为字符串

将将efree@got改为system函数地址

最后调用clear函数,efree的触发system
输出我们的flag

一切如我们所愿
至此exp就算编写完成了



完结撒花
参考
cd8K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6T1L8r3!0Y4i4K6u0W2j5%4y4V1L8W2)9J5k6h3&6W2N6q4)9J5c8Y4q4I4i4K6g2X3y4e0b7J5x3e0R3^5x3K6y4Q4x3V1k6S2M7Y4c8A6j5$3I4W2i4K6u0r3k6r3g2@1j5h3W2D9M7#2)9J5c8U0p5@1x3o6f1J5z5o6t1K6z5l9`.`.
php拿基址和封装p64的板子拿的是csdn上这个佬的代码
特别鸣谢
感谢Tplus大佬的指点教学,给佬磕了
docker load -i php-master.tar
docker run -it your_image_id /bin/bash
docker load -i php-master.tar
docker run -it your_image_id /bin/bash
find . | grep "extensions"
find . | grep "extensions"
<?php@error_reporting(E_ALL);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_FILES['file'])) {
$file = $_FILES['file'];
$upload_dir = '';
$target_file = $upload_dir . basename($file['name']);
$result = move_uploaded_file($file['tmp_name'], $target_file);
if ($result) {
$message = '文件上传成功!';
$msg_class = 'success';
} else {
$message = '文件上传失败';
$msg_class = 'error';
}
} else {
$message = '没有选择要上传的文件';
$msg_class = 'error';
}
}?><!DOCTYPE html><html lang="zh-CN">
<head> <meta charset="UTF-8">
<title>PHP MASTER</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 500px;
margin: 50px auto;
padding: 20px;
}
.upload-box {
border: 2px dashed #ccc;
padding: 30px;
text-align: center;
}
.btn {
background: #007bff;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn:hover {
background: #0056b3;
}
.message {
padding: 15px;
margin: 20px 0;
border-radius: 4px;
}
.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
</style>
</head><body> <h2>PHP MASTER</h2>
<?php if (isset($message)): ?>
<div class="message <?php echo $msg_class; ?>">
<?php echo $message; ?>
</div>
<?php endif; ?>
<form action="" method="post" enctype="multipart/form-data" class="upload-box">
<p>请选择要上传的文件:</p>
<input type="file" name="file" required>
<br><br>
<button type="submit" class="btn">上传文件</button>
</form>
</body></html><?php@error_reporting(E_ALL);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_FILES['file'])) {
$file = $_FILES['file'];
$upload_dir = '';
$target_file = $upload_dir . basename($file['name']);
$result = move_uploaded_file($file['tmp_name'], $target_file);
if ($result) {
$message = '文件上传成功!';
$msg_class = 'success';
} else {
$message = '文件上传失败';
$msg_class = 'error';
}
} else {
$message = '没有选择要上传的文件';
$msg_class = 'error';
}
}?><!DOCTYPE html><html lang="zh-CN">
<head> <meta charset="UTF-8">
<title>PHP MASTER</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 500px;
margin: 50px auto;
padding: 20px;
}
.upload-box {
border: 2px dashed #ccc;
padding: 30px;
text-align: center;
}
.btn {
background: #007bff;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn:hover {
background: #0056b3;
}
.message {
padding: 15px;
margin: 20px 0;
border-radius: 4px;
}
.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
</style>
</head><body> <h2>PHP MASTER</h2>
<?php if (isset($message)): ?>
<div class="message <?php echo $msg_class; ?>">
<?php echo $message; ?>
</div>
<?php endif; ?>
<form action="" method="post" enctype="multipart/form-data" class="upload-box">
<p>请选择要上传的文件:</p>
<input type="file" name="file" required>
<br><br>
<button type="submit" class="btn">上传文件</button>
</form>
</body></html>ZEND_API int zend_parse_parameters(int num_args, const char *type_spec, ...);
ZEND_API int zend_parse_parameters(int num_args, const char *type_spec, ...);
| 参数 | 说明 |
|---|---|
num_args |
PHP 调用时实际传入参数的数量,一般用 ZEND_NUM_ARGS()
|
type_spec |
参数类型字符串,例如 "ll" 表示两个 long 类型 |
... |
用于接收解析结果的变量的地址(按顺序传入) |
| 字符 | 含义 | C 变量类型 |
|---|---|---|
l |
long / int(整型) | zend_long |
d |
double(浮点型) | double |
s |
字符串(和长度) |
char *, size_t
|
b |
bool | zend_bool |
z |
zval *(通用) | zval * |
a |
数组 | zval * |
o |
对象 | zval * |
| ` | ` | 可选参数的分隔符 |
struct _zend_mm_heap {
#if ZEND_MM_CUSTOM int use_custom_heap;
#endif#if ZEND_MM_STORAGE zend_mm_storage *storage;
#endif#if ZEND_MM_STAT size_t size; /* current memory usage */
size_t peak; /* peak memory usage */
#endif zend_mm_free_slot *free_slot[ZEND_MM_BINS]; /* free lists for small sizes */
#if ZEND_MM_STAT || ZEND_MM_LIMIT size_t real_size; /* current size of allocated pages */
#endif#if ZEND_MM_STAT size_t real_peak; /* peak size of allocated pages */
#endif#if ZEND_MM_LIMIT size_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_CUSTOM union {
struct {
void *(*_malloc)(size_t);
void (*_free)(void*);
void *(*_realloc)(void*, size_t);
} std;
struct {
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);
} debug;
} custom_heap;
HashTable *tracked_allocs;
#endif};struct _zend_mm_heap {
#if ZEND_MM_CUSTOM int use_custom_heap;
#endif#if ZEND_MM_STORAGE zend_mm_storage *storage;
#endif#if ZEND_MM_STAT size_t size; /* current memory usage */
size_t peak; /* peak memory usage */
#endif zend_mm_free_slot *free_slot[ZEND_MM_BINS]; /* free lists for small sizes */
#if ZEND_MM_STAT || ZEND_MM_LIMIT size_t real_size; /* current size of allocated pages */
#endif#if ZEND_MM_STAT size_t real_peak; /* peak size of allocated pages */
#endif#if ZEND_MM_LIMIT size_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_CUSTOM union {
struct {
void *(*_malloc)(size_t);
void (*_free)(void*);
void *(*_realloc)(void*, size_t);
} std;
struct {
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);
} debug;
} custom_heap;
HashTable *tracked_allocs;
#endif};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 */
};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_API void* ZEND_FASTCALL _emalloc(size_t size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
{#if ZEND_MM_CUSTOM //如果采用自定义
if (UNEXPECTED(AG(mm_heap)->use_custom_heap)) {
return _malloc_custom(size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
}
#endif //默认的php zend引擎管理器
return zend_mm_alloc_heap(AG(mm_heap), size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
}更多【Pwn-长城杯2025 (php-pwn) simi-final php-master 详解】相关视频教程:www.yxfzedu.com