问题: 在 PHP 8.1.16 上, crypt() 的行为发生了变化,因此如果使用某些 CRYPT_BLOWFISH 不兼容的盐调用它,它会返回 *0 而不是哈希值。如果我们将系统升级到 8.1.16,我们将无法再检查条目。有没有办法在较新的 PHP 版本上复制 crypt() 的旧行为?如果没有,有没有办法可以利用下面用例部分中的数据?
脚本示例:
$salt = '$2a$05$SomeBadSaltHasDollar/$';
$credential = 'my password';
echo crypt($credential, $salt);
// PHP < 8.1.16 output: $2a$05$SomeBadSaltHasDollar/.ry.LY8GXnJWLU9/BIJ5I4VJPRBlp6z.
// PHP >= 8.1.16 output: *0
我需要来自 <8.1.16
的哈希值用例:
考虑下表是使用 PHP 版本生成的 < 8.1.16
哈希 | 重要价值 |
---|---|
$2a$05$SomeBadSaltHasDollar/.ry.LY8GXnJWLU9/BIJ5I4VJPRBlp6z。 | 苹果 |
$2a$05$SomeBadSaltHasDollar/.WguJW8WYSY/4UQb/W/.NJjLxnAQiN12 | 橙色 |
$2a$05$SomeBadSaltHasDollar/.q.uf.A8phuGuZAVL3M9NQ3744jT18oW | 香蕉 |
对于插入此表:
Hash
(crypt()
的输出)+ ImportantValue
检查条目:
crypt()
SELECT ImportantValue FROM table WHERE Hash = {$hashedCredential};
对
crypt()
的调用返回了我们认为包含所使用的原始盐的内容 - 在上面的示例中,似乎 crypt() 只是用 . 替换了 $,但情况并非如此:
$salt = '$2a$05$SomeBadSaltHasDollar/$';
$credential = 'my password';
$hash = crypt($credential, $salt);
// PHP < 8.1.16 output: $2a$05$SomeBadSaltHasDollar/.ry.LY8GXnJWLU9/BIJ5I4VJPRBlp6z.
$saltPartOfHash = substr($hash, 0, 29);
$newHash = crypt($credential, $saltPartOfHash);
echo $newHash == $hash; // different behavior when using hash containing . instead of $
好吧,让我们看一下 PHP 源码树。
diff --git a/php-8.1.15/ext/standard/crypt_blowfish.c b/php-8.1.16/ext/standard/crypt_blowfish.c
index 3806a29..351d403 100644
--- a/php-8.1.15/ext/standard/crypt_blowfish.c
+++ b/php-8.1.16/ext/standard/crypt_blowfish.c
@@ -371,7 +371,6 @@ static const unsigned char BF_atoi64[0x60] = {
#define BF_safe_atoi64(dst, src) \
{ \
tmp = (unsigned char)(src); \
- if (tmp == '$') break; /* PHP hack */ \
if ((unsigned int)(tmp -= 0x20) >= 0x60) return -1; \
tmp = BF_atoi64[tmp]; \
if (tmp > 63) return -1; \
@@ -399,13 +398,6 @@ static int BF_decode(BF_word *dst, const char *src, int size)
*dptr++ = ((c3 & 0x03) << 6) | c4;
} while (dptr < end);
- if (end - dptr == size) {
- return -1;
- }
-
- while (dptr < end) /* PHP hack */
- *dptr++ = 0;
-
return 0;
}
以前,salt 解码例程会在遇到
$
时提前退出,并用零填充输出缓冲区的其余部分,现在它只会出错。
输入盐中的.
将被转换为6位零,因此可以用来实现与
crypt()
相同的结果,但它需要更多的工作。由于输入字符的消耗方式,它实际上也会影响
$
之前的字符。这一切都取决于末尾 22 个字符部分中
$
字符的偏移量。为了使这种依赖性更加明显,我们可以在美元前面使用几个
9
,因为
9
会翻译为
63
(
0x3f
):
$credential = 'my password';
echo crypt($credential, '$2a$05$SomeDollarSalt9999999$')."\n";
echo crypt($credential, '$2a$05$SomeDollarSalt999999..')."\n\n";
echo crypt($credential, '$2a$05$SomeDollarSalt999999$9')."\n";
echo crypt($credential, '$2a$05$SomeDollarSalt999999..')."\n\n";
echo crypt($credential, '$2a$05$SomeDollarSalt99999$99')."\n";
echo crypt($credential, '$2a$05$SomeDollarSalt99996...')."\n\n";
echo crypt($credential, '$2a$05$SomeDollarSalt9999$999')."\n";
echo crypt($credential, '$2a$05$SomeDollarSalt999u....')."\n\n";
在 8.1.16 之前的 PHP 版本中,打印:
$2a$05$SomeDollarSalt9999999.hm2CRfKZ6A8x3vd6BRDTNlN3DDJPW3e
$2a$05$SomeDollarSalt999999..hm2CRfKZ6A8x3vd6BRDTNlN3DDJPW3e
$2a$05$SomeDollarSalt999999$uhm2CRfKZ6A8x3vd6BRDTNlN3DDJPW3e
$2a$05$SomeDollarSalt999999..hm2CRfKZ6A8x3vd6BRDTNlN3DDJPW3e
$2a$05$SomeDollarSalt99999$9uGElX9NYrj.R45tRnH.xGf936EuwfQ8W
$2a$05$SomeDollarSalt99996...GElX9NYrj.R45tRnH.xGf936EuwfQ8W
$2a$05$SomeDollarSalt9999$99uqedqg9AH7TRnGTjNVhwYkmNJOSW5DpC
$2a$05$SomeDollarSalt999u....qedqg9AH7TRnGTjNVhwYkmNJOSW5DpC
预置的盐值不同,因此我们稍后必须手动修复它,但实际的计算结果已经相同。请注意,在前两种情况下,销售中完全相同的字符必须用点替换,尽管美元符号位于不同的位置。在后两种情况下,我们如何分别将 9
变成
6
和
u
。这是因为早期的
break
留下了一些先前的输入位未消耗。具体来说,输入字符被转换为“itoa”字符串
./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
中的“索引”,并且在早期
break
的情况下,仅分别消耗
0x30
或
0x3c
位。为了解决这个问题,我们首先使用简单的
strpos()
模拟相同的查找,应用相同的位掩码,然后通过索引到字符串中将值转换回字符。现在我们已经准备好了重新计算的盐,但正如我们在上面看到的,盐值被逐字添加到输出之前。或者几乎逐字逐句,因为您已经注意到您的
$
变成了
.
。但前提是它是最后一个字符,否则它会被保留。但无论如何,最后一个字符都会变形,没有
9
全部变成
u
。这里的概念完全相同,22 不能被 4 整除,所以我们最终会在盐中得到一些没有被消耗的位,我们可以使用与上面相同的“itoa”字符串索引来修复它。有了这些知识,我们就可以构建传统的盐兼容层:
function BF_crypt_with_legacy_salt($string, $salt)
{
// PHP <=8.1.15 compat: check for $2a$ blowfish salt with dollar
// If this confuses you, go diff ext/standard/crypt_blowfish.c between PHP 8.1.15 and 8.1.16
if(strlen($salt) == 29 && substr($salt, 0, 4) == '$2a$' && $salt[6] == '$')
{
$pos = strpos($salt, '$', 7);
if($pos !== false)
{
$pos -= 7;
$itoa = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
// Transform provided salt into something that matches the decoding from PHP 8.1.15
if(($pos & 0x2) == 0)
{
// Offsets 0 and 1: truncate down to even length before the $
$cryptsalt = substr($salt, 0, 7 + ($pos & 0x1e));
}
else
{
// Offsets 2 and 3: truncate to two before the $, and transform the char before the $
$val = strpos($itoa, $salt[7 + ($pos - 1)]);
if($val === false)
{
return '*0';
}
$cryptsalt = substr($salt, 0, 7 + ($pos - 1)).$itoa[$val & (($pos & 0x1) == 0 ? 0x30 : 0x3c)];
}
for($i = 29 - strlen($cryptsalt); $i > 0; --$i)
{
$cryptsalt .= '.';
}
// But we'll actually need a different salt value to prepend to the output
$last = strpos($itoa, $salt[28]);
$textsalt = substr($salt, 0, 28).$itoa[$last === false ? 0 : ($last & 0x30)];
// Do the actual crypt() call
$crypt = crypt($string, $cryptsalt);
// Restore original salt in output
if(substr($crypt, 0, 29) == $cryptsalt)
{
$crypt = $textsalt.substr($crypt, 29);
}
return $crypt;
}
}
// Otherwise just pass through unchanged
return crypt($string, $salt);
}
您可以在这个 3v4l 代码片段中看到针对与上面相同的输入运行的结果