-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256
NEFARIOUSPLAN-CANONICAL-V1
{"body_md":"## The PoC is the endpoint's own request format\n\nwatchTowr published the detection artifact on June 9, 2026. The exploit is one POST:\n\n```http\nPOST /mics/api/v2/sentry/mics-config/handleMessage HTTP/1.1\nContent-Type: application/x-www-form-urlencoded\n\nmessage=execute system /configuration/system/commandexec 1uname -a\n```\n\nThe response body contains the command's stdout between `` and ``. watchTowr named the file `watchTowr-vs-Ivanti-Sentry-RCE-CVE-2026-10520-CVE-2026-10523.py` and called it a \"Detection Artifact Generator.\" Both halves of that name describe the same request. The same body that confirms the box is vulnerable is the body that runs the command. There is no detection-only variant. The probe and the exploit are the same HTTP transaction.\n\n## handleMessage is a wire-protocol dispatcher\n\nThe endpoint lives in `mics-core-10.5.1-R10.5.1.jar`, the artifact Ivanti replaces in R10.5.2. The Java class is `com.mi.middleware.rest.controller.ConfigServiceController`. Inbound POSTs to `mics-config/handleMessage` arrive there. The controller reads the `message` form field, passes it as a single string to `ConfigServiceHandler.handleMessage(String msg)`, and returns the handler's `String result` in the response body.\n\n`ConfigServiceHandler.handleMessage` opens with `StringTokenizer tokenizer = new StringTokenizer(msg)`. It peels off four pieces in order. The PoC's body parses as:\n\n```\ncommand = \"execute\"\nmodule = \"system\"\nxpath = \"/configuration/system/commandexec\"\nvalue = \"1uname -a\"\n```\n\nWhen `command` equals `\"execute\"`, the handler routes to `ConfigRequestProcessor.handleExecute(xpath, value)`. `handleExecute` looks up the module class implied by the `xpath` prefix, resolves the method named by the leaf segment (`commandexec`), parses `value` as XML into the method's DTO, and invokes the method through `ReflectionUtilities.excuteModuleMethod(...)`. The `commandexec` module's method body is `CommonUtilities.executeNativeCommand`. The XML's `` element is the command. The method returns the command's stdout, which the dispatcher renders back as `...`.\n\nThe format is not a programming oversight or an undocumented internal API hardened by obscurity. It is the wire protocol MICS nodes use to invoke admin actions on each other. `execute system /xpath ` encodes \"run this admin module with these arguments.\" `commandexec` is one module in the catalog. There are others. The HTTP `handleMessage` endpoint exposes the protocol's entrypoint to anything that can reach the port.\n\n## CVE-2026-10523 is the Apache configuration\n\nThe companion CVE is the authentication bypass. It is not in the JAR. It is in the bundled Apache HTTPD configuration. R10.5.1's Apache config lets unauthenticated POSTs reach `/mics/api/v2/sentry/mics-config/handleMessage`. R10.5.2's Apache config adds regex rules that match the path and 302 unauthenticated callers to the login page. That is the entire fix for the auth side of this chain.\n\nThe Java servlet behind the route does not check whether the caller is authenticated. There is no `@PreAuthorize`, no read of `SecurityContextHolder.getContext().getAuthentication()`, no session lookup. The Apache filter is the fence. The JAR has no opinion about who is allowed to call `handleMessage`. Anything that reaches the servlet runs the dispatcher.\n\nThis is the second time Ivanti has shipped this fix in this product. CVE-2023-38035, disclosed August 2023 by Horizon3.ai's James Horseman and Zach Hanley, was exploited as a zero-day against, in Ivanti's wording, \"a limited number of customers\" before the patch landed. Ivanti's 2023 advisory described the root cause as \"an insufficiently restrictive Apache HTTPD configuration on the MICS Admin Portal.\" The 2023 fix was an Apache configuration change that blocked unauthenticated access to `/services/` on TCP/8443. CISA added it to the Known Exploited Vulnerabilities catalog the same week.\n\nThe 2023 endpoint paths and the 2026 endpoint path are different paths. Closing the 2023 paths did not close the 2026 path because there is no enumeration of MICS-reachable URLs at any layer below Apache. The 2023 fix was the specific regex pattern that covered `/services/`. The 2026 fix is the specific regex pattern that covers `mics-config/handleMessage`. There is no Java-side allowlist of admin endpoints; the application's view of \"what is authenticated\" is delegated to whatever regex set Apache currently ships.\n\nThe credited researcher in 2023 was Horizon3.ai. The credited researcher in 2026 is Sonny at watchTowr. Three years apart, two research teams, two endpoint paths, one fence layer.\n\n## The hardcoded patch tells you what the endpoint was for\n\nThe patched `ConfigServiceController.handleMessage` ignores its `message` parameter. The call to `this.configService.handleMessage(...)` is preserved verbatim; only the argument is replaced. Ivanti's hardcoded argument is:\n\n```java\nString result = this.configService.handleMessage(\n \"execute system /configuration/system/commandexec \"\n + \"\\n\"\n + \"1\\n\"\n + \"/bin/cat /sys/devices/virtual/dmi/id/product_name\\n\"\n + \"\"\n);\n```\n\nThe string is a valid MICS wire-protocol message. It tokenizes the same way the attacker's body did. It routes through the same `ConfigServiceHandler.handleMessage`, the same `ConfigRequestProcessor.handleExecute`, the same reflection chain, the same `CommonUtilities.executeNativeCommand`. The shell it ultimately invokes runs `/bin/cat /sys/devices/virtual/dmi/id/product_name`. That file is a kernel-exported string naming the hardware platform. On a Sentry appliance image it returns the model identifier; on a virtual deployment it returns the hypervisor's product string.\n\nThe endpoint exists, on a patched Sentry, to answer that question for some caller. Something inside Ivanti's stack, an internal probe, a paired component, a phone-home, calls `handleMessage` to identify the box, and the call's path through the wire-protocol dispatcher is how the identification arrives. The patch could have returned 410 Gone from the route. The patch could have replaced the controller with a method that read the same kernel file directly and returned its contents. Neither happened. The hardcoded string keeps the dispatcher's path live and pinned to a fixed argument.\n\nThis is not the only way to read a kernel-exported file. A purpose-built handler that returned the DMI string would be a five-line method, no shell, no fork, no tokenizer, no XML. The endpoint runs `/bin/cat` instead because the dispatcher is what was already wired up. The MICS protocol can express \"run a shell command and return its output,\" and the easiest way to extract a one-line file is to call `cat`. The patched endpoint runs a shell command on every call because shell exec is what the dispatcher does, and removing the call would mean removing the answer the internal caller is waiting on.\n\nThe hardcoded string is the patch's confession. The vulnerable handler ran whatever the caller asked. The patched handler runs what Ivanti chose. Both versions of the handler call `CommonUtilities.executeNativeCommand`. Both versions return the shell's stdout to the HTTP caller. The thing that changed is who picked the argument.\n\n## The MICS admin plane is the primitive\n\nThe MICS wire protocol's vocabulary includes `execute system commandexec`. The dispatcher exists because the protocol exists. The shell exec exists because the `commandexec` module exists. The Apache fence exists because the dispatcher cannot be removed without breaking whatever internal caller depends on it.\n\nThis is [unpatchable-primitive](/patterns/unpatchable-primitive). The bug class the Sentry MICS admin plane keeps producing is \"unauthenticated reach into a wire-protocol dispatcher that can run native commands as the JAR's uid.\" Both Sentry CVEs in the public record match it. The 2023 fix narrowed one path. The 2026 fix narrows another. The dispatcher is unchanged in both cases. The architectural property that lets a reachable caller drive shell exec is the same architectural decision in both years: the management plane's IPC vocabulary is the HTTP surface, and the only gate is whichever URL pattern Apache currently denies.\n\nThe 2023 patch made the Apache configuration the single place \"what is MICS-reachable\" is enumerated. The 2026 patch does not change the enumeration; it adds two more entries to it. The patch cadence is not trending down. The credited researchers are different people with different toolchains. The architectural property is stable. Ivanti's fix discipline is to find the URL that was reachable and add it to the Apache deny list.\n\nThe sibling shape published last week, [SolarWinds Serv-U](/posts/solarwinds-servu-cve-2026-28318-patched-the-path-not-the-decoder), named the decoder that remained while the dispatcher and the proxy were changed. Sentry's dispatcher remains while the proxy filter has been changed twice. The decoder Serv-U could not remove was a deflate decompressor RFC 9110 permits and almost nothing legitimately uses. The dispatcher Ivanti will not remove is the protocol two Ivanti services use to talk to each other through an HTTP listener that sits on the public internet.\n\n## What anyone inside the fence still has\n\nThe Apache filter denies unauthenticated callers. It does not deny authenticated callers. An authenticated session of any role that the application admits, the operator login, a lower-privilege account on a multi-tenant deployment, an account whose credentials leaked, can POST the same body the watchTowr PoC posts and reach the same `CommonUtilities.executeNativeCommand` call. The JAR servlet's authorization check is still nothing. The Apache filter's grain is \"authenticated yes/no.\" The dispatcher's grain is \"execute the module the message names.\"\n\nAny future bug that bypasses the Apache filter, a routing differential between Apache and the Tomcat behind it, a URL-encoding the regex did not anticipate, an internal alias the deny list did not enumerate, returns the dispatcher to the public. The MICS protocol is the part that does not move.\n\nPoC: [watchtowrlabs/watchTowr-vs-Ivanti-Sentry-RCE-CVE-2026-10520-CVE-2026-10523](https://github.com/watchtowrlabs/watchTowr-vs-Ivanti-Sentry-RCE-CVE-2026-10520-CVE-2026-10523). watchTowr writeup: [More Evidence That Words Don't Mean What We Thought They Meant](https://labs.watchtowr.com/more-evidence-that-words-dont-mean-what-we-thought-they-meant-ivanti-sentry-pre-auth-os-command-injection-cve-2026-10520/).","closing_line":"Pre-patch, the caller picked the shell command. Post-patch, Ivanti picked the shell command. The shell command remains.","hook_md":"Ivanti Sentry's patch for CVE-2026-10520 makes one change to `ConfigServiceController.handleMessage`. The call to `this.configService.handleMessage(...)` is still there. The return value is still assigned to the same `String result`. The patch keeps the call. It changes the argument.\n\nPre-patch, the argument was the `message` field the HTTP caller posted. Post-patch, it is a string Ivanti hardcoded into the controller: `execute system /configuration/system/commandexec \\n1\\n/bin/cat /sys/devices/virtual/dmi/id/product_name\\n`. The dispatcher behind the call is unchanged. Every invocation of the endpoint still runs a shell command. Ivanti now picks which one.\nPre-patch, the caller picked the shell command. Post-patch, Ivanti picked the shell command. The shell command remains.","post_id":598,"slug":"ivanti-sentry-cve-2026-10520-patched-the-argument-not-the-call","title":"CVE-2026-10520: Ivanti Patched The Argument To handleMessage, Not The Call","type":"initial","unreadable_sentence":"The endpoint was always running a shell command. The patch decided which one."}
-----BEGIN PGP SIGNATURE-----
iHUEARYIAB0WIQRf0htP5+SjynlxywneZjl4jgkQJgUCaijR9gAKCRDeZjl4jgkQ
JiQ5APwKlWWQ+7GurxHq+7NknkCmshfbA5arYKlql/E9mpDdZQEA5j5j2kn4554A
2XlgtG9DjW/UOT6O4UJcmtc+OLUq9gI=
=ntM8
-----END PGP SIGNATURE-----