We are given the following challenge text
Use this API to gift drink vouchers to yourself or your friends!
http://18.104.22.168 <- 2nd instance if the first one is too slow
Vouchers are encrypted and you can only redeem them if you know the passphrase.
Because it is important to stay hydrated, here is the passphrase for water: WATER_2019.
Beers are for l33t h4x0rs only.
The source in [`drinks.py`](https://github.com/newjam/insomnihack-teaser-ctf-2019/blob/master/drinks/drinks.py) is a flask web service that allows two actions, `POST` to `/generateEncryptedVoucher` or `/redeemEncryptedVoucher`.
When supplied with a `recipientName` and a `drink`, `generateEncryptedVoucher` returns a PGP encrypted (and compressed) message.
-----BEGIN DRINK VOUCHER-----
-----END DRINK VOUCHER-----
By examining the code [`drinks.py`](https://github.com/newjam/insomnihack-teaser-ctf-2019/blob/master/drinks/drinks.py), we can see that each drink has a "coupon code" and the the drink voucher plaintext is the concatenation of the `recipientName` and the coupon code corresponding to the `drink`.
The symmetric passphrase used to encrypt the plaintext is also the coupon code for the drink.
`redeemEncryptedVoucher` takes the drink voucher, and the coupon code (aka passphrase) and checks that the supplied coupon code matches the coupon code in the plaintext of the drink voucher, and we see the flag is the the coupon code.
At first I spent a long time researching vulerabilities to gnupg symmetric key encryption and a lot of promising looking, but ultimately fruitless, leads came up.
Eventually, though, I read that pgp compresses the message before encrypting it.
This was the eureka moment, and I realized the if the `recipientName` we supply to `generateEncryptedVoucher` was similar to the coupon code for the `drink`, the length of the drink voucher would be less than if they were disimilar.
Thus we have an oracle which leaks information about the rest of the plaintext!
len(generateEncryptedVoucher('', 'water')) == 179
len(generateEncryptedVoucher('WATER_2019', 'water')) == 179
Since our plaintext is `WATER_2019||WATER_2019`, the common strings are compressed.
len(generateEncryptedVoucher('!@#$%^&*()', 'water')) == 191
because the plaintext is `!@#$%^&*()||WATER_2019`, and can not be compressed as much.
The [solution](https://github.com/newjam/insomnihack-teaser-ctf-2019/blob/master/drinks/client.py) is to start with an prefix and check the length of ciphertext of the prefix appended with each character in the alphabet.
If the length is less than the others, it is considered a candidate in the next round.
In practice, some manual intervention is required to eliminate unlikely prefixes, such as `G1MME________` in favor of more likely prefixes such as `G1MME_B33R_PL`. For example, I left the algorithm to run and this is what it decided the flag was: `G1MME_B33R_PLZ_1MME_B33RY_TH1RSTY`, even though the correct flag is `G1MME_B33R_PLZ_1M_S0_V3RY_TH1RSTY`
This is the same exploit as this ctf challenge, https://systemoverlord.com/2013/04/30/plaidctf-compression/,
as well as another recent ctf, which I forget.
This was a fun challenge, thanks to insomnihack!