首页
社区
课程
招聘
[原创]长城杯2025 (php-pwn) simi-final php-master 详解
发表于: 2025-4-22 02:55 3854

[原创]长城杯2025 (php-pwn) simi-final php-master 详解

2025-4-22 02:55
3854

我们拿到附件之后可以看到文件的结构大致如下

由于之前也没有接触过phppwn,所以比赛现场看到这个文件结构还是很懵逼的
赛后拿去问问gpt

==以下是GPT的回答:==

这个文件的结构看起来非常像是一个 OCI(Open Container Initiative)镜像,也就是说,它很可能是一个用来打包容器镜像的标准格式结构。下面是每个文件或目录的作用解析:

这个 .tar 文件不是普通的 PHP 项目源码压缩包,而是一个 容器镜像文件(OCI 镜像),它可能是使用类似 buildahskopeodocker(带有 --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

bin和free_slot

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就算编写完成了

完结撒花

参考
418K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6T1L8r3!0Y4i4K6u0W2j5%4y4V1L8W2)9J5k6h3&6W2N6q4)9J5c8Y4q4I4i4K6g2X3y4e0b7J5x3e0R3^5x3K6y4Q4x3V1k6S2M7Y4c8A6j5$3I4W2i4K6u0r3k6r3g2@1j5h3W2D9M7#2)9J5c8U0p5@1x3o6f1J5z5o6t1K6z5l9`.`.
php拿基址和封装p64的板子拿的是csdn上这个佬的代码

特别鸣谢
感谢Tplus大佬的指点教学,给佬磕了

附件链接
通过网盘分享的文件:php-master.tar
链接: c37K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6H3j5h3&6Q4x3X3g2T1j5h3W2V1N6g2)9J5k6h3y4G2L8g2)9J5c8Y4y4Q4x3V1j5I4N6s2k6m8b7U0q4J5d9W2)9#2k6V1A6p5x3#2l9J5y4f1V1$3k6g2c8K9e0e0c8m8 提取码: kgv6

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

[培训]科锐逆向工程师培训第53期2025年7月8日开班!

最后于 2025-4-23 21:58 被sparkle666编辑 ,原因: 上传附件链接
收藏
免费 3
支持
分享
最新回复 (5)
雪    币: 2576
活跃值: (4454)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
大佬,附件可否分享一下,好操作学习学习。
2025-4-23 09:25
1
雪    币: 6316
活跃值: (19985)
能力值: ( LV13,RANK:357 )
在线值:
发帖
回帖
粉丝
3
感谢分享,能上传下附件就更好了
2025-4-23 10:18
1
雪    币: 210
活跃值: (130)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
0x指纹 感谢分享,能上传下附件就更好了
附件太大了,我上传不了捏,我看看上传个网盘连接
2025-4-23 18:33
0
雪    币: 203
活跃值: (108)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
2025-5-24 21:05
0
雪    币: 203
活跃值: (108)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
2025-5-24 21:05
0
游客
登录 | 注册 方可回帖
返回