如果返回一个或没有数据库行,那么身份验证中的 sql where 查询是否容易受到定时攻击?

信息安全 验证 php 定时攻击
2021-09-05 14:18:43

假设我有此代码进行身份验证。

   $me = mysql_query("SELECT * from users WHERE id='$_COOKIE[userid]' && password ='$_COOKIE[pass]'") or die (mysql_error());
   $me = mysql_fetch_array($me);

要对网站的用户进行身份验证,用户计算机上的某些内容必须与服务器上的内容相匹配(这个问题不是关于防止 cookie 窃取,因为所有网站都可以窃取 cookie),但是有没有办法使用 sql where to检查它是否在数据库中匹配而不容易受到定时攻击?

如果有人将他们的 cookie 修改为不同的用户名和密码以尝试使用计时攻击找出其他人的密码怎么办?如果$me产生 0 个结果而不是 1 个结果,这会改变运行查询所需的平均时间吗?

如果是,我应该如何更改我的代码,因为我无法弄清楚?我是否应该在上面的代码中添加一个随机延迟,它是否会有所作为,因为我阅读它不会。

4个回答

在查询索引数据库字段时防止定时攻击的一种方法是在服务器端散列值。这类似于攻击者在获得密码哈希后无法登录的情况,因为服务器在检查数据库之前仍会​​应用哈希。

正如marstato的回答所指出的那样,原始代码(来自上面的问题)有很多问题,所以让我们从这段代码开始:

$token = $_COOKIE['token'];
$db->query("SELECT COUNT(*) FROM sessions WHERE token = ?", $token);

攻击者确实可以逐步找到有效的令牌。这通常很难或不可能,但从理论上讲,这是可行的。攻击者会查询“A”,服务器会向数据库询问“A”。然后他们尝试“B”,服务器向数据库询问“B”。如果“A”更快,他们现在尝试“AA”和“AB”。如果速度较慢,他们可能会尝试“BA”和“BB”等。还有更多内容,但概括一下定时攻击的一般概念,这就是它的工作原理。

要解决此问题,您可以这样做:

$secret = 'qJHcsgkED0egeuljhsZr'; // randomly generated
$hashed_token = hash_hmac('sha256', $_COOKIE['token'], $secret);
$db->query("SELECT COUNT(*) FROM sessions WHERE hashed_token = ?", $hashed_token);

让我们再次尝试攻击:攻击者尝试“A”,服务器在数据库中查询“a661024ef...”。攻击者尝试“B”,服务器在数据库中查询“5407624cb ...”。时间是相等的(或至少是随机的),因为两者都不存在,也永远不会引导攻击者获得有效的令牌。

当然,这取决于$secret剩余的秘密。最好的做法是定期更改此设置,以便老员工无法知道秘密并应用攻击,但这确实意味着您还必须更改数据库中的所有值。如果数据库可以执行 HMAC,您可以通过执行以下操作来解决此问题:

$secure_rnd = openssl_random_pseudo_bytes(16);
$randomized_token = hash_hmac('sha256', $_COOKIE['token'], $secure_rnd);
$db->query("SELECT COUNT(*) FROM sessions WHERE hmac(token, '$secure_rnd') = ?", $randomized_token);

在安全方面,这是最好的方法,原因有以下三个:

  • 虽然它在速度上与非索引字段相当,但它可以防止未来的数据库管理员意外索引该字段。通常,人们会在用于查找时索引一个字段。这会导致定时攻击,但是你会如何告诉你未来的数据库管理员呢?最好有代码,这样字段是否被索引都没有关系
  • 如果该字段仅仅是非索引的,仍然可能存在攻击:数据库将遍历每一行并比较值。遍历每一行并比较第一个字符是 O(n),但如果数据库比较更多字符(因为有多个字符匹配),则需要稍长的时间,因此攻击者可以(再次,理论上)观察到这种较慢的响应并了解到他们猜对了任何标记的不止一个字符。使用$secretor$secure_rnd不易受此影响。
  • 它不依赖于$secret旧系统管理员可能知道的静态。

缺点是它是一个 O(n) 慢查询。

一种混合方法,在避免使用 static 的同时利用索引$secret,可能是使用$secret每天更改的 a。在每晚的过渡期间,应用服务器可以先尝试今天的秘密,然后作为后备,尝试昨天的秘密。由于您的令牌以及散列方法应该是安全的(否则您会遇到更大的问题),因此寻找两个值永远不会导致与其他令牌的冲突。更新每条数据库记录后,它就可以停止查询旧的$secret. 更新数据库的 cron 作业可以将机密和升级状态写入配置文件。


也就是说,我从未听说过有人使用这样的定时攻击。比较像 PHP 这样的语言的密码,是的,但不是数据库查询。我知道的大多数会话令牌系统都以这种理论上不安全的方式工作,但它们似乎从未被黑客入侵。这并不意味着它是不可能的,特别是如果它在理论上看起来 100% 可能,那么迟早会有人让它在实践中发挥作用。虽然我不确定今天对于小型网站是否需要所有这些努力,但保护高安全性系统会很好。

不要担心定时攻击。0 和 1 行之间的差异是微乎其微的。

但是,您发布的代码还有许多其他问题:

  • 不要让攻击者发送足够多的请求,以便他们可以利用这里的时间差异。在少量auf身份验证尝试失败后阻止人员
  • $_COOKIE 完全由攻击者控制。您在这里暴露了一个 SQL 注入漏洞,任何半专业的渗透测试人员都可以在几分钟内利用该漏洞
  • 不要在任何地方以明文形式存储密码。不要在您的数据库中执行此操作,并且确保不要将其存储在 Cookie 中,这样它很容易被公众看到。相反,为每个完全随机(参见 CSPRNG)和大(256 位或更多)的用户存储一个永久登录令牌。将其用于基于 cookie 的身份验证
  • 使用盐作为密码。按 id 选择用户记录,然后比较密码。这样做也会减少定时攻击的表面

正确的做法是将识别信息分成两半。前半部分是您查询的内容,后半部分是使用 bcrypt(或 pbkdf2、scrypt 或等效项)加密的秘密。

要理解的关键是在大多数数据库中不可能使查询保持恒定时间,因此您需要解决这个事实。

例如,假设您想创建一个安全的邀请码,用户可以使用邀请码注册并创建一个帐户,您可以像这样构建您的邀请数据库表:

CREATE TABLE invites (
  id SERIAL,
  token_first_half VARCHAR,
  token_second_half VARCHAR CHECK (pass like '$2$12$%'), -- Must be bcrypt version 2 with strength of 12
)

用户将获得一个邀请码XY0F-CD37-HZ5J-KL6P,例如 where XY0F-CD37istoken_first_halfHZ5J-KL6Pis token_second_half您可能希望使用人类可读的base32( https://www.crockford.com/base32.html ) 对这些令牌进行编码。

当用户提供令牌时,您会将其分成两半,并使用前半部分查询数据库,然后通过使用 bcrypt ( https://en.wikipedia.org/wiki/Bcrypt )检查后半部分来验证它. Bcrypt 将在恒定时间内执行此操作,从而避免了定时攻击。

这里重要的一点是,前半部分对时间攻击是开放的,但这无关紧要,因为它只使用选择器来访问我们实际使用的 bcrypt 字符串。

对于您的特定示例,前半部分是用户名,后半部分是密码。请注意,用户名仍然容易受到时间攻击,这可用于用户枚举。

如果由于某种原因无法将令牌分成两半,或者出于性能原因(例如处理 cookie/会话)无法使用 bcrypt,那么您需要使用 HMAC 预先验证秘密令牌。大多数 Web 框架都有一个“安全 cookie”功能,可用于获取和设置 HMAC 安全 cookie 以进行会话管理。只有在 HMAC 被验证后,您才进行数据库查询。这通过拒绝攻击者足够的查询来获取时序信息来防止时序攻击。

如果你想确定,那么你可以做

`SELECT * FROM Users`

然后在针对整个列表检查密码的代码中枚举整个列表。那将接近足够的恒定时间。

我的 PHP 有点生锈,但类似于

$authUser = NULL;
$users = mysql_query("SELECT * from users");
$users = mysql_fetch_array($users);
foreach($users as $user){
    if ($user['id'] = $_COOKIE[userid]' && $user['password']='$_COOKIE[pass]'){
        $authUser = $user;
    }
}

if(is_null($authUser))
     // whatever

但是我仍然认为将实际密码哈希存储在 cookie 中是错误的。这意味着不更改密码就不可能撤销会话。任何类型的会话标识符即使具有 100 年的有效期仍然允许清除会话或更改到期而不更改密码