东北大学第三届 NEX CTF 题解

本次参赛的最大感受就是AI变nb了,今年可以用AI干好多之前无法想象的事情…..

Misc

【简单】签到题

找作者:

alt text

【中等】奇思妙想聪明的小羊

用压缩软件打开图片,发现很明显是一个.git文件夹的结构,随便新建一个文件夹,将压缩包里的东西放进.git文件夹,然后用git status查看,没问题之后git log,发现有2次commit,最新一次是删除flag文件,用git reset还原即可

alt text

Crypto

【简单】Pyyyyyyyyyyyyyyyyyyyython

用AI应用秒了,解放双手

alt text

【简单】来自英仙座的怪兽 & 【中等】怪兽的最后反攻

每次出一个问题,就让DeepSeek补充代码计算即可。

alt text

alt text

代码:

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

from math import gcd

def common_modulus_attack(n, e1, e2, c1, c2):
"""
共模攻击:使用相同模数n和不同指数e1,e2加密的同一明文
"""
# 检查e1和e2是否互质
if gcd(e1, e2) != 1:
raise ValueError("e1和e2必须互质")

# 使用扩展欧几里得算法求s,t使得 e1*s + e2*t = 1
g, s, t = extended_gcd(e1, e2)

# 根据s,t的正负情况计算明文
if s < 0:
c1_inv = pow(c1, -1, n) # 模逆元
part1 = pow(c1_inv, -s, n)
else:
part1 = pow(c1, s, n)

if t < 0:
c2_inv = pow(c2, -1, n) # 模逆元
part2 = pow(c2_inv, -t, n)
else:
part2 = pow(c2, t, n)

# 计算明文: m = c1^s * c2^t mod n
m = (part1 * part2) % n

return m

def extended_gcd(a, b):
"""扩展欧几里得算法"""
if a == 0:
return b, 0, 1
gcd, x1, y1 = extended_gcd(b % a, a)
x = y1 - (b // a) * x1
y = x1
return gcd, x, y

def mod_inverse(a, m):
"""求模逆元"""
gcd, x, _ = extended_gcd(a, m)
if gcd != 1:
raise ValueError("逆元不存在")
return x % m

def rsa_decrypt_manual(p, c, e=65537):
"""
手动实现RSA解密(便于理解原理)
"""
# 计算欧拉函数 φ(p) = p-1
phi = p - 1

# 计算私钥指数 d = e^(-1) mod φ(p)
d = mod_inverse(e, phi)

# 解密
m = pow(c, d, p)

return m, d

def rsa_decrypt_complete(n, p, c, e=65537):
"""
完整的RSA解密流程
"""
# 1. 验证p是n的因子
if n % p != 0:
raise ValueError(f"p={p} 不是 n={n} 的因子")

# 2. 计算q
q = n // p

# 3. 验证p和q都是素数(简单检查)
def is_probable_prime(num):
if num < 2:
return False
for i in range(2, min(int(math.sqrt(num)) + 1, 10000)):
if num % i == 0:
return False
return True

if not is_probable_prime(p):
print(f"警告: p={p} 可能不是素数")
if not is_probable_prime(q):
print(f"警告: q={q} 可能不是素数")

# 4. 计算欧拉函数
phi = (p - 1) * (q - 1)

# 5. 计算私钥指数d
try:
d = pow(e, -1, phi)
except ValueError:
raise ValueError(f"e={e} 在模 φ(n)={phi} 下没有逆元")

# 6. 解密
m = pow(c, d, n)

return m

if __name__ == "__main__":
mode = int(input())
if mode == 1:
x = int(input())
print(int(x ** (1/5)))
elif mode == 2:
# 使用示例
p = int(input('p: '))
c = int(input('c: ')) # 假设这是密文

m, d = rsa_decrypt_manual(p, c)
print(m)
elif mode == 3:
n = int(input('n: '))
p = int(input('p: '))
c = int(input('c: '))
m = rsa_decrypt_complete(n, p, c)
print(m)
elif mode == 4:
n = int(input('n: '))
e1 = int(input('e1: '))
e2 = int(input('e2: '))
c1 = int(input('c1: '))
c2 = int(input('c2: '))
m = common_modulus_attack(n, e1, e2, c1, c2)
print(m)

Reverse

【简单】办公达人

把Excel的隐藏表翻出来,然后根据隐藏表的信息写等效计算代码即可。

alt text

alt text

Web

【简单】签到

第一块是一个隐藏的input组件里,接下来沿着提示找就行

alt text

【简单】puzzle

js代码限制了鼠标右键和键盘,但浏览器可以通过其他方式只使用左键打开控制台,比如说FireFox,右上角的选项卡里就有

alt text

然后打印一下window对象,发现里面就一个是布尔类型的变量,改成true就可以了

alt text

不过需要在2s之内完成,时间很充裕,先复制好语句,刷新之后立刻执行就行

alt text

【简单】校园福利中心

首先看了一下他的main.js代码,发现有一个请求头X-Can-View,默认值是no

alt text

请求前改成yes就行了

alt text

【简单】简易签名的VIP计划

先自己转账一下,看一看请求的格式是什么,可以看到重要的是请求头的Authorization和请求体

alt text

之后看了一下js代码,发现参数校验仅在前端进行。(虽然这里是故意在前端校验的,但看Web开发视频的时候总有人说“参数在前端验证不就行了”之类的话,我估计生产环境真有这种代码~)

alt text

前端根据参数生成JWT,并发送POST请求。因为前端就有可以使用的方法,所以可以直接在控制台调用相应方法,伪造JWT,然后发送请求

alt text

alt text

【中等】NEX文档站

在用户名和密码里输入' OR 1=1 --就可以进去了,说明有SQL注入的漏洞。在没放提示之前我以为flag会在数据库中,因为ctf_docs.users表有三行数据,于是用了时间盲注,等了好久结果没看到有用信息

alt text

之后提示说是在某个地方的flag.txt里,登陆后观察地址,发现/book/*是用来访问文件的,所以让AI构造了几个可能存在的地方测试

alt text

【中等】世界时钟

我看这个网页的JS代码没有混淆,所以直接托管给AI做了

alt text

【中等】老虎机

跟上面一样

alt text

【简单】It’s MyGo!!!

阅读源码发现,发送POST请求,参数mygo的值会被当作命令处理

alt text

alt text

【中等】Ave Mujica

这道题与上面类似,但是网页没有回显,而且限制严格。观察上一个题目源码发现,./templates/assets文件夹的文件被挂载到了/assets路由,只要把输出重定向到这个文件夹里的文件就行。最开始我以为echols这种命令都被限制了,但单独使用却没有问题,后来发现是限制了空格,可以用$IFS绕过去,不过还限制了>符号,使用b64编码绕过去了:

echo$IFS"cHJpbnRlbnYgPiAuL3RlbXBsYXRlcy9hc3NldHMvZW52LnR4dA=="$IFS|$IFS$@base64$IFS-d$IFS|sh

alt text

alt text

【简单】公开的秘密

一看就是DNS记录

alt text

【中等】扭曲的镜像

进去发现输入地址,回显是ping的输出,说明直接执行的终端命令,用&连接第二个命令就行,先ls /观察一下

alt text

【困难】变形的钥匙

阅读源码,发现Python后端虽然校验了输入,但仍然把原始b64传给给终端。正常来说后端已经获取了通过检验后的IPv4输入,直接把解码后的传给终端不就行了。所以说明相同的b64字符串,在Python端和终端表现会不一样。

标准base64会在长度不够的时候在结尾填充=,不会出现在中间,所以我觉得可能Python按照这个进行检验解码,而终端命令可能还会继续阅读接下来的b64编码,实践也证实了确实是这样:

alt text

发送构造好的b64字符串就可以了

alt text

【简单】第一次接触:咖啡店的暗号

解析pkl文件就可以了,不过要定义好相应的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import pickle

# 定义Contact类以兼容pickle文件
class Contact:
def __init__(self, name=None, email=None, phone=None, flag=None):
self.name = name
self.email = email
self.phone = phone
self.flag = flag

# 读取contact.pkl文件
try:
with open('contact.pkl', 'rb') as file:
contact = pickle.load(file)

print("=== contact.pkl 文件内容 ===")
print(f"姓名: {contact.name}")
print(f"邮箱: {contact.email}")
print(f"电话: {contact.phone}")
print(f"标志: {contact.flag}")
print("============================")

except Exception as e:
print(f"读取文件时出错: {e}")

alt text

本来还想接下来继续做完Web剩下的两道题目,但是纱布AI把我自己系统的环境变量打包成pkl,我也没仔细看,导致我系统环境里十多个API KEY全™上传到题目容器里了,害得我™花了30min一个一个去失效API Key😭😭