令和4年秋季情報処理安全確保支援士試験にてDNSキャッシュポイズニングについての問題が出題されました。実際にやったことがなかったので、どうやったらできるか手を動かしてみました。
目次
利用したもの
・AlmaLinux
・BIND 9.11.4-P2-RedHat-9.11.4-26.P2.el7_9.10 (Extended Support Version)
きっかけ
令和4年の秋季情報処理安全確保支援士試験の午後問題にDNSキャッシュポイズニングに関する出題がありました。
いろんなところでDNSキャッシュポイズニングの説明の記事も見ますが、自分でやっている記事はあまり見かけません。
「DNSキャッシュポイズニングって引き起こせるんだろうか。」そう思ったことがきっかけでした。
結論:できた。
学んだこと
最初にやってみて間違って認識をしていたことと、理解が深まったことを記載します。
認識を間違えていたこと
・DNS(bind)をだますには、query IDだけを詐称すればいいわけではない。
理解したこと
・パケットキャプチャからIPパケットフォーマットに則って、データを読み取る方法
・データグラムとDNSパケット(データ)の生成方法
・IPスプーフィングをお手軽にやる方法(ここでは手法について説明しません。)
上記を読んで興味をひかれましたら、読んでいただければ幸いです。
DNSキャッシュポイズニングについて
Addressing Weaknesses in the Domain Name System Protocol(Christoph L. Schuba)
上記論文が最初にDNSキャッシュポイズニングの危険性を提唱しています。
※時代背景や言語がことなるため、私はあまり参照していません。
DNSキャッシュポイズニングについて理解されていなければ、次に進む前に以下を参照しておくことを推奨します。
インターネット10分講座:DNSキャッシュポイズニング
DNSキャッシュポイズニングを引き起こすために重要なポイントがいくつかあります。
- キャッシュサーバにキャッシュポイズニングの対象とするドメインのキャッシュが存在しないこと
- 通信を行う一組のキャッシュサーバと権威サーバにおいて、それぞれのサーバプロセスのポート番号とIPアドレスが一貫していること※1
- キャッシュサーバと権威サーバ間の名前解決に必要な通信において、DNSヘッダーセクションフォーマット内のID(以下クエリIDと記載)が一致していること
- 上記条件に一致したうえで、DNSキャッシュポイズニングを行う端末から権威サーバよりも早く偽装データグラムをキャッシュサーバに送り、攻撃を成功させる必要がある。
検証している中で細かな部分はあれど、重要なのは上記4項目です。
「4.」が本検証の一番の問題点だったと認識しています。
なお、今回はDNSSECの設定は行われていないことを前提としています。
※1
RFC768に記載の通り、データグラムでは送信元ポート番号は「オプション」とされています。データグラムの扱いにおいて、双方向で通信を行えるようにすることは必須ではありません。
RFC 1034- (ドメイン名:概念と機能)、RFC 1035 – (ドメイン名:実装と仕様)においても明記はされていません。BINDやOS側での実装に依存する仕様かと思われます。
疑似的に宛先をチェックするような機構がどこかで動いているのでしょう。
構成
検証の流れ
1.ソースポートランダマイゼーションの無効化
「通信を行う一組のキャッシュサーバと権威サーバにおいて、それぞれのサーバプロセスのポート番号とIPアドレスが一貫していること」において、送信元サーバプロセスのポート番号を固定することで、「上記条件に一致したうえで、DNSキャッシュポイズニングを行う端末から権威サーバよりも早く偽装データグラムをキャッシュサーバに送り、攻撃を成功させる必要がある。」の条件を満たしやすくするために行います。
ソースポートランダマイゼーションを行った場合、権威サーバからキャッシュサーバへの応答には「クエリIDの一致+キャッシュサーバの再帰問い合わせのプロセスに用いられるポート番号の一致」が必要になります。
一方、ソースポートランダマイゼーションを行わないと「クエリIDの一致」(厳密にはIPアドレスでの一致も必要)のみでDNSキャッシュポイズニングが行えてしまいます。
詳細な内容についてはソースポートランダマイゼーションとは何ですか?(ITmedia)を参照してください。
設定前の状態でキャッシュサーバから権威サーバへの再帰問い合わせのパケットを確認してみます。
設定前
キャッシュサーバ(172.23.61.61.)の送信元ポート番号が、上の画像は15744になっています。
2回目は23808になっています。
上記より、ソースポートランダマイゼーション機能が有効化されていることが分かります。
ソースポートランダマイゼーションの設定はBIND9のマニュアルに記載があります。
https://bind9.readthedocs.io/en/v9_18_9/reference.html#query-address
以下の設定を/etc/named.conf(標準パス)に付け加えます。
query-source port 53; query-source-v6 port 53;
このように設定し、再読み込みすることでキャッシュサーバから外部への再帰問い合わせ時の送信元ポート番号が53に固定されます。
上記の通り、何度もアクセスしても送信元ポート番号が変わっていないことが分かります。
2.偽装データグラム作成
本検証を始めるときに山場と感じていました。
データグラムをどう作ればいいのか正直わかりませんでした。
とりあえず、言語選定をしました。大量のパケットを送ることになるため、コンパイル言語のほうが適切かと思い、CかJavaにしようと思いました。しかし、セットアップが楽でいつも使い慣れているという理由でNode.jsを使うことにしました。
だれかがDNSキャッシュポイズニングをするためのライブラリをnpmに公開してくれているのでは、と期待しましたが、見つかりませんでした。
しょうがないので、自分で公式ドキュメントを読みながらプログラムを作ることに。
Node.js documentation(UDP/datagram sockets)
socket.sendのmsgにおいて利用可能な型を確認しました。パケットキャプチャや色々なドキュメントを調べた結果、プログラム上はString型のHexデータを引数に与えれば実現ができそうだとわかりました。
どうやってパケットを生成すればいいのかわからなかったため、データのフォーマットを調べました。
上記記事を読みましたが、1から自分でデータグラムを作るのは非効率的なので、パケットキャプチャをした値をそのまま使ってみることに。
0x0000: 4500 005a 9cd8 0000 3211 3586 d29d fa42
0x0010: ac17 3d3d 0035 0035 0046 8d18 963d 8400
0x0020: 0001 0001 0000 0001 0377 7777 0973 6563
0x0030: 7561 7661 696c 0363 6f6d 0000 0100 01c0
0x0040: 0c00 0100 0100 0001 2c00 0412 b4a7 6a00
0x0050: 0029 1000 0000 8000 0000
冒頭の4500から末尾の0000までをdgram.createSocket(‘udp4’).sendのmsgに含めて、通信を送ったところ、同じバイナリが繰り返される部分がある事に気づきました。
あくまでdgram.CreateSocket(‘udp’)は、データグラムによる通信を行うためのモジュールです。
dgram.CreateSocket(‘udp’)で通信を送ると、IPパケットフォーマットがモジュール側で補完されるようでした。つまり、上記の方法で私が送った通信は、[IPパケット(L3情報)]+[IPパケットみたいなデータグラム+データグラム](L4以上の情報)]となっていたわけでした。
そのため、IPパケットとデータグラムの境目を見極める必要がありました。
パケットの中身は本来1と0のみで信号を作っています。
パケットキャプチャをした結果は、16進数で表現されています。2進数において15は1111であるため、16進数1文字の表記=4ビットと計算することができます。
IPパケットのフォーマットを参照しながらパケットキャプチャ結果を比較すれば後はできそうです。
「0x0000: 4500 005a 9cd8 0000 3211 3586 d29d fa42」
例えば、計算することで上記赤文字の部分が送信元IPアドレスの部分などというように読み取ることができます。
ブラウザの開発者ツールからJavaScriptを使うことで、16進数を10進数に変換できます。
parseInt('d2',16) => 210
parseInt('9d',16) => 157
parseInt('fa',16) => 250
parseInt('42',16) => 66
あらかじめ定められているフォーマットに従って、パケットキャプチャ結果を解析した結果、クエリIDや、ドメインの名前解決結果として応答するIPアドレスの場所を特定できました。
クエリIDを1~65535まで16進数かつ適切にパディングしたうえで、詐称対象とするIPアドレスを組み込むプログラムを作成することができました。
自社のWebサーバ用のFQDN(www.secuavail.com[18.180.167.106]))の名前解決時に、www.example.com[93.184.216.34]のIPアドレスを応答するように偽装データグラムを作成しました。
3.なんどやってもうまくいかない
偽装データグラムの準備ができ、これでうまくいくだろうと思って何十回か試してみましたが、DNSキャッシュポイズニングは成功しませんでした。
これには2つの要因がありました。
まず1つ目は、キャッシュされていないFQDNに対する名前解決のスピードが想像以上に早かったことです。DNSキャッシュポイズニングが難しい要因の一つです。
キャッシュサーバの送受信トラフィックに上限を設ける、stressコマンドなどで負荷を上げることなどを考えましたが、いずれも期待できる結果に直結しない気がしました。
これについては、解決することが難しいものと思われたため、裏技を使うことに。
FWでwww.secuavail.comのNSへの再帰問い合わせを行えないようにすることで、クエリのタイムアウト時間を引き延ばして、キャッシュポイズニングが成功しやすいように調整をしました。
2つめの要因は、送信元IPアドレスでした。
「通信を行う一組のキャッシュサーバと権威サーバにおいて、それぞれのサーバプロセスのポート番号とIPアドレスが一貫していること」の、「IPアドレスが一貫していること」ということが必須であるとは当時思っていませんでした。
キャッシュサーバ側で応答が返ってきた場合には、送信元IPアドレス、宛先ポート番号、送信元ポート番号、クエリIDをチェックしたうえでキャッシュしているのだろうと推測しました。
送信元IPアドレスの詐称(IPスプーフィング)が最後のカギとなったのでした。
4.IPスプーフィング
IPスプーフィングはIPアドレスの詐称です。
TCPでの通信では3ウェイハンドシェイクを行ったうえで、アプリケーションの通信を行います。しかし、UDPでの通信は3ウェイハンドシェイクなどを行わないため、IPスプーフィングの影響がより大きくなります。
DoS攻撃などで攻撃元を隠すために利用するケースがあるなど、この手法がある前提で成り立っている攻撃がいくつかあります。
以前から、どのように行うのか興味があったため、この機会に色々調べてやってみました。
ただ、申し訳ありませんが、IPスプーフィングの方法については、あえて明記しないこととします。
5.改めてDNSキャッシュポイズニングを試す
これまでのことを踏まえたうえで、改めてDNSキャッシュポイズニングを試しました。
何度も試すと以下の結果を得ることができました。
DNSキャッシュポイズニング成功時のdig結果画面
正常なdig結果画面
悪用可能か
試しに、ポイズニングされたwww.secuavail.com(www.example.comのIPアドレスに書き換えられている)にHTTPSでアクセスしてみました。
すると証明書エラーによってリクエストに失敗しました。
証明書を使う理由の一つとして、クライアントがアクセス先のサーバの認証を行うというものがあります。今回は、リクエスト先がwww.secuavai.comであるが、サーバ証明書のCommonName(SAN)と一致しないため、エージェントがリクエストを中断させました。
常時HTTPS化のメリットとしては、よく暗号化が行われることが挙げられますが、このようなメリットもあることを再認識させられました。
まとめ
DNSキャッシュポイズニングは、名前が一人歩きしていて、実際に自分で試したことがある人はほとんどいないのではないでしょうか。
悪意をもってDNSキャッシュポイズニングを行う環境を作る必要がある事と、それを実現させることをハードルはかなり高いと感じました。
攻撃に成功してもHTTPSを使うのであれば、悪影響を及ぼすことは難しいです。
ただ、それ以外のメールなどでは認証が進んでいないため、比較的攻撃を受けやすいのかもしれません。メールサーバについても認証を浸透させていく必要があると感じました。
後日談
シニアエンジニアから、DNSキャッシュポイズニングについて色々と伺いました。
DNSキャッシュポイズニングが問題となった時に、DNSサーバの設定不備によりオープンリゾルバになっているものが多くあった。カミンスキーアタックが発見されるとともに、オープンリゾルバのキャッシュを汚染することで、それらを使用していたユーザの被害が大きくなったという歴史があったとのことです。
今回の検証では、内部に侵入されたような環境でしたが、実際の悪用方法だと、多数のユーザが使用しているオープンリゾルバのIPアドレスを送信元にして、インターネット経由で偽装データグラムを送っていたということですね。
最近自分でDNSサーバを構築することは少なくなりつつあると思います。ただ、システムの設計上どうしても自分でDNSサーバを構築する必要などが出てくると思います。その際にはクエリを受け入れる範囲を確認し、オープンリゾルバにならないように気をつけましょう。
また以下の資料を案内いただきました。
JPRSキャッシュポイズニング攻撃対策
https://jprs.jp/tech/security/2014-04-30-poisoning-countermeasure-resolver-1.pdf
IPスプーフィングを防ぐ機構も伺うことができ、今回の検証を通じて少し理解が深まったような気がします。
個人的にはIPスプーフィングや偽装データを作れるようになったため、かなり発見がありました。
記載されている会社名、システム名、製品名は一般に各社の登録商標または商標です。
当社製品以外のサードパーティ製品の設定内容につきましては、弊社サポート対象外となります。