pwnfreemedium

Paradise Nut

gpnctf

Task: submit one line of C compiled by the pnut C-to-POSIX-shell transpiler (MINIMAL_RUNTIME) and run as an unprivileged user; goal is to read root-owned /flag. Solution: the author added an unbounded gets() to the runtime, causing a heap overflow in pnut's `_N` bash-variable cell model; lay out an adjacent open() filename buffer right after the gets() target so the overflow writes '/flag', yielding arbitrary file read.

$ ls tags/ techniques/
gets_unbounded_overflowpnut_shell_heap_overflowadjacent_buffer_filename_controlarbitrary_file_read_via_openmalloc_layout_offset_tuningspacer_malloc_to_avoid_clobber

$ cat /etc/rate-limit

Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.

Paradise Nut — GPN24 CTF (KITCTF)

Description

Finally a C compiler you can trust!

The player submits one line of C code. chal.sh compiles it with pnut-sh.sh (the pnut transpiler that turns a large subset of C99 into human-readable POSIX shell) and runs the resulting shell script with bash, as the unprivileged user user. The goal is to read /flag.

The title is a pun: Paradise Nut = pnut, and "a C compiler you can trust" is a reference to Ken Thompson's Reflections on Trusting Trust — you actually can't trust this one.

# chal.sh #!/bin/bash printf 'Enter your C code on a single line.\n> ' bash <(./pnut-sh.sh <(head -n1))
# Dockerfile (key lines) FROM ubuntu:26.04 ARG FLAG=GPNCTF{fake_flag} RUN echo "$FLAG" > /flag RUN chmod 400 /flag # only root can read /flag RUN chmod u+s /usr/bin/nl # intended hard path: run `nl /flag` to win RUN useradd user USER user COPY pnut-sh.sh chal.sh ./ ENTRYPOINT [ "socat", "tcp-l:1337,reuseaddr,fork", "EXEC:./chal.sh,stderr" ]

Remote was served over SSL (ncat --ssl <host> 443).

The Dockerfile presents an intended hard path: /flag is chmod 400 root, and /usr/bin/nl is SUID root, so the canonical win is nl /flag — which would require command execution. As shown below, an arbitrary file read bypasses this entirely.

Analysis

pnut-sh.sh is ~6184 lines: the upstream pnut transpiler compiled to a single POSIX shell script, built as the MINIMAL_RUNTIME variant, with exactly one author-added function: _gets().

pnut's shell memory model

pnut emulates C memory in shell. Heap "cells" are bash variables named _1, _2, _3, ... (each holds one integer). malloc is a simple bump allocator over __ALLOC (with RT_FREE_UNSETS_VARS and size headers). Strings are stored one byte (as an int) per cell.

The author-introduced bug

...

$ grep --similar

Similar writeups