コード監査において、SSRF(Server-Side Request Forgery)脆弱性を発見する際には、まず以下のようなキーワードを検索してCMS内でSSRFが利用されている箇所を洗い出す。
/**
* 監査対象の関数
* 1. URL
* 2. HttpClient
* 3. OkHttpClient
* 4. HttpURLConnection
* 5. Socket
* 6. ImageIO
* 7. DriverManager.getConnection
* 8. SimpleDriverDataSource.getConnection
*/
OkHttpClientを検索したところ、OkHttp3をラップしたユーティリティクラスが存在しているのを発見。ただし、このクラスはHTTPプロトコルのみを主にサポートしている。その後、変数urlが制御可能かどうかを確認するために呼び出し元をたどっていく。
ここでは脆弱性を発見した一連の流れのみを記録する。実際の監査ではすべての候補を調査する必要があるが、今回はfetchRemoteFileの呼び出しを追跡した。
最終的にルーティングの箇所に到達し、悪用の可能性があることが判明した。権限チェックの詳細は省くが、このインターフェースは認証なしでアクセス可能であった。
コードのロジックを見ると、typeパラメータを取得し、isExternalUrlで外部URLかどうかをチェックしている。外部URLの場合はエラーを返す。
チェック処理は単純で、URLがhttp://またはhttps://で始まるかどうかを判定している。これがフィルタリングの実態である。
チェックを通過すると、URLの後ろに/index.jsonを連結してパラメータとして渡す。以降のコードを見てみよう。
public static JSON fetchRemoteJson(String fileUrl) throws RebuildException {
String content = fetchRemoteFile(fileUrl);
if (JSONUtils.wellFormat(content)) {
return (JSON) JSON.parse(content);
}
throw new RebuildException("Unable to read data from RB-Store");
}
次にfetchRemoteFileを見る。このメソッドでは、fileUrlがhttpで始まるかどうかを判定し、始まる場合はそのままgetで内容を取得する。httpで始まらない場合は、無関係なURLを連結してしまい悪用できない。しかし、前述の外部URLチェックは既に通っている。
しばらく検討した結果、ここで判定しているのはhttp(コロンなし)であるのに対し、前述のチェックはhttp://やhttps://(コロン2つ+スラッシュ)であることに気づいた。つまり、前方のチェックと後方の判定に不整合があり(多くの脆弱性の原理)、バイパスの可能性がある。そこでhttp:/やhttps:/(コロン1つ+スラッシュ)を試してみることにした。
テストクラスを作成して確認したところ、可能であった。OkHttp3ライブラリはhttp:/やhttps:/をパースできる。
さらに、URLの後ろに/index.jsonを連結される問題に対処する必要がある。様々な方法があるが、ここではパラメータを使って後続の/index.jsonを打ち消し、無効なパラメータとして送り込む手法を選んだ。
例:http://10.82.189.40:18080/setup/load-index?type=http:/127.0.0.1:80/2.txt?x=
このリクエストにより、攻撃者のサーバに置いたファイルの内容を取得できた。ただし、その後JSON解析が行われるため、JSON形式のコンテンツのみがレスポンスに反映される。すなわち、結果は非表示のSSRF(No-echo SSRF)となる。任意のHTTPリソースにアクセスさせることは可能だが、結果が返ってこないため、被害の程度は低いと言える。とはいえ、実際の監査で得られた貴重な経験である。