Tags: crypto writeup 

Rating: 5.0

# Drinks

**Drinks** was the only cryptography challenge of **Insomni'hack Teaser 2019** and ended up being solved by over 30 teams. Despite this apparent "easiness", it featured a clever side-channel attack I never had the chance to try out before.

## Challenge description

```
Use this API to gift drink vouchers to yourself or your friends!
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.
```

Along with the description came a link to a service, as well as its source code.

## Code analysis

The provided code is the following:

```python
from flask import Flask,request,abort
import gnupg
import time
app = Flask(__name__)
gpg = gnupg.GPG(gnupghome="/tmp/gpg")

couponCodes = {
"water": "WATER_2019",
"beer" : "█████████████████████████████████" # REDACTED
}

@app.route("/generateEncryptedVoucher", methods=['POST'])
def generateEncryptedVoucher():

content = request.json
(recipientName,drink) = (content['recipientName'],content['drink'])

encryptedVoucher = str(gpg.encrypt(
"%s||%s" % (recipientName,couponCodes[drink]),
recipients = None,
symmetric = True,
passphrase = couponCodes[drink]
)).replace("PGP MESSAGE","DRINK VOUCHER")
return encryptedVoucher

@app.route("/redeemEncryptedVoucher", methods=['POST'])
def redeemEncryptedVoucher():

content = request.json
(encryptedVoucher,passphrase) = (content['encryptedVoucher'],content['passphrase'])

# Reluctantly go to the fridge...
time.sleep(15)

decryptedVoucher = str(gpg.decrypt(
encryptedVoucher.replace("DRINK VOUCHER","PGP MESSAGE"),
passphrase = passphrase
))
(recipientName,couponCode) = decryptedVoucher.split("||")

if couponCode == couponCodes["water"]:
return "Here is some fresh water for %s\n" % recipientName
elif couponCode == couponCodes["beer"]:
return "Congrats %s! The flag is INS{%s}\n" % (recipientName, couponCode)
else:
abort(500)

if __name__ == "__main__":
app.run(host='0.0.0.0')
```

A quick glance at the code is enough to figure out the goal is to retrieve the value of `couponCodes["beer"]`.

The service is a **Flask app** with two routes accepting **POST** requests containing **JSON-encoded data**: **/generateEncryptedVoucher** and **/redeemEncryptedVoucher**.

`generateEncryptedVoucher` basically creates a **PGP encrypted message** consisting of the string `<recipientName>||<couponCode>` where **recipientName** is a string or arbitrary length controlled by the attacker and **couponCode** is the string in **couponCodes[drink]**, where drink is either **"beer"** or **"water"**, and controlled by the attacker. The encryption is symmmetric, and **couponCodes[drink]** is also used as the passphrase for the encryption.

`redeemEncryptedVoucher` takes a **PGP encrypted message** and a **passphrase**, which it uses to decrypt the message. Then, if the `<couponCode>` part of the decrypted message is the same string as `couponCodes["beer"]`, it is printed along with a congratulations message.

It is probably safe to assume that the length of `couponCodes["beer"]` within the provided code is not arbitrary, and therefore we assume that **the flag is 33 characters long**.

It's the first time I heard of the symmetric mode in **PGP** (*with which I'm not overly familiar*), I needed more context in order to understand how this could possibly be exploited.

## The gnupg Python library

By following the trail of function calls within the GPG class, I ended up finding interesting information looking at the parameters of the `_encrypt` method in the parent class of `gnupg.GPG`, namely `GPGBase`.

```python
def _encrypt(self, data, recipients,
default_key=None,
passphrase=None,
armor=True,
encrypt=True,
symmetric=False,
always_trust=True,
output=None,
throw_keyids=False,
hidden_recipients=None,
cipher_algo='AES256',
digest_algo='SHA512',
compress_algo='ZLIB'):
"""
...
:param str cipher_algo: The cipher algorithm to use. To see available
algorithms with your version of GnuPG, do:
:command:`$ gpg --with-colons --list-config
ciphername`. The default **cipher_algo**, if
unspecified, is ``'AES256'``.

...
:param bool symmetric: If True, encrypt the **data** to **recipients**
using a symmetric key. See the **passphrase**
parameter. Symmetric encryption and public key
encryption can be used simultaneously, and will
result in a ciphertext which is decryptable
with either the symmetric **passphrase** or one
of the corresponding private keys.
...
"""
```

OK, now we know that **AES256** is used by default when the symmetric mode is enabled. I am more familiar with **AES** than I am with **PGP**, but this is bad news: I am pretty sure there is no way to break AES here (*as of today, at least*), unless maybe ECB mode is used (*which I doubt*). Maybe we should parse and analyze what the service produces when generating an encrypted voucher.

## Ciphertext analysis

We issue the following command:

```bash
~$ curl -Ns -H 'Content-Type: application/json' -X POST "http://146.148.126.185/generateEncryptedVoucher" --data '{"recipientName":"xxxx","drink":"water"}' | sed 's/DRINK VOUCHER/PGP MESSAGE/g' > voucher
```

and obtain a file containing a PGP encrypted message. Using `pgpdump` on the file, we get the following output:

```bash
~$ pgpdump voucher
Old: Symmetric-Key Encrypted Session Key Packet(tag 3)(13 bytes)
New version(4)
Sym alg - AES with 256-bit key(sym 9)
Iterated and salted string-to-key(s2k 3):
Hash alg - SHA1(hash 2)
Salt - 73 77 83 1a ef 53 e6 6e
Count - 65011712(coded count 255)
New: Symmetrically Encrypted and MDC Packet(tag 18)(67 bytes)
Ver 1
Encrypted data [sym alg is specified in sym-key encrypted session key]
(plain text + MDC SHA1(20 bytes))
```

Wooow, lots of new things here.

Thanks to the great [RFC 4880](https://tools.ietf.org/html/rfc4880), we get more information about what's going on. Basically, we learn that:

- **AES256** is used in **CFB mode**;
- the **IV** is constant but the **key is salted** and then **hashed several times**. Salt obviously changes at every run.

At that point, I have no clue about why this could be breakable. After spending a few hours spacing out on my couch trying to figure this out, when it dawned on me. There is a parameter of the `_encrypt` method that I should have paid more attention to.

## Side-channel attacks FTW

Yep, there is a `compress_algo` parameter in `_encrypt` whose default value is **'ZLIB'**. The plaintext is compressed before being encrypted, which means that we might be able to leak the flag by choosing appropriate values for `recipientName`: if part or all of our **recipientName** is contained in the flag, we can expect the encrypted message to be smaller than if it isn't. To be perfectly honest, I was wondering if that was actually a new class of attacks. Quickly looking it up revealed that it was not, as **CRIME** and **BREACH** are actually based on this principle (*and while the names were famous, I had never bothered actually looking them up*).

To try out this attack, we first need to determine a 3 character string (*limit I discovered experimenting with a few requests*), which we can easily do since we know that the flag is prefixed by **"||"** in the plaintext string. And then, it's just a matter of running the following quick and dirty code, updating `pw` manually at every round:

```bash
# Was pretty tired, so that's the code I used to flag the challenge and I'm just too lazy to rewrite it :)
pw="||"
for c in `seq 1 33`; do
for i in `cat charset`; do
tmp=$pw;
tmp+=$i;
l=`curl -Ns -H 'Content-Type: application/json' -X POST "http://146.148.126.185/generateEncryptedVoucher" --data "{\"recipientName\":\"$tmp\",\"drink\":\"beer\"}" | sed 's/DRINK VOUCHER/PGP MESSAGE/g' | pgpdump | grep 'New:'`;
echo $i $l;
done;
read s;
pw=$pw$s
done;
```

**And we get the flag**, which once formatted as required is: **INS{G1MME_B33R_PLZ_1M_S0_V3RY_TH1RSTY}**

## Conclusion

I learned a lot about PGP and this side-channel attack doing this challenge. Thanks a lot to the organizers for a great time this year again!

remmer – Jan. 21, 2019, 4:52 p.m.

I feel honored by such a neat write-up! I'm glad you liked the challenge :)