Capture The Flag Solution: reversing the password
Last week, I made a mini Capture The Flag (CTF) about a criminal who changed Barry’s password. The challenge was to come up with the password the criminal chose. This blog will explain how the CTF could be solved.
Here’s the given payload that Barry was able to recover.
7b 0a 20 a0 22 65 76 e5
6e 74 22 ba 20 22 70 e1
73 73 77 ef 72 64 5f e3
68 61 6e e7 65 22 2c 8a
20 20 22 f5 73 65 72 ee
61 6d 65 a2 3a 20 22 e2
63 6f 6c ec 69 6e 22 ac
0a 20 20 a2 6f 6c 64 df
70 61 73 f3 77 6f 72 e4
22 3a 20 a2 3a 5c 78 c3
37 5c 78 c6 34 5c 6e dc
78 41 46 a9 29 37 43 dc
78 31 35 dc 78 44 30 dc
78 46 33 dc 78 44 45 e9
55 3b 22 ac 0a 20 20 a2
6e 65 77 df 70 61 73 f3
77 6f 72 e4 22 3a 20 a2
39 5c 78 c6 41 5c 78 b9
39 5c 78 c3 41 5c 78 c5
44 5c 78 c6 32 58 53 c7
5c 78 44 c4 2d 5c 78 c3
32 5c 78 b8 45 7a 48 eb
22 2c 0a a0 20 22 74 e9
6d 65 73 f4 61 6d 70 a2
3a 20 31 b5 30 31 38 b5
38 38 36 b0 30 30 30 8a
7d 0a
As almost everyone of you noticed, this is data represented in a hexadecimal format. Let’s start by converting the hexadecimal bytes to ASCII bytes. Here’s the result:
{
�"ev�nt"� "p�ssw�rd_�han�e",� "�ser�ame�: "�col�in"�
�old�pas�wor�": �:\x�7\x�4\n�xAF�)7C�x15�xD0�xF3�xDE�U;"�
�new�pas�wor�": �9\x�A\x�9\x�A\x�D\x�2XS�\xD�-\x�2\x�EzH�",
� "t�mes�amp�: 1�018�886�000�}
Here’s a Ruby script to convert the bytes:
data = '7b 0a 20 a0 22 65 76 e5 6e 74 22 ba 20 22 70 e1 73 73 77 ef 72 64 5f e3 68 61 6e e7 65 22 2c 8a 20 20 22 f5 73 65 72 ee 61 6d 65 a2 3a 20 22 e2 63 6f 6c ec 69 6e 22 ac 0a 20 20 a2 6f 6c 64 df 70 61 73 f3 77 6f 72 e4 22 3a 20 a2 3a 5c 78 c3 37 5c 78 c6 34 5c 6e dc 78 41 46 a9 29 37 43 dc 78 31 35 dc 78 44 30 dc 78 46 33 dc 78 44 45 e9 55 3b 22 ac 0a 20 20 a2 6e 65 77 df 70 61 73 f3 77 6f 72 e4 22 3a 20 a2 39 5c 78 c6 41 5c 78 b9 39 5c 78 c3 41 5c 78 c5 44 5c 78 c6 32 58 53 c7 5c 78 44 c4 2d 5c 78 c3 32 5c 78 b8 45 7a 48 eb 22 2c 0a a0 20 22 74 e9 6d 65 73 f4 61 6d 70 a2 3a 20 31 b5 30 31 38 b5 38 38 36 b0 30 30 30 8a 7d 0a'
puts data.split(' ').map { |c| [c].pack('H*') }.join('')
Two things can be noticed here: some bytes are corrupted and it’s a JSON object. In order to work with the JSON, the corrupted bytes have to be fixed first. There were two hints in the CTF: something was up with the Most or Least Significant Bits (MSB/LSB) and that there might be pattern in the corrupted bytes.
By counting the position of the corrupted bytes, it was possible to figure out that every fourth byte was corrupted. It was easy to guess some words, so it was possible to use those words to see what went wrong with the corrupted bytes. Let’s take “event” and “password”. In the initial message, they are represented by the following byte sequence:
Hex: 65 76 e5 6e 74
Current: ev�nt
Expected: event
The third byte, e5, should be 65 instead. Since there seemed to be something up with the bits, let’s convert e5 and 65 to their respective binary value.
E5 | 11100101
65 | 01100101
By looking closely at the bits, it can be observed that the MSB is inverted (0 becomes 1, 1 becomes 0). A script could be used to correct every fourth byte, invert the MSB of that byte, and convert it to ascii again.
data = '7b 0a 20 a0 22 65 76 e5 6e 74 22 ba 20 22 70 e1 73 73 77 ef 72 64 5f e3 68 61 6e e7 65 22 2c 8a 20 20 22 f5 73 65 72 ee 61 6d 65 a2 3a 20 22 e2 63 6f 6c ec 69 6e 22 ac 0a 20 20 a2 6f 6c 64 df 70 61 73 f3 77 6f 72 e4 22 3a 20 a2 3a 5c 78 c3 37 5c 78 c6 34 5c 6e dc 78 41 46 a9 29 37 43 dc 78 31 35 dc 78 44 30 dc 78 46 33 dc 78 44 45 e9 55 3b 22 ac 0a 20 20 a2 6e 65 77 df 70 61 73 f3 77 6f 72 e4 22 3a 20 a2 39 5c 78 c6 41 5c 78 b9 39 5c 78 c3 41 5c 78 c5 44 5c 78 c6 32 58 53 c7 5c 78 44 c4 2d 5c 78 c3 32 5c 78 b8 45 7a 48 eb 22 2c 0a a0 20 22 74 e9 6d 65 73 f4 61 6d 70 a2 3a 20 31 b5 30 31 38 b5 38 38 36 b0 30 30 30 8a 7d 0a'
decoded = data.split(' ').map { |c| [c].pack('H*') }.join('')
result = ''
decoded.chars.each_with_index do |c, i|
if (i + 1) % 4 == 0
# convert the byte to its binary value
new_byte = c.unpack('b*')[0]
# invert the MSB
new_byte = if new_byte[-1] == '0'
new_byte[0...-1] + '1'
else
new_byte[0...-1] + '0'
end
# convert the binary value to ascii
c = [new_byte].pack('b*')
end
# append the (corrected) byte to our result
result << c
end
puts result
Running this script will result in the following JSON object:
{
"event": "password_change",
"username": "bcollin",
"old_password": ":\xC7\xF4\n\xAF))7C\x15\xD0\xF3\xDEiU;",
"new_password": "9\xFA\x99\xCA\xED\xF2XSG\xDD-\xC2\x8EzHk",
"timestamp": 1501858860000
}
In the challenge text, the exact timestamp was given when the password was changed. Since the passwords are still not readable, the timestamp was given to determine you corrected the data successfully. The epoch time (with milliseconds) for August 4, 2017 08:01:00am (-0700 DST) is 1501858860000. Now that we have the correct data, let’s move on to the next step.
The old_password and new_password are still some random bytes, and they don’t look like actual passwords. The strings both contain unreadable characters. Throwing them in any script or programming language would quickly show that both strings have an exact length of 16 bytes. Some of you quickly realized that they might be an md5 fingerprint of the password in their raw form. Usually, an md5 fingerprint is represented in a hex format. Converting them back to hex will result in:
old_password: 3ac7f40aaf2929374315d0f3de69553b
new_password: 394a99caedf2585347dd2dc28e7a486b
This is where a lot of people got stuck. Some people brute forced for hours. However, brute force wasn’t needed to solve the challenge. In my opinion, a CTF needs to have complexity on multiple levels. Reading is important. Obviously, you can’t reverse an md5 fingerprint and so the title of the challenge didn’t make sense in the first place. The clue here was to literally reverse the strings. Here are the reversed strings:
old_password: b35596ed3f0d5134739292faa04f7ca3
new_password: b684a7e82cd2dd7435852fdeac99a493
At this point, some people used rainbow tables to get the plain text passwords. Google was also a good friend here. Using crackstation.net, or a similar service, you’d get the plain text passwords back in a matter of seconds. A double md5 fingerprint was used to store the data: string_reverse(md5(md5(plain_text))).
old_password: p4ssw0rd
new_password: thisiscrazy
As you can imagine, Barry was very happy when the first correct answer came in, within 12 hours after the tweet went out (#TogetherWeHitHarder)! In the 2 days after that, 18 more people solved it. In the remaining time, 10 more people solved the challenge. Over 50 people participated (thanks so much!). Barry will send hoodies to the first 10 people who solved the challenge and have a custom HackerOne profile badge for everyone. People will be notified about their prize in the next few days. Profile badges will come in the next week.
Here’s four writeups that I wanted to highlight (awesome work!):
Joel Margolis: https://gist.github.com/teknogeek/6e13b69ea69b3e33fef39c0bacce5e41
Martin Bajaník: https://gist.github.com/bayotop/12462e76e3b76499c06ecc1aee29d42e
EdOverflow: https://gist.github.com/EdOverflow/94a5f49c0eb9cb80cffde7672727ec18
Philippe Harewood: https://gist.github.com/phwd/2d57a0053035172cf78d517df3fe814f
Thanks everyone for participating — congrats to all of you, you rock!
Jobert (HackerOne co-founder)
PS Barry just found out that the criminal did more than he initially thought. There seems to be a web application that he can’t sign in to anymore. Perhaps he’ll ask you again in the next few weeks to recover some more data…
HackerOne is the #1 hacker-powered security platform, helping organizations find and fix critical vulnerabilities before they can be criminally exploited. As the contemporary alternative to traditional penetration testing, our bug bounty program solutions encompass vulnerability assessment, crowdsourced testing and responsible disclosure management. Discover more about our security testing solutions or Contact Us today.
The 8th Annual Hacker-Powered Security Report