ctf-writeups

java-oracle

Consult the Java Oracle Secure Vault — but beware, its predictions leak more than your morning espresso.
nc challs.watctf.org 2013 

Attachment:

#!/usr/local/bin/python3
import os
from Crypto.Cipher import AES
import json

N = 16

def load_flag():
    try:
        with open("/flag.txt", "r") as f:
            return f.read().strip()
    except:
        return "fakectf{missing_flag_file}"

def count_blocks(length: int) -> int:
    return (length - 1) // N + 1

def helixlite_padding(message: bytes) -> bytes:
    mlen = len(message)
    blocks = count_blocks(mlen)
    target_len = blocks * N
    if mlen % N == 0:
        target_len += N
    pad_len = target_len - mlen
    return message + bytes([pad_len]) * pad_len

def helixlite_unpad(message: bytes) -> bytes:
    if len(message) < N or len(message) % N != 0:
        raise ValueError("Invalid message length")
    pad_len = message[-1]
    if not (1 <= pad_len <= N):
        raise ValueError("Invalid padding length")
    if message[-pad_len:] != bytes([pad_len]) * pad_len:
        raise ValueError("Invalid padding")
    return message[:-pad_len]

def chal():
    FLAG = load_flag()
    k = os.urandom(16)
    m = json.dumps({
        'access_code': FLAG,
        'facility': 'quantum_reactor_z9',
        'clearance': 'alpha'
    }).encode()

    iv = os.urandom(16)
    cipher = AES.new(k, AES.MODE_CBC, iv)
    enc = cipher.encrypt(helixlite_padding(m))
    original = iv + enc

    print(original.hex(), flush=True)
    print("Submit ciphertexts as hex (or type 'quit' to exit):", flush=True)

    while True:
        try:
            line = input("> ").strip()
            if line.lower() in {"quit", "exit", "q"}:
                break

            enc_bytes = bytes.fromhex(line)
            if len(enc_bytes) < 32 or (len(enc_bytes) % 16) != 0:
                print("Invalid data format")
                continue

            if enc_bytes == original:
                print("Valid padding")
                continue

            test_iv, test_ct = enc_bytes[:16], enc_bytes[16:]
            cipher = AES.new(k, AES.MODE_CBC, test_iv)
            pt = cipher.decrypt(test_ct)

            try:
                msg = helixlite_unpad(pt)
                if msg == m:
                    print("Access granted! Flag:", FLAG)
                    break
                else:
                    print("Valid padding")
            except ValueError:
                print("Invalid padding")

        except ValueError:
            print("Parsing error: invalid hexadecimal")
        except KeyboardInterrupt:
            break
        except Exception:
            print("Critical error")

if __name__ == "__main__":
    chal()

A typical padding oralce attack. You can refer to my previous writeup to see how it works.

Attach script:

from pwn import *

# context(log_level="debug")

p = process(["python3", "challenge.py"])
# p = remote(host="challs.watctf.org", port=2013)

original = bytes.fromhex(p.recvline().decode())
print(len(original))

plain = bytearray()
for part in range(16, len(original), 16):
    iv = [0] * 16
    known = [0] * 16
    msg = original[part : part + 16]
    # padding oracle attack
    for i in range(1, 17):
        good = []
        for j in range(1, i):
            iv[16 - j] = known[16 - j] ^ i
        for ch in range(256):
            iv[16 - i] = ch
            p.recvuntil(b"> ")
            p.sendline((bytes(iv) + msg).hex().encode())
            res = p.recvline()
            if b"Valid padding" in res:
                good.append(ch)
        if len(good) == 1:
            known[16 - i] = i ^ good[0]
        else:
            print(good)
            assert False
    plain += bytes([a ^ b for a, b in zip(original[part - 16 : part], known)])
    print(plain)

Since the flag is embedded in the plain text, there is no need to create a fake ciphertext that decrypts to the same plain text. After running the attack script on a VPS near to the server, we can get the flag: watctf{quantum_helix_padding_oracle}.