php-fpm环境的一种后门实现
之前发到安全客上的一篇文章,同步到博客来。
目前常见的php后门基本需要文件来维持(常规php脚本后门:一句话、大马等各种变形;WebServer模块:apache扩展等,需要高权限并且需要重启WebServer),或者是脚本运行后删除自身,利用死循环驻留在内存里,不断主动外连获取指令并且执行。两者都无法做到无需高权限、无需重启WeServer、触发后删除脚本自身并驻留内存、无外部进程、能主动发送控制指令触发后门(避免内网无法外连的情况)。
而先前和同事一块测试Linux下面通过/proc/PID/fd文件句柄来利用php文件包含漏洞时,无意中发现了一个有趣的现象。经过后续的分析,可以利用其在特定环境下实现受限的无文件后门,效果见动图:
测试环境
CentOS 7.5.1804 x86_64
nginx + php-fpm(监听在tcp 9000端口)
为了方便观察,建议修改php-fpm默认pool的如下参数:
1 | # /etc/php-fpm.d/www.conf |
修改后重启php-fpm,可以看到只有一个master进程和一个worker进程:
1 | [root@localhost php-fpm.d]# ps -ef|grep php-fpm |
php-fpm文件句柄泄露
在利用php-fpm运行的php脚本里,使用system()等函数执行外部程序时,由于php-fpm没有使用FD_CLOEXEC处理句柄,导致fork出来的子进程会继承php-fpm进程的所有文件句柄。
简单测试代码:
1 | <?php |
观察访问前worker进程的文件句柄:
1 | [root@localhost php-fpm.d]# ls -al /proc/2439/fd |
确定socket:[1168542]为php-fpm监听的9000端口的socket句柄:
1 | [root@localhost php-fpm.d]# lsof -i:9000 |
访问t1.php后,会阻塞在system调用里,此时查看sleep进程与worker进程的文件句柄:
1 | [root@localhost php-fpm.d]# ps -ef|grep sleep |
可以发现请求t1.php后,nginx发起了一个fast-cgi请求到php-fpm进程,即woker进程里3号句柄socket:[1408425]
。同时可以看到sleep继承了父进程php-fpm的0 1 2 3 7号句柄,其中的0号句柄也就是php-fpm监听的9000端口的socket句柄。
文件句柄泄露的利用
在子进程里有了继承来的socket句柄,就可以直接使用accept函数直接从该socket接受一个连接。下面是一个用于验证的简单c程序以及调用的php脚本:
1 | // test.c |
1 | <?php |
访问t2.php后,观察php-fpm进程以及子进程状态:
1 | [root@localhost html]# ps -ef|grep php-fpm |
可以看到php-fpm多了一个worker进程,子进程test(pid:2957)阻塞在accept函数,所以解析t2.php的这个worker进程(pid:2548)阻塞在php的system函数里,系统调用体现为阻塞在read(),即等待system函数返回。因此master进程spawn出新的worker进程来处理正常的fast-cgi请求,此时php-fpm监听在tcp 9000的这个socket句柄上有两个进程在accept等待新的连接,一个是正常的php-fpm worker(pid:2958)进程,另一个是我们的测试程序test。
此时我们请求一个php页面,nginx发起的到9000端口的fast-cgi请求就会有一定几率被我们的test进程accpet接受到。但是我们测试程序test里面没有处理fast-cgi请求,因此nginx直接向前端返回500。查看tmp目录发现生成了lol文件,说明test进程成功通过accept函数从继承来的socket句柄中接受了一个来自nginx的fast-cgi请求。
1 | [root@localhost html]# ls -al /tmp/systemd-private-165040c986624007be902da008f27727-php-fpm.service-6HI0kT/tmp/ |
至此,我们利用思路就有了:
- php脚本先删除自身,然后用system()等方法运行一个外部程序
- 外部程序起来后删除自身,驻留在内存里,直接accpet从0句柄接受来自nginx的fast-cgi请求
- 解析fast-cgi请求,如果含有特定的指令,拦截请求并执行相应的代码,否则认为是正常请求,转发到9000端口让正常的php-fpm worker处理
这个利用思路的缺点是需要起一个外部的进程。
一个另类的利用方法
到了这里铺垫写完了,进入本文分享的重点部分:如何解决上文提到的需要单独起一个进程来处理fast-cgi请求的不足。
php-fpm解析php脚本,是在php-fpm的worker进程里进行的,也就是说理论上php代码是能访问到worker进程已经打开的文件句柄的。但是php对这块做了封装,在php里通过fopen、socket_create等操作文件、socket时,得到的是一个php resource,每个resource绑定了相应的文件句柄,我们是无法直接操作到文件句柄的。可以通过下面的php脚本简单观察一下:
1 | <?php |
访问t3.php后,查看php-fpm worker进程的文件句柄:
1 | [root@localhost html]# ls -al /proc/2958/fd |
可以看到10秒内只有来自nginx的fast-cgi请求的3号句柄。而10秒后,4号句柄为php脚本中创建的socket,对应php脚本中的$socket资源。
如果我们能在php代码中构造出一个和0号句柄绑定的socket resource,我们就能直接用php的accpet()来处理来自nginx的fast-cgi请求而无需再起一个新的进程。但是翻遍了资料,最后发现php里无法用常规的方式构造指向特定文件句柄的resource。
但是我们发现worker进程在/proc/下面的文件owner并不是root,而是php-fpm的运行用户。这说明了php-fpm的master在fork出worker进程后,没有正确处理其dumpable flag,导致了我们可以用php-fpm worker的运行用户的权限附加到worker上,对其进行操作。
那么我们就有了新的利用思路:
- php脚本运行后先删除自身
- php脚本里用socket_create()创建一个socket
- php脚本释放一个外部程序,使用system()调用,此时子进程继承worker进程的运行权限
- 子进程attach到父进程(php-fpm worker),向父进程中注入shellcode,使用dup2()系统调用将0号句柄复制到步骤2中所创建的socket对应的句柄号,并恢复worker进程状态后detach,退出
- 子进程退出后,php代码里已经可以通过我们创建的socket resource来操作0号句柄,对其使用accept获取来自nginx的fast-cgi连接
- 解析fast-cgi请求,如果含有特定的指令,拦截请求并执行相应的代码,否则认为是正常请求,转发到9000端口让正常的php-fpm worker处理
通过这个利用方法,我们可以将大部分代码都用php实现,并且最终也是以一个被注入过的php-fpm进程的形式存在于服务器上。外部c程序只是用于注入worker进程,复制文件句柄。以下为注入shellcode的c代码:
1 | // dup04.c |
代码中注入的部分参考自网上,shellcode功能很简单,通过syscall调用dup2(0,4),汇编为:
1 | 5: 6a 21 pushq $0x21 |
使用如下php代码进行注入测试并观察效果:
1 | <?php |
访问t4.php后查看文件句柄:
1 | [root@localhost html]# ls -al /proc/3022/fd |
可以看到worker进程在前10秒内只有来自nginx的一个3号句柄;10-20秒多出来的4号句柄socket:[1435131]为php代码中socket_create后创建的socket;20秒后dup4运行结束,dup(0,2)成功调用,0号句柄的socket:[1168542]成功复制到4号句柄。此时php代码中已经可以通过$socket来操作php-fpm监听tcp 9000的socket了。
附上一个简单实现的脚本,通过php来解析fast-cgi并拦截特定请求:
1 | <?php |
利用限制
上面给出的php实现,利用的前提是php-fpm环境,同时有php版本限制,需5.x<5.6.35,7.0.x<7.0.29,7.1.x<7.1.16,7.2.x<7.2.4。因为利用到的两个条件中,worker进程未正确设置dumpable flag这个问题已经在CVE-2018-10545中修复,详情请自行查阅。而另一个条件,在php中通过system等函数来调用第三方程序时未正确处理文件描述符的问题,也已经提交给php官方,但php官方认为未能导致安全问题,不予处理。所以截止目前为止,最新版本的php-fpm都存在文件描述符泄露的问题。
总结
本文分享了一种php-fpm的另类后门实现,但比较受限。该方法虽然实现了无文件、无进程、能主动触发等特性,但是无法实现持续化,php-fpm服务重启后即失效;同时由于真实环境中php-fpm的worker进程众多,fast-cgi请求能被我们accept接受到的几率也比较小,不能稳定的触发。仅希望本文能抛砖引玉,引起大家对该问题进行更深入的探讨。如文中存在描述不准确的地方,欢迎大家批评指正。