ネット証券・銀行口座を狙う「リアルタイム中間者フィッシング(AiTM)」とは?
6桁の確認コードが突破される理由と、パスキーによる確実な守り方
- 01. 「SMSの6桁コード」までその場で中継される新手法: 最新のフィッシングサイト(AiTM)は、入力されたパスワードと6桁の確認コードを0.1秒で本物のサイトへ横流しし、発行された「ログイン済みチケット(Cookie)」を奪い取ります。
- 02. ネット証券での勝手な株売買や不正送金が急増中: 乗っ取られた証券口座で保有株を勝手に売却され、海外の低位株を高値で買わされる(相場操縦の踏み台にされる)被害が全国で多発しています。
- 03. 「メールのリンクを踏まない」+「パスキー(顔・指紋認証)」で構造的に防げる: パスキーは開いているサイトのURL(ドメイン)を暗号的に検証するため、中継型の偽サイトでは鍵が一切作動せず、被害を構造的に防げます。
この記事の要点と4つの章のご案内
| 章構成 | 主なテーマ | 要点と確認ポイント |
|---|---|---|
| 1. 基本と全体像 | 身近なたとえで見る仕組み:なぜ「スマホに届く6桁の番号」だけでは守れないのか? | 「リアルタイム中間者フィッシング(AiTM: Adversary-in-the-Middle)」という手口では、偽サイトが単なるコピー画面ではなく、裏側で本物の銀行・証券サイトとリアルタイムに通信を中継(リバースプロキシ)しています。利用者が偽サイトにID・パスワードと6桁のSMSコードを入力した瞬間、犯人のプログラムがその場で本物のサイトへ転送してログインを成立させ、発行された「ログイン済み通行証(セッションCookie)」を丸ごと盗み取るため、SMS認証やメール確認コードだけでは防ぐことができません。 |
| 2. 現場での進め方と対策 | 図解で見る仕組み:SMS確認コードが横取りされる経路と、パスキーが止める仕組み | 最も確実な対策は、数字を入力する確認コードから「パスキー(FIDO2 / WebAuthn:顔認証・指紋認証)」へ切り替えること、そして銀行や証券サイトを開く際は「メールやSMSのリンクからは原則として開かず、公式アプリまたは自分が登録したブックマークから開くこと」です。パスキーは、ブラウザが現在開いているURL(ドメイン名)を暗号署名の中に直接焼き込むため、たとえ1文字違いの精巧な偽サイト(リバースプロキシ)が間に入っていても、本物のサーバー側で「URLが違う」と即座に拒絶され、物理的に突破できません。 |
| 3. 関連する制度とルール | 実践のポイント:今日から資産と社内アカウントを守る「3つの確実な対策」 | 気づいたその場(5分以内)で、正規のアプリまたは公式窓口から「すべての端末からの強制ログアウト(セッション無効化)」「パスワード変更」「出金・取引の一時ロック」を行ってください。AiTM攻撃ではパスワードを変えても、盗まれたセッションCookieが有効なままだと操作が続けられてしまうため、必ず「ログイン中セッションの強制失効」または緊急停止窓口への連絡が必要です。 |
| 4. 詳しい技術仕様としくみ | 関連資料・解説記事 | これまで「二段階認証(SMSやメールで届く6桁のワンタイムパスワード)さえ設定しておけば、たとえパスワードが漏れても安心だ」と言われてきました。ところが2025年から2026年にかけて、二段階認証を設定していたはずのネット証券口座や銀行口座、会社のクラウドメールが次々と乗っ取られる被害が発生しています。 |
01. 身近なたとえで見る仕組み:なぜ「スマホに届く6桁の番号」だけでは守れないのか?
これまで「二段階認証(SMSやメールで届く6桁のワンタイムパスワード)さえ設定しておけば、たとえパスワードが漏れても安心だ」と言われてきました。ところが2025年から2026年にかけて、二段階認証を設定していたはずのネット証券口座や銀行口座、会社のクラウドメールが次々と乗っ取られる被害が発生しています。
その犯行に使われているのが、「リアルタイム中間者フィッシング(AiTM:Adversary-in-the-Middle)」と呼ばれる「中継型」の偽サイトです。
たとえ話:「銀行の窓口の前に勝手に作られた『偽の受付カウンター』」
昔のフィッシング詐欺は、あなたの暗証番号を紙にメモして盗み、あとから犯人が銀行へ行こうとする手口でした。これなら「1分間だけ有効な合言葉(ワンタイムパスワード)」で防げました。
しかし現在のリアルタイム中間者(AiTM)詐欺は、あなたと本物の銀行窓口のちょうど真ん中に「透明な偽の受付カウンター」を置き、あなたが言った合言葉をその場で0.1秒後に後ろの本物の銀行員へオウム返しで伝える手口です。本物の銀行員は「正しい合言葉ですね、はい、これが本日の入館証(セッションCookie)です」とチケットを渡してしまい、真ん中にいる犯人がそのチケットを横取りして金庫へ入ってしまうのです。
これを防ぐ唯一の道具が「パスキー(FIDO2)」です。パスキーは「今まさに目の前にいる相手の身分証(サイトの正しいURL)が本物でなければ、そもそもカバンから鍵を出さない」という仕組みのため、偽の受付カウンターが間に挟まった瞬間にピタッと止まります。
フィッシング対策協議会への報告件数は年間170万件を超え、金融機関・証券会社・EC・宅配便を装う精巧な誘導が続いています。
有効期限30秒〜60秒のワンタイムパスワード(TOTP/SMS)であっても、自動リバースプロキシ(Evilginx等)は100ミリ秒以下で正規サーバーへ中継します。
NIST SP 800-63B が最高位の「フィッシング耐性MFA」と定義する FIDO2 / WebAuthn パスキーは、オリジン束縛署名により中継型攻撃を構造的に無力化します。
02. 図解で見る仕組み:SMS確認コードが横取りされる経路と、パスキーが止める仕組み
03. 実践のポイント:今日から資産と社内アカウントを守る「3つの確実な対策」
証券・銀行・主要クラウドで「パスキー(FIDO2)」を有効化する
お使いのネット証券、銀行、Microsoft 365、Google、Amazon等のセキュリティ設定画面を開き、SMS確認コードよりも優先して「パスキー(生体認証・デバイス認証)」を登録してください。
メール・SMS・検索広告のリンクからは原則としてログインしない
「口座が凍結されました」「配当金の確認」「セキュリティ更新のお願い」といったメールの青文字リンクは押さず、必ずスマートフォンの公式アプリまたは普段のブックマークから開く習慣を一貫してください。
ログイン通知メールをオンにし、出金先口座の変更ロックをかける
普段使わない時間帯や海外IPからのログインがあった際に即座に気づけるよう通知を有効化し、法人では振込・出金時に2名以上の承認が必要な設定(デュアルコントロール)を有効にします。
Q. なぜネット証券口座を乗っ取った犯人は、現金を直接自分の口座へ振り込まずに、見知らぬ海外株や低位株を勝手に売買することがあるのでしょうか?
クリックして解答と解説を見る ↓
検索結果の一番上に出る「スポンサー広告」にも偽サイトが混じる理由
メールのリンクを開かないよう気をつけている方を狙うため、攻撃者は検索エンジンの広告枠を買い取り、有名な銀行や証券会社の名前で検索した際の一番上に偽サイトを表示させる「マルバタイジング(悪性広告)」を行うことがあります。日頃からよく使う金融機関は、検索するのではなく必ず「公式アプリ」または「ブックマーク」から開く習慣が身を守ります。
関連資料・解説記事
社内システムや顧客向けWebサービスへの「パスキー(FIDO2 / WebAuthn)」導入、およびフィッシング耐性認証への移行支援を承っております。
Set-Cookie ドメイン書き換えプロキシの挙動と、WebAuthn clientDataJSON.origin / rpIdHash の暗号検証コードを詳解しています。
Evilginx 型リバースプロキシによる `Set-Cookie` ドメイン書き換え攻撃の詳細解説と、W3C WebAuthn `rpIdHash`・`clientDataJSON.origin` 暗号束縛による数学的遮断
本モノグラフでは、従来の知識要素+所有要素(SMS OTP / RFC 6238 TOTP)がなぜ TLS 終端型リバースプロキシ(AiTM)に対して無力であるかを HTTP ヘッダ変換レベルで証明し、W3C WebAuthn Level 3 における `authenticatorData[0..32]`(`SHA-256(rpId)`)および `clientDataJSON.origin` の二重束縛検証を Rust 2024 で実装する。
1. 【なぜ RFC 6238 TOTP / SMS OTP はチャネル束縛(Channel Binding)を持たないのか】 時間ベースワンタイムパスワード TOTP(K, t) = Truncate(HMAC-SHA1(K, ⌊t / 30⌋)) の出力は 6桁の整数 d ∈ [0, 999999] であり、その計算式にはクライアントが現在通信している TLS セッション識別子も HTTP オリジン(https://phish.example vs https://bank.example)も一切含まれない。したがって、攻撃者のプロキシ P が被害者ブラウザ C との TLS セッション SC,P から (u, p, d) を抽出し、正規サーバー V との別個の TLS セッション SP,V へ転送した際、正規サーバー V は SP,V を正規ユーザーの通信と識別できず、Set-Cookie: __Host-session=... を P へ返却してしまう。
2. 【WebAuthn における rpIdHash と origin の暗号学的束縛】 これに対し WebAuthn アサーション署名 σ は、ブラウザ(User Agent)が直接 TLS 接続先の FQDN から構築した clientDataJSON({"type":"webauthn.get","challenge":"...","origin":"https://phish.example"})の SHA-256 ハッシュと、認証器が rpId から計算した 32-byte rpIdHash の連結データに対して秘密鍵 sk で生成される:
もしプロキシ P が `clientDataJSON` 内の `"origin":"https://phish.example"` を `"https://bank.example"` に書き換えれば署名 σ の検証が破綻し、書き換えずに中継すれば正規サーバー側のオリジン一致検査で即座に拒絶される。
| Byte Range | Field | Mandatory Server-Side Check |
|---|---|---|
| 0 .. 32 (32B) | rpIdHash | Must equal SHA-256("2026.ok.st") in constant time |
| 32 .. 33 (1B) | flags | Bit 0 (UP=1) & Bit 2 (UV=1: Biometric/PIN Verified) |
| 33 .. 37 (4B) | signCount (u32 BE) | Strictly > stored_sign_count (Clone detection) |
#![forbid(unsafe_code)]
pub const AUTH_DATA_MIN_LEN: usize = 37;
pub const FLAG_USER_PRESENT: u8 = 0b0000_0001;
pub const FLAG_USER_VERIFIED: u8 = 0b0000_0100;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WebAuthnVerifyError {
TruncatedAuthenticatorData,
RpIdHashMismatchAiTmDetected,
UserPresenceNotAsserted,
UserVerificationRequiredForFinancialTx,
ClonedAuthenticatorSignCountRegression { stored: u32, received: u32 },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VerifiedAuthHeader {
pub flags: u8,
pub new_sign_count: u32,
}
/// Verifies the 37-byte AuthenticatorData header against expected RP ID SHA-256 hash
/// and enforces User Verification (UV) + monotonic signCount against AiTM & token cloning.
#[inline]
pub fn verify_webauthn_auth_data(
auth_data: &[u8],
expected_rp_id_sha256: &[u8; 32],
stored_sign_count: u32,
) -> Result<VerifiedAuthHeader, WebAuthnVerifyError> {
if auth_data.len() < AUTH_DATA_MIN_LEN {
return Err(WebAuthnVerifyError::TruncatedAuthenticatorData);
}
// 1. Constant-time RP ID Hash verification (blocks AiTM reverse proxies on lookalike domains)
let mut diff: u8 = 0;
let mut i: usize = 0;
while i < 32 {
diff |= auth_data[i] ^ expected_rp_id_sha256[i];
i += 1;
}
if diff != 0 {
return Err(WebAuthnVerifyError::RpIdHashMismatchAiTmDetected);
}
// 2. Enforce UP (User Present) and UV (User Verified via Biometrics/PIN)
let flags = auth_data[32];
if (flags & FLAG_USER_PRESENT) == 0 {
return Err(WebAuthnVerifyError::UserPresenceNotAsserted);
}
if (flags & FLAG_USER_VERIFIED) == 0 {
return Err(WebAuthnVerifyError::UserVerificationRequiredForFinancialTx);
}
// 3. Parse big-endian u32 signCount and detect cloned hardware keys
let received_count = u32::from_be_bytes([
auth_data[33],
auth_data[34],
auth_data[35],
auth_data[36],
]);
if (received_count != 0 || stored_sign_count != 0) && received_count <= stored_sign_count {
return Err(WebAuthnVerifyError::ClonedAuthenticatorSignCountRegression {
stored: stored_sign_count,
received: received_count,
});
}
Ok(VerifiedAuthHeader {
flags,
new_sign_count: received_count,
})
}