第二届 “Parloo” CTF 应急响应挑战赛

Misc

量子迷宫

Challenge

未来量子实验室的 AI 将机密数据加密成了量子迷宫,每一行代表一个量子比特操作。只有通过逆向坍缩观测,才能还原出被量子噪声掩盖的密钥。实验室遗留的日志文件似乎隐藏着关键线索(flag 格式:palu {32 位 md5})

Solution

palu-1

提取出 QUBIT | 后面的字符(索引为 6)并连接 -> 二进制转字符(字节长度为 8)

1
2
3
4
5
6
7
8
9
10
11
12
data = """
QUBIT|0⟩ → X Gate
QUBIT|1⟩ → Y Gate PHOTON: 2492°
QUBIT|1⟩ → X Gate
...
"""

bits = ''.join([line[6] for line in data.splitlines() if len(line) > 6])
for i in range(0, len(bits), 8):
chunk = bits[i:i+8]
char = chr(int(chunk, 2))
print(char,end="")
1
palu{aea437c12b149750383fe56727ec5344}

时间折叠

Challenge

某服务器系统日志中发现异常的时间戳记录,所有时间均显示为 "1970-01-01 08:00:00"。技术员发现日志中存在与时间无关的二进制干扰信号,你能从被折叠的时空中找到隐藏的 flag 吗?

Solution

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
data = """
[1970-01-01 08:00:00] System boot sequence initiated
[1970-01-01 08:00:00] SYSTEM ALERT: Time anomaly detected at 00000000fe ns
[1970-01-01 08:00:00] SYSTEM ALERT: Time anomaly detected at 10000000ef ns
...
[1970-01-01 08:00:00] SYSTEM ALERT: Time anomaly detected at 380000000f3 ns
[1970-01-01 08:00:00] System entering chronostasis mode
"""

result = ' '.join([line[-5:-3] for line in data.splitlines() if len(line) > 60])

# 将十六进制字符串转换为字节数组
byte_data = bytes.fromhex(result)

# 尝试 XOR 解密
def xor_decrypt(data, key):
return bytes([b ^ key for b in data])

# 假设密钥是一个单字节值(0x00 到 0xFF)
for key in range(256):
decrypted_data = xor_decrypt(byte_data, key)
try:
decrypted_string = decrypted_data.decode('utf-8')
if decrypted_string.startswith("palu"):
print(f"找到匹配的密钥: {key}")
print("还原的目标字符串:", decrypted_string)
break
except UnicodeDecodeError:
continue
else:
print("未找到以 'palu' 开头的字符串")

XOR 解密,爆破得到密钥为 142

1
palu{This_is_A_Sample_Flag_Change_Me!!}

TopSecret

Challenge

你从某机密设施偷取了一份文件,但你被发现了,最后只收到一份乱码,你能从中提取到有用的信息吗?

Solution

看着就像 base64,直接搜 cGFsd

palu-2

cGFsdXtZb3VfcmVfYV9yZWFsXzUwd30= 复制下来拿去解码

palu-3

1
palu{You_re_a_real_50w}

screenshot

Challenge

有个笨比截图 flag 的时候不小心发到群里了,细心和反回撤的群友已经偷偷记录下来了

Solution

苹果手机的无效打码还挺有名的,改一下曝光亮度那些就能出

不过因为我懒得调参数所以就用 StegSolve 了

palu-4

1
palu{why_you_spy_me}

几何闪烁的秘密

Challenge

我们找到了一张神秘的 GIF 图片,里面似乎隐藏着重要信息。仔细观察,你会发现四个几何体在不同时刻闪现出一些奇怪的字符。你能从中提取出完整的 Flag 吗?

Solution

在蓝色圆圈发现 flag 头经过 base64 编码后的结果 cGFsd

分别提取图片里的字母

圆形:cGFsXttcFsdXtcGFdXtt

方形:YXN0XJfYN0ZXfYXNZXJf

三角形:b2Zf2VvbZfZ2vb2ZZ2Vv

五边形:bWV0nl9bV0cn9bWVcnl9

这里拿圆形举例

cGFsXttcFsdXtcGFdXtt ->

cGFsXtt cFsdXt cGFdXtt ->

求同存异得到 cGFsdXtt

同理得到其他几个图形对应的字符串,拼接得到 cGFsdXttYXN0ZXJfb2ZfZ2VvbWV0cnl9

解码得到 flag

1
palu{master_of_geometry}

dorodoro

Challenge

Solution

palu-5

palu-6

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

url = "http://challenge.qsnctf.com:31868/check"

data_template = {
"part1": "",
"part2": "",
"part3": "",
"part4": ""
}

def orange(part):
for n in range(1, 100): # "哦润吉"重复n次
data_template[f"part{part}"] = "哦润吉" * n

response = requests.post(url, json=data_template)

result = response.json()

if result.get("feedback", {}).get(f"part{part}") == True:
print(f"[*] 找到了正确的 part{part} 值!")
print(data_template[f'part{part}'])
return result

# print(orange(1))
# print(orange(2))
# print(orange(3))
print(orange(4))

时间循环的信使

Challenge

某神秘组织通过时间循环传递加密信息,我们在捕获的流量日志中发现异常时间戳。日志文件显示:“在错误的时间做正确的事,在正确的时间解开谜题”,flag 格式为 palu

Solution

先把附件丢给 AI 让它帮忙按时间从小到大排序,然后手动清理数据,只留下循环的

然后把循环的数字拼起来(这里的操作是取每行的最后一个字符)

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
data = """
1745396077|77777777
1745396138|00000000
1745396199|66666666
1745396260|11111111
1745396321|66666666
1745396382|cccccccc
1745396443|77777777
1745396504|55555555
1745396565|77777777
1745396626|bbbbbbbb
1745396687|55555555
1745396748|44444444
1745396809|66666666
1745396870|99999999
1745396931|66666666
1745396992|dddddddd
1745397053|66666666
1745397114|55555555
1745397175|55555555
1745397236|ffffffff
1745397297|33333333
1745397358|11111111
1745397419|77777777
1745397480|33333333
1745397541|55555555
1745397602|ffffffff
1745397663|66666666
1745397724|33333333
1745397785|77777777
1745397846|99999999
1745397907|66666666
1745397968|33333333
1745398029|66666666
1745398090|cccccccc
1745398151|33333333
1745398212|11111111
1745398273|66666666
1745398334|33333333
1745398395|44444444
1745398456|00000000
1745398517|66666666
1745398578|cccccccc
1745398639|55555555
1745398700|ffffffff
1745398761|33333333
1745398822|00000000
1745398883|77777777
1745398944|88888888
1745399005|77777777
1745399066|dddddddd
"""

last_chars = [line[-1] for line in data.splitlines() if line]
print(''.join(last_chars))

拼起来得到 70616c757b54696d655f31735f6379636c3163406c5f30787d,拿去 CyberChef 十六进制转字符串

palu-7

1
palu{Time_1s_cycl1c@l_0x}

时间交织的密语

Challenge

我们在暗网服务器中发现了一个神秘文件,据说是某个黑客组织的「时空密钥」,文件内容似乎由大量时间戳构成。情报显示,只有将时间维度与二进制低语结合才能解开秘密。线索隐藏在时空的起点与终点之间。

Solution

“文件内容似乎由大量时间戳构成” 提示了要把十六进制转为十进制得到时间戳

“将时间维度与二进制低语结合” 提示了要取时间戳的最后两位

“线索隐藏在时空的起点与终点之间” 提示了最终解出来的结果要把第一个和最后一个字符去除,保留中间的部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import struct

with open("timestream.bin", 'rb') as f:
data = f.read()

hex_result = ''

for i in range(0, len(data), 4):
chunk = data[i:i+4]
# 使用大端模式解包为无符号整数(UNIX 时间戳)
timestamp, = struct.unpack('>I', chunk)

# 取模后转为十六进制,去掉前缀 '0x'
hex_digit = format(timestamp % 100, 'x') # 取最后两位
hex_result += hex_digit

flag = bytes.fromhex(hex_result[1:-1]).decode('utf-8') # 保留中间部分
print(flag)
1
palu{Time_1s_B1nary_Whisper}

最弱帕鲁

Challenge

刚学习网络安全的帕鲁下班回来天塌了,自己珍藏的文件居然忘记了密码!不对… 知道密码也没用啊!

Solution

jsteg 隐写,工具下载 lukechampine/jsteg

jsteg.exe reveal 弱帕鲁.jpg
SuperPassw0rdNeverGuess

得到解压密码 SuperPassw0rdNeverGuess

等 WP 复现 ing


Web

CatBank

Challenge

🐱 欢迎光临猫猫银行!一家由 喵星人 运营的银行!你励志要 赚一百万,但……
这家银行的代码是猫爪写的 🐾💻,好像有漏洞?猫猫银行最近有网络波动 在被未知势力攻击!!!

Solution

先注册两个号,就分别叫 1 和 2 吧

登录账号 2 给 1 转账 0.01 元

palu-8

抓包找到这条记录,修改金额后重新发送(金额要大于等于一百万)

palu-9

然后登录账号 1,此时给任意一个用户转账一百万就会显示 flag

palu-10

1
palu{4e730efb2ca3433e806f150c53f4bfa2}

Reverse

PosltionalXOR

Chall

qcoqVh{ebccocH^@Lgt{gt|g

Solution

根据加密算法分析,每个字符的加密密钥为其在字符串中的位置索引加 1。因此,解密时需将每个密文字符与其位置索引加 1 后的值进行异或运算。

1
2
3
4
5
6
7
8
9
ciphertext = "qcoq~Vh{e~bccocH^@Lgt{gt|g"
transformed = []
for idx, char in enumerate(ciphertext):
position = idx + 1
original_ascii = ord(char)
new_ascii = original_ascii ^ position
transformed_char = chr(new_ascii)
transformed.append(transformed_char)
print(''.join(transformed))
1
palu{PosltionalXOR_sample}

PaluFlat

Challenge

帕鲁不是死肥宅,帕鲁不胖!帕鲁头晕目眩绝对不是炫多了!

Solution

用 010editor 打开附件发现是 7z 压缩包,改后缀解压后用 IDA 分析。

我们通过静态分析发现,函数的大致流程如下:

对于每个字符 a1[i]

  1. 使用 "flat""palu" 中的一个字符串循环异或:

    1
    2
    char key = flat_or_palu[i % len];
    char tmp = a1[i] ^ key;
  2. 对异或后的 tmp 做一些变换:

    • 可能是 nibble swap(交换高低 4 位)
    • 可能减去 85
    • 可能按位取反

这些操作根据 n12345 的比特位动态选择。

不过,由于 n12345 = 12345 是固定的,因此每一步的操作是固定的。

既然整个过程是确定性的,我们可以直接暴力枚举所有可能的 ASCII 字符,找到经过 sub_401550 () 后等于 v5 [i] 的字符即可。

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
def encrypt_char(c, index):
# 模拟 sub_401550 的单个字符处理逻辑
# 根据 index 决定使用哪个 key 字符串
key_str = "flat" if (index & 1) else "palu"
key = key_str[index % len(key_str)]
tmp = c ^ ord(key)
tmp = ((tmp << 4) & 0xF0) | ((tmp >> 4) & 0x0F) # Nibble swap
tmp -= 85
tmp = ~tmp & 0xFF # 取反并保持在 byte 范围
return tmp

# v5 数组
v5 = [
84, -124, 84, 68, -92, -78, -124, 84,
98, 50, -113, 84, 98, -78, 84, 3,
20, -128, 67, 19
]

# 转换为无符号字节
v5_bytes = [b & 0xFF for b in v5]

flag = ''
for i in range(len(v5_bytes)):
target = v5_bytes[i]
found = False
for c in range(32, 127): # 可打印 ASCII 范围
if encrypt_char(c, i) == target:
flag += chr(c)
found = True
break
if not found:
flag += '?'

print(flag)
1
palu{Fat_N0t_Flat!}