In layman's terms, a backdoor is *usually* something a computer criminal leaves behind in your system after they've broken in the first time, to save them the trouble of breaking in the hard way in the future. However, backdoors can be also be an intentional security vulnerability inserted into software projects with the hopes that doing so will give the attacker access to your system. See also: [Juniper](http://blog.cryptographyengineering.com/2015/12/on-juniper-backdoor.html). We're going to talk about the second type of backdoor. > There is some programming ahead, but if you can't read code just skip the code blocks and I'll explain what's going on afterwards. # The Underhanded Crypto Contest Starting in 2015, the Crypto & Privacy Village at the [DEFCON Hacking Conference](https://www.defcon.org/) is the home to the [Underhanded Crypto Contest](https://underhandedcrypto.com), a competition to discover and document the best ways to subtly subvert crypto code (and a cryptography focused spiritual successor to the [Underhanded C Contest](http://www.underhanded-c.org)). At DEFCON 23 there were two tracks to this contest: 1. Backdoor GnuPG. 2. Backdoor password authentication. I submitted an entry into the second track (and won). I'm going to explain how my entry works, what tricks I employed to make it plausibly deniable, and some of its immediate implications on software development. # How We Designed Our Password Authentication Backdoor Before I begin, let me say something to any government employees who find this blog post years from now and is considering hiring Paragon Initiative Enterprises to implement a backdoor (or "secure golden key" as they like to call it): **We're not interested.** ## Step One: Fabricate an Excellent Cover Story Right before DEFCON 23, cryptographer Scott Contini posted a blog post about [user account enumeration via exploiting timing side-channels](https://littlemaninmyhead.wordpress.com/2015/07/26/account-enumeration-via-timing-attacks), which work like this: 1. You are attempting to log in to the web app with a username and password. 2. Is the username registered? If yes, continue. Otherwise, say "bad username/password". 3. Verify the password, which is probably [properly hashed with **bcrypt**](https://paragonie.com/blog/2015/08/you-wouldnt-base64-a-password-cryptography-decoded); otherwise say "bad username/password". 4. If step 3 proceeds, the user is authenticated. Failing at step two will take measurably less time (from an attacker's perspective) than failing at step three. By doing so, an attacker can send a bunch of requests and figure out valid usernames, even if the rest of the application is secure. Timing leaks are a back door gold mine. Most developers don't understand them, and most information security professionals are not programmers. Even if you write [obviously insecure cryptography-related code](http://www.openwall.com/lists/oss-security/2015/11/08/1), most developers will probably OK it because they don't know better. But the contest would have been boring if we did that. So far, our master plan looks like this: 1. Propose a solution that pretends to solve the "account enumeration via timing attacks" vulnerability. 2. Hide a backdoor in our solution. 3. Make sure a casual review from an average developer wouldn't raise any red flags. ## Step Two: The Design Phase The `TimingSafeAuth` class is reproduced here, in its entirety: db = $db; $this->dummy_pw = password_hash(noise(), PASSWORD_DEFAULT); } /** * Authenticate a user without leaking valid usernames through timing * side-channels * * @param string $username * @param string $password * @return int|false */ public function authenticate($username, $password) { $stmt = $this->db->prepare("SELECT * FROM users WHERE username = :username"); if ($stmt->execute(['username' => $username])) { $row = $stmt->fetch(\PDO::FETCH_ASSOC); // Valid username if (password_verify($password, $row['password'])) { return $row['userid']; } return false; } else { // Returns false return password_verify($password, $this->dummy_pw); } } } When the `TimingSafeAuth` class is instantiated, it unavoidably creates a "dummy password", derived from a function called `noise()` (adapted from [AnchorCMS](https://github.com/anchorcms/anchor-cms/blob/4d46856a09a05d32f9c6df5254b0f4d478fbe84a/system/helpers.php#L49-L59), defined below): /** * Generate a random string with our specific charset, which conforms to the * RFC 4648 standard for BASE32 encoding. * * @return string */ function noise() { return substr( str_shuffle(str_repeat('abcdefghijklmnopqrstuvwxyz234567', 16)), 0, 32 ); } > Keep this `noise()` function in mind; it's a key piece of the backdoor. After we have an instantiated `TimingSafeAuth` object available to whatever login script needs it, it will eventually pass a username and password to `TimingSafeAuth->authenticate()`, which will perform a database lookup then do one of two things: 1. If the username was found, validate the provided password with the bcrypt hash on file for the user (using `password_verify()`) 2. Otherwise, invoke `password_verify()` with the provided password and the dummy bcrypt hash. Since `$this->dummy_pw` is the bcrypt hash of a randomly generated string, we can always expect option 2 to fail and return `false`, but it will always take about the same amount of time (thus hiding the timing side-channel), right? ### Vulnerabilities Hidden in Plain Sight Okay, the biggest lie is hidden right here: // Returns false return password_verify($password, $this->dummy_pw); This doesn't always return `false`. If an attacker somehow guessed the dummy password that went into `$this->dummy_pw`, this would return `true`! A correct implementation would be: password_verify($password, $this->dummy_pw); return false; But let's say the auditor gives this the benefit of the doubt. "If the dummy password was hard-coded, this would be a concern, but it's randomly generated so it's totally safe, right?" **NOPE!** `str_shuffle()` isn't a [cryptographically secure pseudorandom number generator](https://paragonie.com/blog/2015/07/how-safely-generate-random-strings-and-integers-in-php). To understand why it's not, you have to look at [how `str_shuffle()` is implemented in PHP](https://github.com/php/php-src/blob/71c19800258ee3a9548af9a5e64ab0a62d1b1d8e/ext/standard/string.c#L5440-L5463): static void php_string_shuffle(char *str, zend_long len) /* {{{ */ { zend_long n_elems, rnd_idx, n_left; char temp; /* The implementation is stolen from array_data_shuffle */ /* Thus the characteristics of the randomization are the same */ n_elems = len; if (n_elems <= 1) { return; } n_left = n_elems; while (--n_left) { rnd_idx = php_rand(); RAND_RANGE(rnd_idx, 0, n_left, PHP_RAND_MAX); if (rnd_idx != n_left) { temp = str[n_left]; str[n_left] = str[rnd_idx]; str[rnd_idx] = temp; } } } See the line that says `rnd_idx = php_rand();`? That's `rand()`, a [trivially crackable linear-congruent generator](https://jazzy.id.au/2010/09/21/cracking_random_number_generators_part_2.html). (See also: [this StackOverflow answer](https://stackoverflow.com/a/15494343/2224584).) So, for a quick recap: * If you guess the dummy password, the `TimingSafeAuth->authenticate()` method will return `true` * The dummy password is generated by an insecure and predictable random number generator (taken from a real-world PHP project) * Only developers intimately familiar with cryptography and PHP's internals would realize that this is dangerous This is useful, but not quite exploitable. Let's gift-wrap our intentional vulnerability in the implementation phase. ## Step Three: Implementing the Backdoor Our login form looks like this (comments preceded with a `#` were added by us in this post, and were not part of the contest entry): authenticate($_POST['username'], $_POST['password']); # Take note of the type cast to (int). if ($userid) { // Success! $_SESSION['userid'] = $userid; header("Location: /"); exit; } } } # This is the login form: require_once dirname(__DIR__).'/secret/login_form.php'; } else { # This is where you want to be: require_once dirname(__DIR__).'/secret/login_successful.php'; } And now for the last code block (`login_form.php`, the code that generates the login form for an unauthenticated user):