Posted on 11 Dec 2016 by Marco Squarcina and Lorenzo Veronese
Despite not being able to pwn this service during the game, we had a great time exploiting it just a few days ago! Why so late you may ask! Well, this is the story…
Background
Last summer I (Marco) spent one month in Paris doing a research internship at Cryptosense, an academic spin-off of INRIA and Ca’ Foscari. As the name suggests, the company deals with cryptography and developed a range of products for the automatic analysis of crypto applications. A couple of weeks ago I was testing some Java programs with the Cryptosense Analyzer, a tool that attaches to the JVM running an application and produces a security report of the crypto usage by analysing the calls to cryptographic libraries. I decided to include the cartographer service to the set of applications I was going to test. It sounded like a perfect candidate being a Java-based program performing some crypto operations. The report generated by the tool turned out to be very useful, so we decided to share our experience and publish a write-up for the service… here we go!
Service Overview
Cartographer is a web service that allows users to store maps (i.e. png images) in an encrypted form.
The two forms in the main page allow to:
select a png and upload it to the service (Upload Map)
download a png provided that we have the correct id and key (Activate Uploaded Map)
After uploading or activating a map, the png is rendered in the page and the pair (id, key) is persistently saved in the local storage of the browser.
The list of recently added maps ids can be displayed by accessing the /chunks.html page.
Service Analysis
The standard approach would be to decompile the service to statically understand what it is supposed to do. We went through this path during the CTF and figured out that Cartographer is a Kotlin program using the Spring Framework. We recovered all the useful functions to understand how it works (more on this later), but we actually missed the vulnerability just by manually reviewing the code…
As previously said in the intro, we played with the service as a blackbox and relied on the Cryptosense Analyzer to test it for us. Five crypto usage rules failed to apply to the provided trace.
The most interesting ones are the number 35 (use of unauthenticated encryption) and 32 (initialization vector has the same value of the key). Both rule violations are detailed below:
It looks like that the service fired a few encryptions using AES in Cipher Block Chaining (CBC) mode with the PKCS#5 padding. Let’s try to dig into the decompiled code to confirm our findings. The two application paths /images/encrypt and /images/decrypt enable the encryption and decryption of maps using, respectively, the cartographer.crypto.DefaultCryptography.encrypt and cartographer.crypto.DefaultCryptography.decrypt methods. The following snippet is taken from cartographer.crypto.DefaultCryptography.decrypt:
where getCipherSpec() is a method defined in the cartographer/crypto/CryptographySettings class that returns the cipherSpecSetting variable:
Therefore, the code performs a decryption with the cipher spec AES/CBC/PKCS5Padding and sets the IV to the value of the key, confirming our initial claims. This is very interesting by itself: if the service enables the decryption of arbitrary data and if padding errors can be somehow detected, we may be able to perform a Vaudenay attack to recover sensitive data. Thanks to the special case in which key == IV we may even push the attack further to leak the encryption key.
Let’s move on. Chunks are the pieces of data containing the encrypted images. By performing a GET request to /chunks/_recent it is possible to access the list of recent chunk ids. Then, each chunk can be downloaded by providing the correct chunk id at /chunks/<chunk_id>.
The structure of a chunk can be devised by analysing the code in DefaultChunkCryptography.decrypt:
A chunk is made up of a header, containing the length of the metadata,
some metadata, containing a json with a session key, and finally the encrypted image:
The image is encrypted using the sessionKey (which is also the key we can enter to activate a previously uploaded map), while the metadata block containing the sessionKey is secured with the masterKey. Assuming that flags are stored within images, accessing the masterKey of an opponent team will enable us to decrypt all their chunks and get the flags.
Attack
The Vaudenay attack referenced before allows an attacker to obtain the plaintext of encrypted blocks by exploiting the padding method expected by the application. The figure below depicts a standard CBC decryption. We say that c1 is the last byte of the encrypted block C1, C2 is the encrypted block next to C1 and p2 is the last byte of the plaintext block P2.
If an attacker can submit arbitrary data to be decrypted by the application, he may tamper with encrypted blocks to produce an invalid padding in the resulting plaintext to leak useful information. In the following example, we xor the last byte of C1 with g2 (a guess of the possible value of p2) and with 0x01. By asking the application to decrypt the two blocks C1 C2, it’s easy to see that the last byte of P2 will be equal to 0x01 when g2 = p2 and thus the application will not complain with padding errors since P2 is correctly padded according to PKCS#5. For all the other cases in which g2 != p2, the application will report an error about an invalid padding. By iterating the attack over all the bytes of the block, it is possible to recover the whole plaintext in P2 with just 128*BLOCK_SIZE attempts on average.
Things get even more interesting when the key is used as the IV. Let’s see how the first block gets decrypted in this case. We denote with k the last value of the IV and with p1 the last byte of the first plaintext block P1.
In this service the IV is not public (…it’s the piece of data we’d like to leak!), hence it is not possible to perform a standard Vaudenay attack to obtain the first plaintext block, although it would be possible to get the subsequent ones. On the other hand, what happens if we are not interested in leaking P1 because it’s a known data and we need to recover the key? The figure below depicts our modified version of the Vaudenay attack in which we alter the block P1 to contain - as its last byte - p1 ⊕ g1 ⊕ 0x01, where g1 is a guess of the value of k. By asking the application to decrypt P1 C1, we know that g1 = k as soon as no padding errors are returned! Notice that this attack corresponds to the previous one by replacing C1 with P1 and C2 with C1. Again, by iterating the attack over all the bytes of the block, it is possible to access the value of the whole key!
Remember that the metadata portion of each chunk contains some json-encoded data encrypted under the master key. With some luck, we may be able to recover enough plaintext of the first block from the metadata structure to get our hands over the masterKey! That’s exactly the case since the first 15 bytes of the metadata structure are {"sessionKey":". We miss the last byte, but that’s not really a problem since it’s enough to run the attack and bruteforce the last byte of the recovered key until it allows to decrypt C1 to the expected value.
Putting everything back together, the exploit should 1) download any chunk from the chunks list 2) extract the master key using the modified Vaudenay attack explained before on the metadata portion of the chunk 3) decrypt the whole metadata portion using the master key to access the session key 4) decrypt the image using the session key and extract the flag (which is a text entry of the PNG file).
Note that the master password is randomly generated during the first startup of the service and then its value is fixed. Hence, during the CTF it would have been enough to execute step 2) just once to access the master key of a team. Subsequent attacks would require only to download chunks from that team and decrypt them offline using the previously leaked master key.
Detecting this kind of attack during the game would have been painful, at the very least!
Full Code
We can finally steal flags! Here is an example
Python exploit
We also have a Haskell implementation for those who feel brave :)