php
<?php
include 'flag.php';
###初始化
$yds = "dog";
$is = "cat";
$handsome = 'yds';
###遍历所有POST/GET提交的数据
#把每个字段名变成变量名,字段值变成变量值
foreach($_POST as $x => $y){
$$x = $y;
}
foreach($_GET as $x => $y){
$$x = $$y;
}
foreach($_GET as $x => $y){
if($_GET['flag'] === $x && $x !== 'flag'){
exit($handsome);
}
}
if(!isset($_GET['flag']) && !isset($_POST['flag'])){
exit($yds);
}
if($_POST['flag'] === 'flag' || $_GET['flag'] === 'flag'){
exit($is);
}
echo "the flag is: ".$flag;
这是一道非常经典的 PHP变量覆盖 漏洞题目。解开这道题的核心思路是:我们无法让程序正常执行到最后一行 echo $flag,因为前面的所有 if 条件最终都会触发 exit() 提前结束程序。所以,我们的目标是利用变量覆盖漏洞,把真正的 $flag 的值,注入到 exit() 要输出的变量中。
第一步:理解代码的核心机制
代码中有三个最关键的变量初始化:
php
$yds = "dog";
$is = "cat";
$handsome = 'yds';
接着是两个遍历数组的代码,这是漏洞的根源:
- POST请求处理 :
$$x = $y;- 意思是:如果POST传入
a=b,那么就会变成$a = "b"。这是字符串赋值。
- 意思是:如果POST传入
- GET请求处理 :
$$x = $$y;- 意思是:如果GET传入
a=b,那么就会变成$a = $b。这是变量引用赋值(把变量b的值赋给变量a)。这是我们解题的关键!
- 意思是:如果GET传入
第二步:分析 Exit(退出)条件
程序有三个退出点,我们必须从这三个退出点中选一个来输出 flag:
退出点 1:
php
foreach($_GET as $x => $y){
if($_GET['flag'] === $x && $x !== 'flag'){
exit($handsome);
}
}
- 条件 :GET参数中必须包含
flag这个键,且flag的值必须等于另一个GET参数的键名 ,同时那个键名不能是flag本身。 - 输出 :
$handsome
退出点 2:
php
if(!isset($_GET['flag']) && !isset($_POST['flag'])){
exit($yds);
}
- 条件 :GET 和 POST 都没有 传入
flag参数。 - 输出 :
$yds
退出点 3:
php
if($_POST['flag'] === 'flag' || $_GET['flag'] === 'flag'){
exit($is);
}
- 条件 :POST传入的
flag值为字符串"flag",或者 GET传入的flag值为字符串"flag"。 - 输出 :
$is
第三步:构造 Payload 绕过
我们的终极目标是:利用 GET 请求的 $$x = $$y 特性,把 $flag 的值赋给 $yds、$is 或 $handsome,然后触发对应的 exit()。
💡 解法一:最简单的解法(利用退出点2,输出 $yds)
退出点2的条件是:GET和POST都没有传入 flag。
如果我们只传入 GET 参数:?yds=flag
- 执行 GET 遍历
$$x = $$y:此时$x = "yds",$y = "flag"。代码执行变成$yds = $flag。此时$yds的值变成了真正的 flag! - 退出点1检查:因为没有传入
flag参数,$_GET['flag']为空,条件不满足,跳过。 - 退出点2检查:
!isset($_GET['flag'])成立(没传flag),!isset($_POST['flag'])成立(没传flag),触发exit($yds)! - 此时
$yds已经是真正的 flag 了,程序输出 flag 并结束。
Payload: ?yds=flag
💡 解法二:利用退出点3(输出 $is)
退出点3的条件是:GET传入 flag=flag。
如果我们构造 GET 参数:?is=flag&flag=flag
- 执行 GET 遍历
$$x = $$y:- 当遍历到
is=flag时:$x="is",$y="flag",执行$is = $flag。此时$is变成了真正的 flag! - 当遍历到
flag=flag时:$x="flag",$y="flag",执行$flag = $flag。相当于自己赋值给自己,保护了$flag没有被覆盖掉。
- 当遍历到
- 退出点1检查:
$_GET['flag']是"flag"。遍历到is时,$x="is","flag" === "is"不成立;遍历到flag时,虽然"flag" === "flag"成立,但是$x !== 'flag'不成立(因为此时 $x 就是 'flag'),所以跳过。 - 退出点2检查:传入了
$_GET['flag'],条件不满足,跳过。 - 退出点3检查:
$_GET['flag'] === 'flag'成立!触发exit($is)! - 此时
$is已经是真正的 flag,程序输出 flag 并结束。
Payload: ?is=flag&flag=flag
💡 解法三:利用退出点1(输出 $handsome,稍微进阶)
退出点1的条件是:GET传入 flag=x,且还要有一个参数名叫 x(x不能是flag)。
比如我们传入:?handsome=flag&flag=handsome
- 执行 GET 遍历
$$x = $$y:- 如果先遍历到
handsome=flag:$handsome = $flag。成功覆盖! - 然后遍历到
flag=handsome:$flag = $handsome。此时$flag的值变成了刚才的$handsome(也就是真正的flag),$flag没丢。
(注意:PHP中数组遍历顺序通常和参数书写顺序一致,所以这样写是安全的)
- 如果先遍历到
- 退出点1检查:
$_GET['flag']的值是"handsome"。当遍历到handsome=flag时,$x="handsome"。条件$_GET['flag'] === $x即"handsome" === "handsome"成立!且$x !== 'flag'也成立!触发exit($handsome)! - 此时
$handsome已经是真正的 flag,程序输出 flag。
Payload: ?handsome=flag&flag=handsome
📚 总结与学习要点
- 变量覆盖漏洞 :当遇到
$$var这种可变变量时,一定要警惕。如果允许用户控制键名和键值,就可以覆盖程序原有的变量。 - 逆向思维 :当正常的执行逻辑(
echo $flag)被重重exit()阻断时,不要死磕如何走到最后,而是思考如何利用exit()当作我们的"输出面板"。 $$x = $y与$$x = $$y的区别 :前者只能赋值字符串,后者可以赋值变量的引用。本题中$flag的值我们不知道,所以不能用 POST 的$$x=$y来让某个变量等于 flag,只能用 GET 的$$x=$$y来进行变量间的值传递。