Author: isis

# python-gnupg audit

2013-02-01 Fri

## Table of Contents

- 1 gnugp._main_()
  - 1.1 comments
  - 1.2 def copydata(instream, outstream)
    - 1.2.1 L79:
    - 1.2.2 L78:
    - 1.2.3 L88:
  - 1.3 def threadedcopydata(instream, outstream):
    - 1.3.1 L99:
  - 1.4 def writepassphrase(stream, passphrase, encoding):
    - 1.4.1 L110:
- 2 class Verify(object)
- 3 class ImportResult(object)
- 4 class ListKeys(list):
- 5 class Crypt(Verify):
  - 5.1 def _init_(self, gpg)
    - 5.1.1 L338
- 6 class GenKey(object)
- 7 class DeleteResult(object)
- 8 class Sign(object)
- 9 class GPG(object)
  - - 9.1 L474:
  - 9.1 def _init_(self, gpgbinary='gpg', gnupghome=None, verbose=False, useagent=False, keyring=None)
    - 9.1.1 L494-495:
  - 9.2 def opensubprocess(self, args, passphrase=False)
    - 9.2.1 L515:
  - 9.3 def collectoutput(self, process, result, writer=None, stdin=None)
  - 9.4 def handleio(self, args, file, result, passphrase=None, binary=False)
    - 9.4.1 L601:
  - 9.5 def sign(self, message, **kwargs)
    - 9.5.1 L617-619:
  - 9.6 def signfile(self, file, keyid=None, passphrase=None, clearsign=True, detach=False, binary=False)
    - 9.6.1 L632-635:
    - 9.6.2 L626-641:
  - 9.7 def verify(self, data):
    - 9.7.1 L668-670:
  - 9.8 def verifyfile(self, file, datafilename=None)
    - 9.8.1 L683:
    - 9.8.2 L684:
    - 9.8.3 L690:
  - 9.9 def importkeys(self, keydata)
    - 9.9.1 L749:
  - 9.10 def recievekeys(self, keyserver, *keyids)
    - 9.10.1 L770:
  - 9.11 def exportkeys(self, keyids, secret=False)
    - 9.11.1 L795-796:
  - 9.12 def listkeys(self, secret=False)
    - 9.12.1 L827:
  - 9.13 def genkey(self, input)
    - 9.13.1 L864:
  - 9.14 def genkeyinput(self, **kwargs)
    - 9.14.1 L981-983:
  - 9.15 def encryptfile(self, file, recipiencts, sign=None, …)
    - 9.15.1 L939:
  - 9.16 def encrypt(self, data, recipients, **kwargs):
    - 9.16.1 L997:
  - 9.17 def decrypt(self, message **kwargs):
    - 9.17.1 L1003:
  - 9.18 def decryptfile(self, file, alwaystrust=False, passphrase=None, output=None)
    - 9.18.1 L1013:
- 10 POC

## 1 gnugp._main_()

### 1.1 comments

L58 NullHandler?? see self.writepassphrase L61 there nifty check for p3k

### 1.2 def copydata(instream, outstream) cleanup

copies data from one stream to another, 1024 bytes at a time.

#### 1.2.1 L79: bad_logic

instream is apparently a file descriptor, but is not checked nor encased in a try/except block.

#### 1.2.2 L78: hanging_fd bad_logic

while True: loop, should be

    with open(instream) as instrm:

#### 1.2.3 L88: bad_exception_handling

    except:

should catch an IOError, or whatever specific error is raised for broken pipes.

### 1.3 def threadedcopydata(instream, outstream):

#### 1.3.1 L99:

this just wraps self.copydata in a thread

### 1.4 def writepassphrase(stream, passphrase, encoding): vuln cleanup

#### 1.4.1 L110: writes_passphrase_to_disk

logger writes passphrase into debug log. this should be patched.

## 2 class Verify(object)

basic parsing class, no errors found

## 3 class ImportResult(object)

basic parsing class, no errors found

## 4 class ListKeys(list):

basic parsing class, no errors found

## 5 class Crypt(Verify):

basic parsing class, no errors found

### 5.1 def _init_(self, gpg) cleanup

#### 5.1.1 L338 mro_conflict

    Verify.__init__(self,gpg)

should be changed to:

    super(Verify, self).__init__(gpg)

## 6 class GenKey(object)

basic parsing class, no errors found

## 7 class DeleteResult(object)

basic parsing class, no errors found

## 8 class Sign(object)

basic parsing class, no errors found

## 9 class GPG(object) exploitable

#### 9.1 L474: cleanup

    cls.__doc__

should go directly underneath class signature

### 9.1 def _init_(self, gpgbinary='gpg', gnupghome=None, verbose=False, useagent=False, keyring=None) bug

#### 9.1.1 L494-495: type_error

    if gnupghome and not os.path.isdir(self.gnupghome):
        os.makedirs(self.gnupghome,0x1C0)

    In [20]: os.makedirs?
    Type:       function
    String Form:<function makedirs at 0x7f8ddeb6cc08>
    File:       /usr/lib/python2.7/os.py
    Definition: os.makedirs(name, mode=511)
    Docstring:
    makedirs(path [, mode=0777])
    Super-mkdir; create a leaf directory and all intermediate ones.
    Works like mkdir, except that any intermediate path segment (not
    just the rightmost) will be created if it does not exist.  This is
    recursive.

    setting mode=0x1c0 is equivalent to mode=hex(0700), which
    may cause bugs on some systems, see
    http://ubuntuforums.org/showthread.php?t=2044879

    this could be do to the complete lack of input validation in
    os.makedirs, and it's calling of the os.mkdir() built-in, which
    may vary depending on the python compilation:

    Source:
    def makedirs(name, mode=0777):
        """makedirs(path [, mode=0777])

        Super-mkdir; create a leaf directory and all intermediate ones.
        Works like mkdir, except that any intermediate path segment (not
        just the rightmost) will be created if it does not exist.  This is
        recursive.
        """
        head, tail = path.split(name)
        if not tail:
            head, tail = path.split(head)
        if head and tail and not path.exists(head):
            try:
                makedirs(head, mode)
            except OSError, e:
                # be happy if someone already created the path
                if e.errno != errno.EEXIST:
                    raise
            if tail == curdir:           # xxx/newdir/. exists if xxx/newdir exists
                return
        mkdir(name, mode)

### 9.2 def opensubprocess(self, args, passphrase=False) vuln

#### 9.2.1 L515: unvalidated_user_input

    cmd.extend(args)

cmd is a list of strings, eventually joined with cmd=' '.join(cmd), and the args are unvalidated in this function. Then this concatenation of args is fed directly into subprocess.Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE). THIS SHOULD BE PATCHED.

### 9.3 def collectoutput(self, process, result, writer=None, stdin=None)

sends stdout to self.readdata() and stderr to self.readresponse()

### 9.4 def handleio(self, args, file, result, passphrase=None, binary=False) vuln cleanup

#### 9.4.1 L601: unvalidated_user_input type_check_in_call

    p = self._open_subprocess(args, passphrase is not None)

you shouldn't assign or type check in a function call

### 9.5 def sign(self, message, **kwargs) cleanup

#### 9.5.1 L617-619: hanging_fd

calls self.makebinarystream(), which leaves the file descriptor for the encoded message to be encrypted hanging between scopes.

### 9.6 def signfile(self, file, keyid=None, passphrase=None, clearsign=True, detach=False, binary=False) cleanup

#### 9.6.1 L632-635: bad_logic

    if detach:
        args.append("--detach-sign")
    elif clearsign:
        args.append("--clearsign")

the logic here allows that if a user erroneously specifies both options, rather than doing what the system gnupg would do (that is, do –clearsign, and ignore the –attach-sign), python-gnupg would ignore both.

#### 9.6.2 L626-641:

input 'args' into self.opensubprocess() is defined as static strings.

### 9.7 def verify(self, data): cleanup

#### 9.7.1 L668-670: hanging_fd

same hanging file descriptor problem as in self.sign()

### 9.8 def verifyfile(self, file, datafilename=None) vuln cleanup

#### 9.8.1 L683: hanging_fd

more potentially hanging file descriptors…

#### 9.8.2 L684: hanging_fd

oh look, another hanging file descriptor. imagine that.

#### 9.8.3 L690: unvalidated_user_input

    args.append('"%s"' % data_filename)

well, there's the exploit. see included POC script.

### 9.9 def importkeys(self, keydata) vuln

#### 9.9.1 L749: unvalidated_user_input

this function could potentially allow an attacker with a GPG exploit to use it, because it passes key generation parameter directly into the internal packet parsers of GPG. however, without a GPG exploit for one of the GPG packet parsers (for explanation of GPG packets look into pgpdump), this function alone is not exploitable.

### 9.10 def recievekeys(self, keyserver, *keyids) vuln

#### 9.10.1 L770: unvalidated_user_input

    args.extend(keyids)

### 9.11 def exportkeys(self, keyids, secret=False) vuln

#### 9.11.1 L795-796: unvalidated_user_input

args problem again. exploitable though parameter ``keyids``.

### 9.12 def listkeys(self, secret=False)

#### 9.12.1 L827:

args is static string.

### 9.13 def genkey(self, input) cleanup

#### 9.13.1 L864:

args, passed to self.handleio(), which in turn passes args directly to Popen(), is set to a static string. this function is halfway okay, though it really could be more careful with the ``input`` parameter.

### 9.14 def genkeyinput(self, **kwargs) vuln

#### 9.14.1 L981-983: unvalidated_user_input

this function could potentially allow an attacker with a GPG exploit to use it, because it passes key generation parameter directly into the internal packet parsers of GPG. however, without a GPG exploit for one of the GPG packet parsers (for explanation of GPG packets look into pgpdump), this function alone is not exploitable.

### 9.15 def encryptfile(self, file, recipiencts, sign=None, …) vuln

#### 9.15.1 L939: unvalidated_user_input

several of the inputs to this function are unvalidated, turned into strings, and passed to Popen(). exploitable.

### 9.16 def encrypt(self, data, recipients, **kwargs): vuln

#### 9.16.1 L997: unvalidated_user_input

exploitable, passes kwargs to self.encryptfile()

### 9.17 def decrypt(self, message **kwargs): vuln

#### 9.17.1 L1003: unvalidated_user_input

kwargs are passed to self.decryptfile(), unvalidated, making this function also exploitable

### 9.18 def decryptfile(self, file, alwaystrust=False, passphrase=None, output=None) vuln

#### 9.18.1 L1013: unvalidated_user_input

unvalidated user input: this function is also exploitable

## 10 POC

CANNOT INCLUDE FILE ../python-gnupg-0.3.1/python-gnupg-exploit.py

Date: 2013-02-01 Fri

Author: isis

isis@leap.se

Org version 7.9.2 with Emacs version 24

Validate XHTML 1.0