drinks -- InsomniHack19
drinks
by goulov and mandlebro
Points: 137 (Dynamic, 34 solves)
Description:
Use this API to gift drink vouchers to yourself or your friends! http://drinks.teaser.insomnihack.ch 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.
Given: Source code
We solved this challenge 3rd, which was nice!
Solution:
TLDR
-
Discover that GPG default symmetric encryption compresses the messages before encrypting.
-
Notice that the passphrase (\(k\)) is appended to the name (\(n\)) in the message (\(m\)) to be encrypted as \(m = n || k\).
-
We can try \(n = || i\) for all \(i\), and check which \(i\) has the minimal length, this is the one which got compressed, and so the one in \(k\) (\(k\) is the FLAG).
Description
The challenge was quite small, it was basically a server with two endpoints. We have no idea if this is the intended solution, as we only used one of them.
Following its name drinks
, in the first endpoint ( /generateEncryptedVoucher ), the server provided the user the ability to generate a voucher for a drink. For this, one was required to give to the server its Name
(\(n\)) and a drink
, either water or beer, with which it would check the dictionary for the corresponding couponCode
(\(d\)). Using GPG default symmetric encryption algorithms, the server would then provide the voucher as the encryption (\(Enc(key,message)\)) in the form:
\[v = Enc(d, n || d)\]
The second endpoint ( /redeemEncryptedVoucher ) had the functionality of redeeming the voucher. Here, if one is able to redeem a voucher for beer, the flag is given to him. The server expected a voucher and a passphrase, decrypted the voucher with the passphrase, and parsed the result as Name
and couponCode
, and checked if couponCode
is the one of beer
, if so one would win (in fact the FLAG is just the couponCode
with the standard INS{}
, which is why we did no use this endpoint).
The attack
We want to find the couponCode
for beer
, as this is the FLAG.
We start by looking at the cipher used… Trying to find out block sizes and this type of stuff, and innocently using the same repeated character many times we were seeing that the length of the ciphertext did not grow. This was very odd to us, but a look at the documentation (https://www.gnupg.org/documentation/manpage.html) gave us the answer:
-z n Set compression level to n. A value of 0 for n disables compression. Default is to use the default compression level of zlib (normally 6).
GPG uses compression prior to encrypting the message. This, together with the fact that the passphrase is included in the message as is something we control (remember that we cipher \(n||d\), where \(n\) we control and \(d\) is the passphrase/FLAG) allows us to easily find the FLAG. The sketch of the algorithm is as follows:
-
For all possible \(i\) set \(n = ||i\) and send \((n, beer)\) to the first endpoint.
-
Check the responses, one voucher will have a smaller length that the others (since \(n\) was a repeated pattern, zlib compressed it).
-
Now, just repeat by trying all possibilities for \(j\), i.e the next character, \(n = ||ij\).
Doing this until there is no compression anymore we got the couponCode
which with INS{} gives the FLAG:
INS{G1MME_B33R_PLZ_1M_S0_V3RY_TH1RSTY}
Resources:
This is very simmilar to a previously known attack, CRIME (https://en.wikipedia.org/wiki/CRIME). The code we used can be found in solve_drink_thread.py and drink_utils.py.