← Back to Writeups
CUSTOMMediumCrypto

Goracle

XESXOR9/2/20264 min read
#crypto#custom#medium#cbc_padding_oracle_attack#go_binary_reverse_engineering#bytewise_plaintext_recovery

Goracle

Platform: CUSTOM | Category: Crypto | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: cbc_padding_oracle_attack, go_binary_reverse_engineering, bytewise_plaintext_recovery

Summary

Walkthrough of the CUSTOM Crypto Goracle. Task: a Go server encrypts the flag with AES-CBC and exposes encrypt/decrypt JSON commands. Solution: reverse the binary to confirm a PKCS#7 padding oracle, then recover the remote flag block-by-block

Challenge overview

Наш криптограф научился Go, теперь попробуй разберись что он там понаписал…

The challenge provided goracle.zip with user_server, a statically linked 64-bit Go ELF, plus a remote service at 62.173.140.174:10700. The local binary contained a redacted placeholder for main.flag, so the real flag had to be recovered from the remote instance.

The goal was to understand the protocol, identify the cryptographic mistake, and use it to decrypt the remotely encrypted flag. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Recon

go tool objdump exposed the original source path:

/Users/ilyashalnev/PycharmProjects/hackerlab_dev/padding_oracle/server/server.go

The most useful recovered functions were:

  • main.encrypt
  • main.decrypt
  • main.handleConnection
  • main.main

Reverse engineering showed the following behavior:

  • main.encrypt generates a random 16-byte IV, uses AES-CBC with a process-wide random key created in main.init, applies PKCS#7 padding, prepends the IV, and returns the result as base64.
  • main.decrypt base64-decodes user input, requires at least one block for the IV, decrypts with AES-CBC, and runs pkcs7Unpad.
  • If unpadding fails with an error containing padding, the server returns JSON status invalid_padding.
  • If unpadding succeeds, the server returns JSON status success.
  • main.handleConnection accepts line-delimited JSON commands:
    • flag → returns { "ciphertext": "..." }
    • encrypt → encrypts chosen plaintext
    • decrypt → returns only a status string ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

That last endpoint is the oracle: it does not reveal plaintext, but it reveals whether the decrypted plaintext ended with valid PKCS#7 padding.

Against the remote service:

  • {"command":"flag"} returned
hpZzAtuwzqFIXoHcIH7cSv4vovxPN1SAggz7G6+EQjFThbnCdjhf58HIm4yKreYWy6PEMtNANC+wJ8poW7yuHw==
  • Sending that ciphertext unchanged to decrypt returned {"status":"success"}.
  • Flipping bytes in the ciphertext produced {"status":"invalid_padding"}.

So this was a textbook AES-CBC PKCS#7 padding oracle. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Root cause

In CBC mode, each plaintext block is computed as:

P_i = D_K(C_i) XOR C_{i-1}

If an attacker can modify C_{i-1} and ask the server whether the final plaintext has valid PKCS#7 padding, the attacker can recover the intermediate value D_K(C_i) one byte at a time. Once that intermediate block is known, the real plaintext follows immediately by XORing it with the original previous block.

This challenge gave exactly the conditions needed:

  • controllable ciphertext input,
  • AES-CBC with PKCS#7,
  • a stable secret key,
  • and, most importantly, different responses for valid vs invalid padding. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The reversing step mattered because it confirmed that invalid_padding was specifically tied to pkcs7Unpad, while the embedded local flag was only a placeholder. That meant the local binary was for protocol analysis, but the remote oracle was necessary to recover the real flag.

Exploitation

The returned ciphertext is 64 bytes total, split into four 16-byte blocks:

  • block 0: IV
  • block 1: ciphertext block 1
  • block 2: ciphertext block 2
  • block 3: ciphertext block 3

For each target block, I attacked the previous block byte-by-byte from the end toward the beginning. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

For a target byte position:

  1. Force all already solved trailing bytes to decrypt to the desired padding value.
  2. Bruteforce the current byte in the previous block.
  3. Submit the modified ciphertext with {"command":"decrypt","ciphertext":"..."}.
  4. If the oracle returns success, the guess produced valid padding.
  5. Recover the intermediate byte, then XOR it with the original previous-block byte to recover the plaintext byte.

In practice the full attack took roughly 5k oracle queries.

Recovered plaintext blocks: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

text
CODEBY{h4rd_tIm3
REDACTED
REDACTED}\x03\x03\x03

After removing PKCS#7 padding, the flag was:

text
CODEBY{REDACTED}

Practical exploit script

python
#!/usr/bin/env python3
import base64
import json
import socket

HOST = "62.173.140.174"
PORT = 10700
BLOCK = 16


def request(obj):
    with socket.create_connection((HOST, PORT)) as s:
        s.sendall(json.dumps(obj).encode() + b"\n")
        data = b""
        while not data.endswith(b"\n"):
            chunk = s.recv(4096)
            if not chunk:
                break
            data += chunk
    return json.loads(data.decode())
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍


def oracle(ciphertext: bytes) -> bool:
    reply = request(
        {
            "command": "decrypt",
            "ciphertext": base64.b64encode(ciphertext).decode(),
        }
    )
    return reply.get("status") == "success"


def recover_block(prev_block: bytes, cur_block: bytes) -> bytes:
    intermediate = [0] * BLOCK
    plaintext = [0] * BLOCK
    crafted = bytearray(prev_block)

    for pad in range(1, BLOCK + 1):
        idx = BLOCK - pad

        for j in range(BLOCK - 1, idx, -1):
            crafted[j] = intermediate[j] ^ pad
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

        for guess in range(256):
            crafted[idx] = guess
            if oracle(bytes(crafted) + cur_block):
                intermediate[idx] = guess ^ pad
                plaintext[idx] = intermediate[idx] ^ prev_block[idx]
                break
        else:
            raise RuntimeError(f"No valid padding for byte {idx}")

    return bytes(plaintext)


def main():
    ct_b64 = request({"command": "flag"})["ciphertext"]
    ct = base64.b64decode(ct_b64)
    blocks = [ct[i:i + BLOCK] for i in range(0, len(ct), BLOCK)]
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

    recovered = b""
    for i in range(1, len(blocks)):
        recovered += recover_block(blocks[i - 1], blocks[i])

    pad = recovered[-1]
    flag = recovered[:-pad]
    print(flag.decode())


if __name__ == "__main__":
    main()

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Takeaways / Lessons

  • Mastering cbc_padding_oracle_attack, go_binary_reverse_engineering, bytewise_plaintext_recovery was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-04-04 • CUSTOM • Goracle • Difficulty: Medium