短信验证码多手机号注入漏洞

漏洞概述

在登录接口的手机号输入字段中,通过逗号分隔输入多个手机号(如 13800000099,13800000000),可能导致短信网关同时向多个号码发送相同验证码,进而引发账号越权登录或账号信息覆盖等安全问题。

漏洞原理

多数短信网关(如阿里云SMS、腾讯云SMS)的批量发送接口本身支持逗号分隔的多手机号格式。当后端未对用户输入的手机号进行严格格式校验时,攻击者构造的多手机号字符串可能被直接透传至短信网关,触发批量发送逻辑。

攻击前提条件

  • 登录/注册接口接受手机号作为身份标识
  • 后端未对手机号字段做严格的单号码格式校验(如正则 ^1[3-9]\d{9}$
  • 短信网关接口支持逗号分隔的批量发送

漏洞触发流程分支图

exported_image.png

详细场景分析

情况A:单号码 + 验证码即可登录

触发条件: 后端在发送验证码时将输入拆分为多个号码分别发送,但在验证阶段仅校验「号码 + CODE」的匹配关系。

攻击流程:

  1. 攻击者在手机号字段输入 攻击者号码,目标号码
  2. 系统向两个号码发送相同验证码
  3. 攻击者用自己的手机接收验证码
  4. 攻击者提交 目标号码 + 验证码 完成登录

危害等级: 高危 — 可直接越权登录任意用户账号

实际利用场景:

POST /api/sms/send HTTP/1.1
Content-Type: application/json

{"phone": "13800000099,13800000000"}

-- 响应: 验证码已发送 --

POST /api/login HTTP/1.1
Content-Type: application/json

{"phone": "13800000000", "code": "123456"}

-- 响应: 登录成功,返回目标用户token --

情况B-1:整体字符串登录,后端校验顺序

触发条件: 后端在验证阶段也接受逗号分隔的字符串,并按顺序取第一个(或最后一个)号码作为登录身份。

攻击流程:

  1. 攻击者输入 目标号码,攻击者号码 发送验证码
  2. 攻击者用自己的手机接收验证码
  3. 攻击者提交 目标号码,攻击者号码 + 验证码 登录
  4. 后端取第一个号码(目标号码)作为登录身份

危害等级: 高危 — 通过控制号码顺序实现定向账号劫持

实际利用场景:

POST /api/sms/send HTTP/1.1
Content-Type: application/json

{"phone": "13800000000,13800000099"}

POST /api/login HTTP/1.1
Content-Type: application/json

{"phone": "13800000000,13800000099", "code": "654321"}

-- 后端逻辑: phone.split(",")[0] → 13800000000 --
-- 响应: 以13800000000身份登录成功 --

情况B-2:整体字符串作为新账号,覆盖原有数据

触发条件: 后端完全不校验手机号格式,将 13800000099,13800000000 视为一个合法的新用户标识进行注册/登录。

攻击流程:

  1. 攻击者输入 目标号码,攻击者号码 发送验证码并登录
  2. 系统创建新账号,标识为整个字符串
  3. 在某些实现中,新账号的创建过程会触发与第一个号码关联的数据覆盖(如唯一索引冲突时的 UPSERT 操作)

危害等级: 中高危 — 可能导致目标用户数据被覆盖或污染

实际利用场景:

POST /api/login HTTP/1.1
Content-Type: application/json

{"phone": "13800000000,13800000099", "code": "111222"}

-- 后端逻辑: 未找到该"手机号"对应账号,执行注册 --
-- 数据库操作: INSERT ... ON CONFLICT(phone_prefix) DO UPDATE --
-- 结果: 13800000000 的用户资料被覆盖 --

根因分析

环节 问题 说明
输入校验 手机号格式未做严格正则匹配 应限制为 ^1[3-9]\d{9}$
短信网关调用 未对入参做二次清洗 直接透传用户输入至网关API
验证码校验 验证逻辑与发送逻辑的手机号处理不一致 发送时拆分,验证时未拆分(或反之)
账号体系 未对用户标识做唯一性和格式约束 允许非标准格式的字符串作为账号标识

修复建议

1. 输入层严格校验(最关键)

import re

def validate_phone(phone: str) -> bool:
    """严格校验单个中国大陆手机号"""
    return bool(re.match(r'^1[3-9]\d{9}$', phone.strip()))

2. 短信发送前二次校验

def send_sms(phone: str, code: str):
    # 即使前端已校验,后端必须再次校验
    if not validate_phone(phone):
        raise InvalidPhoneError("手机号格式非法")
    # 确保不包含分隔符
    if any(sep in phone for sep in [',', ';', ' ', '
']):
        raise InvalidPhoneError("手机号包含非法字符")
    sms_gateway.send(phone, code)

3. 验证码绑定单一手机号

def verify_code(phone: str, code: str) -> bool:
    # 验证时同样严格校验格式
    if not validate_phone(phone):
        return False
    stored = redis.get(f"sms_code:{phone}")
    return stored == code

4. 数据库层约束

ALTER TABLE users 
ADD CONSTRAINT chk_phone_format 
CHECK (phone ~ '^1[3-9][0-9]{9}$');

测试用例(验证修复效果)

测试输入 预期结果
13800000099 正常发送验证码
13800000099,13800000000 拒绝,提示格式错误
13800000099;13800000000 拒绝,提示格式错误
13800000099 13800000000 拒绝,提示格式错误
1380000009 (10位) 拒绝,提示格式错误
23800000099 (非1开头) 拒绝,提示格式错误

总结

该漏洞的核心在于输入校验缺失短信网关批量发送能力的意外暴露。攻击者无需任何权限即可利用,且在情况A下可实现零交互的任意账号登录。修复优先级应为:输入校验 > 网关调用清洗 > 验证逻辑一致性 > 数据库约束。

温馨提示:本文内容仅用于合法授权的安全学习与研究交流,严禁用于未授权渗透测试、漏洞利用或任何违法行为。

涉及企业或平台的未公开漏洞信息,请遵循负责任披露原则,勿公开传播可直接复现的敏感细节。

如存在侵权、错误信息或不当内容,请联系站方处理,我们将及时核实并删除。邮箱:admin@baimaojianghu.com。