[BJDCTF2020]Mark loves cat (WriteUp)

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';

接着是两个遍历数组的代码,这是漏洞的根源:

  1. POST请求处理$$x = $y;
    • 意思是:如果POST传入 a=b,那么就会变成 $a = "b"。这是字符串赋值
  2. GET请求处理$$x = $$y;
    • 意思是:如果GET传入 a=b,那么就会变成 $a = $b。这是变量引用赋值(把变量b的值赋给变量a)。这是我们解题的关键!

第二步:分析 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

  1. 执行 GET 遍历 $$x = $$y:此时 $x = "yds", $y = "flag"。代码执行变成 $yds = $flag此时 $yds 的值变成了真正的 flag!
  2. 退出点1检查:因为没有传入 flag 参数,$_GET['flag'] 为空,条件不满足,跳过。
  3. 退出点2检查:!isset($_GET['flag']) 成立(没传flag),!isset($_POST['flag']) 成立(没传flag),触发 exit($yds)
  4. 此时 $yds 已经是真正的 flag 了,程序输出 flag 并结束。

Payload: ?yds=flag


💡 解法二:利用退出点3(输出 $is)

退出点3的条件是:GET传入 flag=flag

如果我们构造 GET 参数:?is=flag&flag=flag

  1. 执行 GET 遍历 $$x = $$y
    • 当遍历到 is=flag 时:$x="is", $y="flag",执行 $is = $flag此时 $is 变成了真正的 flag!
    • 当遍历到 flag=flag 时:$x="flag", $y="flag",执行 $flag = $flag。相当于自己赋值给自己,保护了 $flag 没有被覆盖掉
  2. 退出点1检查:$_GET['flag']"flag"。遍历到 is 时,$x="is""flag" === "is" 不成立;遍历到 flag 时,虽然 "flag" === "flag" 成立,但是 $x !== 'flag' 不成立(因为此时 $x 就是 'flag'),所以跳过。
  3. 退出点2检查:传入了 $_GET['flag'],条件不满足,跳过。
  4. 退出点3检查:$_GET['flag'] === 'flag' 成立!触发 exit($is)
  5. 此时 $is 已经是真正的 flag,程序输出 flag 并结束。

Payload: ?is=flag&flag=flag


💡 解法三:利用退出点1(输出 $handsome,稍微进阶)

退出点1的条件是:GET传入 flag=x,且还要有一个参数名叫 x(x不能是flag)。

比如我们传入:?handsome=flag&flag=handsome

  1. 执行 GET 遍历 $$x = $$y
    • 如果先遍历到 handsome=flag$handsome = $flag成功覆盖!
    • 然后遍历到 flag=handsome$flag = $handsome。此时 $flag 的值变成了刚才的 $handsome(也就是真正的flag),$flag 没丢。
      (注意:PHP中数组遍历顺序通常和参数书写顺序一致,所以这样写是安全的)
  2. 退出点1检查:$_GET['flag'] 的值是 "handsome"。当遍历到 handsome=flag 时,$x="handsome"。条件 $_GET['flag'] === $x"handsome" === "handsome" 成立!且 $x !== 'flag' 也成立!触发 exit($handsome)
  3. 此时 $handsome 已经是真正的 flag,程序输出 flag。

Payload: ?handsome=flag&flag=handsome


📚 总结与学习要点

  1. 变量覆盖漏洞 :当遇到 $$var 这种可变变量时,一定要警惕。如果允许用户控制键名和键值,就可以覆盖程序原有的变量。
  2. 逆向思维 :当正常的执行逻辑(echo $flag)被重重 exit() 阻断时,不要死磕如何走到最后,而是思考如何利用 exit() 当作我们的"输出面板"。
  3. $$x = $y$$x = $$y 的区别 :前者只能赋值字符串,后者可以赋值变量的引用。本题中 $flag 的值我们不知道,所以不能用 POST 的 $$x=$y 来让某个变量等于 flag,只能用 GET 的 $$x=$$y 来进行变量间的值传递。
相关推荐
祁白_4 小时前
无字母数字 Webshell 绕过
笔记·web安全·测试·ctf
深邃-6 小时前
【Web安全】-BurpSutie实战讲解(2):BP代理模块,BP重放模块,BP爆破模块,BP爬虫功能,BP解码模块,BP对比模块
爬虫·计算机网络·安全·web安全·网络安全·burpsutie
wanhengidc6 小时前
云手机中虚拟技术的功能
运维·服务器·网络·安全·web安全·智能手机
txg6668 小时前
网络安全领域简报(2026年5月9日—5月16日)
安全·web安全
zyl837211 天前
前端开发网络安全注意事项
安全·web安全
OpenAnolis小助手1 天前
Anolis OS Linux Dirty Frag 漏洞安全声明
linux·安全·web安全·龙蜥社区
技术不好的崎鸣同学1 天前
WEB安全之XSS专题:前沿案例与实战分析
安全·web安全·xss
淼淼爱喝水1 天前
DVWA和Pikachu命令注入漏洞检测实验
安全·web安全·php·pikachu·dvwa
Bruce_Liuxiaowei1 天前
2026年5月第3周网络安全形势周报
人工智能·安全·web安全·网络安全·系统安全