Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is PHP's password_verify() safe against extremely long passwords (DoS attack)?

Tags:

c

security

php

hash

The general attack scenario:

In 2013 Django had a general vulnerability as an attacker could create extremely intense CPU calculations via very large passwords [see the security notice here]. I'm unsure if this is still possible when using PHP's password_verify() and other password-hashing methods without any further checks.

The PHP documentation says:

Using the PASSWORD_BCRYPT for the algo parameter, will result in the password parameter being truncated to a maximum length of 72 characters.

But, PHP's code MAYBE says something different:

The C code behind PHP 5.5.0's password_verify() function however does not limit the passed argument directly (maybe on a deeper level inside the bcrypt algorithm ?). Also, the PHP implementation does not limit the argument.

The question:

Is password_verify() (and other functions of the same function set) vulnerable against DoS via maxed out POST parameters ? Please also consider site-wide config situations of POST upload sizes much larger than 4MB.

like image 228
Sliq Avatar asked Mar 09 '15 20:03

Sliq


1 Answers

The password is limited to 72 characters internally in the crypt algorithm.

To see why, let's look at crypt()'s source: ext/standard/crypt.c

    } else if (
            salt[0] == '$' &&
            salt[1] == '2' &&
            salt[3] == '$') {
        char output[PHP_MAX_SALT_LEN + 1];

        memset(output, 0, PHP_MAX_SALT_LEN + 1);

        crypt_res = php_crypt_blowfish_rn(password, salt, output, sizeof(output));
        if (!crypt_res) {
            ZEND_SECURE_ZERO(output, PHP_MAX_SALT_LEN + 1);
            return NULL;
        } else {
            result = zend_string_init(output, strlen(output), 0);
            ZEND_SECURE_ZERO(output, PHP_MAX_SALT_LEN + 1);
            return result;
        }

The password field is a simple char* field. So there's no length information. All that's passed is a normal pointer.

So if we follow that through, we'll eventually land at BF_set_key.

The important part is the loop:

for (i = 0; i < BF_N + 2; i++) {
    tmp[0] = tmp[1] = 0;
    for (j = 0; j < 4; j++) {
        tmp[0] <<= 8;
        tmp[0] |= (unsigned char)*ptr; /* correct */
        tmp[1] <<= 8;
        tmp[1] |= (BF_word_signed)(signed char)*ptr; /* bug */

        if (j)
            sign |= tmp[1] & 0x80;
        if (!*ptr)
            ptr = key;
        else
            ptr++;
    }
    diff |= tmp[0] ^ tmp[1]; /* Non-zero on any differences */

    expanded[i] = tmp[bug];
    initial[i] = BF_init_state.P[i] ^ tmp[bug];
}

BF_N is defined to be 16. So the outer loop will loop 18 times (BF_N + 2).

The inner loop will loop 4 times. 4 * 18 == 72.

And there you have it, only 72 characters of the key will be read. No more.

Note

Now, there's an interesting side-effect to that algorithm. Because it uses C-Strings (strings terminated by a \0 null byte), it's impossible for it to use anything past \0. So a password that contains a null-byte will lose any entropy past it. Example: http://3v4l.org/Y6onV

like image 99
ircmaxell Avatar answered Nov 12 '22 09:11

ircmaxell