保存此数据:使用 PHP 8.1.16 之前的坏盐复制 crypt() 行为

问题描述 投票:0回答:1

问题: 在 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 香蕉

对于插入此表:

  1. 我们接受凭证和一些重要值作为输入
  2. 我们会使用预先保存的盐(使用河豚)。不幸的是,这不是河豚兼容的盐,我们当时没有意识到这一点(没有抛出错误)。
  3. 我们将插入
    Hash
    crypt()
    的输出)+
    ImportantValue

检查条目:

  1. 用户将提供他们的凭据
  2. 我们将使用相同的预先保存的盐来使用
    crypt()
  3. 生成哈希值
  4. 我们将在数据库中搜索包含该哈希的所有记录:
    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 crypt blowfish
1个回答
0
投票

好吧,让我们看一下 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 代码片段中看到针对与上面相同的输入运行的结果

© www.soinside.com 2019 - 2024. All rights reserved.