☰
设备ID绕过双因素认证的信任边界漏洞与修复
2026/10/22 6:09:20 网站建设 项目流程

双因素认证被攻破的情况,绝大多数时候并不是因为算法被破解,也不是动态口令本身有致命漏洞,而是业务逻辑在“第二因素”的定义上先松了手。我在授权范围内做过不少应用系统的安全验证,见过一个反复出现的问题:很多系统为了用户体验,加了一个“信任设备/记住这台设备”的功能,开发实现时图省事,直接把设备ID当成第二因素的凭证。设备ID一旦被当成密钥,双因素验证就名存实亡了。

这篇文章从设备ID绕过双因素身份验证的思路出发,说明这类问题为什么会出现、在受控环境下如何验证、线上怎么发现,以及防御方应该如何修复。适合安全测试人员、后端开发、业务安全同学一起看,尤其是那些正在设计“设备管理”或“免二次验证”功能的团队。

1. 先看信任边界:设备ID到底能不能作为第二因素

1.1 设备ID的本质是“识别变量”,不是“验证变量”

设备ID指的是一串用来标识某台设备的字符串,常见形式有:

  • Android设备的Android ID、IMEI
  • iOS设备的IDFV、IDFA
  • Web端通过浏览器指纹计算的住房ID
  • 服务端首次下发并存在本地存储中的UUID

这串ID看起来是一台设备的专属标识,很多开发人员会下意识认为它天然就带有某种安全属性。但仔细想一下,它的效力基本上等于你的门牌号。门牌号知道你是哪一户,代表了“身份”,但任何人都可以报出你的门牌号,它本身不能证明“你”这个人此刻就在这扇门后面。

设备ID也是一样。它往往存在于客户端缓存、请求参数、或者HTTP Header里。只要攻击者拿到了一次登录请求的流量,或者从某个泄露的数据库中拿到设备ID,完全可以在另一个地方重新提交。设备ID不是秘密,它顶多算一个公开的标识。

1.2 双因素认证里的“第二因素”到底要求什么

业界公认的双因素认证,第二因素通常覆盖下面几类:

因素类型典型形式安全特征
知道某物密码、PIN码需要用户记忆,理论上只有本人知道
持有某物OTP动态口令、U盾、安全芯片需要物理或逻辑空间独占,短暂有效
生物特征指纹、人脸、声纹与本人强绑定,不易复制

“持有某物”这个因素之所以可信,是因为它满足几个条件:

  • 临时性:动态口令通常会过期,一次性使用
  • 独占性:物理令牌或手机在某个时间只能被一个人持有
  • 不可批量克隆性:复制成本高

设备ID呢?它是一个长期不变、随处可见、批量可改的字符串。不符合上述任何一条,更准确地说它只是一个“环境信号”。如果把它加入白名单,作为跳过OTP的凭据,那就等于把第二因素降级成了“一个公开可见的标签”。这种情况下再讨论绕过双因素保护已经没有意义——双因素实际只剩一个因素了。

1.3 容易踩坑的业务场景

我有理由怀疑,80%的设备ID绕过双因素问题,都出在同一个功能设计上:记住设备。

典型流程如下:

  1. 用户输入账号密码,完成第一因素。
  2. 服务端下发一次性验证码到手机或邮箱。
  3. 用户输入验证码,完成第二因素。
  4. 服务端发现这是“常用设备”,于是把设备ID记录到白名单。
  5. 下次登录时,只要设备ID在白名单内,就不再要求验证码。

第5步就是问题核心。

如果设备ID的校验逻辑是“设备ID等于白名单中的值”,而不是严格校验“这个设备ID是哪一个用户、在哪次登录中、通过哪种方式登记的”,那这个白名单基本就形同虚设。更常见的是为了并发优化,把绑定表设计得非常宽松,同一设备ID可以属于多个用户,或者同一个用户在不同设备上共用同一个设备ID。

很多系统上线联调、安全测试也没覆盖这一层,只验证了“输入正确验证码才能进来”,却没有验证“设备ID是否会和用户身份强绑定”。结果就是:攻击者拿到了某个被泄露的设备ID,直接替换进登录请求,就绕过了双因素。

提示:下文所有设计验证、测试思路,都应当在你拥有合法授权的系统、自己的测试账号、独立的测试环境里进行。针对生产环境里不属于你管辖的系统尝试绕过,不属于安全测试范畴。

2. 设备ID的生成、传递与校验逻辑到底哪里容易出问题

2.1 三种典型的设备ID实现

不同团队实现设备ID的方式不一样,安全强度也完全不同。

类型生成方式可复制性典型问题
硬件标识读取IMEI、MAC地址、Android ID中等,部分可以被修改iOS、Android版本升级后逐步收紧权限
浏览器/设备指纹根据UA、Canvas、WebGL、存储信息组合计算较高,通过无头浏览器或指纹库可模拟指纹依赖的部分信息本身可伪造
服务端派发ID服务端下发UUID,客户端保存很高,直接抄走即可如果不绑定用户,几乎等于空cookie

这三种实现里,第三种最普遍,也最容易出问题。因为它就是一个随机字符串,服务端如果不校验“这个UUID来源于哪个设备、哪个用户”,那纯属是把堡垒的钥匙挂在门口。

2.2 设备ID在请求链路里的几个传递位置

设备ID传到哪里,决定它容易被谁看到。常见的传递方式有:

  • 请求Header:如X-Device-Id: abc123
  • 请求Body:在JSON参数里附一个deviceId字段
  • Cookie:设备ID作为cookie的一部分发送
  • 客户端存储:Web端的localStorage、移动端的SharedPreferences

所有传输位置都是客户端可读的。也就是说,任何能访问客户端存储、能看到一次请求流量的人,都能拿到设备ID并复用。

这里特别要说一种情况:很多前后端分离的系统,前端会把设备ID固定写在请求拦截器里。后端拿到之后只做了“非空校验”,没有做“这是不是服务端之前派发过且仍然合法有效的ID”的校验。于是测试时只要手工修改请求里的设备ID,后端就会把这个新ID重新当作可信设备写入白名单。这在测试中表现为:一个全新设备ID首次出现,居然直接跳过验证码。

2.3 最容易忽视的三种缺陷模式

根据我的经验,下面三类逻辑几乎天天见到,不是某个特定公司的特例。

第一种是设备ID完全可被客户端指定。后端不区分“设备注册ID”和“登录绑定ID”。任何未登记过的设备ID,只要出现在请求里就会被自动登记并信任。问题在于,这等于攻击者自报了一个门牌号,系统就相信了他来自这个地址。

第二种是设备ID与账号解耦。同一台设备,用户A登录过,用户B再登录时,系统不会因为“这个设备ID已经绑定其他账号”而拒绝。于是攻击者可以把目标账号的请求标记成自己已知的设备ID,绕过新设备验证。很多系统把绑定关系保存在类似“设备表”里,却没有在数据库里加上(device_id, user_id)唯一约束,正是这种缺陷的直接原因。

第三种是验证后的会话没有和设备绑定。即使设备ID绑定了用户,次生问题仍然存在:登录会话本身不记录发起登录时的设备ID。也就是说,一个已经通过双因素验证的会话,如果被攻击者拿到并换了新的设备ID继续使用,服务端仍会把它当作已验证用户。这是会话固定与设备信任缺少联动的典型后果。

2.4 一段常见的问题代码长什么样

以下代码是为了说明问题而简化的伪码,它代表了最初的信任模型设计缺陷:

def verify_second_factor(user, device_id): # 判断 device_id 是否在用户的可信设备白名单中 if device_whitelist.get(device_id) == user.user_id: # 可信设备,直接放行,跳过 OTP session = create_trusted_session(user.user_id) return session else: # 不可信设备,走完整二次验证 if check_otp(user.phone, receive_otp()): bind_device(user.user_id, device_id) return create_session(user.user_id) return None

问题点拆开看很清楚:

  • 白名单根据什么样的“device_id”判断?如果攻击者知道任意一个已绑定设备的ID,就能进入可信分支。
  • bind_device时是否验证过这个设备名已经绑定了别人?没有。
  • “可信会话”有效期多长?如果设置成一月、半年甚至永久有效,风险持续时间就被拉得很长。

这类代码并不复杂,但它把“设备ID”从一个辅助识别字段升格成了安全凭据,这是问题根源。

3. 在不影响正常用户的前提下检测设备绑定强度

3.1 测试准备清单

在受控环境验证这个逻辑,不需要白帽团队多强的工具链,准备以下条件就够:

  • 一台可正常发起请求的调试客户端(浏览器调试工具、抓包代理均可)
  • 服务端支持新建设备、记录设备列表的完整测试环境
  • 至少两个测试账号
  • 一段允许修改请求参数的抓包工具或命令行客户端

如果是在代码层做验证,还需要能查看数据库里设备绑定表的权限,看看里面的字段设计是否包含用户维度。

3.2 推荐覆盖的边界测试用例

以下测试用例全部建议以“防御回归测试”的形式纳入安全用例库,不是为了介绍攻击技巧,而是帮开发团队确认自己的信任模型没有放松:

测试场景操作方式合格标准
跨账号复用账号A绑定设备X,用账号B携带设备X登录必须拒绝或强制要求OTP
设备ID反转用户本地修改设备ID为另一随机值必须要求重新走第二因素
未知设备直接出现一个从未登记的设备ID首次请求不得直接授权
删除后重放清空本地设备ID,再提交旧的设备ID不得保留原信任状态
设备列表关联一个设备ID先绑定账号A,再绑定账号B数据库层应无法存两条记录

每一个用例记录的预期结果,都应当是你希望系统达到的安全底线。如果某个用例失败了,说明设备白名单和“第二因素”之间在逻辑上没有解开。

3.3 测试过程里如何记录证据

安全测试如果不是为了自动化,那就一定需要留下清晰证据,方便帮助研发复现。记录格式可以参考:

  • 请求URL和请求方法
  • 设备ID字段在请求中的位置
  • 当前账号、设备ID、测试步骤
  • 实际返回内容(是否返回免验证状态)
  • 数据库里绑定表前后对比

这个证据链同时也能作为推动安全修复的客观依据。上线评审时,团队可以明确看到“这个请求没有经过OTP校验”是什么数据使它绕过的,而不是听一堆含糊的描述。

3.4 测试时的红线

这里有一个需要反复强调的注意事项:这类测试中,最容易误伤的是生产环境的真实用户。比如随机把设备ID改掉,触发了真实用户账号的绑定覆盖,可能会把对方账号的登录状态搞乱。所以务必遵守:

  • 只在测试环境执行
  • 只使用自己可控的测试账号
  • 不要读取生产真实用户登录日志
  • 测试完成后清理测试账号和设备绑定记录

4. 线上运行时,通过异常信号发现设备信任漏洞

很多团队觉得把设备ID绑定功能上线就算完事了,直到某一天出现“某账号在陌生设备免验证登录”的投诉,才意识到问题。实际上,这类逻辑缺陷在线上会有明显的异常信号,只是平时缺少针对性的监控。

4.1 常见异常现象速查

异常现象可能原因建议优先级
同一个设备ID同时出现在大量不同账号的登录记录里设备ID被多个账号共用,绑定关系未隔离高
同一账号短时间内出现多种设备ID但都标记为可信设备ID校验过松,或设备ID本身可批量伪造高
删除客户端缓存后旧设备ID仍被反复接受服务端未清理旧绑定,信任状态不失效中
某设备ID的首次登录时间远远早于注册时间ID字段异常,可能是请求参数内被篡改中
新设备首次请求就跳过验证码设备ID可直接被客户端伪造并自动登记最高

审计SQL和日志时,尤其要关注device_id、user_id、created_at、ip_address这几组字段的关联组合。正常用户的设备ID不会频繁变化,不会同一个ID跨多个账号长期存在。

4.2 风控系统里应该设定的监控维度

不建议只靠WAF或网关层去拦设备ID,因为设备ID绕过属于业务逻辑层面的问题,需要风控系统配合。建议设置以下指标:

  • 同一设备ID关联的用户数阈值,超过阈值触发风控
  • 同一用户使用的可信设备数峰值,超过正常区间触发OTP
  • 可信设备平均生命周期,过于长且无失效机制的标记为风险
  • 请求里设备ID与UA、IP变化剧烈冲突的设备ID告警

这些监控的落点不是为了惩罚某个用户,而是提醒系统:“设备信任”已经出现了反常信号,必须回归到用户主要的身份验证流程。

5. 修复方案:把设备ID放回它该在的位置

5.1 核心原则

设备ID永远不要单独作为认证因素。

正确打开方式有两层:

  1. 第一层:设备ID只是风控信号,只影响风险梯度,不影响认证是否通过。
  2. 第二层:即使要做“可信设备免验证码”,也必须在用户完成一次完整双因素验证后,以服务端生成的安全凭证形式下发,而不是认可客户端上报的字符串。

这一原则要在架构评审时讲清楚,不然每次版本迭代都可能重新松绑。

5.2 具体加固步骤

第一,服务端不要信任客户端上报的设备ID。可以考虑由服务端生成一个随机设备序列号,即使用HMAC或签名算法对设备相关参数做校验,至少确保设备ID不是一件自由改装的外套。示例实现:

import hmac import hashlib device_salt = get_secret_key() def derive_device_id(user_id, raw_device_fingerprint): digest = hmac.new( device_salt.encode(), f"{user_id}:{raw_device_fingerprint}".encode(), hashlib.sha256 ).hexdigest() return digest

好处在于,攻击者光知道设备原始信息,也无法在不知道盐值的情况下构造出服务端认可的设备ID。

第二,数据库绑定关系必须加上账号维度。表结构至少包含user_id, device_id, first_verified_at, last_used_at,同时上UNIQUE(user_id, device_id)约束。这样可以保证一个设备ID最多只信任一个账号。

第三,登录会话必须绑定设备ID。创建会话时把设备指纹一并写入会话记录,后续请求检测到会话与当前设备不一致,立即要求重新验证。这一步在有些项目里被忽略,但恰恰是重放设备ID攻击的主要拦截点。

第四,设备失效机制要保留。设备被用户主动解绑、设备长期未使用、设备安全状态变化,都要使对应信任状态失效,不能永远躺在白名单里。

第五,新设备首次出现时的逻辑不要走“白名单直接放行”,而是重定向到完整OTP流程,并把“设备是否可信”写在风险评分里,而不是写在认证结果里。

5.3 修复后回归测试清单

  • 新设备首次请求,必须弹OTP
  • 同一设备ID切换账号,必须被拒绝
  • 服务端注销会话后,旧会话不得复用旧设备ID
  • 即使请求中带有正确的设备ID,也必须带上对应的会话凭证才有资格通过
  • 设备列表可以展示最近认证时间,方便用户自查

这类回归测试放自动化安全测试里面非常适合,逻辑固定,预期明确,不容易产生误报。

5.4 业务体验与安全的平衡

修复不代表彻底砍掉“记住设备”这个功能,更合理的做法是做分级策略:

  • 常规登录:密码 + 验证码仍然保留
  • 可信设备:只用于识别“用户常用的设备”
  • 高风险操作(转账、改密、新增收款方):即使可信设备也要进行二次验证
  • 低风险操作:可信设备下允许下调验证强度

这既照顾了用户体验,也没有把安全边界完全抛开。设备ID可以作为业务风控里的一个参考维度,但不是决策维度。

6. 我的实践经验与一点感想

我在之前的一次安全评估中遇到过类似场景。某个内部系统把“设备免验证码”做成了全局白名单,设备ID直接存在Notes里,接口从Header读取后对比,后续连OTP验证逻辑都没有附带有效期概念。实际验证时,只要把抓包工具里的X-Device-Id字段换成目标用户的设备ID,再配合已知的账号密码,即可在另一台设备上无障碍登录。整个过程没有爆破,也没有高深的利用技术,纯粹是信任模型摆错了位置。

这件事给我的一个重要提醒是:安全测试不是升级无畏的Burp插件,而是不断回到信任边界上追问一句,这个字段到底凭什么被信任。设备ID能帮助回答问题“你从哪来”,但它无法回答“你被认证过吗”。当系统把第二个问题寄托在第一个答案上,双因素就离失效不远了。

最后给个小建议:如果你怀疑自己的系统有这类问题,最简单的验证方式就是拿自己的测试账号,把登录请求里的设备ID修改成一个完全陌生的值,然后再走一遍登录流程。如果系统直接放行,那说明“设备信任”这条边界已经失守,找研发一起把信任模型重新设计一遍。修复过程中别忘了把会话绑定和设备ID唯一约束一起加上,只改其中一环,很可能会出现新的缝隙。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询