Back to Intelligence

ZDI-26-850: Microsoft Qlib MLflowRecorder Deserialization RCE — Detection and Remediation Guide

SA
Security Arsenal Team
November 2, 2026
11 min read

The Zero Day Initiative has published ZDI-26-850, disclosing a deserialization of untrusted data vulnerability in Microsoft Qlib — Microsoft's open-source, AI-oriented quantitative investment platform. The flaw resides in the MLflowRecorder component and carries a CVSS score of 7.8 (High). Exploitation allows a remote attacker to execute arbitrary code on an affected installation. The catch that tempers the score — but does not eliminate the risk — is the user interaction requirement: the target must open a malicious file or visit a malicious page for the payload to fire.

If your organization runs quantitative research, algorithmic trading pipelines, or any ML experimentation stack built on Qlib, this deserves immediate attention. Data science workstations and research servers are frequently under-managed relative to production infrastructure — they run with broad network access, hold API keys to data providers and brokerages, and are rarely covered by EDR policy parity. A deserialization bug in a component whose entire job is loading experiment artifacts is precisely the kind of foothold an adversary wants in a research environment.

This post breaks down the vulnerability from a defender's perspective, provides hunt logic for the most probable post-exploitation behaviors, and walks through remediation and hardening steps you can execute today.

Technical Analysis

Affected Product and Component

AttributeDetail
ProductMicrosoft Qlib (open-source Python ML/quant platform)
ComponentMLflowRecorder (experiment tracking recorder backed by MLflow)
AdvisoryZDI-26-850
CVSS7.8 (High)
Weakness classCWE-502 — Deserialization of Untrusted Data
Attack vectorLocal file/page interaction (user must open malicious content)
AuthenticationNone required for the delivered payload itself
ImpactArbitrary code execution in the context of the user/process running Qlib

Specific affected version ranges will be enumerated in the ZDI advisory and any subsequent Microsoft/PyPI release notes — verify the exact fixed version at http://www.zerodayinitiative.com/advisories/ZDI-26-850/ and the Qlib GitHub releases page before declaring remediation complete.

How the Vulnerability Works

Python deserialization vulnerabilities of this class almost universally trace back to unsafe object reconstruction — typically pickle (or a wrapper such as joblib, cloudpickle, or MLflow's own artifact loaders) being invoked on data the application did not produce and did not validate.

The MLflowRecorder's purpose is to log and later load experiment artifacts: trained models, metrics snapshots, parameters, and serialized objects. The defensive-relevant attack chain looks like this:

  1. Delivery. The attacker gets a malicious serialized artifact in front of the victim — a booby-trapped experiment file, a poisoned shared artifact store, a malicious "research notebook bundle" downloaded from a page, or a file swapped into a shared MLflow tracking directory.
  2. Trigger. The victim opens or loads the artifact through a Qlib workflow that routes through MLflowRecorder — loading a prior experiment, resuming a recorder session, or importing results.
  3. Deserialization. The vulnerable code path reconstructs the attacker-controlled object. In Python, a crafted pickle payload abuses __reduce__ to invoke os.system, subprocess.Popen, or equivalent during unpickling — code executes at load time, before any "model" is even used.
  4. Execution context. The payload runs with the privileges of the Python process running Qlib — which on research boxes is frequently a local admin, a service account with data-lake credentials, or a container with mounted secrets.

From a SOC perspective, the single most reliable observable is this: a Python interpreter (often with qlib or mlflow in its command line or module load path) spawning an operating system shell or an unexpected child process. Legitimate quant workflows almost never need python.exe to spawn cmd.exe, powershell.exe, or /bin/sh.

Exploitation Status

At the time of writing, ZDI-26-850 is a published advisory with no confirmed in-the-wild exploitation and no CISA KEV listing. However, treat the barrier to weaponization as low: Python deserialization is a thoroughly documented technique, proof-of-concept tooling for malicious pickle generation is publicly available, and the open-source nature of Qlib means attackers can diff the vulnerable code path directly. The window between advisory publication and opportunistic exploitation of data-science tooling is typically short — these environments are high-value and low-friction targets.

Detection & Response

The detection strategy here focuses on the post-deserialization behavior, because the malicious artifact itself can take unlimited forms. Behavior survives payload mutation; file signatures do not.

Sigma Rules

YAML
---
title: Python Interpreter Spawning Shell — Possible Deserialization Exploitation
id: 8c2e4f61-3b7a-4d29-9e15-6f8a2c4d5e7f
status: experimental
description: Detects python processes spawning command shells or script interpreters, consistent with code execution triggered by unsafe deserialization (e.g., pickle __reduce__ payloads) in Python applications such as Microsoft Qlib / MLflow workflows.
references:
  - http://www.zerodayinitiative.com/advisories/ZDI-26-850/
  - https://attack.mitre.org/techniques/T1059/
author: Security Arsenal
date: 2026/04/06
tags:
  - attack.execution
  - attack.t1059.006
logsource:
  category: process_creation
  product: windows
detection:
  selection_parent:
    ParentImage|endswith:
      - '\python.exe'
      - '\python3.exe'
      - '\pythonw.exe'
      - '\conda.exe'
  selection_child:
    Image|endswith:
      - '\cmd.exe'
      - '\powershell.exe'
      - '\pwsh.exe'
      - '\wscript.exe'
      - '\cscript.exe'
      - '\mshta.exe'
      - '\rundll32.exe'
      - '\certutil.exe'
      - '\bitsadmin.exe'
  condition: selection_parent and selection_child
falsepositives:
  - Build systems, test harnesses, and installer scripts that legitimately invoke shells from Python
  - Some Jupyter kernels executing cell magic with shell escape sequences
level: high
---
title: Qlib or MLflow Process Establishing Unexpected Outbound Connection
id: 2f9d1a47-8c5e-4b36-a791-3d6e9f0b1a2c
status: experimental
description: Detects python processes referencing qlib or mlflow initiating outbound network connections to non-standard ports, which may indicate command-and-control or data staging following deserialization-based code execution.
references:
  - http://www.zerodayinitiative.com/advisories/ZDI-26-850/
  - https://attack.mitre.org/techniques/T1071/
author: Security Arsenal
date: 2026/04/06
tags:
  - attack.command_and_control
  - attack.t1071.001
logsource:
  category: network_connection
  product: windows
detection:
  selection_img:
    Image|endswith:
      - '\python.exe'
      - '\python3.exe'
      - '\pythonw.exe'
  selection_cli:
    CommandLine|contains:
      - 'qlib'
      - 'mlflow'
  filter_ports:
    DestinationPort:
      - 80
      - 443
      - 5000
      - 8080
  condition: selection_img and selection_cli and not filter_ports
falsepositives:
  - MLflow tracking servers legitimately hosted on custom ports
  - Internal data provider APIs on non-standard ports
level: medium
---
title: Shell Spawned by Python on Linux Research Hosts
id: 5b8c3e92-1f4d-4a68-b237-9c0d7e5f2a18
status: experimental
description: Detects /bin/sh, /bin/bash, or download utilities spawned as children of python interpreters on Linux, consistent with pickle deserialization payloads executing system commands.
references:
  - http://www.zerodayinitiative.com/advisories/ZDI-26-850/
  - https://attack.mitre.org/techniques/T1059.004/
author: Security Arsenal
date: 2026/04/06
tags:
  - attack.execution
  - attack.t1059.004
logsource:
  category: process_creation
  product: linux
detection:
  selection_parent:
    ParentImage|endswith:
      - '/python'
      - '/python3'
      - '/python3.10'
      - '/python3.11'
      - '/python3.12'
  selection_child:
    Image|endswith:
      - '/sh'
      - '/bash'
      - '/dash'
      - '/curl'
      - '/wget'
      - '/nc'
      - '/ncat'
      - '/base64'
  condition: selection_parent and selection_child
falsepositives:
  - Legitimate subprocess calls in automation, CI/CD, and data pipeline tooling
  - Package managers or setup scripts invoking shell from Python
level: high

KQL (Microsoft Sentinel / Defender)

This query hunts for the core behavioral indicator — Python interpreters with Qlib/MLflow context spawning shells or LOLBins — and enriches with network activity from the same devices to surface possible C2 follow-on behavior.

KQL — Microsoft Sentinel / Defender
// Hunt: Python (qlib/mlflow context) spawning shells — possible ZDI-26-850 deserialization exploitation
let Lookback = 7d;
let SuspiciousChildren = dynamic(["cmd.exe","powershell.exe","pwsh.exe","mshta.exe","wscript.exe","cscript.exe","rundll32.exe","certutil.exe","sh","bash","curl","wget","nc"]);
let PyShell =
    DeviceProcessEvents
    | where TimeGenerated > ago(Lookback)
    | where InitiatingProcessFileName in~ ("python.exe","python3.exe","pythonw.exe","python","python3")
       or InitiatingProcessCommandLine has_any ("qlib","mlflow")
    | where FileName in~ (SuspiciousChildren)
    | project
        DeviceName, AccountName,
        ParentProcess = InitiatingProcessFileName,
        ParentCmd = InitiatingProcessCommandLine,
        ChildProcess = FileName,
        ChildCmd = ProcessCommandLine,
        TimeGenerated;
PyShell
| join kind=leftouter (
    DeviceNetworkEvents
    | where TimeGenerated > ago(Lookback)
    | where InitiatingProcessFileName in~ ("python.exe","python3.exe","pythonw.exe","python","python3")
    | where RemotePort !in (80, 443, 5000, 8080)
    | summarize NetHits = count(), RemoteIPs = make_set(RemoteIP, 10) by DeviceName, TimeGenerated
) on DeviceName
| project TimeGenerated, DeviceName, AccountName, ParentProcess, ParentCmd, ChildProcess, ChildCmd, NetHits, RemoteIPs
| order by TimeGenerated desc;

If you ingest Linux research hosts via Syslog/CEF rather than MDE, pivot the same logic against Syslog and CommonSecurityLog — filter on ProcessName matching python and CommandLine containing qlib or mlflow, then correlate child process events on the same host within a tight window.

Velociraptor VQL

For live hunting across research workstations and ML servers, this artifact enumerates running Python processes with Qlib/MLflow context and pairs them with their current network connections — useful both for triage of a suspected compromise and for scoping which hosts even run the vulnerable software.

VQL — Velociraptor
-- Hunt: Python processes with qlib/mlflow context and their child processes / network connections
-- Relevant to ZDI-26-850 (Microsoft Qlib MLflowRecorder deserialization RCE)
LET procs = SELECT Pid, Ppid, Name, CommandLine, Exe, Username, CreateTime
FROM pslist()
WHERE CommandLine =~ '(?i)qlib|mlflow'
   OR Name =~ '(?i)python'

LET kids = SELECT Pid, Ppid, Name, CommandLine, Username
FROM pslist()
WHERE Name =~ '(?i)cmd|powershell|pwsh|mshta|rundll32|certutil|sh$|bash|curl|wget|nc$'

SELECT procs.Pid AS PythonPid,
       procs.Name AS PythonProcess,
       procs.CommandLine AS PythonCmdLine,
       procs.Username AS RunAs,
       kids.Pid AS ChildPid,
       kids.Name AS ChildProcess,
       kids.CommandLine AS ChildCmdLine
FROM procs
LEFT JOIN kids ON kids.Ppid = procs.Pid
VQL — Velociraptor
-- Scoping artifact: which endpoints have qlib/mlflow installed (pip metadata on disk)
SELECT FullPath, Size, Mtime
FROM glob(globs=[
  'C:/Users/*/AppData/Local/Programs/Python/*/Lib/site-packages/qlib*/PKG-INFO',
  'C:/Users/*/AppData/Local/Programs/Python/*/Lib/site-packages/mlflow*/METADATA',
  'C:/ProgramData/anaconda3/Lib/site-packages/qlib*/PKG-INFO',
  'C:/ProgramData/anaconda3/Lib/site-packages/mlflow*/METADATA',
  '/usr/lib/python3*/site-packages/qlib*/PKG-INFO',
  '/usr/local/lib/python3*/dist-packages/qlib*/PKG-INFO',
  '/home/*/.local/lib/python3*/site-packages/qlib*/PKG-INFO',
  '/opt/*/lib/python3*/site-packages/qlib*/PKG-INFO'
])

Remediation and Inventory Script

Because Qlib is a Python package deployed across diverse environments (conda envs, venvs, containers), the first remediation challenge is simply finding every install. This script inventories Qlib/MLflow installations on Linux research hosts, reports versions, and upgrades to the latest available release — run it via your configuration management tooling and adapt the paths for your environment. Always confirm the fixed version against the ZDI advisory and Qlib release notes before relying on version checks alone.

Bash / Shell
#!/bin/bash
# ZDI-26-850 — Microsoft Qlib MLflowRecorder deserialization remediation helper
# Run as root or via sudo on Linux research/ML hosts. Review before executing in production.

set -u
REPORT="/var/log/qlib_zdi_26_850_remediation_$(date +%Y%m%d_%H%M%S).log"
echo "=== ZDI-26-850 Qlib inventory & remediation — $(hostname) ===" | tee "$REPORT"

# 1. Locate every python/pip on the system (covers system python, conda, venvs)
find / -maxdepth 6 -type f \( -name "pip3" -o -name "pip" \) 2>/dev/null | while read -r PIPBIN; do
    QLIB_VER=$("$PIPBIN" show pyqlib 2>/dev/null | awk '/^Version:/{print $2}')
    MLFLOW_VER=$("$PIPBIN" show mlflow 2>/dev/null | awk '/^Version:/{print $2}')
    if [ -n "$QLIB_VER" ]; then
        echo "[FOUND] $PIPBIN -> pyqlib $QLIB_VER | mlflow ${MLFLOW_VER:-not-installed}" | tee -a "$REPORT"
        # 2. Upgrade to latest available release (verify fixed version vs. ZDI advisory first)
        "$PIPBIN" install --upgrade pyqlib mlflow 2>&1 | tee -a "$REPORT"
        NEW_VER=$("$PIPBIN" show pyqlib 2>/dev/null | awk '/^Version:/{print $2}')
        echo "[POST-UPGRADE] $PIPBIN -> pyqlib $NEW_VER" | tee -a "$REPORT"
    fi
done

# 3. Flag suspicious serialized artifacts in MLflow artifact stores for manual review
#    Pickle payloads execute on load — unknown .pkl/.pickle files in shared dirs are high-risk
echo "--- Serialized artifacts in common MLflow locations (review for untrusted origin) ---" | tee -a "$REPORT"
find / -maxdepth 8 -type f \( -name "*.pkl" -o -name "*.pickle" \) \
    \( -path "*/mlruns/*" -o -path "*/mlflow/*" -o -path "*/qlib/*" \) \
    -mtime -30 2>/dev/null | tee -a "$REPORT"

# 4. Quick check: any python process currently running with qlib/mlflow loaded
echo "--- Active python processes referencing qlib/mlflow ---" | tee -a "$REPORT"
ps auxww | grep -Ei 'python.*(qlib|mlflow)' | grep -v grep | tee -a "$REPORT"

echo "=== Done. Review $REPORT and cross-check versions against http://www.zerodayinitiative.com/advisories/ZDI-26-850/ ===" | tee -a "$REPORT"

Remediation

  1. Confirm the fixed version and patch immediately. Check the ZDI advisory (http://www.zerodayinitiative.com/advisories/ZDI-26-850/) and the Qlib GitHub repository/PyPI release history for the release that resolves the MLflowRecorder deserialization flaw. Upgrade every installation with pip install --upgrade pyqlib mlflow — and remember that Qlib is frequently installed inside conda environments, venvs, Jupyter kernels, and Docker images, all of which must be patched individually. Rebuild and redeploy container images; do not assume base images are current.

  2. Treat all serialized artifacts as untrusted input. Until patched — and as a permanent posture change — do not load experiment artifacts, model files, or recorder sessions from external, shared, or downloaded sources without provenance verification. Enforce this in procedure, not just policy: a deserialization flaw means the act of opening the file is the exploit.

  3. Isolate MLflow tracking servers and artifact stores. Restrict write access to shared MLflow tracking directories and artifact backends (S3 buckets, NFS shares, local mlruns/ paths) to authenticated CI/CD and known researchers. An attacker who can drop a file into your artifact store has a delivery mechanism that bypasses email and web filtering entirely.

  4. Constrain the blast radius. Run Qlib workloads under dedicated, least-privilege accounts — never as local admin or root. Remove cloud credentials, brokerage API keys, and data-lake tokens from interactive research shells where possible; use short-lived, scoped credentials. Segment research VLANs from production trading and corporate networks, and egress-filter ML hosts so Python processes can only reach approved endpoints (PyPI mirror, MLflow server, data providers).

  5. Harden the runtime. Where workflows permit, prefer safe serialization formats (safetensors, ONNX, SavedModel) over pickle for model exchange, and evaluate controls such as restricted unpicklers or artifact-signing pipelines for internally produced models. Deploy the Sigma and KQL detections above to your research fleet now — most SOCs have materially weaker telemetry coverage on data-science hosts than on general endpoints, and this advisory is a concrete reason to close that gap.

  6. Monitor for follow-on exploitation. ZDI advisories frequently precede public PoCs for Python deserialization bugs. Subscribe to the Qlib GitHub security feed and ZDI advisory updates; if a CVE identifier is subsequently assigned, re-run scoping with that identifier for asset management traceability.

Related Resources

Security Arsenal Penetration Testing Services AlertMonitor Platform Book a SOC Assessment vulnerability-management Intel Hub

Is your security operations ready?

Get a free SOC assessment or see how AlertMonitor cuts through alert noise with automated triage.