Midnight Parcel -450MPTS
The streets are quiet, the lights are fading, and one delivery is still waiting to be collected.
Plus http://54.72.82.22:8440
The landing page presents itself as a simple parcel collection desk. Two cards are offered: a
collection desk carrying a download link for parcel.json, and a desk console that accepts GET,
POST and PATCH requests to local paths. The help text beneath the console documents
GET /api/parcel and POST /api/receipt with a hex parcel string.
parcel.json was:
{"parcel":"f989a62eb693b12abd793ff509201480688cd318f1c25ed237a11f60d4205a8833b10d61c5a53091ba32fb8d86afae986ebbb995cb7abcca4f7f9d50047b69be3276bce6e68c835f2e494b283e13c4c1929896ee8533244aeea21df7f1677c43"}96 bytes after hex decoding. That is 6 blocks of 16, which is consistent with AES-CBC.
GET /api/parcel returned the same blob. GET /api/receipt returned {"status":"damaged"}.
POST /api/receipt with the original parcel returned {"status":"pending"} with HTTP 202.
Every other input produced {"status":"damaged"} with 422. The status field therefore gives a
clean valid versus invalid oracle, which is what makes a padding oracle attack practical here.
Checking the available methods returned little beyond what was already visible. OPTIONS on both endpoints listed GET, POST and PATCH. PATCH behaved identically to POST in this case. No other surface presented itself, so attention turned to the receipt oracle.
The last hex digit was flipped first, which produced a damaged status. The entire last byte was
then brute-forced across all 256 values using a small script carrying Firefox headers and a
thread pool, since single requests were slow. Two values returned a pending status: the
original 43 and 29. Every other value returned damaged.
That result is ambiguous at first reading. In CBC mode P_last = Dec(C_last) xor C_prev_last,
so modifying the last byte of the last ciphertext block randomises the entire final plaintext
block and yields roughly one hit in 256. Two hits are possible by chance. Modifying the last
byte of the preceding block, by contrast, flips only the final plaintext byte, so exactly one
hit should correspond to a padding byte of 01. Byte 79, the last byte of block 4 and thus
the control byte for the last byte of block 5, was tested next. Two hits appeared again:
c1 (the original) and c2. These differ by 0x03, which is 0x01 xor 0x02. That is the
classic signature of an original padding value of 02 02. One of the two hits is the genuine
02 02, the other is a forged 01.
Flipping the second-to-last byte confirmed the distinction: the forged 01 remained valid
while the original 02 02 broke. The final two plaintext bytes were therefore 02 02.
Padbuster was attempted first, since it is the usual tool for this class of attack, with the hex
sample embedded in a JSON POST body and damaged as the error string. It hung and was
abandoned in favour of a purpose-built loop. For each target block the parcel is truncated so
that the target block becomes last, after which the preceding block is forged byte by byte from
the end:
pad = 16 - pos
forged[j] = intermediate[j] xor pad for j > pos
brute-force forged[pos] until status is pending
intermediate[pos] = forged[pos] xor pad
plain[pos] = intermediate[pos] xor original_prev[pos]Where two hits occurred on the first byte, the second-to-last-byte flip technique selected the
01.
Block 5 was processed first:
intermediate: 1f41d9d3dfb8ba671c7c79105c6ec6c3
plain: 2d3765353934393832353238627d0202 -> b'-7e594982528b}\x02\x02'The block ends with } followed by 02 02, consistent with the tail of a flag. Block 4:
plain: 622d373930332d346634652d62343261 -> b'b-7903-4f4e-b42a'Block 3:
plain: 3a207361666374667b38613939653662 -> b': safctf{8a99e6b'Block 2:
plain: 6576656e696e672064656c6976657279 -> b'evening delivery'Block 1:
plain: 5265636569707420666f722074686520 -> b'Receipt for the 'Concatenating blocks 1 through 5 and stripping the two padding bytes yields:
Receipt for the evening delivery: safctf{8a99e6bb-7903-4f4e-b42a-7e594982528b}Block 0 is the IV, so nothing further is recoverable by this technique. The middle segment
8a99e6bb-7903-4f4e-b42a-7e594982528b follows a UUID shape of 8-4-4-4-12 hex groups, which is
consistent with the surrounding content.
The intermediates and the full plaintext were saved under loot/, the request logs under
evidence/, and the decrypt script under payloads/ for reruns.
An early assumption that this decrypted text was the flag itself proved incorrect. It is an
intermediate token. The page script contains a fallback path that POSTs {"answer": ...} to
/submit, an endpoint the desk console text does not advertise. Posting the decrypted token
there returns the actual flag:
POST /submit
Content-Type: application/json
{"answer":"safctf{8a99e6bb-7903-4f4e-b42a-7e594982528b}"}Response, saved under evidence/submit_response.json:
{"message":"safctf{f895fa37be9a374582ff7694a4748862}","ok":true}Flag:
safctf{f895fa37be9a374582ff7694a4748862}The reusable lesson from this exercise concerns truncation rather than tool choice. To decrypt block N, only blocks 0..N are sent, so that N becomes the last block and the padding check applies to it. Once that approach was in place each byte took roughly 15 seconds, and the plaintext emerged progressively across all six blocks.
