最简单的OTP泄漏漏洞:利用HTTP头污染实现OTP绕过

本文详细介绍了如何通过HTTP头污染漏洞实现OTP验证绕过。作者通过Shodan搜索发现目标应用,在注册过程中发现API通过GET请求发送数据,利用重复的Mobileusername头成功让服务器向多个邮箱发送相同OTP码,最终实现账户劫持。

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

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

漏洞挖掘过程

如何识别目标?

为了识别潜在目标,我使用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
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))
    
    '''
     这里应该有重复头检查但没有实现
    '''
    
    # 向所有邮箱发送相同的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="无效请求:只需要一个Mobileusername头。")
    
    # 向所有邮箱发送相同的OTP
    send_otp_email(usernames, otp)
    return {'verificationID': verification_id}

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

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