1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【実例】DNS Rebinding攻撃でローカルホストのAPIが外部から叩かれる仕組みと防御策

1
Posted at

DNS Rebinding攻撃は、Same-Origin Policy(SOP)をバイパスしてブラウザを踏み台にし、被害者のlocalhost上で動くAPIサーバーへ外部から任意リクエストを送り込む攻撃手法である。ローカルで動く管理ダッシュボード、開発サーバー、IoT機器の設定UIなどが標的になりうる。本記事では仕組み、攻撃シナリオ、防御策をExpressの実装コードとともに解説する。

攻撃の仕組み: TTL 0とDNSレコード差し替え

SOPはオリジン(スキーム+ホスト名+ポート)が同一の場合のみクロスオリジンアクセスを許可する。DNSリバインディングはこの「ホスト名」の解釈を悪用する。

  1. 攻撃者が制御する evil.attacker.com のAレコードのTTLを0秒にしておく
  2. 被害者が http://evil.attacker.com/payload.html にアクセスする
  3. ブラウザがDNSキャッシュを破棄した直後、攻撃者のDNSサーバーは evil.attacker.com のAレコードを 127.0.0.1 に差し替える
  4. payload.html内のJavaScriptが http://evil.attacker.com:8080/admin にfetch()する
  5. ブラウザは evil.attacker.com と同一オリジンと判断してSOP制限をかけず、実際のリクエストはlocalhost:8080に届く
[攻撃者DNS]
evil.attacker.com TTL=0 → (最初) 1.2.3.4  → (切替後) 127.0.0.1
                                    ↓                       ↓
                             malicious page           Admin API bypass

攻撃シナリオ

個人開発でよくある構成を例に取る。

  • localhost:8080 でExpress製のAdmin APIが動いている
  • APIはAuthorizationヘッダを要求しない(ローカルからしか叩けないと思っている)
  • 開発者がブラウザで evil.attacker.com の罠ページを踏む

この状況でDNSリバインディングが成立すると、攻撃者はAdmin APIの全エンドポイントに自由にアクセスできる。機密データの取得、設定の書き換え、シェルコマンドの実行インターフェースがある場合はRCEまで至る。

防御策1: Host headerの検証(Express実装)

サーバー側でリクエストの Host ヘッダを検証することで、DNSリバインディングを無効化できる。ブラウザはリクエスト先のホスト名をHostヘッダにセットするため、差し替え後のDNSで 127.0.0.1 に到達しても evil.attacker.com というHostヘッダが残る。

const express = require('express');
const app = express();

const ALLOWED_HOSTS = new Set([
  'localhost',
  '127.0.0.1',
  '[::1]',
]);

function validateHost(req, res, next) {
  const host = (req.headers['host'] || '').split(':')[0];
  if (!ALLOWED_HOSTS.has(host)) {
    res.status(403).json({
      error: `Forbidden host: ${host}`,
    });
    return;
  }
  next();
}

app.use(validateHost);

app.get('/admin', (req, res) => {
  res.json({ status: 'ok', data: 'sensitive' });
});

app.listen(8080, '127.0.0.1', () => {
  console.log('Listening on 127.0.0.1:8080');
});

ALLOWED_HOSTS に開発用の固定ホスト名だけを列挙する。動的なサブドメインは入れてはならない。

防御策2: バインドアドレスを127.0.0.1に限定

Node.jsのhttp.createServer()は、デフォルトで 0.0.0.0(全インターフェース)にバインドする場合がある。開発サーバーは明示的に 127.0.0.1 にバインドすることで、外部ネットワークからのパケットは届かなくなる。

// NG: 全インターフェースにバインド
app.listen(8080);

// OK: loopbackのみ
app.listen(8080, '127.0.0.1');

ただしDNSリバインディングはブラウザを経由するためloopback経由で届く。バインド制限だけでは防げない点に注意が必要である。

防御策3: DNSリバインディング対策ルーターの設定

多くのブロードバンドルーターには「DNSリバインディング保護」設定がある。ローカルサブネットのIPアドレスを返す外部DNSクエリをドロップする機能で、企業ネットワークや自宅環境では有効化を確認する。

Linuxのdnsmasqベースのルーターではデフォルトで有効になっていることが多い。

# dnsmasqのDNSリバインディング保護確認
grep -r "stop-dns-rebind" /etc/dnsmasq.conf /etc/dnsmasq.d/ 2>/dev/null
# なければ追加
echo "stop-dns-rebind" >> /etc/dnsmasq.conf

防御策4: プライベートネットワークアクセス制限(Chrome)

Chrome 98以降、Private Network Access(PNA)制限が段階的に強化されている。パブリックなWebページからプライベートIPへのfetchにはプリフライトが必要になり、サーバー側が Access-Control-Allow-Private-Network: true を返さない限りブロックされる。

開発サーバー側でこのヘッダを返さないようにすることが、追加の防御になる。

// プライベートネットワークアクセスを意図的に拒否するミドルウェア
app.use((req, res, next) => {
  if (req.headers['access-control-request-private-network']) {
    res.status(403).end();
    return;
  }
  next();
});

チェックリスト

  • サーバーの Host ヘッダを許可リストで検証しているか
  • 開発サーバーのバインドアドレスが 127.0.0.1 になっているか
  • Admin系エンドポイントに認証(Bearer token等)がかかっているか
  • ルーターのDNSリバインディング保護が有効か
  • ローカルAPIにCSRFトークンを要求しているか

まとめ

DNS Rebindingは「ローカルからしか見えないから安全」という思い込みを崩す攻撃である。Host headerの検証と認証の追加は数行で実装できる。開発段階からこの2つを入れておくだけで、ほとんどのDNSリバインディングシナリオは無効化できる。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?