Skip to content

Ivanti Connect Secure exploited chain: X-DSCGI-PATH dispatch split, write-then-delete execution race, and VPN-edge post-exploitation tradecraft

Event in one line: On 2026-10-13 Ivanti patched and CISA KEV'd CVE-2026-104935 (authentication bypass by spoofing, CWE-290, unauthenticated RCE, KEV due date 2026-10-16), and Volexity published the full technical chain behind it: a front-end/back-end dispatch split where the URL path decides authorization but an X-DSCGI-PATH header decides which CGI actually runs, chained to a write-then-validate file race in wu.cgi (file written to a client-predictable MD5-derived path, executed via the same header trick before the invalid-ZIP check deletes it), chained to a root privilege escalation through the dsinvoked broker trusting DSCache task definitions — unauthenticated root RCE on a fully patched 22.8R2.4 appliance, with a separate unpatched privesc still open.

Why this is durable operator content: every leg is a reusable hunt pattern on appliances and monolithic web stacks (URL-vs-header dispatch differentials, MD5-of-filename predictable write paths, execute-before-delete races, privileged brokers trusting shared config stores), and the post-exploitation inventory is a field manual for what to collect from a compromised VPN concentrator during an authorized red-team engagement.

Primary sources

The chain, leg by leg (as published)

  1. Authentication bypass — dispatch split. Request GET /dana-na/auth/url_default/welcome.cgi (public, auth-free), and add the header X-DSCGI-PATH: /home/webserver/htdocs/dana/fb/smb/wu.cgi (a non-public CGI running in a trusted context). The URL is checked against the auth gate; the header selects the destination handler. Result: any protected CGI on the box is reachable through a public URL by naming it in the header.
  2. RCE — write-then-validate race. wu.cgi's upload handler takes a file (attacker used a Perl script named *.zip), computes MD5 of the client-supplied filename, and writes the raw content to /home/runtime/tmp/<MD5(filename)> before validating that it is a real ZIP. Validation failure deletes the file — but it exists on disk for a window. The attacker fires parallel requests with X-DSCGI-PATH: /home/runtime/tmp/<MD5(filename)> (leg 1's primitive now doubles as an execution router) and wins the race before deletion. Repeat bursts raise hit probability. Payload chain was Perl → Bash → in-memory Base64+zlib Python run via exec(), so the real payload never touched disk.
  3. Privilege escalation — broker trusts shared config store. The privileged root service dsinvoked looks up each maintenance task's program path in DSCache, a shared ICS config store. The in-memory payload patches the cgi-server process's copy of /home/lib/libdsplibs.so (single instruction byte, via /proc/self/mem), uses it to rewrite a DSCache task entry to point at the Perl interpreter, asks dsinvoked to run the task with a malicious script argument → root. Then it restores the DSCache entry and the patched byte, checks /etc/shadow readability as the success oracle, and excises its window from /home/runtime/dlogs/debuglog and /var/log/audit/audit.log. The on-disk library is never modified, so Ivanti's Integrity Checker Tool sees nothing.
  4. Operation shape. No web shell, no backdoor, no separate C2: the entire chain is re-executed per command and results return in the HTTPS response ("in-band" channel). First-stage to full root, repeated at will, disk-clean.

Hunt patterns this generalizes (any appliance, not just ICS)

  • URL-gate vs header-dispatch differential. Fingerprint any stack that accepts X-*PATH, X-Original-URL, X-Rewrite-URL, X-DSCGI-*, or similar routing headers: send the header on a public URL and diff behavior against the same URL alone. The auth decision and the dispatch decision living in different components (front-end vs back-end/CGI broker) is the exact class behind CVE-2023-46805, CVE-2024-21887, and now this. On the target, header acceptance on public endpoints with a content/response mismatch is boundary-crossing evidence — treat protected-path dispatch as authorized-window testing only.
  • Predictable write path from attacker input. MD5(filename-you-supplied) is fully attacker-computable pre-upload. Grep/attack-surface rule: any upload handler whose destination path is derived from a hash or transform of a client-controlled name yields a raceable, addressable write primitive.
  • Write → validate → delete = race window. The security check happens after the write and before (via deletion) any potential execution. Anything that can touch the file in between — and on these boxes the same header-router can execute it — beats the validator. This is the validate-vs-write axis from the wiki's Sept 19 WP upload page with a built-in execution router stapled on.
  • Privileged broker that reads its task table from shared config. If a root service's "which program do I run" answer comes from a store the lower-privilege context can influence (DSCache, cron-like tables, plugin registries, unit files), poisoning one entry converts cgi-server access into root. Check whether task lookups are validated against an on-disk allowlist or just trusted state.
  • In-band request/response C2 defeats beacon-based detection. No persistence, no C2 host, per-command re-exploitation: nothing for endpoint or network beacon detection to see. For detections/red-team dashboards, the signal is the request pattern, not any implant.

Authorized validation boundary (perimeter appliance, KEV-active product)

This product is under active mass exploitation; the auth bypass itself creates real sessions and logs. Boundaries:

  1. Confirm scope and ownership first (scope doc, cert SANs, /dana-na/ branding, inventory). Never probe out-of-scope ICS/Neurons/Pulse nodes.
  2. Version evidence without exploit: ICS admin-disclosure paths and version strings per customer permission; fixed bands are 25.1.3.0+, 22.8R2.5+, 22.7R2.14 and prior. A patched-but-exploitable-before-Oct-13 window matters for engagements — ask the customer for patch date vs engagement dates.
  3. Header-acceptance probe (low-impact): request a public welcome page with and without an X-DSCGI-PATH header naming another public path; log any response/timing/length differential proving header-influenced dispatch exists. Do not point the header at protected CGIs or /home/runtime/tmp/ paths against production — that crosses the boundary and is lab-or-approved-window only.
  4. Lab validation on an owned/downgradeable ICS instance is the only place to prove the race or protected-CGI reachability; proof should be a marker file or id-style output inside the lab, never production command execution.
  5. Evidence to keep: scoped host, source IP, timestamps, request/response artifacts with the header battery, version/patch evidence, and (authorized recon only) whether the box runs dsstatmond-style hidden-directory pcap collectors. Keep credentials, LDAP material, config dumps, and real session data out of every artifact.

VPN-edge post-exploitation inventory (red-team tradecraft from the published activity)

Volexity's reconstruction of UTA0576's actions on the concentrator doubles as a checklist of what a compromised edge VPN device is for:

  • Harvest the appliance's own secrets first: LDAP/domain service-account credentials stored on-box, auth-method config (/home/etc/pulse/, /home/etc/, /home/*.cfg), local auth logs mined for valid usernames.
  • Sniff the plaintext it already sees: the device sits inline with LDAP/HTTP/FTP/telnet traffic; a renamed tcpdump (dsstatmond) writing rotating pcaps under a dot-prefixed hidden dir (/data/.icscap/cap*), parsed offline for LDAP simple binds, HTTP Basic, and NTLM challenge material.
  • Live off the directory with the service account: LDAPv3 queries for LAPS passwords, BitLocker recovery keys, userPassword/unixUserPassword attributes, logon-script paths; results as zlib+Base64 JSON in /tmp/ exfiltrated over the same in-band channel.
  • Kerberos on the edge: custom Kerberos tooling for service-ticket requests (Kerberoast-capable) and offline credential testing straight from the VPN's network position.
  • Spray from inside the perimeter: password-spraying helpers run on the appliance so the traffic looks like the VPN's own.
  • Stay clean: selective log-window excision, self-deleting payload files, restored state, /dev/null everything — assume the integrity checker is blind to memory-only patches.

For defensive side of an engagement scope this maps directly to "what would an attacker get from our concentrator" — the same queries are the audit list.