徳丸本2版の実習環境をDockerに移植した話

# はじめに
拙著「[安全なWebアプリケーションの作り方 第2版](https://www.amazon.co.jp/dp/4797393165/)（以下、徳丸本と略記）」では、脆弱性を読者が試せるための実習環境を提供しています。書籍でご案内しているのは、VirtualBox上のバーチャルイメージとして提供する形式で、中身はDebian9上で動くウェブアプリケーションです。VirtualBoxを採用した理由は、WindowsとMacの両方で無料利用できるからです。
しかし、2020年にApple SiliconのM1 Macが登場し、当時Virtual BoxがM1 Macに対応していなかった[^1]ことから、M1/M2 Mac上で実習環境が使えないという問題が起きました。有志の方が、[Apple Silicon Mac(M1 Mac)で徳丸本の実習用仮想マシンを動かす - Qiita](https://qiita.com/bamboo_and_tyre/items/2e5dd35d0b03235e9ed9)などの記事を公表いただいたりしたものの、設定が大変などハードルの高いものでありました。
このため、現在の多くの方が使っているDocker上に実習環境を移植しました。[こちら](https://wasbook.org/download/)からダウンロード可能ですが、Basic認証の際に徳丸本記載のIDとパスワードが必要です。ダウンロードしたZIPファイルは、unzipした後に、docker compose up -d するだけで使用できます。詳しくは[ドキュメント](https://wasbook.org/wasbook-docker.html)を御覧ください。ドキュメントの参照にはパスワードは必要ありません。


[^1]:2022年10月10日に[VirtualBox 7.0開発者プレビュー](https://www.virtualbox.org/manual/UserManual.html)としてApple Silicon対応版が出ています。

# 徳丸本実習環境の構成
徳丸本実習環境は、外見上はPHPによる単純なウェブアプリケーションのように見えるので、Docker対応も簡単そうに見えると思いますが、実は中身は結構複雑な構成になっています。以下は、主なソフトウェアの一覧です。

|サービス|ソフトウェア|バージョン|目的等|
|:-|:-|:-|:-|
|リバースプロキシ|Nginx|1.10.3|キャシュ系の脆弱性のため|
|Webサーバー|Apache|2.4.25|PHP稼働|
|PHP|PHP|5.3.3/7.0.27|脆弱なアプリケーション記述|
|Perl|Perl|5.24.1|Perl CGI; HTTPヘッダインジェクション等|
|データベース|MariaDB|10.1.26|SQLインジェクション等|
|メール配信サーバー|Postfix|3.1.8|メール送信系の脆弱性|
|POP3/IMAP4|Dovecot|2.2.27|メール送信系の脆弱性|
|サーブレットコンテナ|Tomcat|8.5.14|Servletの脆弱性|
|Webメール|Roundcube|1.2.3|メールクライアント|
|XML操作ライブラリ|Libxml2|2.7.8/2.9.4|XXE脆弱性実習のため|
|DB管理ツール|phpMyAdmin|4.6.6|実習補助|
|OS（Linux）|Debian|9|OS|

## PHPのバージョン
主なコンテンツ（脆弱なスクリプト）はPHP 5.3.3（CGIモード）で動いています。これは、古いPHPでないと発現しない脆弱性に対応するためで、代表的なものとしてNULLバイト攻撃があります。PHPは5.3.4以降でNULLバイト攻撃に対応したため、NULLバイト攻撃を体験するにはPHP 5.3.3以前のものを使う必要があります。一方、新しいバージョンのPHPが必要な箇所もあり、その場合はPHP7.0.27（Debian9標準パッケージのもの）を使い分けています。
## メール関連の脆弱性対応
メールヘッダ・インジェクションを体験するにはメールの送受信の仕組みが必要なため、Postfix、Dovecot、RoundCubeがインストールされています。徳丸本初版は「お手元のメールクライアントソフトをお使いください」という体でしたが、今どきメールクライアントを使っている人も少ないし、意外と設定が面倒ということもあって、第2版ではWebメールとしてRoundCubeをバンドルしています。
## HTTPヘッダインジェクション対応
PHPでHTTPヘッダインジェクションを再現するためには、PHP 5.1.1以前が必要ですが、こちらは「あまりに古すぎる」という点で見送りました[^2]。このため、HTTPヘッダインジェクションはCGIプログラムの形で紹介しており、そのためPerlを使っています。
[^2]:実は徳丸本VMにはPHP5.1.1がインストールされていますが、利用されていません。
## XXE脆弱性対応
徳丸本2版ではXXE（XML外部実体参照）脆弱性についても説明していますが、これをPHPで再現するにはlibxml2の2.9未満のバージョンを使う必要があります。徳丸本2版の実習環境では、この目的のためにPHP5.3.3からlibxml2 2.7.8を呼び出すように構成しています。
## Javaのサンプルを動かす
徳丸本では、大半のサンプルスクリプトをPHPとJavaScriptで記載していますが、少数ながらJava Servletのものがあります。一つはXXEのサンプルとして、もう一つはレースコンディションでServlet独特の脆弱性を説明するためです。このため、徳丸本VMにはTomcat 8.5.14がインストールされています。
## キャッシュ関連の脆弱性
徳丸本2版から、キャッシュ関連の脆弱性も説明しています。これを再現するにはキャッシュサーバーが必要ということで、この目的のためにNginxをリバースプロキシとして設置しています。

# 課題と解決策
徳丸本実習環境の構成を説明しましたが、これをDocker上で解決するための解決策について説明します。

## 全体構成をどうするか
まず、Dockerの構成をどうするかです。VirtualBox版のVMは以下のような構成になっており、サービスとして、Nginx、Apache、Tomcat、MariaDB、Postfix、Dovcotが動いています（下図）。

![wasbook-vm.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22718/28c8ea34-1027-c48d-bd3f-0d349660b8e7.png)

Dockerのベストプラクティスとしては、これら「全部入り」にするのではなく、サービス毎にコンテナを用意することが推奨されています。検討の初期段階では「全部入り」も考えたのですが、ここは素直にサービス毎にコンテナを分けることにしました。
さらに、phpMyAdminやPostfix+Dovecot([Docker Mailserver](https://github.com/docker-mailserver/docker-mailserver))、RoundCubeについては、セットアップ済みのDockerイメージが提供されているので、使用も検討したのですが、補助的なツールとしては重たい構成に感じたので、以下のようにしました。

- メール関連: [MailCatcher](https://mailcatcher.me/)を用いる
- DB管理ソフト: phpMyAdminに代えて[Adminer](https://www.adminer.org/)（1つのPHPファイルだけで動くDB管理ソフト）を用いる

最終的なコンテナの構成は以下となりました。
![wasbook-container.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22718/1f507856-3fbe-64e7-3c43-fcf871bd4022.png)

### PHP 5.3.3どうする?
前述のように、徳丸本2版の実習環境には、PHP 5.3.3が使われています。[Docker Hub](https://hub.docker.com/)にはPHP 5.3.3も存在しますが、ARM64版がなかったり、特殊な構成だったりで採用せず、独自にビルドすることにしました。古いPHPをビルドするのは、古めのLinuxディストリビューションの方がやりやすいので、ビルド用コンテナとしてDebian 9(Stretch)を用いました。
PHPから利用するライブラリとしては、OpenSSL 0.9.8zh、libxml2 2.7.8、MySQL 5.0.15もビルドして用いています。パージョンが古いのは、PHP側に合わせているのと、前述のXXE脆弱性のデモのためです。
Debian9でビルドしたPHPバイナリでもDebianの最新版 11(bullseye)でも動くので、実行環境はDebian 11としました。

## Apacheコンテナ
ApacheコンテナのDockerファイルは、Debian 11ベースのApacheモジュール版のPHP 8.1をベースにしています。加えて、前述のPHP 5.3.3 CGIモードのバイナリをコピーしています。CGIモードにしているのは、Apacheモジュール版のPHPでは複数バージョンの同居が難しいからです。PHPバイナリは、ARM64用とAMD64用に別コンテナでビルド済みのものを、DockerのARG変数によりARM64またはAMD64のバイナリをアーキテクチャに応じてコピーしています。

```Dockerfile
ARG TARGETARCH
# 中略
# アーキテクチャに応じて PHPバイナリをコピー
ADD  php-5.3.3.bin-${TARGETARCH}.tar.gz /usr/local/bin/
# アーキテクチャに応じて libxml2 をコピー
WORKDIR /usr/local/libxml2.7.8/lib
COPY libxml2.so.2.7.8-${TARGETARCH} libxml2.so.2.7.8
RUN ln -s libxml2.so.2.7.8 libxml2.so.2 \
    && ln -s libxml2.so.2.7.8 libxml2.so
```

上図でlibxml2 2.7.8もコピーしていますが、前述のXXE脆弱性の実習のためです。使用しているDockerイメージには元々PHP 8.1.13とlibxml2 2.9.10がインストールされていますが、こちらは脆弱性対策済みのデモ用に用いています。
AdminerについてはPHPスクリプトをDockerfileでCOPYしていますが、Debianの標準パッケージにあるのでapt installでもよかったと後で思いました。

### sendmailコマンドの配置(msmtp)
メールヘッダ・インジェクションの実習でPHPからメールを送信するには、ApacheコンテナからMailコンテナにSMTPでメールを配送する必要があります。PHPMailerのような高機能のライブラリを用いてメール送信する場合は、ライブラリの機能でメールサーバーに配送できますが、徳丸本のサンプルではmb_send_mail関数を用いており、この場合はPHPからsendmailコマンドを呼び出す形になります。このため、sendmail互換のMTA（Postfix、exim4等）を設置して…となるのですが、これだとApacheとPostfixがコンテナ内で同居することになります。このため、[msmtp](https://marlam.de/msmtp/)を用いることにしました。この辺はDocker内のPHPからメール送信する場合の定番の方法かと思います。msmtpはDockerファイル内でaptにて導入しています。

```Dockerfile
RUN apt install -y msmtp msmtp-mta 
```

msmtpの設定ファイル（/etc/msmtprc）は以下の通りです。

```
account default
host mail
port 25
from "postmaster@example.jp"
logfile /var/log/apache2/msmtp.log
```
logfileが/var/log/apache2/以下に配置されている理由は、msmtpが提供するsendmailコマンドの処理はApacheの権限で動く（メールデーモンとして動くのではない）ので、Apacheからアクセスできる場所にログを書き込む必要があるからです。[こちらのissue](https://github.com/CampbellSoftwareSolutions/docker-osticket/issues/11)に記載された方法も考えましたが、パーミッション設定の手抜きでこうしています。

## Mailコンテナ
Mailコンテナには前述のようにMailCatcherを使っています。MailCatcherはメール送信のテスト等によく用いられるツールで、受信したメールを単に画面表示するRubyベースのツールです。SMTPの受信とウェブの画面表示の機能があります。[こちらのDockerイメージ](https://hub.docker.com/r/schickling/mailcatcher/)を利用しました。AMD64とARM64の両方に対応しているので好都合です。
下図はMailCatcherの画面例で、実習環境のウェルカムメッセージです。

![mailcatcher.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22718/ebc8b3b6-06fa-15a5-936f-8a59159c9af6.png)

## Nginxコンテナ、MariaDBコンテナ、Tomcatコンテナ
Nginx、MariaDB、Tomcatの各コンテナはDocker社提供の標準イメージを設定しているだけなので説明は省略します。

## 接続先のホスト名とポートの問題
「やられサイト」として有名な[OWASP Juice Shop](https://github.com/juice-shop/juice-shop)等もDockerで提供されていますが、接続先としては、http://localhost:3000/ のような形式になっています。一方、徳丸本実習環境は以下が異なります。

- example.jp（脆弱なサイト）、api.example.net（APIのホスト）、trap.example.com（罠サイト）と役割ごとにサイト名が明記されている
- 罠サイト（trap.example.com）から脆弱なサイト（example.jp）にリンクがある、サイトからAPIにXHRで呼び出すなど、ポート番号を変更しにくい条件がある

すなわち、このままでは80や443という使用頻度の高いポートが衝突してしまうという問題があります。また元々の設定内容ですが、/etc/hostsの編集が必須となり、管理者権限が必要なため地味にハードルが高いようです[^3]。
なにか良い解決策はないかと思っていたところ、Dockerコンテナ側にProxyを立てて、Proxy経由でアクセスしてもらえばよいと思いつきました。ProxyとしてはApacheのmod_proxyの機能を用いることにしました（Nginxはフォワードプロキシの機能を標準では提供しません）。接続のイメージは以下のようになります。

![docker-zap-proxy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22718/6a075fe0-79a8-1c80-0d05-899892445c51.png)

上図のように、ブラウザからのリクエストは、ブラウザ→OWASP ZAP（診断用プロキシ）→Apache Proxy（フォワードプロキシ）→Nginx（リバースプロキシ）→Apache（ウェブサーバー）という経路をとります。
この構成により以下のメリットが生じます。

- Dockerコンテナが公開するポート番号は自由に設定できる
- ホスト名の名前解決はコンテナ内部でできるので/etc/hostsの設定が必要ない

ホスト名の指定については、docker-compose.ymlのnginxサービスにて、ホスト名をaliasとして定義しています。
```yaml
  nginx:
    build: nginx
    # Nginxを直接公開する場合は以下の3行を有効化する
    # ports:
    #  - ${WEB_IP:-127.0.0.1}:80:80
    #  - ${WEB_IP:-127.0.0.1}:443:443
    depends_on:
      apache:
        condition: service_started    
    networks:
      internal:
        aliases:
          - example.jp
          - api.example.net
          - trap.example.com
          - internal.example.jp
```

# まとめにかえて
徳丸本実習環境をM1/M2 Macに対応しなきゃと思いつつ、ずいぶん経ってしまいましたが、ようやくリリースすることができました。作業を開始する前は「面倒くさいんだよなー」と作業がはかどらなかったのですが、いざはじめてみると何だか楽しくなってしまい、当初は「メールは面倒だから後回しで」、「Tomcatはあまり使ってないから後回しでもいいかな」などと思っていましたが、PHPの部分が立ち上がったあたりで何だか楽しくなってしまい、一気に作業を進めることができました。
まだ移植に由来する不具合など残っているかと思いますが、お気づきの点は[サポートメーリングリスト](https://wasbook.org/ml.html)から教えていただければ幸いです。

[^3]:/etc/hostsの編集くらい楽勝だろ、というツッコミがありそうですが、/etc/hostsの編集が難しい層にもセキュリティの知識が届き始めていると思いたいですw