CSAW 2013 Quals - Web 400: CryptoMatv2
For this challenge, reyammer and I teamed up. Half of the credit belongs to him :)
The web400-2 challenge of this year's CSAW qualification round was CryptoMat version 2, an homage to challenge of last year's qualification round. There, the solution to the CryptoMat challenge was a XSS that you could leverage by sending a plaintext to Dog that would encrypt to an XSS. The bot simulating the Dog user would then execute that code and through a redirect you could leak the cookie, which, in turn, allows us to log in as Dog. This year, to prevent cross-site scripting attacks, the ciphertext was base64 encoded before inserting it into the website, thus, effectively, sanitizing it. In the end, part of the solution to this challenge were similar to last year's challenge: the ciphertext was used as the attack vector.
First, after we found out that the challenge was a revamped version from last year, we read up on write-ups. Right after, we identified an XSS in the title of a message in the search functionality if the message was decrypted. As a shot in the dark without expecting much to happen, we send some messages to Dog. We figured out pretty quickly that read messages are being marked dark gray while unread message have a transparent (bright gray) background. After a few minutes, Dog still didn't read the messages we send him, and we looked for alternative vulnerabilities.
Since the web service was using AES-CBC encryption, which depends on the initialization vector (IV), we asked us how Dog would decrypt a message without knowing the IV and we checked if the same IV is reused by simply encrypting the same text with the same key, if the ciphertext is the same, then the IV is the same and we can simple calculate the IV by decrypting the ciphertext with an IV of 0x00 and XORing it with the original plaintext.
We quickly verified our hunch by simply encrypting the same message with the same key and comparing the ciphertext, the IV was indeed constant.
We then simply derived the IV with the following Python snippet, where the encoded base64 string is simply what the web interface printed when we encrypted the plaintext
abcdefgh01234567 with our key
This gave use the IV (hex encoded):
Since we found an XSS in the search functionality, we decided to check for some other parameters and randomly entered xxx & xxx into the search query (somewhat hilariously, this is exactly the same dragonsector was using, according to their write-up). Instead of querying the database, those values yielded a MySQL error:
Is the web app actually encrypting the message with the key and searching the database for the ciphertext? To verify this, we encrypted the message with the key locally and tried two different kind of paddings (PKCS-7 padding and zero-byte padding). Ultimately, we were able to reproduce the ciphertext with zero-byte padding.
Apparently, the search functionality works as follows:
- Take key and the query text
- Encrypt the text with the key (pad the key with zero bytes if necessary)
- Search the database for the ciphertext
- Return any title and text for messages that have been found
Now, we only needed to construct a ciphertext that was an SQL injection. That isn't too hard: we just take our desired ciphertext and decrypt it with a key of our choice, the resulting plaintext, when encrypted with our key, produces the same ciphertext, thus the SQL injection. The only potential problem might be that key and message have to contain only printable characters, which we checked quickly by encrypting the following plaintext:
This plaintext has some very nice properties and works in either case, without giving an error. In case that unprintable characters are okay, we will get back a 32-byte long ciphertext, otherwise we will get back a 16-byte one. Now, we can easily verify if unprintable characters are okay or not without having to take care of any padding. This is the case because both are aligned to the block-size of AES-CBC. Luckily for us, unprintable characters worked just fine as input for the plaintext and we didn't need to verify that the same holds for the key, or even to brute-force the key so that the plaintext for our SQL injection ciphertext contains only printable characters. Instead, we could simply take our SQL injection, decrypt it and submit the plaintext to CryptoMat. During the competition, we actually did not realize that there is a page parameter that we could have utilized to set the offset of the MySQL query and we simply filtered out all of the tables that we didn't want to care about. In the end, our query ended up pretty long because of this and the limit of 10 rows per query:
After filtering out all the uninteresting tables, only two tables remained:
user. From here on, we can simply dump the column names per table, starting with
The most interesting three columns were:
We were quite stumped that the encryption key was stored in the database, so much for secure, but it made our work easier in the end.
This year, we did not need to trace messages from Cat to Dog to get the key, instead, it was handed to us on a silver platter.
According to the challenge description, Dog was holding the key.
Instead of filtering by user, we just made an educated guess that the message will be among the early ones and they are likely to have small id.
We simply made use of the
order by id our query already had to dump all messages.
We used the following three queries to dump the interesting data:
After decrypting the messages with the corresponding key, we retrieved the key:
Similarly to our solution to WidgetCorp, during the competition, we utilized a mess of curl and Python to get everything working. Instead of exposing you to this chaotic mess, the following snippet is much easier to follow and more straight-forward: