HITCON 2016: Hackpad (Crypto, Forensics, 150pts)

My site was hacked. The secret was leaked.
hitcon{H4cked by a de1ici0s pudding '3'}

Upon opening the file you are given in Wireshark, it's filled with a bunch of http packets connecting to the local IP. I filtered out the http packets and looked at the content. The first thing I noticed is that packets 6, 16, and 26 all include encrypt(secret) and a long hex string. I then noticed that packet 34 had form data of msg assigned to the hex value from packet 26. Then in packet 36 you have data of md5(decrypt(msg)) = e5d3583f3e05b9242a1933fd5d245200. After that you have a bunch of packets with form data of "msg" with an increasing number as its values.

From there I jumped to the bottom of the pcap and notice that packet 406036 had md5(decrypt(msg)) = d41d8cd98f00b204e9800998ecf8427e. I then decided to filter the pcap for packets containing "md5(decrypt(". You then get 322 packets with md5 hashes of what seems to be successful decrypts. I made a list of all the md5 hashes and inputted them into a database to see if any had been discovered. Several of them had, and were hashes of relatively short strings. The one that caught my eye was this series:

fbade9e36a3f36d3d676c1b808451dd7 MD5 : z  
d41d8cd98f00b204e9800998ecf8427e [Not found]  
4a7d1e9b095afae4bd7ee738d43cc319 [Not found]  
c124b8a0bf2422f917d3176ede349688 [Not found]  
ca9bd4dda2ce5866a03528875f61fde4 [Not found]  
b0314c28ff731eb02228ea6a9dec0041 [Not found]  
c0b8a73c95ec02a5b516f553b4e685eb [Not found]  
0097a5630473cc67f1acdd91a0b4514b [Not found]  
e7fb843dc603d6332a7d5a25228d6f7c [Not found]  
594644d5f06e30c4e2e2043d866dff01 [Not found]  
b7f50e0f888d0d8c4065804bd0319b44 [Not found]  
c754a0a0d7d013008baf72de67fffec9 [Not found]  
7ca34f1c6c23eb2e3ec060302679f6d0 [Not found]  
3036e084a414f2b9331917fc749c567f MD5 : ~D|E  
8fc6706777f6c2a5a0f329b412380b40 MD5 : ~D|  
7acae7d0d6141897ce1ed8740e072948 MD5 : ~D  
4c761f170e016836ff84498202b99827 MD5 : ~  
d41d8cd98f00b204e9800998ecf8427e [Not found]  

(Note that d41d8cd98f00b204e9800998ecf8427e is the md5 hash of nothing) From here I hypothesized that the hashes above this list would include the string ~D|E plus an extra character. I wrote a script that checked every possible character appended to that to see if it matched the md5 hash above. I was able to match the hash 7ca34f1c6c23eb2e3ec060302679f6d0 with an extra character, and I continued this process up to 4a7d1e9b095afae4bd7ee738d43cc319. This is the md5 hash of ~D|E +o����r, a 15 character string consisting of some unprintable characters.

I then wrote a script that did this for every series of hashes that started with d41d8cd98f00b204e9800998ecf8427e, and I was able to compile a list of strings each 15 characters long. Once I had this, I noticed that in the packet data the back half of msg always matched a block of encrypt(secret) from packet 26. What I realized from here is that they were doing something analogous to a padding oracle attack and getting the md5 hashes down to a single character so they could get back most of the decrypted block.

I also noticed that the first block from encrypt(secret) was never used, informing me that it is some sort of initialization vector. I then assumed that the strings I had recovered from the md5 hashes were the cipher text ran throuhg the decryption alogrithm but not Xor'd with the previous cipher text (I assumed this to be CBC mode). Thus, I wrote this script to pull out the strings, add a null charcter to them to have each chunk 16 bytes, and then Xor each block with the cipher text of the previous block. The script can be seen here (the file md5s_extracted is the list of the hashes from the pcap):

import md5

def reverse_md5(hashes):  
    guess = ""
    final = ""
    for x in range(len(hashes)-1, 0, -1):
        for i in range(256):
            m = md5.new()
            guess_temp = guess.decode("hex") + chr(i)
            if m.digest().encode("hex") == hashes[x]:
                if x <= 1:
                    print "On: " + str(x) + ". Found:"
                    print m.digest().encode("hex")
                    print guess_temp + " Length: " + str(len(guess_temp))

                    final = guess_temp.encode("hex") + "00"
                guess = guess_temp.encode("hex")
    return final

f = open("md5s_extracted")  
raw = f.read()  

raw = raw.split("\n")  
raw = raw[0:321]  
print raw

text = ""

for i in range(1, len(raw)-16, 16):  
    print "On index: " + str(i)
    text += reverse_md5(raw[i:i+16])

print text

CT = "3ed2e01c1d1248125c67ac637384a22d997d9369c74c82abba4cc3b1bfc65f026c957ff0feef61b161cfe3373c2d9b905639aa3688659566d9acc93bb72080f7e5ebd643808a0e50e1fc3d16246afcf688dfedf02ad4ae84fd92c5c53bbd98f08b21d838a3261874c4ee3ce8fbcb96628d5706499dd985ec0c13573eeee03766f7010a867edfed92c33233b17a9730eb4a82a6db51fa6124bfc48ef99d669e21740d12656f597e691bbcbaa67abe1a09f02afc37140b167533c7536ab2ecd4ed37572fc9154d23aa7d8c92b84b774702632ed2737a569e4dfbe01338fcbb2a77ddd6990ce169bb4f48e1ca96d30eced23b6fe5b875ca6481056848be0fbc26bcbffdfe966da4221103408f459ec1ef12c72068bc1b96df045d3fa12cc2a9dcd162ffdf876b3bc3a3ed2373559bcbe3f470a8c695bf54796bfe471cd34b463e9876212df912deef882b657954d7dada47"  
CT = CT.decode("hex")

text = text.decode("hex")

for x in range(0, len(text)-16, 16):  
    temp = ""
    for i in range(16):
        temp += chr(ord(text[x + i])^ord(CT[x + i]))
    print "X: " + str(x)
    print temp

print len(CT)  
print len(text)

#hitcon{H4cked by a de1ici0s pudding '3'}

The output contains the first 15 bytes of plaintext from each block, which can then be easily converted into the flag hitcon{H4cked by a de1ici0s pudding '3'}.