这是一个非常典型的 PHP 代码审计与命令/代码执行(RCE)类型的 CTF 题目。

题目核心在于通过控制 v1 和 v2 两个 GET 参数,在一个 eval() 函数中实现我们想要的恶意代码执行。
其中的核心代码为:
php
if(isset($_GET['v1']) && isset($_GET['v2'])){
$v1 = $_GET['v1'];
$v2 = $_GET['v2'];
if(preg_match('/[a-zA-Z]+/', $v1) && preg_match('/[a-zA-Z]+/', $v2)){
eval("echo new $v1($v2());");
}
}
输入限制: preg_match('/a-zA-Z+/', v1)。这个正则表达式的意思是:只要变量中包含至少一个英文字母即可通过。它并没有使用 \^ 和 限制开头和结尾。这意味着我们可以在 v1 和 v2 中夹带数字、特殊符号、甚至换行符,只要里面有字母就行。
执行点: eval("echo new v1(v1(v1(v2());");
这行代码在原生 PHP 中是在尝试实例化一个类。形式相当于:echo new 类名(函数名());。
我们先尝试查看当前目录下的文件名:
构造payload为:?v1=Exception&v2=system('ls')

过第一步看到了一个叫 fl36dg.txt 的文件,接下来只需要将 ls 命令替换为 cat 命令去读取它。
构造payload为:?v1=Exception&v2=system('cat fl36dg.txt')

再查看原代码

得到flag为:
ctfshow{d2939111-4c45-4925-b947-60068a86e0f6}
核心解题思路(函数参数优先求值特性)
要突破 new v1(v1(v1(v2()); 这个死板的面向对象结构,有一个非常巧妙的底层逻辑漏洞可以利用。
在 PHP 引擎解析这条语句时,它的执行顺序(优先级)是自内向外的:
PHP 必须先知道传给类的构造函数的参数值是什么。
因此,它会首先尝试执行并计算括号内部的 $v2()。
执行完 v2() 拿到返回值后,再执行外部的 new v1。
如果我们利用前面提到的正则缺陷,直接往 $v2 中注入带有参数的恶意函数,比如:
v2=system('ls')
那么带入 eval 后,拼接出来的代码实际上变成了:
php
eval("echo new $v1(system('ls')());");
为什么这样能抢先执行命令?
当 PHP 开始解析内层参数 system('ls')() 时:
它会先把 system('ls') 当作一个整体去调用。此时,系统的 ls 命令已经提前触发,并直接把当前目录下的文件名吐到了页面上。
命令执行完后,system 函数会返回输出结果的最后一行字符串。
接着,PHP 才会尝试把这个"返回的字符串"当作一个动态函数名,加上后面的 () 去执行。这显然会因为找不到对应的函数而报错(抛出类似 eval()'d code 的异常)。
但没关系,因为此时我们想要的命令已经执行完毕,执行结果已经输出了!
为了让外层的 new v1 语法合法而不至于在编译阶段直接卡死,我们只需给 v1 传一个 PHP 自带的合法类名即可,比如 Exception(异常类)或 ReflectionClass(反射类)。
简单来说:v1=Exception 本身并不能执行命令,它的作用是充当一个"语法挡箭牌",让 PHP 顺利通过编译,从而给内层的命令执行创造机会。
我们可以把整个过程拆解为编译阶段和执行阶段来看:
- 编译阶段:通过语法检查(为什么必须填 Exception 这样的类名)
如果你的 Payload 是 ?v1=Exception&v2=system('ls'),带入后端后,eval 拼接出来的完整代码是:
PHP
eval("echo new Exception(system('ls')());");
PHP 的 eval() 在运行这段代码时,首先会进行语法编译。
编译器看到 new v1(...) 的结构时,它必须确保 v1 对应的字符串是一个在 PHP 中真实存在的、合法的类名。
如果你把 v1 留空,或者随便填一个不是类的字符串(比如 v1=abcd),PHP 在编译阶段就会直接抛出 Fatal error: Class 'abcd' not found(致命错误),整段代码直接胎死腹中,根本不会往下执行。
而 Exception 是 PHP 官方自带的内置异常类。填入它,PHP 编译器就会认为:"哦,这是一句合法的'实例化一个异常类'的语法。" 从而允许这段代码通过编译,进入下一个执行阶段。
- 执行阶段:参数优先求值与命令"抢跑"
通过编译后,代码开始真正执行。PHP 遇到 new Exception( 参数 ) 时,底层有一个铁律:必须先计算出括号里面作为参数的值,才能把这个值传给类的构造函数。
于是,PHP 引擎的执行指针开始向内层移动,去解析参数:
Plaintext
echo new Exception( system('ls')() );
└──────┬───┘
- 首先执行这里
命令抢先触发:
在解析 system('ls')() 时,PHP 会先将 system('ls') 当作一个函数进行调用。此时,系统的 ls 命令被激活,直接在服务器终端执行,并把结果(比如 fl36dg.txt)直接打印到了前端页面上。
命令执行完毕:
system('ls') 执行完后,会返回它输出的最后一行字符串(假设是 "index.php")。
后续的报错(无关紧要):
此时,原本的代码结构变成了 echo new Exception("index.php"());。PHP 接下来会尝试把字符串 "index.php" 当作函数名去调用(因为后面还有个 () ),这显然会报错。