1.题目概览
我们拿到附件之后可以看到文件的结构大致如下:
由于之前也没有接触过phppwn,所以比赛现场看到这个文件结构还是很懵逼的赛后拿去问问gpt。
==以下是GPT的回答:==
这个文件的结构看起来非常像是一个OCI(Open Container Initiative)镜像,也就是说,它很可能是一个用来打包容器镜像的标准格式结构。下面是每个文件或目录的作用解析:
blobs/
◆这是 OCI 镜像中的“二进制大对象”存储目录,里面存放的是镜像的各个层(layer)的压缩数据,比如文件系统层、配置层等。
◆通常会以哈希值作为子目录,里面的文件是实际的镜像数据(例如.tar.gz
文件)。
index.json
◆这是 OCI 镜像的索引文件,描述了这个镜像有哪些 manifest。
◆通常包含了镜像的入口信息,比如哪个是默认的 manifest、架构(如 amd64)、平台(如 linux)等。
manifest.json
◆描述这个镜像的结构,包括:
依赖了哪些层(layer)
镜像配置(config)
层的哈希值、大小、媒体类型等
◆类似 Docker 镜像里的manifest.json
。
oci-layout
◆是 OCI 镜像规范必须包含的一个元数据文件,内容非常简单,说明了镜像采用的 OCI layout 的版本(通常是"1.0.0"
)。
repositories
◆一些 OCI 工具会在打包时自动添加这个文件,用来记录镜像所属的仓库(repository)名。
◆类似 Docker 镜像里的 tag 信息。
总体理解
这个.tar
文件不是普通的 PHP 项目源码压缩包,而是一个容器镜像文件(OCI 镜像),它可能是使用类似buildah
、skopeo
、docker
(带有--format oci
参数)等工具打包生成的。
2.启动容器
经过gpt分析,这是一个docker打包好的容器镜像文件,所以可以通过docker启动,并进入docker容器查看具体的文件结构。
docker load -i php-master.tardocker run -it your_image_id /bin/bash
通过命令可以直接docker 加载这个镜像,然后运行进入,就可以查看这个容器内大致的文件结构了。
3.分析目标
同kernel那类题目有些相似,一般php pwn都是会在一个extensions的文件夹下放入一个.so的动态链接库文件,pwn手分析的目标一般都是这些.so文件,出题人留下的漏洞一般都写在这些动态链接库当中。
本题也不例外,我们可以用:
find . | grep "extensions"
查找extensions目录所在的位置,可以在这个目录下找到我们需要分析的.so动态链接库文件。
4.建立映射关系的启动
找到了需要分析的文件,我们如何从镜像中将之提取出来方便我们本机分析呢?
我们用到docker-compose 组件,创建一个yml启动配置文件,建立本机与容器之间的映射关系,就可以在本机与docker容器之间建立一个类似共享文件夹的东西,在容器中将这些.so文件复制到 这个共享文件夹中,就可以拉出来正常分析了
docker-compose.yml
# 版本号version: '3'#启动的服务services:#服务名 php-master:#镜像 image: php-master:attachment-v1#映射的端口, ports: - "80:80" - "28888:9191"# 将主机的28888端口映射到容器中的9191端口#文件夹映射 将当前主机目录下的data文件夹作为共享文件夹,映射到容器中的/var/www/html/exp文件夹中 volumes: - ./data:/var/www/html/exp
我们只需要在容器中将这些.so文件 cp 到 /var/www/html/exp文件夹下,就可以在本机的data文件下看到了,也就可以拿去ida分析了。
5.总体逻辑说明
index.php
<?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: 10px20px; border: none; border-radius: 4px; cursor: pointer; } .btn:hover { background: #0056b3; } .message { padding: 15px; margin: 20px0; 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>
我们的题目是一个apache启动的index.php,可以做一个任意文件上传,我们应该像web题一样上传一个可以执行的.php文件并访问他执行,这里也就顺便把fix说了加个后缀过滤,不让上传php文件就完事了,所以exp也得用php写了。
6.vuln .so文件分析
题目这里给的信息已经很明显了,直接取名vuln.so明显有问题。
打开看到的信息大致如下,出题人还是很友好的,符号表全部都有保留方便逆向分析。
在解析具体函数之前介绍一下
0)zend_parse_parameters函数 (GPT写的)
???? 函数原型
ZEND_API int zend_parse_parameters(int num_args, const char *type_spec, ...);
参数说明:
num_args | ZEND_NUM_ARGS() |
type_spec | "ll" 表示两个 long 类型 |
... |
常见类型字符
l | zend_long | |
d | double | |
s | char * size_t | |
b | zend_bool | |
z | zval * | |
a | zval * | |
o | zval * | |
1)zif_construct函数
还原结构体,大致如下
2)zif_allocate函数
3)zif_overwrite函数
同时汇编观察发现,zend_parse_parameters 函数的传入参数其实很多,修正一下。
这下对劲了。。
4)zif_clear函数
基本的需要利用的漏洞函数都已解析完毕。
注意到,本质是堆的UAF 任意地址写,同时overwrite函数没有检查init。
7. php源码 -> emalloc 和 efree 以及堆 大致粗略分析
从题目的docker容器中可以查找到对应的php版本
php --versionPHP 8.1.20 (cli) (built: Jun 13 2023 12:02:18) (NTS)Copyright (c) The PHP GroupZend Engine v4.1.20, Copyright (c) Zend Technologies
下载它的源码
heap相关结构体
_zend_mm_heap
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_CUSTOMunion {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};
_zend_mm_chunk
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 */};
注释写的还算比较好理解
emalloc
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引擎管理器returnzend_mm_alloc_heap(AG(mm_heap), size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);}
bin和free_slot
/* * bin - is one or few continuous pages (up to 8) used for allocation of * a particular "small size". */struct_zend_mm_bin { char bytes[ZEND_MM_PAGE_SIZE * 8];};struct_zend_mm_free_slot { zend_mm_free_slot *next_free_slot;};
zend_mm_alloc_heap
static zend_always_inline void *zend_mm_alloc_heap(zend_mm_heap *heap, size_t size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC){ void *ptr;#if ZEND_DEBUG size_t real_size = size; zend_mm_debug_info *dbg;/* special handling for zero-size allocation */ size = MAX(size, 1); size = ZEND_MM_ALIGNED_SIZE(size) + ZEND_MM_ALIGNED_SIZE(sizeof(zend_mm_debug_info));if (UNEXPECTED(size < real_size)) {zend_error_noreturn(E_ERROR, "Possible integer overflow in memory allocation (%zu + %zu)", ZEND_MM_ALIGNED_SIZE(real_size), ZEND_MM_ALIGNED_SIZE(sizeof(zend_mm_debug_info)));returnNULL; }#endifif (EXPECTED(size <= ZEND_MM_MAX_SMALL_SIZE)) { ptr = zend_mm_alloc_small(heap, ZEND_MM_SMALL_SIZE_TO_BIN(size) ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);#if ZEND_DEBUG dbg = zend_mm_get_debug_info(heap, ptr); dbg->size = real_size; dbg->filename = __zend_filename; dbg->orig_filename = __zend_orig_filename; dbg->lineno = __zend_lineno; dbg->orig_lineno = __zend_orig_lineno;#endifreturn ptr; } elseif (EXPECTED(size <= ZEND_MM_MAX_LARGE_SIZE)) { ptr = zend_mm_alloc_large(heap, size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);#if ZEND_DEBUG dbg = zend_mm_get_debug_info(heap, ptr); dbg->size = real_size; dbg->filename = __zend_filename; dbg->orig_filename = __zend_orig_filename; dbg->lineno = __zend_lineno; dbg->orig_lineno = __zend_orig_lineno;#endifreturn ptr; } else {#if ZEND_DEBUG size = real_size;#endifreturnzend_mm_alloc_huge(heap, size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC); }}
对于php的堆利用大多数都采用小堆块,我们直接看小堆块的申请逻辑。
zend_mm_alloc_small
find . | grep "extensions"
0
zend_mm_alloc_small_slow
find . | grep "extensions"
1
至此emalloc的流程大致的解析完毕
efree
find . | grep "extensions"
2
和emalloc很像,我们直接跟到最后那个函数,看看他如何处理free掉的堆块。
zend_mm_free_small
find . | grep "extensions"
3
总体是十分简洁好看的,就是简单的链表头插
那么我们不难得出一个大体的结论和逻辑
emalloc
efree
直接头插链入freelist
而且好像没有做很多检查和限制,有点像tcache 的逻辑
而事实上,我们完全可以把它当作tcache 来攻击
8. gdb如何调试
在4. 中提到的端口映射,实际上我们只用将gdbserver传入类似共享文件夹中,然后在容器中启动监听,再让主机启动gdb target remote ip:port 即可调试上。
用我们上文写好的启动配置 启动
可以清晰看到我们的端口映射关系和启动信息
在主机中访问本地ip,可以看到docker容器中的80端口已经被映射了出来,我们可以访问到这个服务了。
1)docker容器
我们在docker容器中传入gdbserver 并运行
find . | grep "extensions"
4
2)本机(虚拟机)
直接 target remote 本机已经映射好的端口就可以远程调试上了
嫌麻烦的话可以写一个启动脚本
9.编写exp
在写之前我们可以看看vuln.so的保护
[*] '/root/Desktop/pwn/php_master/data/vuln.so'Arch: amd64-64-littleRELRO: Partial RELROStack: No canary foundNX: NX enabledPIE: PIE enabled
got表可写
首先是套板子的介绍,我们打pwn都需要直到各种基址,这样题目中的vuln的got表偏移,对于我们而言才有的意义。
1)获取基址
find . | grep "extensions"
5
只要没有禁用我们的读写权限,就可以读出基址
否则还是走老路,找一些show的函数,泄露基址
2)uaf利用
find . | grep "extensions"
6
3)调试
由yaml文件,我们的
find . | grep "extensions"
7
用上文所讲的方法就可以连接到容器的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就算编写完成了
4)远程上传exp.php 并执行
参考https://blog.csdn.net/qq_54218833/article/details/140528238php拿基址和封装p64的板子拿的是csdn上这个佬的代码
附件请点击“阅读原文”查看
看雪ID:sparkle666
https://bbs.kanxue.com/user-home-1010243.htm
# 往期推荐
1、
2、
3、
4、
5、
6、
球分享
球点赞
球在看
点击阅读原文查看更多
推荐站内搜索:最好用的开发软件、免费开源系统、渗透测试工具云盘下载、最新渗透测试资料、最新黑客工具下载……
还没有评论,来说两句吧...