
By Javier Medina ( X / LinkedIn)
TL;DR
During an offensive engagement, we found a publicly accessible status page for an ERP service. From there, anyone could start/stop and delete logs without authentication, then download debug logs that included full SQL queries exposing usernames and password-related values. Those values were not one-way hashes. They were reversible AES-CBC ciphertext derived from the username. No more, no less. No bruteforce, no injection, just a chain of bad design decisions that collapsed under its own weight.
0# What are old dives?
Old Dives is an educational series about vulnerabilities we have found in real environments during previous offensive work. These are not recent n-day vulnerabilities, but cases that already have CVE identifiers long time ago and, in most cases, should be patched or mitigated, with a very low risk of exploitation. These are cases where the most important thing is not the novelty, but the lesson that can be learned.
Many real-world failures are not the result of exotic exploit chains against top-tier targets. In fact, most come from standard systems based on assumptions of trusted environments, proprietary shortcuts for standard problems, lax protections, convenience over security, and security mechanisms that were never designed to deal with hostile inputs. This is where old dives comes in, exposing and reasoning, from the attacker’s perspective, the decisions that made these exploit possible.
If we had to sum it up in a single sentence, we would say that they are our B-sides, and we think you might like them.
1# Our First Case
Our first case happened during a offensive exercise a few years ago (report says 2021). The client was a medium-sized company and their ERP was RPS 2019. A Tier-2 ERP (~4000 clients) commonly found in industrial SMEs. The kind of software that runs factories and warehouses.
We were in the recon phase and then we saw a URL that we always click on, mostly out of habit:
/RPS2019Service/status.html
“Status” pages are usually boring. Gray pages that say “OK! Services are fine.” and you close the tab. But this one wasn’t like that… this one was anything but boring.
2# A status that is too informative
The status page looked, at first glance, like a regular health dashboard: RPS running, deployment versions, and a list of enabled services. The problem is that it was public. Well… Information leaks happen… it may not be impressive, but it is common. Or so we thought, at first.
Upon closer inspection, there were also two buttons in the upper right corner that clearly should not appear on an unauthenticated page under any circumstances: run/stop log and delete log.
Even in internal-only deployments, exposing operational controls through a web page is risky. Exposing them without auth is stupid. We tested the obvious. The buttons worked. We could start logging and retrieve the results without having user or any privileges on the system.
So we downloaded the logs.
That’s when it became clear this wasn’t just an exposed status page. The logs contained extensive debug output, including the exact SQL queries the application sent to the database. And among them, authentication-related queries.
A simplified, structurally faithful redacted example looked like this:
SQL LOG (EXAMPLE, REDACTED)2021-03-09 10:15:00.680 - DEBUGGetEntity<User> -> 1 rows (...)SELECT VALUE t0 FROM ...WHERE t0.CodUser = N'John'AND t0.Password = N'Xhl/Vn[***]F2F1W[-REDACTED-]=='
“N’John” was a real username. And “N’Xhl/Vn6[-REDACTED-]==” base64-like value at the end was what the application used in the authentication check.
Even if the rest of the application were perfect, this alone is a serious violation of basic security principles. Logs are not a dumping ground for whatever comes to mind. Exposing debug output on a production web interface is bad enough. Doing it while leaking sensitive authentication details is worse. But if that same output is also written to a log that anyone can retrieve, it’s catastrophic.
3# The “Hash” that wasn’t a hash
After the initial shock, we left the log running and performed some tests. Then we downloaded the file and checked what it had captured. What we found was oddly consistent in one way. The login query always looked the same. What wasn’t consistent was the value of the password field.
Non-Existent Users
We used an incorrect account to see what would happen with a user that didn’t exist. And, of course, what we found was pure fantasy.
SQL LOG (EXAMPLE, REDACTED)2021-03-09 10:27:31.510 - DEBUG(..)SELECT VALUE t0 FROM ...WHERE t0.CodUser = N'Jhon'AND t0.Password = N'Password'
Logs with hashes are bad. Logs with plaintext passwords for usernames with typos are… art.
Existing Users
When the username did exist, the password wasn’t printed in plain text. Instead, as we said, the query showed a short Base64-looking fixed-lenght value. For example, these:
Xhl/Vn[***]F2F1W[-REDACTED-]==Do3e56[***]9IDz+[-REDACTED-]==
It looked like a hash. To be fair, you can’t prove it isn’t one without knowing how it’s generated, but a few details made us suspicious.
First, it looked like Base64, including the == padding. Of course, hashes can be stored as Base64, but in most real systems you usually see clear bcrypt/Argon2 formats or simple hex strings. Base64 is not impossible, it just felt like an unusual choice.
So we did a very simple test. We kept the password exactly the same and only changed the username to another real user. If it was a hash of the password, the result should not depend on the username. But it did. Same password, different valid username, and the Base64-looking value changed too.
Maybe it wasn’t a hash. It didn’t prove encryption, but it confirmed the value depended on the username.
4# If You Get Creative, We’ll Get Nasty.
The most important thing to understand about security through obscurity is that it does not stop anyone who really wants to get in.
At that point, we suspected that the Base64 value was not just a hash, but we didn’t want to continue guessing. So we did what any serious attacker would do; stop speculating and go to the source. In this engagement we had lawful access to another RPS demo server, so we took the relevant DLLs and looked inside. The usual reversing process of follow the code path until it explains itself.
The answer came quickly.
It was AES-128 in CBC mode, and the result was stored as Base64. AES itself is fine. It’s a solid lock. The problem was how the lock was being used. because AES-CBC needs a key and an IV. One should be secret, the other should be unpredictable.
In this design, neither really was. The key and the IV were derived from the username, padded to 16 bytes using a fixed character (‘b’). And, for extra style points, they were the same value. So if the username was “John”, then “John” wasn’t just an identifier. For all practical purposes, it was everything you needed to reverse the encryption and recover the password.
Decrypting passwordS for fun and profit
At this point, there is nothing more satisfying than writing down how to reverse one of the administrators’ passwords and take complete control of the ERP system.
// KEY / IVbyte[] key = Encoding.ASCII.GetBytes("bbbbbbbbUsername");byte[] iv = Encoding.ASCII.GetBytes("bbbbbbbbUsername");// The ciphertext from the logstring ciphertext = "Xhl/Vn[***]F2F1W[-REDACTED-]==";// Decrypt itConsole.WriteLine(DecryptAES(ciphertext, key, iv));
5# What to take away (and what to do)
This kind of vulnerability is dangerous precisely because it doesn’t look like a traditional attack. There is no malicious payload, no noisy exploitation. You could deploy an EDR, an NDR, and layers of firewalls and still miss it, because the interaction is legitimate from the product’s point of view. We only click status, enable logs and download logs. It’s the software that is doing the exfiltration. So, if the only lesson is patch and move on, we’ve understood nothing.
The real lesson is that a good defense will never be able to compensate for bad design.
We must be able to engrave the following principles for any product team:
- Diagnostics are not a public feature.
- Logs are sensitive data. Treat them like credentials. Apply access control, redaction and retention limits.
- Passwords are one-way. If you can decrypt them, you already failed.
- Use standards. Don’t get creative.
And in addition to this, if we want to do something practical tomorrow, we should:
- Inventory all status, health, debug, metrics and admin endpoints. Ensure they require auth, or are network-restricted and protected.
- Review logging configuration looking for SQL logging, sensitive fields, and verbose auth traces.
- Check the adoption of modern password hashing with salts and appropriate work factors. Bcrypt/Argon2 are your friends. Never let reversible encryption.
- Try to separate support tooling from user production surfaces. If support needs logs, provide them through controlled channels, not through a public web page.
6# Closing Thoughts
Ultimately, cases like this serve as a perfect wake-up call. We spend a lot of time worrying about complex, state-of-the-art chains of exploitation, but we often find ourselves compromised by features that simply work as intended… the problem is that the foresight is based on highly questionable design decisions.
That is the essence of this series, that is its educational value. To highlight that many flaws that allow serious compromises of our information systems are not solely due to one or two lines of erroneous code, but rather have their cause in the mindset behind them.
Until the next dive!