Solving the ROBOT CTF

After the publication of the previous post on ROBOT (, some people wanted to see a proof of concept of the attack, so I started developing a set of tools that demonstrated an attack using a Bleichenbacher oracle. After a first prototype with a very simple oracle (a method returning true/false) and checking that the attack itself worked, I managed to modify it to connect to a vulnerable TLS server and perform the attack based on its behavior against incorrectly padded pre-master-secret values. After successfully checking it against YAWS (a web server based on Erlang, and so vulnerable according to the ROBOT paper), I remembered that the authors of the ROBOT paper had prepared a CTF that I had previously ignored, as I didn’t think I would be able to solve it… Was the tool I had developed enough to solve it?
The first step in the CTF was decrypting a message in base64 that was in CMS format. This message is encrypted using a key shared by a vulnerable server (, at port 7777). Not knowing exactly what CMS was, my first idea was decoding the base64 data and seeing if some recognizable text appeared. Bad luck, the content is binary. Still, after taking a quick look at the CMS RFC I saw that the format was based on ASN-1. Using an ASN.1 viewer (for instance, gives quite promising results.

A RSA-encrypted section that most likely encrypts the AES key (the 256-byte long OCTECT STRING in the upper block) and a binary data section (the part tagged as [0] in the lower block) that is identified as AES-128-CBC encrypted data (as stated by the OBJECT IDENTIFIER), with what could be the associated IV (the 16-byte long OCTET STRING in the same block).  After this, the next step is checking that the server ( at port 7777) is vulnerable. Sending encrypted correctly and incorrectly padded pre-master-secrets, it looks like it allows us to identify encrypted pre-master-secrets starting with 0x00 0x02, so it’s a good quality oracle.
With some small modifications to my tool (the exact alert codes did not match), I can send the encrypted RSA data and decrypt it. As it uses PKCS#1 padding, its decrypted value must start with 0x00 0x02 and so the oracle attack should work without additional modifications. After 2 or 3 hours, the calculations end.

The result is what looks like a correctly padded value (starting with 0x00 0x02), and of 16 bytes (underlined in white in the image), corresponding to 128 bits, so chances are it is indeed the AES key. Taking this key and the IV data that was in the message I use openssl to try to decrypt the data and the result is…

Not really unexpected. I should have been (pleasantly) surprised if everything had worked at the first try. Still, everything looked so correct… Both the encrypted data and the IV were easily extracted from the ASN.1 message, and the use of the oracle had returned a correctly-sized key (as the attack doesn’t assume the length of the unpadded value, the chances of obtaining an incorrect value of the correct length are quite low). I look several times at the arguments I passed to openssl and everything seems OK… Here the algorithm, this one the key, here the IV, the data… Everything is correct, isn’t it? Well, it isn’t and probably many of you already noticed. I wasn’t telling openssl to DECRYPT the data, and the default operation is encryption. I was telling it to re-encrypt the already encrypted data, so garbage (actually, twice encrypted data) was the only thing I was going to get.
So, I added the corresponding argument (-d) to decrypt the data and executed openssl again. After the first mistake I was not too confident that anything good was coming out of this, so I expected another disappointment…

The output was ASCII data!!!!! Level 1 of the CTF was solved! The content of the message are the instructions for level 2 of the CTF. Apparently, I had to sign a message so that it could be verified using the public key included within the level 1 message.
The problem was that the instructions indicated that there was somewhere a vulnerable HTTPS server… and that I had to find it by myself. So, no host and port like in level 1. Anyway, I imagined that the vulnerable server had to be in one of the domains I had seen during the CTF (“ctf”, “target” or “final”) so finding it seemed like a piece of cake.
The next three days were the most frustrating ones of all the CTF. Of course, none of the HTTPS servers in those domains returned the expected public key. I executed nmap on them and I connected to every port (conveniently ignoring the little voice reminding me that I had been told it WAS an HTTPS server). Every port either closed the connection, returned clear text or used SSL with a different public key. I even checked different server names using SNI, assuming the web server was sharing the same port but under a different domain.
Needless to say, that line of reasoning led me to a dead end. After trying lots and lots of possible host names, I wasn’t closer to finding the server. Eventually I found myself zooming in the ROBOT images trying to find hidden text, and wondering if I should try some kind of steganography tool.
Fortunately for me, at that moment my sanity returned. Even though there was some chance that the information to find the server was hidden in such a convoluted way, it was a complete deviation from level 1. All that was needed to solve level 1 was UNDERSTANDING the attack. So, the information had to be in two specific places: the decrypted level 1 message and the ROBOT paper. If it was hidden somewhere else in an esoteric way, well… I had no chance to find it by February 1st anyway.
So, I did what I should have done before… Re-read the ROBOT paper. More specifically the part regarding message signing… Ok, let’s see… To verify that they actually signed the message we should create a file with the signature they provide, download the certificate from the search engine and use openssl to verify… Wait, SEARCH ENGINE? Let’s see… I have a public key and I want to find a certificate with that key… and I hadn’t realized I could actually SEARCH for it? Surely the facepalm was heard all around the globe… I only had to calculate the SHA256 of the public key and use the search engine to obtain the certificate associated to the public key.

Well, at least there is some certificate, signed by “Let’s Encrypt” as the other ones in the CTF and valid from December 16th… It was quite promising and, as for the content of the certificate:

It did in fact contain a hostname, and it had nothing to do with (so I would never have been able to find it by myself). After connecting to the server and checking that the certificate used the specific key I was looking for, I almost cried.
Ok, so on to signing the message. In this case, “signing” the message, as performed by rsautl, means encrypting the message with the signer’s private key. With the original attack, we had a public-key encrypted value and we obtained the decrypted one. But the process of decrypting a public-key encrypted value is equivalent to encrypting with the private key some plaintext. As such, the attack is perfectly valid, but instead of using the encrypted pre-master-secret we will use the plaintext message to be signed, with the correct padding, and as a result we will obtain the “signed” message. The padded message is as follows (unpadded, and in binary and hex formats).

But wait… the attack assumes that the “decrypted” value (in this case, the signature) starts with 0x00 0x02. Chances are the private-key encrypted result will not start with those two bytes. Can it be used anyway?
Well, in fact it’s quite simple. I only needed to add an additional step that transformed my padded message into a correctly encrypted pre-master-secret that was accepted by the server. Something very similar to the first step in the normal attack. Once I have this valid pre-master-secret, I can apply the attack on it and obtain its decrypted value. As the relationship between the message to be signed and this pre-master-secret is known, the signature can be derived from the decrypted value by reversing the transformation.
So I modified my code in order to first derive a valid encrypted pre-master-secret from my padded message. In this case, the speed of the attack was very low. As the key size was higher than in level 1 (4096 bits), each attempt took longer than before, so it took quite a long time to obtain the result, but eventually, it returned a valid result.

So… what is all this information? The value “padded-message” is the correctly padded message to be signed. After executing lots of oracle invocations, the attack has found a correct value for “s” (in this case, 37398) that (after public-key encryption) transforms the padded message into an encrypted value that, when decrypted, is accepted by the server as a correctly padded pre-master-secret. After performing the attack, “decrypted” holds the value that the server accepted as pre-master-secret. And to transform “decrypted” into the signature, I only have to transform it with the inverse of “s”, which is also shown as “s_1”. After al the operations, the value shown in “result” should be the signed message. Using xxd I save it into a file (and check its content again using xxd).

And if everything was correct, openssl should correctly verify this as the original message.

Well, at least openssl considers that the value is a valid signature… But the real test is checking it with the CTF server, so I upload it and…


CTF solved!

Deja un comentario