//nefariousplan

CVE-2026-10520: Ivanti Patched The Argument To handleMessage, Not The Call

pattern

cve

proof of concept

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.

Pre-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 <commandexec>\n<index>1</index>\n<reqandres>/bin/cat /sys/devices/virtual/dmi/id/product_name</reqandres>\n</commandexec>. The dispatcher behind the call is unchanged. Every invocation of the endpoint still runs a shell command. Ivanti now picks which one. Pre-patch, the caller picked the shell command. Post-patch, Ivanti picked the shell command. The shell command remains.

The PoC is the endpoint's own request format

watchTowr published the detection artifact on June 9, 2026. The exploit is one POST:

POST /mics/api/v2/sentry/mics-config/handleMessage HTTP/1.1
Content-Type: application/x-www-form-urlencoded

message=execute system /configuration/system/commandexec <commandexec><index>1</index><reqandres>uname -a</reqandres></commandexec>

The response body contains the command's stdout between <result><success> and </success></result>. 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.

handleMessage is a wire-protocol dispatcher

The 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.

ConfigServiceHandler.handleMessage opens with StringTokenizer tokenizer = new StringTokenizer(msg). It peels off four pieces in order. The PoC's body parses as:

command = "execute"
module  = "system"
xpath   = "/configuration/system/commandexec"
value   = "<commandexec><index>1</index><reqandres>uname -a</reqandres></commandexec>"

When 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 <reqandres> element is the command. The method returns the command's stdout, which the dispatcher renders back as <result><success>...</success></result>.

The 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 <xml> 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.

CVE-2026-10523 is the Apache configuration

The 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.

The 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.

This 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.

The 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.

The 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.

The hardcoded patch tells you what the endpoint was for

The 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:

String result = this.configService.handleMessage(
    "execute system /configuration/system/commandexec "
  + "<commandexec>\n"
  + "<index>1</index>\n"
  + "<reqandres>/bin/cat /sys/devices/virtual/dmi/id/product_name</reqandres>\n"
  + "</commandexec>"
);

The 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.

The 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.

This 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.

The 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.

The MICS admin plane is the primitive

The 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.

This is 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.

The 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.

The sibling shape published last week, SolarWinds Serv-U, 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.

What anyone inside the fence still has

The 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."

Any 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.

PoC: 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.

Pre-patch, the caller picked the shell command. Post-patch, Ivanti picked the shell command. The shell command remains.