見落としがちなサーバサイドPDF生成における脆弱性:SSRFやLFIによるシステムの侵害
高度診断部アプリケーションセキュリティ課の渡部です。
領収書や請求書発行等においてPDF生成が行われるような実装は多く存在します。サーバサイドでHTMLをもとにPDFを生成する方法は複数あり、例としてwkhtmltopdfを用いる手法、puppeteerを用いる手法、mPDFを用いる手法等が挙げられます。
本記事では、サーバサイドにおいて、HTMLを元にPDFを生成する際に発生しうるサーバサイドリクエストフォージェリ(SSRF)やローカルファイルインクルージョン(LFI)の脆弱性について、実装例を交えながら紹介します。
サーバサイドにおけるHTMLインジェクション
HTMLインジェクションといえば、クライアントのブラウザ上で発生するクロスサイトスクリプティングを思い浮かべる人も多いのではないでしょうか。クライアントサイドの観点では、クロスサイトスクリプティングによる任意のJavaScriptプログラムの実行やページの見かけ上の改竄といったリスクがあることが広く知られており、外部入力値を出力する際は適切にエスケープ処理をする対策が一般的に行われています。近年ではWebアプリケーションフレームワークがこのようなエスケープ処理を自動的に行うことも多く、手動でエスケープ処理を実装する機会は減っているかもしれません。
では、サーバーサイドでのHTMLからPDFの生成処理についてはどうでしょうか。基本的には、クライアントサイドと同様に、PDF生成元となるHTMLに外部入力値を利用する場合には適切なエスケープ処理が求められます。PDF生成処理におけるHTMLインジェクションについてはCTFでも出題されており(Insomni'hack CTF 2019のEzgen、HITCON CTF 2022のweb2pdf等)、攻撃者にとってはある程度知られている手法です。しかしこのような攻撃に対して脆弱なWebアプリケーションは、現実世界においてもたびたび見受けられます。
リスクという観点では、クライアントサイドにおけるHTMLインジェクションとサーバサイドにおけるHTMLインジェクションは異なってきます。次節において、HTMLをもとにしたPDF生成処理の脆弱な実装例と、それぞれのリスクについて説明します。
PDF生成処理におけるサーバサイドHTMLインジェクションのリスク
クライアントサイドにおけるHTMLインジェクションと大きく異なる点として、次のリスクが考えられます。
- サーバサイドリクエストフォージェリ(SSRF)による内部エンドポイントへのアクセス
- ローカルファイルインクルージョン(LFI)による非公開ファイルの奪取
SSRFについては、iframeタグ等を利用し内部ネットワーク上の他システムにアクセスするといった悪用が可能です。例えばEC2で動作する環境において、IMDSv1が用いられている場合、EC2インスタンスに割り当てられているIAMロールの認証情報を取得し、さらなる侵害につながる可能性が考えられるでしょう。
LFIでは、PDFを生成するプロセスの権限の範囲内ではあるものの、ソースコードの奪取やログの取得といった悪用が可能です。奪取したソースコードに外部サービスのクレデンシャル情報が記述されている場合等においてはさらなる侵害につながることもあります。
また、PDF生成ライブラリによっては直接的に任意コード実行が可能な場合もあります。PHPのライブラリの1つであるDompdfの例では、バージョン1.2.1未満において、CSSで指定されるフォント情報の取り扱いに起因した任意コード実行の脆弱性(CVE-2022-28368)が発見されています。Dompdfの該当バージョンのものを利用している環境にてHTMLインジェクションがある場合、サーバサイドにおけるPDF生成を起点としてサーバの侵害につながってしまいます。
サーバサイドPDF生成処理における脆弱性の事例
HTMLをベースとするPDF生成処理における脆弱性の事例についていくつか紹介します。
脆弱性報奨金制度のプラットフォームとして最も著名なHackerOneの例では、2023年にPDF生成処理に起因したSSRFの脆弱性が発見されました。この脆弱性により、AWSのメタデータサーバにアクセスしてIAMロールの認証情報を取得可能でした。CVSSスコアは最大値の10であり、報奨金として25,000米ドルが支払われています。
参考:https://hackerone.com/reports/2262382
また著名な配車サービスの1つであるLyftにおいては、2018年に乗車履歴情報に関するPDF生成処理に起因したSSRFの脆弱性が発見され、同様にIAMロールの認証情報を取得可能であることが指摘されました。影響度の高さから、当時の最大値の報奨金が支払われています。
参考:https://hackerone.com/reports/885975
このような点からも、世界的に著名なサービスでさえ見落としがちな脆弱性であり、システム構成に依存するものの非常に大きな影響を及ぼす脆弱性であることがわかります。
wkhtmltopdfを用いた脆弱な実装の例と攻撃方法
wkhtmltopdfはHTMLからPDFを行うツールの代表例です。2023年1月に開発が終了していますが、現在も本ライブラリを利用しているアプリケーションは多いのではないでしょうか。
wkhtmltopdfでは、バージョン0.12.5まではローカルファイルの参照が標準で許可されており、HTMLインジェクションがある場合にシステム上の非公開ファイルを取得可能であることが指摘されていました。これを受けてバージョン0.12.6では標準で無効化されたものの、PDF生成元となるファイルが複数のファイルにより構成されている(CSSファイルを読み込む等)状況等において、意図的に有効化している場合もあるでしょう。アクセス可能なファイルやフォルダを限定せず、単にenable-local-file-access
オプションを用いてしまうと、HTMLインジェクションが存在する場合は同様に非公開ファイルの取得が可能となります。
では、バージョン「0.12.5」を用いた脆弱なアプリケーション例と攻撃方法について紹介します。
脆弱な実装例と攻撃手法
脆弱なアプリケーションの例を次のリポジトリから取得し実行してみましょう。
次のようにDockerを用いて起動し、http://localhost:5000
にアクセスすると、宛名を入力して領収書を発行するアプリケーションが表示されます。なお、初回起動時はコンテナのビルドのため少々時間を要します。
$ git clone https://github.com/gmo-ierae/HTML2PDF-vuln-demo
$ cd HTML2PDF-vuln-demo/wkhtmltopdf
$ docker compose up
このアプリケーションの実装の一部について説明します。領収書PDFを発行するエンドポイントは次の通りです。パラメータ「receipt_to」に指定された宛名が領収書のHTMLテンプレート内の文字列と置き換えられていること、特段とエスケープ処理を行っていないことが確認できます。
# wkhtmltopdfを利用したPDF生成を行うエンドポイント
@app.route('/receipt', methods=['POST'])
def receipt_pdf():
file_id = str(uuid.uuid4())
receipt_to = request.form.get('receipt_to') or ''
template = open('./templates/pdf_template.html').read().replace('###to###', receipt_to)
cmd = ['/usr/local/bin/wkhtmltopdf', '-', f'./data/{file_id}.pdf']
subprocess.run(cmd, input=template.encode(), timeout=5, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
return send_file(f'data/{file_id}.pdf', download_name='receipt.pdf')
また、ローカルホストからのみアクセス可能なページとして次のエンドポイントがあります。このエンドポイントにブラウザからアクセスすると、エラーとなることが確認できます。
※ 見かけ上はローカルホストへのアクセスだが、実際にはコンテナ上で動作するアプリケーションへのアクセスのため
# 内部用エンドポイントを想定したもの
@app.route('/internal', methods=['GET'])
def internal():
if request.remote_addr != '127.0.0.1':
return 'internal access only'
return 'This is an internal page'
SSRFの脆弱性の攻撃例
先ほど記述した内部エンドポイント「/internal」におけるIPアドレス制限をSSRFを用いて回避してみましょう。攻撃文字列を次に示します。非常に単純なものであり、iframeタグを利用し、ソースとして「http://localhost:5000/internal
」を指定するようなものとなっています。
<iframe src="http://localhost:5000/internal"></iframe>
これを宛名として指定し発行すると、領収書内に内部エンドポイントへアクセスした際のスクリーンショットが含まれていることが確認できます。
このときのアプリケーションへのアクセスログを確認すると、iframeタグを挿入したことにより、レンダリング時に「127.0.0.1」からのアクセスが生じていることが確認できます。
app-1 | 127.0.0.1 - - [04/Sep/2024 07:43:08] "GET /internal HTTP/1.1" 200 -
app-1 | 192.168.127.1 - - [04/Sep/2024 07:43:08] "POST /receipt HTTP/1.1" 200 -
LFIによる非公開ファイルの取得
次に、LFIによりシステム上の非公開ファイルを取得する攻撃例について示します。先ほど示した通り、wkhtmltopdfのバージョン0.12.5を用いています。このバージョンにおいては、少し挙動が変更されており、単純にfileスキームを用いたURLをiframeタグのsrcとして利用することは出来なくなりました。
<iframe src="file:///etc/passwd"></iframe>
しかしながら、この制約を回避する次のような方法が発見されました。
- iframeタグのsrcに、fileスキームを用いたURIにリダイレクトするHTTPエンドポイントを指定する
- XMLHttpRequestを用いて取得する
参考:
https://github.com/wkhtmltopdf/wkhtmltopdf/issues/3570
https://github.com/wkhtmltopdf/wkhtmltopdf/issues/4536
ここでは、2つめの方法について例を示します。XMLHttpRequestを利用しfile:///etc/passwd
を取得するような下記の攻撃文字列を送信すると、iframeタグを用いた場合とは異なり当該ファイルの内容が取得できていることが確認できます。
<script>
xhr = new XMLHttpRequest();
xhr.open("GET", "file:///etc/passwd");
xhr.onload = function(){
document.write(this.responseText)
};
xhr.send();
</script>
サーバサイドでHTMLをもとに安全にPDFを生成するためには
ここまでHTMLをベースとしたPDF生成処理で生まれがちな脆弱性と攻撃手法について紹介しました。では、このような処理を安全に実装するためにはどのようにすればよいでしょうか。
最も重要な点は、外部入力値といった信頼できない文字列を用いる際に適切にエスケープ処理することです。そのうえで、多層防御として次のような実装を追加するとより安全になるでしょう。
- PDF生成ライブラリが提供するセキュリティ機構を使う
- アクセス可能なローカルファイルの制限(wkhtmltopdfの
allow
オプション等)
- アクセス可能なローカルファイルの制限(wkhtmltopdfの
- サンドボックス環境でのPDF生成
- PDF生成処理におけるリソース制限
- タイムアウトの設定やPDF生成を行うプロセス数の制限等
- 利用するPDF生成ライブラリの脆弱性情報の収集および更新
また、アプリケーションの要件次第ではあるものの、サーバサイドでPDFを生成するのではなく、クライアントブラウザのPDF印刷機能を利用して生成する方法も考えられるでしょう。
おわりに
本ブログでは、サーバサイドでHTMLをもとにPDFを生成する処理で発生しがちなHTMLインジェクションとその影響について紹介しました。
GMOサイバーセキュリティ byイエラエではバグバウンティやセキュリティコンテストなどで活躍するセキュリティエンジニアが実施する「Webペネトレーションテスト <シナリオ型> / <調査型>」を提供しています。標準的なWebアプリケーション診断では検出ができないような脆弱性も検出し、リスクを評価します。
また、ソースコードを共有いただくことによってより精度の高い調査を実施することが可能な他、気になるCVEやライブラリ、フレームワークを踏まえた脆弱性調査やご相談も可能です。
是非お気軽にご相談ください。