The Details Behind the Akeeba Backup Vulnerability
It’s been a month since our disclosure of a low-severity vulnerability affecting Akeeba Backup version 3.11.4, which allowed an attacker to list and download backups from a target website using the extension’s JSON API. As promised, here’s the technical details describing how it was possible for us to send valid requests to the API and download our test website’s database and file backups.
Getting to Know the Code’s Structure
Here’s where the main event takes place. Note that $request->body contains our decrypted JSON payload. This will be useful later on:
This checks whether or not the JSON payload was successfully decoded (regardless of what it contained). Not a big issue so far, but it’s important to mention in order to fully understand what comes next. Once this condition is fulfilled, we move to this code snippet:
Notice the commented lines at the top? That pretty much explains it all. If the $request->encapsulation parameter is set to self::ENCAPSULATION_RAW, then aditional checks will be performed to verify that we’re a legit user. If the parameter is set to anything else, this means we originally sent an unencrypted request which was successfully decoded earlier by $request->body = $this->json_decode($body). The script won’t perform any checks under the assumption that the encrypted payload was decoded by a user who knew the site’s secret key (a legitimate user). This basically means that our payload, once decrypted, resulted in a valid JSON statement. Which leads us to this last chunk of code…
… this verifies whether we sent a valid method name, and displays Invalid method if it doesn’t pass the check.
Wondering where we’re going with all of this? Hold tight, we’re getting to the interesting part!
Having all of this in mind, we made the following observation: if we brute force a single byte as our encrypted payload, which gives us 256 possible values (0x00 to 0xff), we should only get 10 bytes where the API prints an Invalid method message. Do you see why?
Those are integer values (0 to 9), which are valid payloads. This displays a very interesting behavior. If the cipher text we sent (once decrypted) gives a valid JSON statement, we receive the “Invalid method” message. Otherwise, we receive the Authentication failed message.
Of course, we can’t do much with this alone, we had to use crypto-magic tricks to show how this could be used by a malevolent user.
How AES-CTR Decryption Works
Don’t worry, we won’t look too deeply at AES internals, just a quick look at how the CTR mode of operation works.
Basically, the block cipher encryption box is where AES does its thing, encrypting whatever we give it using a specific key (which, in this case, we don’t know). What’s interesting about the CTR mode is that it takes two specific things for input: a nonce (which we control by concatenating it to our ciphertext) and a counter value (which automatically increments whenever a block is decrypted).
We can safely assume that if we control the nonce input, we should always get the same output from the magic box (we’ll call this output “IMV” for the purpose of this post).
In case you don’t know what that weird symbol (⊕) is, this is the XOR operator (eXclusive OR), which is used here as a XOR cipher between the IMV and our ciphertext (over which we have full control) resulting in the decrypted form of our payload.
Still following? Nice, we’re getting to the actual attack.
How All These Things Come Together
Returning to where we left off earlier, we had 10 bytes which, once decrypted, return integers (0 to 9). Knowing that the following XOR operations are true: A⊕B=C, B⊕C=A and A⊕C=B, we can plan the following attack scenario:
- Take one of the 10 bytes we know are integers, which we’ll refer as I.
- Assume that it is a “1” character perform the following operation: “I ⊕ 0x31 = IMV[1]” (Note: 0x31 is the ASCII value for “1”)
- We don’t really know if our byte was indeed a “1” (this would mean our IMV is incorrect) so we will need to confirm by bruteforcing a second byte (which we’ll refer as J) and using our current IMV as a key to encrypt a curly bracket (“{“ or 0x7b), giving the following operation (see the “+” as a concatenation operator here): (IMV ⊕ 0x7b)+J = PAYLOAD
- If J, which we are bruteforcing, returmed the “Invalid method” message only once, it means we found our first IMV and can calculate the second one by XORing variable J with the byte value of a closing curly bracker “}” (0x7d), thus: “J ⊕ 0x7d = IMV[2]”.
- If it returned the message multiple times (or didn’t find anything), the decrypted value we got was not what we intended. We should therefore return to step #1 and try another byte, as there is only one valid two-byte JSON payload beginning with “{“, which is “{}”.
Once we have the first two IMVs, we can simply repeat the process with another type of JSON statement (for example, strings) to bruteforce the next IMVs. Once we have enough IMVs to fully encrypt the payload we want to send, we can get the API to list, download or delete the website’s backups.
No comments yet.