
Cryptography fails quietly. There is no warning, because the application keeps serving traffic exactly as it did before. The control that was supposed to be holding the line simply isn’t.
The application in this particular engagement was a commercial ASP.NET document management platform deployed in a customer environment. It made three cryptographic decisions:
Together, these decisions produced an unauthenticated path from the open internet to code execution on the web server.
The application routed document downloads through a file-retrieval handler that accepted an opaque, encoded file path parameter. An administrator-facing configuration menu described this behavior as query string encryption, with the stated goal of keeping server-side file paths out of the browser. The mechanism behind that toggle was not encryption, and the Armadin team deduced the encoding scheme through a separate endpoint in the application that returned encoded strings for plaintext paths supplied by the caller.
By providing multiple different inputs to this encoding oracle and observing the output, we determined that the custom encoding scheme Base64-encoded the input, transposed the characters in groups of four, and rotated the resulting string by half its length. The transformation was deterministic and used no key, so reversing it required no secret material. We implemented the inverse in a script that decoded live parameters back to absolute server paths and encoded arbitrary paths of our choosing into values the application would accept.
The difference between obfuscation and encryption is whether reversing it requires a key the attacker doesn’t have. The configuration menu promised the second and shipped the first.
With a working encoder, we were able to address the file-retrieval handler directly, and the handler did not require authentication to serve what it was asked for. A path-disclosure weakness had become a local file read (LFR) vulnerability, exposing application source code and secrets, and it took no credentials.
We used this LFR vulnerability to download a utility DLL from the application and load it into a public .NET decompiler. The decompiled source contained a static TripleDES key and initialization vector, which were written directly into a utility class and compiled into the shipped assembly. These keys were not generated per installation, and they were not stored in a protected configuration section. The same values shipped in every deployment of that software. Because of the LFR vulnerability, they were recoverable by anyone on the internet.
The application had replaced ASP.NET’s standard ViewState feature with its own custom implementation. The homemade ViewState implementation encrypted the ViewState data with that hardcoded key rather than relying on the platform’s MAC validation. On each POST request, a shared base page class decoded, decrypted, and decompressed the ViewState field, then passed the result to a .NET object deserializer backed by `BinaryFormatter` with no type validation.
Encryption answers the question, “Can the client read this?” Integrity validation answers the question, “Did the client write this?”
The application needed the second question answered and instead answered the first. Because the deserializer sits directly behind that check, the key was the only thing standing between an anonymous HTTP request and the server’s object graph.
We generated a well-known .NET gadget chain with a code execution payload, compressed and encrypted it with the application’s own key and IV, encoded it to match the custom transport format, and submitted it in the custom ViewState field of a POST to the unauthenticated login page. The server accepted the payload as its own work, deserialized it, and executed the code. Every page inheriting from that base class was affected, including pre-authentication pages.
The RCE was blind. The application did not include the command output in the HTTP response, and egress restrictions prevented Armadin from receiving HTTP or DNS callbacks from commands like curl or ping. Additionally, the app’s execution context did not have permission to write inside the web root.
In order to prove that code execution occurred, we fell back on a response differential in the same file-retrieval handler, which worked on the Windows temp directory (`C:\Windows\Temp`). Requests for files that existed produced an HTTP 302, whereas requests for files that did not exist produced an HTTP 200 with a verbose error.
We used that behavior as an assertion over three requests:
In addition to achieving RCE, we discovered that the same key protected the authentication tokens the application carried in its client-side state. We built tooling to intercept the field, decrypt it, modify its contents, and re-encrypt it for submission. Because the server treated successful decryption as proof of authenticity, whatever we wrote back was accepted as the application’s own state. We exploited this weakness to forge a token for a privileged account and obtain administrative access without a password.
1. Let the platform handle the cryptography, and make sure every installation gets its own keys.
Modern web frameworks ship with built-in protection for the data an application hands to a browser and later accepts back. That protection checks whether the returned data is the same data the server sent, and the control stops an attacker from rewriting it. When a development team replaces that mechanism with one of its own, it takes on the responsibility for a problem the framework has already solved. In this case the replacement checked only whether the data could be decrypted rather than whether it had been altered.
The key matters just as much as the mechanism. A key written into source code and compiled into the product ships to every customer, so a single copy of the software exposes the key protecting every installation. Armadin recommends that teams generate key material per installation, store it outside the application code, and ask third-party vendors directly how their products handle this before deployment occurs.
2. Protect files with authentication, not with a hard-to-read URL.
Scrambling a file path so that it looks meaningless in the address bar hides information from a casual observer, but it does not decide who is allowed to retrieve the file. Anyone who figures out the scrambling can ask for whatever they want.
We recommend that file download endpoints verify the requestor's identity and permissions on every request, and that they serve content only from a defined directory for user-facing files. That second step keeps configuration files, application binaries, and other system content out of reach, even when an attacker can control the requested path.
Discover how a safe, managed Hyperattack™ from Armadin gives you the tools and confidence to eliminate truly exploitable risk across your environment. Learn more about Hyperattacks.