| # PoC β code execution when loading a CNTK v2 (.cntk) model |
|
|
| Loading an untrusted CNTK v2 model with the normal `cntk.Function.load()` runs code |
| embedded in the file. A CNTK graph can hold a Python `UserFunction` node, and the node |
| stores the function's `module` and `class` as plain strings. On load CNTK's UDF |
| deserializer does `exec("from {module} import {class}")` on those attacker-controlled |
| strings, so the file alone decides what gets imported and run. |
|
|
| `model_inject_linux.cntk` needs no external file: the `module` is byte-patched to |
| `builtins` and the `class` string carries a newline-injected payload, so loading it |
| runs `os.system('id ...')` and drops `/tmp/PWNED_cntk`. |
|
|
| ## Files |
| - `model_inject_linux.cntk` β self-contained malicious model (stdlib-only, no attacker .py). |
| - `verify.py` β loads it and prints the marker. |
| - `model_evil.cntk` + `evil_udf.py` β the planted-module variant (module named in the model is imported from sys.path). |
| - `build_inject_linux.py` β how the injected model was built. |
| - `Dockerfile` β the exact clean-room image (python:3.6 + cntk 2.7 + OpenMPI 1.10). |
|
|
| ## Reproduce |
| CNTK 2.7 is py3.5/3.6 only. In the clean room I confirmed it on a stock wheel: |
| ``` |
| docker build -t cntk-poc . # python:3.6-slim + pip install cntk==2.7 + OpenMPI 1.10 |
| docker run --rm -v "$PWD":/poc -w /poc cntk-poc python verify.py |
| # marker after : True |
| # uid=0(root) gid=0(root) groups=0(root) |
| # INJECTED_NO_EXTERNAL_FILE |
| ``` |
| Or on any py3.6 box: `pip install cntk==2.7` then `python verify.py`. |
|
|
| ## Why it matters |
| Any service that loads a user-supplied `.cntk` runs attacker code as the loading process. |
| It is the default load path, no flag turns it off, and ModelScan does not scan `.cntk` |
| at all, so the file looks clean. |
|
|