最简单的OTP泄露漏洞:人人都能利用的OTP绕过技术

本文详细介绍了通过HTTP头参数污染实现OTP验证码泄露的技术细节。作者发现目标应用在注册过程中使用GET请求发送数据,通过重复Mobileusername头字段可让服务器向多个邮箱发送相同的OTP验证码,从而实现账户劫持。文章包含完整的漏洞发现、利用和修复方案。

OTPs For Everyone: 最简单的OTP泄露漏洞

tinopreter · 6分钟阅读 · 2025年10月20日 · 2374次阅读

Akwaaba! abusua。我遇到了一个"参数"污染案例,但不是常见的那种。请继续关注并跟随我的分析。另外,如果你还没有看过,请查看我之前的Android应用内容提供程序漏洞利用文章。是的,你也可以通过这个SSO漏洞利用获得轻松的150美元赏金。

来源:Zaraki

侦察过程

如何识别目标?

为了识别潜在目标,我使用shodan.io搜索共享相同SSL证书通用名称(CN)的域和IP地址,特别是那些匹配通配符模式如*.target.com的。这种方法有助于识别可能属于同一组织或基础设施的服务。

例如,如果你正在调查像medium.com这样的域,你可以检查其SSL证书以找到CN值。一旦识别,你可以使用Shodan枚举具有包含相同CN的证书的其他主机,可能揭示相关资产或配置错误的端点。

检查CN值的步骤:

  1. 点击地址栏中的挂锁图标
  2. 点击"连接安全",将打开另一个弹出框
  3. 在新的弹出框中,点击证书图标,将出现最终的弹出框 查看通用名称(CN),它应该告诉你证书是为哪个域名颁发的。

有时在漏洞搜寻过程中,我会遇到一个奇怪的子域,不确定它是否属于目标组织。一个快速的检查方法是通过比较其SSL证书的CN与其主网站(或任何其他已知应用)的CN。如果匹配,则可能是其基础设施的一部分。

使用的Shodan搜索查询:

1
Ssl.cert.subject.CN:"*.target.com" http.title:"Login"

从结果中,我识别出一个允许用户账户注册的应用。我们称这个应用为insecure.target.com。

识别漏洞

在注册过程中,有一个最终阶段需要验证邮箱。一个6位数的代码会发送到你的邮箱,你可以进行验证。

回到Burp Suite查看API请求的样子,我注意到一些奇怪的地方:

  • 所有API请求都是GET方法
  • 它们没有表单数据参数

起初我有点困惑,应用到底是如何将我的注册数据发送到服务器的?于是,我仔细查看了每个API请求。然后,我找到了:一个到/api/auth/mobileVerifyRequestOTP的GET请求。令人惊讶的是,没有特殊的查询参数。相反,用户的注册数据完全通过HTTP头发送。

其他表单数据也通过不同的API端点通过GET请求发送。

这很不寻常,但我想这对他们有效。

利用头部污染

使用与参数污染相同的思路,我尝试用两个不同的邮箱地址复制Mobileusername头,看看服务器会如何处理。令人惊讶的是,它接受了两个并响应了一个verificationID。(注意:这不是OTP本身,只是一个用于跟踪注册会话的ID。)

其中一个邮箱是我的wearehackerone.com(重定向到Gmail),另一个来自Temp-Mail.org。首先检查我的Gmail,我看到OTP代码已经到达,OTP代码是399336。

我快速检查了Temp-Mail的收件箱,相同的399336也到达了那里。这确认了漏洞的存在。服务器接受了两个邮箱并向每个发送了相同的代码。

这是因为服务器以一种奇怪的方式处理重复的Mobileusername头。它没有拒绝或覆盖重复项,而是将两个值视为数组的一部分,并向列出的每个邮箱地址发送相同的OTP代码。通过检查我Gmail账户收到的邮件中的To头,确认了这种行为,它清楚地显示了两个用逗号分隔的邮箱地址。甚至Gmail也显示另一个收件人收到了相同的OTP消息。

什么可能导致这个漏洞?

我没有看过他们的后端代码;这只是我的推测。但为了演示这种漏洞可能如何发生,请查看下面的Flask代码片段:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from flask import Flask, request
import random

@app.route('/api/auth/mobileVerifyRequestOTP?isEmail=true', methods=['GET'])
def send_otp():
    # 从GET请求获取'Mobileusername'头的所有值
    usernames = request.headers.getlist('Mobileusername')
    # 生成单个OTP代码
    otp = str(random.randint(100000, 999999))
    verification_id = str(random.randint(100000, 999999))
    '''
     这里应该有重复头检查但没有实现
    '''
    # 向所有邮箱发送相同的OTP
    send_otp_email(usernames, otp)
    return {'verificationID': verification_id}

这种发现的影响是什么?

首先,可以使用他人的邮箱创建和验证账户。我发现服务器将第一个Mobileusername头与正在创建的账户绑定。因此,恶意用户可以将受害者的邮箱设置为第一个头,将自己的邮箱设置为第二个。他们会收到OTP,完成验证,账户将在受害者的邮箱下创建。

缓解措施

可以通过实施简单的重复头检查来适当缓解此问题。如果检测到Mobileusername头有多个值,服务器应拒绝请求或仅安全地处理第一个值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from flask import Flask, request
import random

@app.route('/api/auth/mobileVerifyRequestOTP?isEmail=true', methods=['GET'])
def send_otp():
    # 从GET请求获取'Mobileusername'头的所有值
    usernames = request.headers.getlist('Mobileusername')
    # 生成单个OTP代码
    otp = str(random.randint(100000, 999999))
    verification_id = str(random.randint(100000, 999999))
    
    # 验证只有一个头存在
    if len(usernames) != 1:
        abort(400, description="Invalid request: only one Mobileusername header is required.")
    
    # 向所有邮箱发送相同的OTP
    send_otp_email(usernames, otp)
    return {'verificationID': verification_id}

感谢阅读,如果你有任何问题,可以在Twitter @tinopreter上私信我。在LinkedIn上与我联系:Clement Osei-Somuah。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计