In this write-up I’ll cover my solution to Assured’s MQTT challenge, I’ll also explain what their intended solution was.
For this challenge, Assured provided a hostname, port username and password. They also had a small device with two needles in their booth, the left one was moving periodically and you were expected to move the right one.
The first step was figuring out what we were talking with, nmap can be particularly useful to find these kind of things and it showed of this was an MQTT server:
# nmap -A
-p 8883 -Pn Starting Nmap 7.70 ( https://nmap.org ) at 2019-05-23 13:06 CEST Nmap scan report for ( ) Host is up (0.047s latency). PORT STATE SERVICE VERSION 8883/tcp open secure-mqtt? |_mqtt-subscribe: Connection rejected: Not Authorized Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port Device type: general purpose Running (JUST GUESSING): Linux 3.X|4.X (90%) OS CPE: cpe:/o:linux:linux_kernel:3.10 cpe:/o:linux:linux_kernel:4.4 Aggressive OS guesses: Linux 3.10 (90%), Linux 3.10 - 3.16 (90%), Linux 3.10 - 3.12 (89%), Linux 4.4 (89%), Linux 4.9 (89%), Linux 4.0 (88%) No exact OS matches for host (test conditions non-ideal). Network Distance: 26 hops Nmap done: 1 IP address (1 host up) scanned in 21.14 seconds
Notice that although the service detection displays only the port name (hence the question mark at the end) we can see that the mqtt-subscribe module actually resulted in a Not Authorized error, so we can be pretty positive this was an MQTT server.
Some compilation and howto read up time later, I had subscribed to the server and was receiving messages and responses:
-p 8883 -u user -P -t '#' -v challenge aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa challenge/err A challenge/in mosquitto version 1.4.15 challenge/out 77+9ZmZmZg== jidhage left SecFest19 -100 metrics/exchange  arrow turn test Aaaaaaaäaaaaaaaaaaaaaaaasaaaaaaaaaaaaaaaaaaaasaaaaaaaaasaaa assured Secfest mqtt Secfest Secfest Secfest
Notice the use of ‘#’ to subscribe to all topics as we weren’t provided a list to start with. As can be seen we have some data, including some spam, although some stuff like the “jidhage left” quote are a bit funny. But this information doesn’t help much. If we sat down for some time we’d see commands like the following:
challenge/in AGljY2Lm+g== challenge/in AGljYmLVyw== challenge/in AGljbWLF9Q== challenge/in AGljbGL2xA== challenge/in AGliZWJ7bA== challenge/in AGliZGKU challenge/err encoded: Unexpected length challenge/in AGliZ2IdDg== challenge/in AGliZmIuPw== challenge/in AGliYWK3qA== challenge/in AGliYGKEmQ== challenge/in AGliY2LRyg== challenge/in AGliYmLi+w== challenge/in AGlibWLyxQ== challenge/in AGlibGLB9A== challenge/err A:190 Integer out of bounds challenge/in AGlibWLyxQ== challenge/in AGliYmLi+w==
We can notice already two errors in there: “encoded: Unexpected length” and “A:190 Integer out of bounds”. The next step was trying to send our own commands, we did this using the following command line.
-p 8883 user -P -t challenge/in -m "Our message".
We could see the messages appeared to be base64 encoded so we started by decoding it. This resulted in a string like ‘\x00iccb\xe6\xfa’. The first dead end was assuming that the 4 middle bytes were base32 encoded as they were all alphabet letters, I think that for some of the inputs I saw that was not the case but as I was unsure if inputs had any effects or were reliable I considered decoding and encoding them, sadly I couldn’t get any valuable information from that.
If we tried to modify the string then we would get the following error:
challenge/in AGIiY2LRyg== challenge/err XModem: integrity check failed
Looking around we can find out that XModem uses a CRC code of 16 bits with polynomial 0x1021. Sadly, trying to create the code for the provided string fails miserably for any CRC-16 algorithms I could test. We can though be pretty positive that the last 2 bytes are the CRC seeinng how they change accross valid commands.
So we have a binary protocol using CRC but we don’t know the parameters being used. A CRC algorithm has basically 3 parameters, the polynomial, an initial value (that is xored on the input) and a final value that is xored on the output. Additionally we have to consider that the last two bytes might be on little or big endian and whether the reverse CRC algorithm was being used or not. Overall these parameters provide 50-bits of entropy which is too much to just bruteforce them. Fortunately, Greg Ewing’s article on reverse engineering CRC is pretty useful to find a solution for this problem. The second dead end, was some bug on my code and assumptions which resulted in wrong parameters, I solved this by refactoring the code and sitting down again to double check my assumptions, below follows the procedure that the good code used.
I assumed the CRC was 16 bits based on the XModem error and the fact the last 2 bytes of the command were binary. So for the first stage all that was left to crack was the polynomial, the endianness of the output and whether the reverse algorithm was in use. For this we can use the formula provided by Greg that explains how the CRC changes if you XOR messages, so given three messages a,b and c CRC_poly(a^b) ^ CRC_poly(a^c) == unknown_CRC(a^b) ^ unknown_CRC(a^c) if the polynomial was a good candidate. This calculation is a bit slow, so we can try to filter with three arbitrary messages, and then check the candidates with all the other ones where we didn’t get a missmatch. Some minutes later, this gave us the polynomial, 0x1021, big endian and forward algorithm. So far so good, also a quick check for that polynomial will hint you that it is used by the CRC-CCIT algorithm used by XModem which seems to match the error message.
The next step was discovering the initial XOR, here I was lucky and I assumed it to be 0 when I erroneously tried to first find the final xor value. For this I tried to find the value for which CRC_poly(a) ^ CRC_poly(b) == CRC_poly(a^b) which gave the value 61471, finally an attempt to match the initial value resulted in 47787, this probably works because the messages have the same length (see the equations by Ewing).
I then tried tampering the messages and found that things worked as expected, but I got a CRC error when making the message larger, originally I suspected this was caused by a bug on Assured’s code, and was told all messages should have the same length to work, later I have started to suspect this may be caused by me not doing the math correctly when finding the CRC parameters.
Finally all I did was start tampering with the message bytes, I found that replacing \x00 with \x03 resulted in the following message “challenge/success Congratulations!” and called the Assured folks to prove I had solved their challenge after confirming the left indicator had moved.
So how was it supposed to be solved?
Well if you see the “A:190: Integer out of bounds message”, you’ll see it is 5 bytes, if you xor that with the original message you’ll see that results in the key ASSUR, you can then assume the last two bytes are ED and xor them to see messages are something as follows:
A:001 followed by the CITT CRC of the message and then XORed with the key ASSURED before submission. My approach probably worked because I matched the right XORin parameter and the messages were the same length, had the messages different length I guess my luck would have been different.
What have I learnt?
I probably should make a proper tool to decrypt CRC given some input messages 😛