Making a hash of authentication on the way to RCE

Jul 6, 2021 22:45 · 727 words · 4 minute read

About a year ago ICS-CERT published ICSMA-20-184-01 regarding several vulnerabilities I found in OpenClinic GA , an open source hospital information management system (HIMS), while examining it in search of severe unauthenticated vulnerabilities, especially RCE.

As I scrutinized the authentication code in checkLogin.jsp, I noticed an if statement with two different user authentication methods called: initialize and initializeAuto. The former involves (aside: I’ll be using the present tense because that is what I am used to in writeups, but the code we’re discussing is in older versions) a fairly typical “hash input password, then compare to hashed password in DB” routine. But initializeAuto (in User.java) looks a little unusual:

Why is aPassword being converted to an integer prior to comparison with the result of hashPassword (the argument to which was this.password, which had been set based on the encryptedpassword database column – i.e, the user’s password hash, not the plaintext password itself)? Let’s take a look at that method:

It turns out that hashPassword doesn’t operate how we’d expect it to given its name, i.e., by taking a plain-text password as input and giving a cryptographic hash as output. Rather, it takes the user’s SHA-1 password hash and returns the sum of each of its bytes.

So aPassword is converted to an integer because it is not actually a password but a sum of the bytes of the hash of a password.

And this has drastic effects on the search space for a valid value. The search space for even a relatively weak, common password is hundreds of thousands or millions of possibilities. And a 20-byte SHA-1 hash represents 1,461,501,637,330,902,918,203,684,832,716,283,019,655,932,542,976 possibilities. But the sum of 20 signed bytes can be at its very smallest -2560 if all 20 bytes are -128 and at its very largest 2540 if all 20 bytes are 127. Hence the search space is comparatively miniscule, at 5101 possible values.

This was assigned CVE-2020-14494.

Furthermore, not each of these possibilities is not equally likely; sums with small absolute values are more likely than those with large absolute values. Even if you are like me and lack the requisite math skills to prove this, it is pretty easy to show experimentally with common passwords :

Using the same experimental approach, we can see that after 10 low-absolute-value guesses an attacker has an approximately 1% chance of authenticating successfully.

There is an account lockout mechanism to contend with after excessive authentication attempts. But even with this in place, after making guesses with enough different users (OpenClinic GA uses integer user IDs rather than usernames, so no real reconnaissance is necessary) an attacker has a decent chance of getting authenticated access to the application.

What’s more, the lockout mechanism turns out to be defective as well. Once an account has been locked, the user is provided with a “request ID” which is to be given to an administrator. The administrator will enter the request ID into the application and generate an “unblock code” and then give that code to the user. The user then submits the unblock code alongside the request ID and his or her account is unlocked.

However, these values are validated using the hard-coded mathematical formula shown below:

Hence “0” and “0” constitute a perfectly adequate request ID – unblock code pairing with this formula. So now we can make as many authentication attempts as we please (CVE-2020-14484) – though given the previous issue we will never need more than 5101.

(Side note: a couple of more conventional authentication bypasses [CVE-2020-14485] provide much simpler routes to this point, but they are not especially interesting to talk about.)

So now we’re authenticated. What next? Looking through the pages of the application, executeSQL.jsp seems promising. Indeed, it turns out that it’s exactly what it says on the tin: a page that allows administrators to run arbitrary SQL queries. Unfortunately, it lacks a permission check and is vulnerable to forced browsing by authenticated-but-non-admin users (CVE-2020-14491).

The primary release of OpenClinic GA at the time of testing was a Windows installer that bundled the application, Tomcat, and MySQL. And permissions are such that the classic SELECT … INTO OUTFILE technique to drop a webshell works, giving us RCE (CVE-2020-14493; note that CVE-2020-14488, CVE-2020-14490, and advisory vulnerability 4.2.10 present possible RCE paths as well).

According to OpenClinic GA’s developer, these issues are all fixed as of version 5.170.5.