Goracle
Goracle
Platform: hackerlab | Category: Crypto | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-04-04 | Status: Solved Techniques: cbc_padding_oracle_attack, go_binary_reverse_engineering, bytewise_plaintext_recovery
Summary
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 with a CBC padding-oracle attack.
Goracle — Hackerlab
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.encryptmain.decryptmain.handleConnectionmain.main
Reverse engineering showed the following behavior:
main.encryptgenerates a random 16-byte IV, uses AES-CBC with a process-wide random key created inmain.init, applies PKCS#7 padding, prepends the IV, and returns the result as base64.main.decryptbase64-decodes user input, requires at least one block for the IV, decrypts with AES-CBC, and runspkcs7Unpad.- If unpadding fails with an error containing
padding, the server returns JSON statusinvalid_padding. - If unpadding succeeds, the server returns JSON status
success. main.handleConnectionaccepts line-delimited JSON commands:flag→ returns{ "ciphertext": "..." }encrypt→ encrypts chosen plaintextdecrypt→ 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
decryptreturned{"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:
- Force all already solved trailing bytes to decrypt to the desired padding value.
- Bruteforce the current byte in the previous block.
- Submit the modified ciphertext with
{"command":"decrypt","ciphertext":"..."}. - If the oracle returns
success, the guess produced valid padding. - 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 read-only • copy allowedCODEBY{h4rd_tIm3 REDACTED REDACTED}\x03\x03\x03
After removing PKCS#7 padding, the flag was:
text read-only • copy allowedCODEBY{REDACTED}
Practical exploit script
python read-only • copy allowed#!/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()