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/
$ 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
- [crypto][free]Just Follow the Recipe— kitctf
- [pwn][Pro]Temporal— bluehensctf
- [pwn][free]priority-queue— b01lersc
- [pwn][Pro]Easy Overflow 3— spbctf
- [pwn][free]0xDiablos— hackthebox