$ cat writeup.md…
$ cat writeup.md…
alfactf
Task: a web shop stored purchase transactions as AES-CBC ciphertext with a separate HMAC, then a Rust validator finalized orders asynchronously. Solution: use SQL injection to modify only the IV, bitflip the first plaintext block from our user ID to owner 1000001, bypass balance deduction, receive all required categories, and claim the flag.
$ cat /etc/rate-limit
Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.
Дресс-код
English summary: the target was a clothing shop where purchases were first written as encrypted pending transactions and later processed by a Rust validator. The goal was to obtain one item from each required clothing category and then call /check_dresscode to receive the flag.
Source review showed that the Python shop created purchase transactions as JSON and encrypted them with AES-CBC:
plaintext = json.dumps(data).encode() iv = os.urandom(16) cipher = AES.new(AES_KEY, AES.MODE_CBC, iv) ciphertext = cipher.encrypt(pad(plaintext, AES.block_size)) mac = hmac.new(MAC_KEY, ciphertext, hashlib.sha256).hexdigest()
The important flaw is that the HMAC covers only ciphertext, not iv.
The Rust validator repeated the same assumption:
fn verify_mac(&self, ciphertext: &[u8], expected_mac: &str) -> bool { let mut mac = HmacSha256::new_from_slice(&self.mac_key).expect("HMAC key error"); mac.update(ciphertext); let result = mac.finalize().into_bytes(); hex::encode(result) == expected_mac }
In CBC mode, changing the IV changes the first plaintext block after decryption while keeping the ciphertext untouched. Because the MAC ignored the IV, the validator would still accept the modified transaction.
The order comment update route was SQL injectable:
query = f"UPDATE transactions SET comment = '{new_comment}' WHERE id = '{order_id}'"
That let us update arbitrary columns of our own pending order record. We did not need to change the ciphertext or MAC at all; we only needed to change iv server-side.
The purchase JSON was created as:
trx_data = { 'from': session['user_id'], 'to': recipient_id, 'items': [item['id'] for item in items_in_cart], 'amount': total }
Its first bytes were predictable:
{"from": "<user_id>",
...
$ grep --similar