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

漏洞概述

在登录接口的手机号输入字段中,通过逗号分隔输入多个手机号(如 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下可实现零交互的任意账号登录。修复优先级应为:输入校验 > 网关调用清洗 > 验证逻辑一致性 > 数据库约束。

## 漏洞概述 在登录接口的手机号输入字段中,通过逗号分隔输入多个手机号(如 `13800000099,13800000000`),可能导致短信网关同时向多个号码发送相同验证码,进而引发账号越权登录或账号信息覆盖等安全问题。 ## 漏洞原理 多数短信网关(如阿里云SMS、腾讯云SMS)的批量发送接口本身支持逗号分隔的多手机号格式。当后端未对用户输入的手机号进行严格格式校验时,攻击者构造的多手机号字符串可能被直接透传至短信网关,触发批量发送逻辑。 ## 攻击前提条件 - 登录/注册接口接受手机号作为身份标识 - 后端未对手机号字段做严格的单号码格式校验(如正则 `^1[3-9]\d{9}$`) - 短信网关接口支持逗号分隔的批量发送 ## 漏洞触发流程分支图 ![exported_image.png](https://jhapi.baimaojianghu.com/uploads_v2/20260519/786be27dfaa770695e2bde2d23185794-1779182594-1f033c36513d3787.png) ## 详细场景分析 ### 情况A:单号码 + 验证码即可登录 **触发条件:** 后端在发送验证码时将输入拆分为多个号码分别发送,但在验证阶段仅校验「号码 + CODE」的匹配关系。 **攻击流程:** 1. 攻击者在手机号字段输入 `攻击者号码,目标号码` 2. 系统向两个号码发送相同验证码 3. 攻击者用自己的手机接收验证码 4. 攻击者提交 `目标号码 + 验证码` 完成登录 **危害等级:** 高危 — 可直接越权登录任意用户账号 **实际利用场景:** ```http 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. 后端取第一个号码(目标号码)作为登录身份 **危害等级:** 高危 — 通过控制号码顺序实现定向账号劫持 **实际利用场景:** ```http 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 操作) **危害等级:** 中高危 — 可能导致目标用户数据被覆盖或污染 **实际利用场景:** ```http 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. 输入层严格校验(最关键) ```python import re def validate_phone(phone: str) -> bool: """严格校验单个中国大陆手机号""" return bool(re.match(r'^1[3-9]\d{9}$', phone.strip())) ``` ### 2. 短信发送前二次校验 ```python def send_sms(phone: str, code: str): # 即使前端已校验,后端必须再次校验 if not validate_phone(phone): raise InvalidPhoneError("手机号格式非法") # 确保不包含分隔符 if any(sep in phone for sep in [',', ';', ' ', '\n']): raise InvalidPhoneError("手机号包含非法字符") sms_gateway.send(phone, code) ``` ### 3. 验证码绑定单一手机号 ```python 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. 数据库层约束 ```sql 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。

参与讨论 (35)

B
Beyond2026-05-26 16:19

视作全新账号 直接被拒绝了

飞雪2026-05-26 13:13

牛的

T
tutubaba2026-05-26 07:12

看看,学习

L
Lෆiiೄ೨2026-05-24 17:32

学到了

是雾琪大人2026-05-24 16:58

学到了

6
6ufui2026-05-24 15:06

强太强师傅

T
tutubaba2026-05-23 08:36

厉害

洛水芽2026-05-22 19:37

太强了大佬

不会挖漏洞2026-05-22 18:38

太强了

M
MH2026-05-22 17:06

太强了👍

湫东坡_4032026-05-22 14:11

无敌大手

H
Hello3912026-05-22 00:03

学到了

C
C10T012026-05-21 18:18

学到了

T
tutubaba2026-05-21 12:25

我学一下

无处不在2026-05-21 10:38

好详细的介绍,学到了

P
PTbest2026-05-21 10:22

无敌

V
Viax2026-05-21 09:32

学到了

(
(@#@)2026-05-20 20:17

第一次见tql

P
pu9maker2026-05-20 16:10

这也可以,太厉害了

从河望不见2026-05-20 14:18

哼😾?

X
xmcx2026-05-20 14:09

66

.
.........2026-05-20 11:36

现在应该很少了

土豆吃薯条(变成土豆薯条)2026-05-20 10:59

可以的师傅

弗兰克2026-05-20 10:54

后面的资料覆盖之前没有想到tql

6
662026-05-20 08:33

tql佬

S
Sylveu2026-05-20 08:10

tql牛

O
Omg2026-05-20 07:22

牛佬

浮生若梦2026-05-20 03:20

强无敌

L
Lytin2026-05-20 02:20

学到了

D
Demolish2026-05-20 02:16

可以可以,很适合我这种新手

想法飞鸡2026-05-19 22:05

学到了

Z
zzz112026-05-19 22:02

学到了

S
Stars to me.2026-05-19 21:19

tql佬

P
paopao2026-05-19 17:19

tqlD佬