一般攻击场景:
在2013年,Django有一个普遍的漏洞,因为攻击者可以通过非常大的密码创建极其密集的CPU计算[请参阅此处的安全通知]。我不确定在使用 PHP 的 password_verify() 和其他密码哈希方法而无需任何进一步检查的情况下这是否仍然可行。
PHP文档说:
使用 algo 参数的 PASSWORD_BCRYPT将导致密码参数被截断为最大长度 72 个字符。
但是,PHP 的代码可能会说一些不同的东西:
然而,PHP 5.5.0 的 password_verify() 函数背后的 C 代码并没有直接限制传递的参数(也许在 bcrypt 算法内部的更深层次上?此外,PHP 实现不限制参数。
问题:
password_verify()(以及同一函数集的其他函数)是否容易受到 DoS 的最大化 POST 参数的攻击?还请考虑 POST 上传大小远大于 4MB 的站点范围的配置情况。
加密算法内部限制为 72 个字符。
要了解原因,让我们看看crypt()
的来源: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;
}
password
字段是一个简单的char*
字段。所以没有长度信息。传递的只是一个普通指针。
因此,如果我们坚持下去,我们最终将降落在BF_set_key
.
重要的部分是循环:
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
定义为 16。所以外循环将循环 18 次(BF_N + 2
)。
内部循环将循环 4 次。 4 * 18 == 72。
有了它,只会读取密钥的 72 个字符。没有了。
注意
现在,该算法有一个有趣的副作用。因为它使用 C 字符串(以 '0
空字节结尾的字符串),所以它不可能使用过去 '0
的任何内容。因此,包含空字节的密码将丢失通过它的任何熵。示例:http://3v4l.org/Y6onV