[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 来进行变量间的值传递。
相关推荐
李豆豆喵1 天前
013-基础入门-身份鉴权&参考文章&发包工具项目等
web安全
有浔则灵2 天前
GORM钩子函数详解
网络·安全·web安全
Sagittarius_A*2 天前
CSRF 跨站请求伪造:伪造请求攻击、绕过手段与底层防御
安全·web安全·csrf·dvwa
祁白_2 天前
sqlmap工具
web安全·网络安全·sql注入·sqlmap
艺杯羹2 天前
网络安全攻防演进:从单人砸门到AI博弈的五次战争
人工智能·安全·web安全·ddos·ip·ip欺骗
HackTwoHub2 天前
等级保护现场测评系统重磅更新,支持 AI 接入,可录入全品类资产清单,自动化巡检核查,批量导出测评归档文件
运维·人工智能·安全·web安全·网络安全·自动化·系统安全
川石课堂软件测试2 天前
安全测试|服务器安全加固方法
服务器·功能测试·测试工具·jmeter·mysql·web安全·单元测试
Sagittarius_A*2 天前
Command Injection 命令注入漏洞:系统命令拼接缺陷与执行利用技术
网络·安全·web安全·dvwa·命令注入漏洞
Android洋芋2 天前
漏洞挖掘与赏金攻略
网络·安全·web安全