#+TITLE:     python-gnupg audit
#+AUTHOR:    isis
#+EMAIL:     isis@leap.se
#+DATE:      2013-02-01 Fri
#+DESCRIPTION:
#+KEYWORDS:
#+LANGUAGE:  en
#+OPTIONS:   H:3 num:t toc:t \n:nil @:t ::t |:t ^:t -:t f:t *:t <:t
#+OPTIONS:   TeX:t LaTeX:t skip:nil d:nil todo:t pri:nil tags:not-in-toc
#+INFOJS_OPT: view:nil toc:2 ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js
#+EXPORT_SELECT_TAGS: export
#+EXPORT_EXCLUDE_TAGS: noexport
#+LINK_UP:   
#+LINK_HOME: 
#+XSLT:

[2013-02-01 Fri]

* gnugp.__main__()
** comments
L58 NullHandler?? see self._write_passphrase
L61 there nifty check for p3k
** def _copy_data(instream, outstream)                              :cleanup:
   copies data from one stream to another, 1024 bytes at a time.
*** L79:                                                          :bad_logic:
    instream is apparently a file descriptor, but is not checked nor
    encased in a try/except block.

*** L78:                                               :hanging_fd:bad_logic:
    while True: loop, should be 
    : with open(instream) as instrm:
*** L88:                                             :bad_exception_handling:
    : except: 
    should catch an IOError, or whatever specific error is raised for broken
    pipes.
** def _threaded_copy_data(instream, outstream):
*** L99:
    this just wraps self._copy_data in a thread
** def _write_passphrase(stream, passphrase, encoding):        :vuln:cleanup:
*** L110:                                         :writes_passphrase_to_disk:
    logger writes passphrase into debug log. this should be patched.
* class Verify(object)
  basic parsing class, no errors found
* class ImportResult(object)
  basic parsing class, no errors found
* class ListKeys(list):
  basic parsing class, no errors found
* class Crypt(Verify):
  basic parsing class, no errors found
** def __init__(self, gpg)                                          :cleanup:
*** L338                                                       :mro_conflict:

    #+BEGIN_SRC python
    Verify.__init__(self,gpg)
    #+END_SRC

    should be changed to:

    #+BEGIN_SRC python
    super(Verify, self).__init__(gpg)
    #+END_SRC
* class GenKey(object)
  basic parsing class, no errors found
* class DeleteResult(object)
  basic parsing class, no errors found
* class Sign(object)
  basic parsing class, no errors found
* class GPG(object)                                             :exploitable:
*** L474:                                                           :cleanup:
    : cls.__doc__ 
    should go directly underneath class signature
** def __init__(self, gpgbinary='gpg', gnupghome=None, verbose=False, use_agent=False, keyring=None) :bug:
*** L494-495:                                                    :type_error:
    
    #+BEGIN_SRC python
    if gnupghome and not os.path.isdir(self.gnupghome):
        os.makedirs(self.gnupghome,0x1C0)
    #+END_SRC

    #+BEGIN_EXAMPLE
    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:
    #+END_EXAMPLE

    #+BEGIN_SRC python
    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)
    #+END_SRC

** def _open_subprocess(self, args, passphrase=False)                  :vuln:
*** 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.

** def _collect_output(self, process, result, writer=None, stdin=None)
   sends stdout to self._read_data() and stderr to self._read_response()

** def _handle_io(self, args, file, result, passphrase=None, binary=False) :vuln:cleanup:
*** 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

** def sign(self, message, **kwargs)                                :cleanup:
*** L617-619:                                                    :hanging_fd:
   calls self._make_binary_stream(), which leaves the file descriptor for
   the encoded message to be encrypted hanging between scopes.

** def sign_file(self, file, keyid=None, passphrase=None, clearsign=True, detach=False, binary=False) :cleanup:
*** L632-635:                                                     :bad_logic:
    #+BEGIN_SRC python
    if detach:
        args.append("--detach-sign")
    elif clearsign:
        args.append("--clearsign")
    #+END_SRC

    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.

*** L626-641:
    input 'args' into self._open_subprocess() is defined as static strings.
    
** def verify(self, data):                                          :cleanup:
*** L668-670:                                                    :hanging_fd:
    same hanging file descriptor problem as in self.sign()

** def verify_file(self, file, data_filename=None)             :vuln:cleanup:
*** L683:                                                        :hanging_fd:
    more potentially hanging file descriptors...
*** L684:                                                        :hanging_fd:
    oh look, another hanging file descriptor. imagine that.
*** L690:                                            :unvalidated_user_input:
    : args.append('"%s"' % data_filename)
    well, there's the exploit. see included POC script.

** def import_keys(self, key_data)                                     :vuln:
*** 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.    

** def recieve_keys(self, keyserver, *keyids)                          :vuln:
*** L770:                                            :unvalidated_user_input:
    : args.extend(keyids)

** def export_keys(self, keyids, secret=False)                         :vuln:
*** L795-796:                                        :unvalidated_user_input:
    args problem again. exploitable though parameter ``keyids``.

** def list_keys(self, secret=False)
*** L827:
    args is static string.

** def gen_key(self, input)                                         :cleanup:
*** L864:
    args, passed to self._handle_io(), 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.

** def gen_key_input(self, **kwargs)                                   :vuln:
*** 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.

** def encrypt_file(self, file, recipiencts, sign=None, ...)           :vuln:
*** L939:                                            :unvalidated_user_input:
    several of the inputs to this function are unvalidated, turned into
    strings, and passed to Popen(). exploitable.

** def encrypt(self, data, recipients, **kwargs):                      :vuln:
*** L997:                                            :unvalidated_user_input:
    exploitable, passes kwargs to self.encrypt_file()

** def decrypt(self, message **kwargs):                                :vuln:
*** L1003:                                           :unvalidated_user_input:
    kwargs are passed to self.decrypt_file(), unvalidated, making this
    function also exploitable

** def decrypt_file(self, file, always_trust=False, passphrase=None, output=None) :vuln:
*** L1013:                                           :unvalidated_user_input:
    unvalidated user input: this function is also exploitable

* POC
#+INCLUDE: "../python-gnupg-0.3.1/python-gnupg-exploit.py" python
