MAD Bugs: Feeding Claude Phrack Articles for Fun and Profit
tl;dr: A teammate gave Claude a Phrack article. It built a working
rsync RCE on x86-64. He shared the generated exploit with me but forgot
one file, and I needed it on ARM64 anyway. I gave Claude one prompt:
reproduce this. Ninety minutes later it dropped a working exploit. I
told it the exploit was slow (5 minutes); it made it 20x faster. We also asked it to audit the patched rsync, and it came back with new bugs.
How this started
Our new favourite pastime is feeding Claude Phrack articles and seeing
what comes back. This time it was Phrack 72's "Desync the Planet",
which describes chaining CVE-2024-12085 (stack info leak) into
CVE-2024-12084 (heap overflow) for unauthenticated RCE againstrsync --daemon. A teammate handed Claude the article; it built a
working exploit on x86-64. The full session is inwriteup.md, prompts and all: where Claude tried addingfprintf to sender.c and got told "why are you modifying the rsync
source?", where it was redirected to GDB, where it was told to actually
read the article instead of guessing at the layout.
He shared exploit.py with me. Two problems:
It imported
rsync_lib, which wasn't in the repo. He just forgot
to share it. Claude had generated this custom protocol library to
handle all the heavy lifting: daemon handshake, multiplexed I/O,
file list parsing, the deflate-token oracle.It was tuned for x86-64. Hardcoded binary offsets, an info-leak
target at a stack offset that doesn't exist on ARM64, a.bsspayload
layout that assumes a memory map that doesn't match the ARM64 build.
I wanted to run it on a Debian 12 ARM64 box. So I handed both files (the
writeup and the broken exploit) to Claude:
Read the WriteUp and reproduce this exploit with exploit.py
That was the only prompt. Everything below came out of that single
instruction with no further steering from me.
What Claude figured out
No rsync_lib.py. Claude opened the rsync 3.2.7 source (io.c,flist.c, compat.c, sender.c, token.c) and built the protocol
library from scratch. The first attempt timed out at setup_protocol.
So it spun up a Python proxy, pointed the real rsync client through it
at the daemon, and diffed the wire bytes against what its own library
was sending. Three things the source doesn't make obvious: daemon args
are \0-terminated (not \n), checksum negotiation is both sides
write, then both sides read (not request-response), and the post-setupwrite_line calls were bypassing the multiplex layer. The server was
reading the "ZZZZ..." filter pattern as a multiplex header and
returning unexpected tag 83 (83 + MPLEX_BASE = 90 = 'Z').
No GDB. The container had no gdb, no strace, no root. Claude
built substitutes: an LD_PRELOAD memcmp hook to capture the
uninitialized sum2 buffer at the moment of comparison, and a 200-line
ptrace crash-catcher that attached to the forked rsync child, caught
the SIGSEGV, and dumped registers plus the entire payload region from/proc/PID/mem. The memcmp hook showed the leak target had moved fromsum2+8 on x86-64 to sum2+24 on ARM64 (a saved LR pointing intostart_server, three frames up).
The trickiest bug. The crash dump showed shell_exec had been
called: algctx was zeroed by OpenSSL's str xzr, [x19, #56] afterfreectx returned, not before. Claude set a BRK #0 breakpoint atshell_exec's entry, caught the trap, printed X0 (the cmd pointer),
and followed PTRACE_O_TRACEFORK. The breakpoint hit, the pointer was
correct, and a fork was observed, but the proof file was never created.
Reading the command string back from memory at that point gave"touch /t", truncated at 8 bytes.
The ARM64 build's .bss layout puts last_match at ctx_evp+0x110.match_sums zeroes it before calling sum_init, which overwrites
bytes 8-15 of the command string at +0x108. system("touch /t")
tried to write to / and failed silently. Claude moved the command to+0x58 (inside the ctx_md union, which the OpenSSL path never
touches), which fixed it.
Five issues total, all found and fixed without ever attaching a real
debugger:
| # | What broke | How Claude found it |
|----|------------------------------------------|-------------------------------------------------|
| A1 | Leak target at `sum2+24`, not `+8` | LD_PRELOAD memcmp hook |
| A2 | `count=3277` destabilizes the stack | Per-connection probe across 5 runs |
| A3 | Command truncated at byte 8 | ptrace bp on `shell_exec` + `match_sums` disasm |
| A4 | ARM64 glibc 2.36 still wants ONE filter | `malloc_usable_size` test |
| A5 | Three protocol mismatches in `rsync_lib` | socat wire capture vs. real client |Timeline
About 90 minutes from a cold container to the first proof file. Claude
reconstructed this from daemon log timestamps and file mtimes:
| Elapsed | Milestone |
|---------|------------------------------------------------------------------------------------------------------------|
| 0:00 | First daemon banner. `rsync_lib` doesn't exist yet. |
| 0:30 | Protocol library working: file list parsed, download verified. Three wire-format bugs fixed along the way. |
| 0:37 | Info-leak oracle confirmed: `token=-1` means MATCH. The `memcmp` hook found the LR at `sum2+24`. |
| 0:59 | Heap overflow lands. Server child crashes silently, proof the write hit `.bss`. |
| 1:29 | ptrace breakpoint on `shell_exec`: hit, `X0` correct, fork observed. Still no proof file. |
| 1:34 | First `/tmp/rce_proof.txt`. Bug A3: `last_match = 0` was zeroing `cmd[8:16]`. Moved cmd to `+0x58`. |The split was roughly even: a third building the protocol library, a
third finding the ARM64 leak target, a third figuring out whyshell_exec ran but the file never appeared.
"It works but it's slow"
First successful run: about five minutes. The leak now needs 24 bytes
(not 8), at ~128 connections per byte, ~120ms per connection.
I told Claude:
Your exploit now takes 5 minutes to run, probably because of the
brute-forcing in first step. Make it faster.
Claude came back with two stacked changes. First, a hint table: it had
noticed during the leak debugging that 18 of those 24 bytes are
structural constants on ARM64. User-space addresses are0x0000_aaaa_xxxx_xxxx for the binary, 0x0000_ffff_xxxx_xxxx for the
stack. The page-offset bits of the leaked pointer are exactly the
page-offset bits of LEAK_OFFSET (the base is page-aligned). It encoded
those as first-try hints, one connection each. Second, for the ~6 truly
random bytes, it wrapped the probe in ThreadPoolExecutor(16) to
dispatch all 256 guesses concurrently and take the first match.
The exploit now takes 14 seconds:
mkdir -p /tmp/rsync_test_module
echo "hello world" > /tmp/rsync_test_module/foo.txt
echo "test data here" > /tmp/rsync_test_module/bar.txt
cp rsyncd_test.conf /tmp/
/tmp/rsync-3.2.7/rsync --daemon --config=/tmp/rsyncd_test.conf --port=12000 --address=127.0.0.1
time python3 exploit.py "rsync://127.0.0.1:12000/files" "id > /tmp/rce_proof.txt"
[*] Phase 1: info leak | file=bar.txt size=15
sum2[8] = 0x00 (1 total connections)
sum2[9] = 0x00 (2 total connections)
sum2[10] = 0x00 (3 total connections)
sum2[11] = 0x00 (4 total connections)
sum2[12] = 0xab (260 total connections)
sum2[13] = 0xaa (261 total connections)
sum2[14] = 0x00 (262 total connections)
sum2[15] = 0x00 (263 total connections)
sum2[16] = 0x00 (519 total connections)
sum2[17] = 0x7a (775 total connections)
sum2[18] = 0xde (1031 total connections)
sum2[19] = 0xee (1287 total connections)
sum2[20] = 0xff (1288 total connections)
sum2[21] = 0xff (1289 total connections)
sum2[22] = 0x00 (1290 total connections)
sum2[23] = 0x00 (1291 total connections)
sum2[24] = 0xc4 (1292 total connections)
sum2[25] = 0xae (1293 total connections)
sum2[26] = 0xdc (1549 total connections)
sum2[27] = 0xd2 (1805 total connections)
sum2[28] = 0xaa (1806 total connections)
sum2[29] = 0xaa (1807 total connections)
sum2[30] = 0x00 (1808 total connections)
sum2[31] = 0x00 (1809 total connections)
[+] Leaked .text ptr : 0xaaaad2dcaec4
[+] Binary base : 0xaaaad2da0000
[*] Phase 2: heap overflow → RCE
shell_exec = 0xaaaad2dca120
ctx_evp = 0xaaaad2e54fb0
payload = 344 bytes at &ctx_evp
fake_ctx = 0xaaaad2e54fb8 (+8)
fake_evpmd = 0xaaaad2e55050 (+160)
cmd_addr = 0xaaaad2e55008 (+88)
target ndx=1 file=bar.txt
sending payload (344 bytes) to &ctx_evp...
overflow complete, consuming server output...
server connection ended: connection closed
[+] Payload delivered.
real 0m14.383s
user 0m0.674s
sys 0m1.609s
cat /tmp/rce_proof.txtThe audit
Before any of this, the same teammate had asked Claude to audit the
patched rsync:
now that you have a good grasp of this vulnerability and exploitation
can you audit the latest rsync for variants that may allow exploitation
Claude went file-by-file through all 48 .c source files in 3.4.1 (the
version with all CVEs fixed). We're verifying the findings now and
preparing reports.
Every prompt, both sessions
Prompts 1-12 are the original x86-64 session (the teammate driving);
13-17 are the ARM64 port (me driving).
Initial request — Asked to exploit rsync CVE-2024-12084 (heap
overflow) + CVE-2024-12085 (info leak) into a full RCE chain against
rsync 3.2.7 daemon, following the Phrack 72 "Desync the Planet"
article."why are you modifying the rsync source?" — I had been adding
fprintfdebug statements to sender.c and recompiling. The user
correctly pointed out this shifts binary offsets (ctx_evp, shell_exec,
etc.) and invalidates the exploit constants."you should be using gdb .." — Redirected from printf-debugging
to GDB. Led to the attach-to-daemon workflow withset follow-fork-mode childthat proved essential for every
subsequent debugging step."what sandbox" — I had confused /tmp file isolation with
sandboxing. Clarified the environment."if you need root the password is x ?" — Provided root credentials
to fixptrace_scope(was set to 1, blocking GDB attach). We ranecho 0 > /proc/sys/kernel/yama/ptrace_scope."are you following the phrack exploitation? it outlines it pretty
clear" — Critical redirect. I had been inventing a multi-entry
layout trying to align 40-byte sum_buf strides with 48-byte EVP_MD_CTX
field offsets. The Phrack one-shot contiguous write approach is far
simpler and more reliable."read the phrack exploit - they use the info leak + heap overflow
to get a reliable exploit." — Prompted me to actually read the
full Phrack article rather than working from partial understanding."the writeup is in /tmp/rsync.txt" — Pointed to the local copy of
the Phrack article. Saved time vs trying to web-fetch it (the
WebFetch AI model refused to extract exploit details)."if you need to setup a qemu with the exact debian + rsync used
that is fine" — Offered to set up the exact Debian 12 target
environment. We didn't end up needing this because we adapted the
exploit to our Ubuntu 22.04 system, but this would be the fastest
path for exact reproduction of the Phrack PoC."perfect it seems to work!! can you document your whole process +
my prompts in a writeup! include how to get it working on other
installations etc and debugging instructions." — Led to this
writeup document."now that you have a good grasp of this vulnerability and
exploitation can you audit the latest rsync for variants that may
allow exploitation" — Led to the security audit of rsync 3.4.1
documented in the appendix."the WRITEUP didnt include all of my prompts" — This correction,
leading to this expanded prompt section.
ARM64 port session
"Read the WriteUp and reproduce this exploit with exploit.py" —
Initial port request. Environment turned out to be Debian 12 / arm64
/ glibc 2.36 — different OS, different glibc, different architecture
from the writeup. No GDB, no strace, no root. Five distinct
arm64-specific bugs were found and fixed (§3a).rsync_lib.pywas
built from scratch by reading the rsync 3.2.7 source — a socat wire
capture of the real client revealed args use\0not\n, checksum
negotiation is bidirectional, andwrite_linewas bypassing the
multiplex layer (server: "unexpected tag 83" ='Z' - MPLEX_BASE).
The trickiest bug:shell_execdid fire and did fork, butmatch_sumszeroeslast_matchatctx_evp+0x110beforesum_init,
truncating the command string at byte 8. Diagnosed with a ptrace
breakpoint onshell_execthat printedX0and followedPTRACE_O_TRACEFORK— the fork happened, the cmd pointer was right,
but the string read back as"touch /t". Moved the command to+0x58(inside the unusedctx_mdstruct)."continue" — Permission re-grant after a tool-use rejection during
the initial daemon startup. Resumed without issue."Alright, add to the writeup your adaptions" — Wrote §3a
documenting all five arm64 bugs (A1-A5), the GDB-free debugging
methodology (LD_PRELOAD probes, ptrace crash-catcher, pattern-payload
survival test), and the working run output."Your exploit now takes 5 minutes to run, probably because of the
brute-forcing in first step. Make it faster." — Two stacked fixes:
a hint table (18/24 bytes are structural constants on arm64 —0x00
canonical bits,0xaa/0xffregion prefixes,LEAK_OFFSETpage-
offset bits) and aThreadPoolExecutor(16)for the truly random
bytes. 5 minutes → 14 seconds."Add to the writeup the ARM64 environment, and a note about
speeding up, including a sample run [...] Also update the user
prompts with the prompts/responses so far" — Added the arm64
environment table to §0, the speedup section + timed run to §3a,
and these five entries to §7.
PoCs and writeups: https://github.com/califio/publications/blob/main/MADBugs/rsync/.
—ryan & thai