资讯动态

无数字字母命令执行:PHP异或取反与Shell通配符的RCE利用

发布时间:2026/8/4 5:48:56 来源:尧图企业网站定制
1. 从一道CTF题看命令执行的“无字天书”最近在复盘一些经典的Web安全挑战特别是CTFshow平台上那道关于“无数字字母命令执行”的题目感触颇深。这道题的精髓不在于让你用system($_GET[cmd])这种直白的姿势去执行ls或cat /flag而是设置了一个极其苛刻的“牢笼”你构造的Payload里不能出现任何数字0-9和字母a-z, A-Z。这听起来几乎是不可能的任务毕竟我们熟悉的命令、路径、参数哪一个不是由字母和数字构成的但恰恰是这种限制逼着我们跳出常规的思维定式去探索PHP、Linux系统底层那些“非主流”的字符和特性如何能被巧妙地组合成一把打开RCE大门的钥匙。这不仅是CTF的技巧更是一种对漏洞利用深度理解的训练理解了它你就能看透很多WAFWeb应用防火墙规则背后的逻辑与盲区。这道题的核心场景是我们面对一个存在命令执行漏洞的PHP后端但代码层面对传入的参数进行了严格的过滤只允许非数字、非字母的字符通过。我们的目标就是突破这层过滤最终实现任意命令执行RCE并获取目标通常是一个名为flag的文件。整个过程就像在用一套被限制的“积木”拼出完整的句子我们需要利用PHP的字符串操作、类型转换、位运算甚至是Linux Shell的通配符和特殊变量来“无中生有”地创造出我们需要的命令字符。2. 突破字符集限制的核心原理PHP的“魔法”与Shell的“通配”为什么没有数字字母我们依然能执行命令这依赖于PHP语言特性和操作系统Shell环境的几个关键特性。理解这些是构造Payload的基础而不是盲目地套用网上现成的“通杀”Payload。2.1 PHP中的字符串构造“魔术”在PHP中字符串不仅可以通过引号定义还能通过多种方式动态生成。当字母和数字被禁用时我们主要依靠以下几种“原料”异或运算XOR这是最核心的技巧之一。在PHP中两个字符串进行异或运算^运算符并不是对字符串整体操作而是对字符串中每个字符的ASCII码进行按位异或然后返回结果字符。关键在于很多我们需要的字符可以通过两个非字母数字的字符异或得到。例如字符a的ASCII码是97字符{是123字符~是126。计算{^~得到的是 123 ^ 126 5这并不是一个可打印字符。但如果我们精心挑选字符对就能得到可打印的字母或数字。不过更常见的做法是利用不可见字符或特殊字符进行大量组合通过脚本暴力枚举出能生成目标字符的字符对。例如?和~异或可能得到字母。在CTF中我们常会看到类似$_(%01^)这样的构造其结果是变量$_ 被赋值为一个字母。取反运算~取反运算是对字符ASCII码的二进制位进行翻转0变11变0。一个字符取反后会得到另一个字符。更重要的是对一个字符串取反两次就会得到原字符串。即~~a在效果上不等于a因为涉及运算但~某字符会产生一个新字符这个新字符很可能也在我们的允许列表内。我们可以先构造出某个字符的取反形式可能是一串乱码然后再次取反得到原字符。例如~灏在PHP中会产生一个ASCII字符。通过精心选择中文字符或其他多字节字符其取反后的结果可能正好落在字母数字范围内。这是“无数字字母”RCE的另一种强大武器。自增运算符PHP中字符变量支持自增操作。$aa; $a;后$a会变成b。更妙的是对字符串进行自增会按照字母表顺序推进。那么我们如何得到初始的a呢我们可以利用PHP的类型转换和数组。在PHP中空数组[]转换成字符串是Array。取其第一个字符就有了A。但题目可能过滤了字母。另一个神奇的技巧是利用未定义变量或空值。在PHP中未定义的变量在字符串上下文中是空字符串。但是如果我们对空字符串进行自增会发生什么$a; $a;此时$a的值是1整数。这似乎没用。但如果我们用.连接符呢看这个例子$_[]; $_$_.$_。[].[]的结果是字符串ArrayArray。我们取这个字符串的某个位置不我们有更巧妙的$_[]; $_$_[];。$_现在是一个空数组我们试图用空字符串作为键去访问它这会返回NULL。在PHP中NULL自增会得到1。即$a$_[]; $a;后$a是1。我们得到了数字1但这道题过滤了数字所以1不能用。不过如果我们得到了A或a就可以通过连续自增得到其他字母。那么A怎么来我们可以通过异或或取反得到!感叹号ASCII 33然后自增得到双引号34... 这离A65还很远。所以更直接的方法通常是先通过异或/取反构造出a或A的“种子”。实际上在真实的解题和利用中我们通常不会从零开始用手算这些组合。而是会编写一个PHP脚本遍历所有允许的非字母数字字符例如!\#$%()*,-./:;?[\\]^_{|}~以及空格、制表符等计算它们两两异或、取反后的结果看是否能生成我们需要的字母a-z, A-Z和数字0-9。一旦找到这样的映射关系我们就可以用允许的字符拼凑出执行命令所需的关键字如system、cat、ls、tac、more、less、head、tail、nl、awk、sed、grep、find、exec、passthru、shell_exec、popen、proc_open等。2.2 Linux Shell的通配符“魔法”即使我们在PHP层面用异或拼出了system这个函数名我们还需要传递参数给它比如cat /flag。cat和/flag也包含字母。这时Linux Shell的通配符就派上用场了。?和*?代表任意单个字符*代表任意多个字符。假设flag文件在当前目录且我们知道文件名是4个字母比如flag我们可以尝试cat ????.???或cat f*。但f是字母被过滤了。如果目录下只有flag这一个文件那么cat *就能直接打印其内容。这是最理想的情况。/???/???或/*/*用于匹配路径。例如/bin/cat可以用/???/???来匹配。因为/、b、i、n、c、a、t都是字母但用通配符?替代后只要路径存在且唯一Shell就会将其扩展为正确的路径。同理/flag可以用/*/*或/???来匹配前提是根目录下没有其他相同长度的文件或目录干扰。利用环境变量和内部变量Shell中有一些特殊的变量其值包含字母。${PATH}包含字母但我们可以用截取。${PATH:0:1}可以取出第一个字符通常是/。${PATH:1:1}可能是u或b等。但截取语法:和数字0、1可能被过滤。${SHLVL}、${$}这些是数字但数字被过滤了。${_}这是一个非常关键的变量。在Shell中${_}代表上一个执行的命令的最后一个参数。如果我们能先执行一个命令让它的最后一个参数是我们需要的字母就可以通过${_}引用它。但初始状态我们需要一个命令来“启动”这个链条。$0代表当前Shell或脚本的名称。如果我们的请求以某种方式触发了脚本执行$0可能会包含路径信息其中可能有字母。在“无数字字母”的极端限制下通配符是我们引用已知命令和文件的主要手段而环境变量的利用则更考验技巧和对场景的理解。2.3 组合技从字符到函数再到命令整个利用链可以概括为以下几步构造PHP函数名利用PHP的异或(^)、取反(~)、自增()等操作用允许的非字母数字字符拼接出字符串system、exec、passthru或shell_exec等。通常我们会将其赋值给一个变量比如$_。构造命令字符串同样利用上述方法构造出我们要执行的命令比如cat /flag或ls /。但更常用的方法是构造一个尽可能短的命令比如ls或*然后通过Shell通配符去匹配真正的命令和文件。或者我们只构造出cat或tac参数部分用通配符*代替。动态函数调用在PHP中如果变量$a的值是字符串system那么$a(ls)就等价于system(ls)。这就是可变函数。我们利用这一点用第一步构造的函数名变量去调用第二步构造的命令字符串变量。利用Shell通配符传递参数在可变函数调用时参数不一定非要是一个完整的字符串。我们可以直接传递$a(*)这里的*是一个通配符字符串它会被PHP的system函数传递给Shell由Shell进行扩展。如果当前目录下只有一个文件flag那么system(*)就会尝试执行flag这个文件。如果flag不可执行则会报错。但我们可以用cat *或tac *来查看文件内容。因此我们需要构造的命令可能是cat *或tac *。一个高度简化的Payload骨架可能是这样的注意以下字符仅为示意实际需要根据允许字符集计算?php // 假设通过异或运算我们用字符 ^ 和 ~ 拼出了字符串 system并赋值给变量 $a $a 某种运算得到的system; // 假设我们拼出了字符串 cat *赋值给变量 $b $b 某种运算得到的cat *; // 执行 $a($b); ?在真实的无数字字母场景下整个Payload会是一串看起来像乱码的非字母数字字符通过eval或assert等函数执行。3. 实战构造手工与脚本的结合艺术理解了原理我们来看如何一步步构造出可用的Payload。这里我们假设一个最简单的场景存在一个eval($_GET[cmd])的漏洞点但cmd参数的内容会被正则表达式/[0-9a-z]/i过滤掉所有数字和字母。3.1 确定可用字符集与生成工具首先我们需要明确哪些字符是“安全”的。通常除了数字和字母外大部分特殊字符都是可用的例如! \ # $ % ( ) * , - . / : ; ? [ \ ] ^ _ { | } ~以及空格、制表符、换行符等。注意单双引号可能需要转义取决于代码上下文。接下来我们需要一个生成工具。手工计算异或组合是不现实的。我们可以写一个简单的PHP脚本在本地运行来寻找生成目标字符的字符对。?php // 定义允许的字符集 $allowed_chars array_merge( str_split(!\#$%\()*,-./:;?[\\]^_{|}~ ), [\t, \n, \r] // 注意换行符可能在传输中被处理 ); // 我们想要生成的字符集小写字母、大写字母、数字但数字被过滤这里主要生成字母 $target_chars array_merge(range(a, z), range(A, Z)); echo 寻找异或组合\n; foreach ($target_chars as $target) { foreach ($allowed_chars as $c1) { foreach ($allowed_chars as $c2) { if (($c1 ^ $c2) $target) { echo sprintf(目标: %s %s ^ %s (ASCII: %d %d ^ %d)\n, $target, $c1, $c2, ord($target), ord($c1), ord($c2)); break 2; // 找到一对就跳出内层循环 } } } } echo \n寻找取反组合\n; foreach ($target_chars as $target) { $inverted ~$target; // 取反后是一个多字节字符或不可见字符 // 我们需要检查这个取反后的字符是否在我们的允许字符集中通常不是因为取反后多是ASCII127的字符 // 但我们可以检查取反后的字符的字符串表示中是否全部字符都在允许集内。这比较复杂。 // 更实用的方法是遍历允许字符对其取反看结果是否等于目标字符。 foreach ($allowed_chars as $c) { if (~$c $target) { // 注意~$c 是对整数的位运算需要确保类型 // 在字符串上下文中~$c 会产生一个字符。但这里直接比较可能有问题。 // 实际中常用 urlencode(~$c) 来观察。 echo sprintf(目标: %s ~%s\n, $target, $c); break; } } } ?运行这个脚本我们可以得到一份“字典”记录着如何用两个允许的字符通过异或得到我们需要的字母。例如我们可能会发现s ^ ^ a // 但a不允许所以这个组合无效。需要两个字符都在允许集内。实际上更常见的做法是我们不是直接生成完整的单词system而是先生成一个“种子”字符比如点号.或者空格 然后通过自增和连接操作逐步“生长”出我们需要的字符串。但自增操作符本身是字母可能被过滤。不过我们可以用1吗1是数字被过滤。所以自增路径在严格过滤下可能走不通。因此异或构造是主流方法。网上有现成的工具比如用Python或PHP写的脚本可以自动生成用于无字母数字WebShell的Payload。其核心逻辑就是暴力枚举所有允许字符的两两异或组合生成所有可打印字符然后从中拼出system、cat等关键字。3.2 构造一个真实的Payload示例假设经过我们的生成器计算我们得到了如下映射仅为示例实际映射关系需计算s!^Ty#^Zs$^W(另一个s我们可以复用)t%^Ye^Sm\^U注意上面的T,Z等是大写字母它们本身也是被过滤的所以这个映射无效。我们必须确保参与异或的两个字符都在非字母数字的允许集内。一个经典的、真正可用的技巧是利用PHP的位运算和字符串的特性。例如在PHP中^的结果是什么这是两个反引号执行运算符异或结果是空字符。这没用。网络上流传的一个经典Payload构造方法是先得到字符串POST或GET。怎么得到可以通过$_[]; $_$_.$_得到ArrayArray然后取下标。但取下标需要数字或字符串数字被过滤。我们可以用$_[]; $_$_[]得到NULL然后... 这条路径在严格过滤下很艰难。更实用的方法利用取反和不可见字符。PHP中对一串二进制数据进行取反可以得到另一串二进制数据。如果我们把目标字符串如system的每个字符取反得到一个新的字符串通常包含很多不可见字符或扩展ASCII字符然后将这个取反后的字符串作为代码的一部分。在Payload中我们再次对这个取反后的字符串进行取反就能还原出system。例如system取反后的字符串在PHP中用~运算符是另一个字符串。我们可以用如下方式构造?php $cmd ~灏捳䍬⁤; // 这个乱码字符串是 system 取反后的结果 // $cmd 现在就是字符串 system ?那么我们只需要在Payload中写出这个乱码字符串并在前面加上~运算符。但乱码字符串本身可能包含字母吗我们需要检查其二进制表示。system的ASCII码序列是 115, 121, 115, 116, 101, 109。对每个字节取反~x 255-x得到 140, 134, 140, 139, 154, 146。这些值在扩展ASCII范围内对应的字符可能不是字母数字但可能是其他可打印或不可打印字符。我们需要确保这些字符在允许的字符集内。如果出题人允许所有非字母数字字符那么这些扩展ASCII字符很可能被允许除非过滤了ASCII值大于127的字符。因此一个典型的取反构造Payload如下假设我们通过计算得到了system取反后的字符串为\x8C\x86\x8C\x8B\x9A\x92这里用十六进制表示$_~\x8C\x86\x8C\x8B\x9A\x92; // $_ 现在是 system $__~\x9C\xA0\x8C\xA1; // 假设这是 cat 的取反 $_($__); // 执行 system(cat )但我们需要执行cat *。所以我们需要构造cat *的取反字符串。*的ASCII是42取反是213即\xD5。所以完整的Payload可能看起来像这样经过URL编码cmd$_~%8C%86%8C%8B%9A%92;$__~%9C%A0%8C%A1%D5;$_($__);这里%8C%86%8C%8B%9A%92是system取反后的URL编码%9C%A0%8C%A1%D5是cat *取反后的URL编码。注意在实际解题时我们需要一个脚本来准确计算目标字符串取反后的十六进制表示并确保这些十六进制值对应的字符在允许的字符集中。如果出题环境过滤了某些特殊字符比如\x00空字节、\x0a换行符我们的Payload还需要调整。3.3 利用通配符绕过命令构造如果我们无法构造出完整的cat /flag字符串比如/和flag中的字母难以构造那么Shell通配符就是救星。假设我们成功构造出了system函数那么我们可以直接传递通配符作为参数$_~%8C%86%8C%8B%9A%92; // $_ system $_(*); // 尝试执行当前目录下的所有文件如果当前目录下只有一个可读文件flag且cat命令可用在PATH中我们可以尝试$_~%8C%86%8C%8B%9A%92; $__~%8C%A0%8C; // 假设这是 cat 的取反 $_($__. *); // 拼接出 cat *但拼接操作符.是点号是允许的。所以我们可以分开构造cat和*然后拼接。*本身就是一个允许的字符。更进阶的如果我们连cat都构造困难可以尝试其他命令。比如tac反向输出、more、less、head、tail、nl加行号、awk、sed。甚至可以用find命令来读取文件find / -name flag -exec cat {} \\;但这条命令里字母太多了。一个更短的命令是/???/??? /???/???/???/???/???如果运气好这可能会被扩展为/bin/cat /home/ctf/flag之类的。但这需要路径匹配唯一。在CTF环境中通常目录结构很干净这种可能性存在。4. 从原理到防御给开发者的启示这种“无数字字母”的命令执行利用方式虽然看起来像是CTF中的“奇技淫巧”但它深刻揭示了安全防御的薄弱环节。它绕过了基于正则表达式的简单黑名单过滤例如只过滤数字和字母利用了编程语言和操作系统的深层特性。4.1 为什么这种攻击会成功过滤规则过于简单开发者可能认为命令执行无非就是执行ls、cat、whoami等由字母数字组成的命令因此过滤掉字母数字就万事大吉。这种思维是典型的“基于已知攻击模式的防御”而非“基于行为本质的防御”。对PHP语言特性了解不足许多开发者知道eval、system危险但未必清楚PHP中字符串可以通过异或、取反、自增等方式动态生成更不清楚可变函数调用$func($args)的机制。这使得他们低估了攻击者的“创造力”。对操作系统Shell特性了解不足Shell的通配符、环境变量、命令替换等特性功能强大但也为攻击者提供了绕过限制的途径。如果代码最终会将参数传递给Shell如通过system、passthru、exec的shell_exec模式、反引号运算符那么任何对用户输入的不完全过滤都可能带来风险。4.2 如何有效防御此类攻击对于开发者而言防御命令执行漏洞的最高原则是永远不要将用户输入直接传递给任何可以执行系统命令的函数。如果业务上确实需要动态执行命令这种情况极少则必须采取多层防御措施白名单优于黑名单不要试图过滤“危险字符”因为你永远无法穷尽所有危险组合。应该定义明确允许的字符集或命令模式。例如如果只需要执行特定的几个命令如ping、traceroute那么只允许这些命令字符串并且严格校验参数如IP地址格式。避免使用危险函数尽可能用PHP内置的函数替代系统命令。例如用scandir()代替ls用file_get_contents()代替cat用unlink()代替rm。使用参数化或转义如果必须使用系统命令应使用escapeshellarg()或escapeshellcmd()函数对用户输入进行转义。escapeshellarg()会给参数加上单引号并转义其中的单引号确保参数被当作一个整体字符串。escapeshellcmd()会转义Shell元字符。注意这两个函数的使用场景略有不同需根据情况选择。降低权限运行Web服务器进程如php-fpm、Apache mod_php应该以最低必要权限的用户身份运行如www-data、nobody并限制其文件系统访问范围使用chroot或容器。禁用危险函数在PHP配置文件php.ini的disable_functions列表中禁用system、exec、passthru、shell_exec、proc_open、popen、eval、assert等函数。这是最直接有效的防护措施之一。输入验证与输出编码对所有用户输入进行严格的类型、长度、格式验证。对所有输出到HTML页面的内容进行编码防止XSS攻击虽然这与RCE无关但属于整体安全基线。4.3 对安全研究者的意义对于安全研究员和CTF选手来说研究“无数字字母RCE”这类技术其价值在于深入理解漏洞原理它迫使你去理解PHP的Zend引擎如何处理字符串和变量理解Shell的解析过程。这种深度理解有助于你在黑盒测试或代码审计中发现更隐蔽的漏洞。锻炼绕过技巧现实中的WAF和过滤机制往往比CTF题目更复杂但也可能存在逻辑缺陷。练习这些极端情况下的绕过能培养你发现过滤规则盲点的敏锐性。编写更好的防御代码只有了解攻击者是如何思考的才能写出更安全的代码。知道有“异或构造”这种方法你就会明白仅仅过滤字符列表是远远不够的。5. 举一反三其他语言与场景下的类似技巧“无数字字母”的限制虽然源于PHP CTF题但类似的思维可以在其他场景下应用。5.1 在Python中的命令执行绕过Python中也有eval和os.system等函数。如果过滤了字母数字如何构造字符串Python同样支持字符串的运算和转换。利用chr()函数和数字但数字被过滤了。不过我们可以通过运算得到数字例如len([])是0len([[]])是1True是1False是0。通过布尔值和列表长度的组合可以构造出任意数字然后通过chr()得到字符。例如chr(len([[]])len([[]]))得到chr(2)但这离我们需要的字母还很远。更常见的是利用str()、repr()和特殊对象的属性名。利用getattr和__builtins____builtins__是一个包含内置函数的模块。我们可以通过getattr(__builtins__, eval)来获取eval函数。但eval这个字符串包含字母。我们可以用str()和slice或者通过其他对象的类名拼接出来吗这非常复杂。利用Base64编码一个更简单的方法是如果我们可以执行任意Python代码我可以先构造一个Base64编码的命令字符串然后用__import__(base64).b64decode来解码执行。但base64这个字符串也包含字母。不过我们可以通过bytes和int来构造吗这回到了原点。实际上在Python中实现严格无字母数字的RCE比PHP更困难因为Python对语法的要求更严格。但历史上也存在一些利用特殊命名空间和对象属性的技巧。5.2 在JavaScript中的利用在Node.js或浏览器JS中如果eval或Function构造函数可用且过滤了字母数字如何构造利用[]、![]等得到数字[]是0![]是1。通过多个!和[]的组合可以得到true、false进而通过转换为1和0。然后通过位运算和String.fromCharCode()构造字符。这是JSFuck jsfuck.com 编码的原理它仅用[]()!六个字符就能编写任何JavaScript代码。利用反引号模板字符串和constructor例如[][filter][constructor]return console.log(1)可以执行代码。但filter、constructor、return、console、log都包含字母。JSFuck正是解决了这个问题它用那六个字符构造出了所有这些字符串。因此JSFuck是JavaScript领域“无数字字母”代码执行的终极体现。它证明了只要给予足够的“积木”即使只有六种图灵完备的语言就能表达任何逻辑。5.3 在文件上传与WebShell中的变形“无数字字母”的思想也可以应用于WebShell的编写。例如上传一个不含字母数字的PHP文件。文件内容可以是?~%8C%86%8C%8B%9A%92(ls /);?。这样即使文件名被重命名只要扩展名是.php且服务器解析就能执行。或者利用.htaccess文件将特定文件后缀如.abc解析为PHP然后上传内容为上述Payload的.abc文件。这些技巧在渗透测试中用于绕过简单的WAF或上传过滤规则时可能会起到奇效。6. 总结与个人实操心得回顾这道“无数字字母命令执行”的题目它更像是一个引子引导我们进入Web安全中代码执行漏洞的深水区。我个人的体会是面对这类限制最重要的不是记忆某个特定的Payload而是掌握其背后的“元技能”理解语言的核心特性无论是PHP的字符串运算、类型转换还是JavaScript的类型强制转换、对象模型或是Shell的扩展机制深入理解这些特性是构造Payload的基础。平时多阅读语言规范和安全文章比临时抱佛脚更有用。善用自动化工具但明白其原理网上有很多现成的Payload生成工具例如用于生成无字母数字WebShell的脚本。在CTF或渗透测试中使用它们可以节省时间。但务必花时间阅读工具的源代码理解它生成的Payload是如何工作的。这样当下次遇到变种过滤时你才能自己调整工具或手工修改Payload。分步调试与信息收集在真实漏洞利用中环境往往比CTF复杂。你的Payload可能因为PHP版本、Magic Quotes设置、open_basedir限制、Shell环境差异等原因而失败。因此构造Payload时最好采用“分步达成”的策略。先尝试执行phpinfo()或echo 1;来确认代码执行是否成功。然后尝试读取环境变量、目录列表逐步探索目标系统而不是一开始就试图执行cat /flag。防御思维与攻击思维同步作为开发者在写代码时就要思考“如果我是攻击者我会如何绕过这个过滤” 采用白名单、严格转义、最小权限原则。作为安全研究者在测试时也要思考“开发者的防御逻辑是什么它的边界在哪里” 这种双向思维能让你在两者角色中都做得更好。最后这道题也提醒我们安全是一个动态对抗的过程。没有一劳永逸的防御方案。今天有效的过滤规则明天可能就被新的绕过技巧突破。保持学习保持好奇深入原理才是应对万变威胁的不二法门。在下次遇到看似固若金汤的过滤时不妨想想如果连数字和字母都不能用我还能做些什么这种思维训练其价值远超过解出一道CTF题本身。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价