
This is the first blog post of a three-part series on SolarWinds vulnerabilities.
Armadin recently discovered CVE-2026-28326, an unauthenticated remote code execution (RCE) vulnerability by chaining multiple bugs inside of SolarWinds ARM (Access Rights Manager). Upon discovering the vulnerability, we notified SolarWinds immediately and want to thank them for the cooperative disclosure experience.
> TL;DR:
SolarWinds ARM contained a hard-coded secret in the installer used to authenticate a certless client, which opened another .NET deserialization vulnerability that can be exploited with a gadget that does not fall under class-name restrictions. A successful attack results in arbitrary command execution as the service principal (i.e., Domain-Admin-equivalent that would allow complete internal Active Directory domain post compromise).
Not the CPU architecture! According to SolarWinds: “SolarWinds® Access Rights Manager (ARM) helps IT and security teams provision, deprovision, manage, and audit user access rights across systems, data, and files in the corporate network.” We saw this and naturally thought it might be attractive to malicious actors because it’s an easy one hop away from domain compromise.
ARM retained the server-collector architecture from older versions. We learned from its previous CVEs (CVE-2023-35182 / -40057 and CVE-2024-28991) that it had a history of .NET deserialization flaws. We discovered that SolarWinds has revamped the protocols and those inter-component channels are now wrapped in the gRPC protocol.
One of SolarWinds’ remediations for the 2023 anonymous .NET deserialization RCE was to implement mTLS plus admin-minted certificate enrollment in front of the remoting service. On top of that, a class-name denylist was placed in front of the deserializer to reject known malicious gadgets. The dangerous primitive itself, however, was kept in place: a BinaryServerFormatterSink configured with TypeFilterLevel.Full. That setting tells BinaryFormatter to apply no type restriction of its own, so the only thing between an attacker and full object-graph deserialization is the denylist bolted on in front of it.
We found that the ARM remoting service—implemented in the SolarWinds.ARM.Remoting and `SolarWinds.ARM.Server.Grpc assemblies and hosted by the armServer service— exposes its SolarWinds_ARM_Remoting.Remoting listener as a gRPC/HTTP-2 endpoint on TCP/55555 with TLS enabled. The service exposes two methods: Send and Subscribe. Agent messages target a registered remoting object by URI. The valid objects are:
jobCenter gaffer tracerCenter tracerServer ServerDebugger GenericGafferCredentialServiceA valid agent must name one of the objects above in its gRPC request, e.g., grpc://host:55555/jobCenter. Otherwise, the dispatcher adds “Requested Service not found” before it ever deserializes the supplied objects.
Using this knowledge, we looked at how this service handles authentication.
First, we identified that a client does not need to present a valid certificate to complete the TLS handshake. The listener is configured with a client-certificate mode equivalent to request-but-don’t-verify: the server asks for a certificate, but the handshake still succeeds when the client offers none. How? The ARM server decides to use only TLS here to provide channel encryption, not authentication. That behavior was bizarre enough for us to keep digging, and we then found the actual application layer authentication gate, which decides whether to trust the caller:
verify = verifyLocalRequests && verifyClientCertificate && verifyWebSessionHere the code checks three conditions, and we found that the verifyClientCertificate is where it all falls apart. If you happen to have the app, look closely. It has a certless branch to determine if the caller presents no client certificate that it will authenticate a client solely based on two headers (timestamp, x-remoteclient-request) the server returns true if the conditions match and the client is doing gRPC remote requests.
Here is a breakdown of the validation mechanisms:
url = "https://%s:%d/" % (host, port)
ts = datetime.datetime.utcnow().strftime("%Y%m%d%H%M%S") # ;2 min window
mac = base64.b64encode(hmac.new(SECRET.encode(), (url+"|"+ts).encode(),
hashlib.sha256).digest()).decode()
md = [("url", url), ("timestamp", ts), ("x-remoteclient-request", mac)]Normally, this would be fine if the x-remoteclient-request header is calculated with a post-quantum secure secret. A certless client would still have to possess the secret to authenticate to the server. However, upon further research, we found secret-like constants inside the installer shipped under etc/*.xml configuration files. We then traced those values and confirmed they are the ones consumed by the ARM server’s authentication verifier.
We also confirmed that no code regenerates these secrets during or after installation: no MSI custom action and no setup script. At that point we were confident the value was a hard-coded constant shipped and used by this build of the ARM server, not a per-install secret. A value baked into an installer that anyone can download is, by definition, public.
After being able to authenticate to the gRPC endpoint and bypass the client-authentication requirement, we reached the service dispatcher with serialized .NET objects.
Before touching anything dangerous, we wanted to be certain the bypass was real and not an artifact of a misread. The remoting surface gave us a clean, non-destructive way to test it. Alongside Send/Subscribe, the service exposes a read-only method that simply returns the current connection table. Notably, it is the one method on this surface that carries a per-caller Authorize attribute. However, both Send and Subscribe do not.
That let us run a simple A/B test that never sends a gadget:
Control: Call the read-only method with no forged headers → the interceptor rejects the call (StatusCode = Unavailable).Forged: Call it with the forged url / timestamp / x-remoteclient-request headers → the call succeeds and returns the connection table.That confirmed the missing authorization problem in passing: a caller that has just proven it is unauthenticated is nonetheless allowed to invoke methods that carry no authorization check.
With authentication bypassed and no per-caller authorization on Send/Subscribe, a crafted message can reach the deserialization sink: the BinaryServerFormatter sink running at TypeFilterLevel.Full (i.e., BinaryFormatter with no type restriction of its own).
Reaching the sink is only half the story. Every off-the-shelf payload we sent was rejected by ARM’s own mitigation, which requires the first NRBF record to be a MethodCall or MethodReturn, and runs a 27-regex class-name denylist over the record’s class names covering TypeConfuseDelegate, ObjectDataProvider, WindowsIdentity, PSObject, ActivitySurrogate, System.Reflection.*, and the rest of the usual ysoserial roster. The graphic below summarizes the process:

Well…it’s pretty trivial with some LLM help. We found exactly such a gadget that bypasses all constraints SolarWinds has put in place. The takeaway for defenders and vendors is not the gadget—the point is that a denylist was the wrong control here.
One thing to note is that the successful exploit signal is a crash rather than something verbose that we normally see. The reason is because the gadget does its work while the object graph is still being deserialized. Moments later, the runtime tries to finish materializing the result into the type the method signature expected, and that conversion fails. A successful exploit therefore comes back as a TargetInvocationException / InvalidCastException originating from the deserialization path.
Put together, the chain is short: a shipped, hard-coded secret defeats the mTLS/enrollment gate. The absence of per-caller authorization lets the request reach the sink, and a gadget denylist fires inside BinaryFormatter (which Microsoft has officially deprecated). The result is arbitrary command execution on the ARM server as the armServer service principal—a Domain-Admin-equivalent account from a single unauthenticated TCP connection to port 55555.
SolarWinds described the vulnerability as affecting ALL versions of ARM. Code execution as the ARM service principal is not a foothold on one host. It usually has authority over the directory ARM was installed to manage. That account can read and rewrite ACLs across Active Directory, file systems, and Exchange. It can also provision accounts. Unauthenticated RCE in this process is, in practical terms, domain compromise.
Network detection: Alert on inbound gRPC/HTTP-2 connections to TCP 55555 on the ARM host from any source that is not a known collector or console. Also alert on an x-remoteclient-request metadata header appearing on a Send call from an unexpected source, or a burst of Send calls with no preceding console session. Both are signs the forged-header authentication bypass is in use rather than the normal collector workflow.
Payload shape detection: Exact payload strings are withheld here to avoid enabling mass exploitation while ARM is unpatched, but if you have visibility into decrypted 55555 traffic, hunt for an NRBF MethodCall whose argument carries an unusually large embedded string blob rather than the small, typed arguments a legitimate call uses. That shape is a stronger signal than any single class name, since a working gadget here is, by design, one that the class-name denylist doesn’t recognize.
Host and process detection: Add an EDR rule flagging cmd.exe, powershell.exe, or any other shell/interpreter spawned as a child of the ARM service process (armServer). That process should never spawn a shell, so this catches the post-deserialization execution step directly, independent of how the attacker got there.
Application log detection: Forward ARM’s application logs to your SIEM and alert on TargetInvocationException / InvalidCastException occurring on the deserialization path. This is the post-exec fault signature described above. By the time it’s logged, the command has already run, so treat it as a confirmed compromise indicator rather than an early warning and pair it with the network and process detections above to catch the attack earlier in the chain.
Thank you for reading this blog post! If you find any mistakes, please reach out to us or find me on LinkedIn, and I will adjust accordingly. This is the first blog post in a three-part series. Part 2 focuses on unauthenticated SYSTEM RCE via the SolarWinds WPM Player Self Update. Part 3 covers unauthenticated SYSTEM RCE on the SolarWinds Orion Core.
Thank you to Brett Hawkins, Craig Wright, Tyler Casey, and the rest of the Armadin team’s support for making this blog post series possible.
AI-driven threats require a new approach and the right platform to keep your environment secure. Discover the value of a safe Hyperattack™ from Armadin. Request a demo.
Learn more about Hyperattacks.