paint-brush
永远不要依赖 UUID 进行身份验证:生成漏洞和最佳实践经过@mochalov
2,388 讀數
2,388 讀數

永远不要依赖 UUID 进行身份验证:生成漏洞和最佳实践

经过 Ivan Mochalov8m2024/05/01
Read on Terminal Reader

太長; 讀書

使用 UUID 进行身份验证、发现漏洞以及安全实施策略的风险和最佳实践。
featured image - 永远不要依赖 UUID 进行身份验证:生成漏洞和最佳实践
Ivan Mochalov HackerNoon profile picture
0-item

用于身份验证的 UUID

如今,几乎没有人没有在极度沮丧的情况下点击过“恢复密码”按钮。即使看起来密码确实是正确的,恢复密码的下一步也大多很顺利,只需访问电子邮件中的链接并输入新密码即可(我们不要欺骗任何人;它几乎不是新的,因为在按下讨厌的按钮之前,您已经在第一步中输入了三次密码)。


然而,电子邮件链接背后的逻辑需要仔细审查,因为如果生成过程不安全,就会出现大量漏洞,导致未经授权访问用户帐户。不幸的是,下面是一个基于 UUID 的恢复 URL 结构示例,许多人可能遇到过这种结构,但它并不遵循安全准则:


 https://.../recover/d17ff6da-f5bf-11ee-9ce2-35a784c01695


如果使用这样的链接,通常意味着任何人都可以获得您的密码,就这么简单。本文旨在深入研究 UUID 生成方法,并选择不安全的方法应用它们。

什么是UUID

UUID 是一个 128 位标签,通常用于生成伪随机标识符,具有两个有价值的属性:足够复杂和足够独特。大多数情况下,这些是 ID 离开后端并在前端明确显示给用户或通常通过 API 发送且能够被观察到的关键要求。与 id = 123(复杂性)相比,它使人们难以猜测或暴力破解,并且当生成的 id 与之前使用的重复时可以防止冲突,例如从 0 到 1000 的随机数(唯一性)。


“足够”的部分实际上来自于,首先,通用唯一标识符的一些版本,这为它留下了很小的重复可能性,然而,这很容易通过额外的比较逻辑来缓解,并且由于其发生的条件难以控制,因此不会构成威胁。其次,本文描述了各种 UUID 版本的复杂性,一般来说,除了进一步的极端情况外,它被认为是相当好的。

后端实现

数据库表中的主键似乎依赖于与 UUID 相同的复杂和唯一原则。由于许多编程语言和数据库管理系统广泛采用内置方法来生成 UUID,因此 UUID 通常是识别存储的数据条目的首选,也是连接一般表格和按规范化拆分子表格的字段。通过 API 发送来自数据库的用户 ID 以响应某些操作也是常见的做法,这样可以简化统一数据流的过程,而无需额外生成临时 ID 并将其链接到生产数据存储中的 ID。


就密码重置示例而言,该架构更可能包含一个负责此类操作的表,该表在用户每次单击按钮时插入带有生成的 UUID 的数据行。它通过向与用户关联的地址发送电子邮件(通过其 user_id ),并在打开重置链接后根据用户拥有的标识符检查要为哪个用户重置密码,从而启动恢复过程。但是,对于用户可见的此类标识符,存在安全准则,并且 UUID 的某些实现以不同程度的成功满足了这些准则。

过时的版本

UUID 生成版本 1 将其 128 位拆分为使用生成标识符的设备 MAC 地址 48 位、时间戳 60 位、存储的 14 位用于递增值和 6 位用于版本控制。因此,唯一性保证从代码逻辑中的规则转移到硬件制造商,他们应该为生产中的每台新机器正确分配值。只留下 60+14 位来表示有用的可变有效负载会降低标识符的完整性,尤其是在其背后有如此透明的逻辑的情况下。让我们来看看 UUID v1 的生成序列:


 from uuid import uuid1 for _ in range(8):    print(uuid1())
 d17ff6da-f5bf-11ee-9ce2-35a784c01695 d17ff6db-f5bf-11ee-9ce2-35a784c01695 d17ff6dc-f5bf-11ee-9ce2-35a784c01695 d17ff6dd-f5bf-11ee-9ce2-35a784c01695 d17ff6de-f5bf-11ee-9ce2-35a784c01695 d17ff6df-f5bf-11ee-9ce2-35a784c01695 d17ff6e0-f5bf-11ee-9ce2-35a784c01695 d17ff6e1-f5bf-11ee-9ce2-35a784c01695



可以看出,“-f5bf-11ee-9ce2-35a784c01695”部分始终保持不变。可更改部分只是序列 3514824410 - 3514824417 的 16 位十六进制表示。这是一个表面的例子,因为生产值通常在生成时时间间隔更长,因此与时间戳相关的部分也会发生变化。60 位时间戳部分还意味着标识符的更重要部分在更大的 ID 样本中会明显发生变化。核心要点保持不变:UUIDv1 很容易猜到,无论它最初看起来多么随机。


从给定的 8 个 ID 列表中仅取第一个和最后一个值。由于标识符是严格生成的,因此很明显在给定的两个 ID 之间只生成了 6 个 ID(通过减去十六进制可变部分),并且可以明确找到它们的值。这种逻辑的推断是所谓的三明治攻击背后的底层部分,旨在通过知道这两个边界值来暴力破解 UUID。攻击流程很简单:用户在目标 UUID 生成之前生成 UUID A,并在生成之后生成 UUID B。假设具有静态 48 位 MAC 部分的同一设备负责所有三次生成,它会为用户设置 A 和 B 之间的一系列潜在 ID,目标 UUID 位于其中。根据生成的 ID 与目标之间的时间接近度,范围可以是暴力破解方法可以访问的量:检查每个可能的 UUID 以在空中找到现有的。


在前面描述的密码恢复端点的 API 请求中,这意味着发送数百或数千个带有后续 UUID 的请求,直到找到表明现有 URL 的响应。通过密码重置,它会导致这样一种设置,用户可以在他们尽可能紧密控制的两个帐户上生成恢复链接,以按下他们无权访问但只知道电子邮件/登录名的目标帐户上的恢复按钮。然后知道发往具有恢复 UUID A 和 B 的受控帐户的信件,并且可以在不访问实际重置电子邮件的情况下强制使用目标链接来恢复目标帐户的密码。


漏洞源于仅依赖 UUIDv1 进行用户身份验证的概念。通过发送允许重置密码的恢复链接,可以假设通过点击该链接,用户就被验证为应该接收该链接的人。这是身份验证规则失败的部分,因为 UUIDv1 暴露在直接的暴力攻击下,就像有人知道邻居两扇门的钥匙是什么样子就可以打开自己的门一样。

加密不安全函数

UUID 的第一个版本主要被认为是遗留的,部分原因是生成逻辑仅使用标识符大小的较小部分作为随机值。其他版本(如 v4)试图通过为版本控制保留尽可能少的空间并留下最多 122 位作为随机负载来解决此问题。一般来说,它将总可能的变化带到了惊人的2^122 ,目前被认为满足了关于标识符唯一性要求的“足够”部分,从而满足了安全标准。如果生成实现以某种方式显著减少了随机部分的剩余位,则可能会出现暴力破解漏洞。但是没有生产工具或库,情况应该如此吗?


让我们稍微沉迷于密码学,仔细看看 JavaScript 的 UUID 生成常见实现。这是依赖math.random模块生成伪随机数的randomUUID()函数:

 Math.floor(Math.random()*0x10);


而随机函数本身,简而言之,它只是本文主题感兴趣的部分:

 hi = 36969 * (hi & 0xFFFF) + (hi >> 16); lo = 18273 * (lo & 0xFFFF) + (lo >> 16); return ((hi << 16) + (lo & 0xFFFF)) / Math.pow(2, 32);


伪随机生成需要以种子值为基础,在其上执行数学运算,以产生足够随机的数字序列。此类函数完全基于此,这意味着如果使用与之前相同的种子重新初始化它们,则输出序列将匹配。相关 JavaScript 函数中的种子值包含变量 hi 和 lo,每个变量都是 32 位无符号整数(0 到 4294967295 十进制)。出于加密目的,需要两者的组合,因此几乎不可能通过知道它们的倍数来明确地反转两个初始值,因为它依赖于大数整数分解的复杂性。


两个 32 位整数合在一起,为猜测初始化函数生成 UUID 背后的 hi 和 lo 变量带来了2^64可能的情况。如果以某种方式知道了 hi 和 lo 值,则无需费力复制生成函数,并知道它生成的所有值以及由于种子值暴露而将来会生成的所有值。但是,安全标准中的 64 位在可测量的时间段内可以被视为无法容忍暴力破解,因此它没有意义。与往常一样,问题来自特定的实现。Math.random Math.random()从 hi 和 lo 中分别获取各种 16 位,将其转换为 32 位结果;但是,由于.floor()操作,其上的randomUUID()再次移动了值,并且唯一有意义的部分突然间完全来自 hi。它不会以任何方式影响生成,但会导致加密方法崩溃,因为它仅为整个生成函数种子留下2^32种可能的组合(不需要强制 hi 和 lo,因为 lo 可以设置为任何值并且不会影响输出)。


暴力破解流程包括获取单个 ID 并测试可能生成该 ID 的可能高值。通过一些优化和普通笔记本电脑硬件,它只需几分钟,并且不需要像 Sandwich 攻击那样向服务器发送大量请求,而是离线执行所有操作。这种方法的结果会导致复制后端使用的生成函数状态,以获取密码恢复示例中的所有已创建和未来重置链接。防止漏洞出现的步骤很简单,并且需要使用加密安全函数,例如crypto.randomUUID()

总结

UUID 是一个很棒的概念,它使许多应用领域的数据工程师的工作变得轻松很多。但是,它永远不应该用于身份验证,因为在本文中,它生成技术的某些情况下存在缺陷。这显然并不意味着所有 UUID 都是不安全的。不过,基本方法是说服人们不要将它们用于安全目的,这比在文档中设置复杂的限制来说明使用哪些 UUID 或如何不为此目的生成 UUID 更有效,更安全。