Webフレームワークの脆弱性対策コードを読む (Playframework part1)
追記
2019/09/04現在、報告されているPlayframeworkの脆弱性の全て (ver2系のみ)まとめました。 http://brutalgoblin.hatenablog.jp/entry/2019/08/20/133641
はじめに
フレームワークの脆弱性対策コードを普段読まれる方は割と少ないと思うので自分も勉強がでら読みながら問題となった事象、対策のコードではどうなっているかといった事をまとめてみます。
※注意
脆弱性の対策コードを読む中で、中にはクリティカルな脆弱性かつ、PoCも公開されていないものにも関わらず、
調査中PoCが思いついてしまうかもしれません。
ただ、そういった場合は若干濁した書き方をするかもしれませんがご了承ください。
また、筆者の知識不足で誤った書き方をするかもしれませんが、その際はガンガン指摘を頂けますと嬉しいです。
Playframework Part1
初回はPlayframeworkの脆弱性について調べていきます。
(しばらくはPlayframeworkの脆弱性のみまとめていきます。)
今回はScalaの主要WebフレームワークであるPlayframeworkの脆弱性対策コードを読んでいきます。
Playframeworkのセキュリティアップデートは以下のURLにまとまっています。
CVE-2018-13864 (Path Traversal)
こちらが今回の対象脆弱性です。 https://www.playframework.com/security/vulnerability/CVE-2018-13864-PathTraversal
CVSS: 6.7 影響バージョン: Play 2.6.12-2.6.15
最初のターゲットはパストラバーサルです。
最近のWebアプリエンジニアであれば「パストラバーサルなんてそんな簡単に起こらないでしょw」と笑っている事が多いかもしれません。
ただ、フレームワークやNginxやTomcatといった物でも、結構な頻度でパストラバーサルの脆弱性やバグハンティングのレポートが上がっています。
実際、nginxであれば末尾の / が無かっただけでパストラバーサルが発生してしまったといったことが容易に起こります。
興味ある方はTomcatやNginxにおけるパストラバーサルの脆弱性について書かれている以下の資料を是非見てください。 台湾のバグハンター Orange TsaiさんのBlackHat発表資料です。メチャクチャ面白いです。 https://i.blackhat.com/us-18/Wed-August-8/us-18-Orange-Tsai-Breaking-Parser-Logic-Take-Your-Path-Normalization-Off-And-Pop-0days-Out-2.pdf
さて、本題です。
今回の脆弱性 CVE-2018-13864 の概要を読みますと、
Playframework のAssets(静的ファイルなどのディレクトリ)用の
Controllerにある、Windows限定のパストラバーサルの脆弱性になります。
早速コードを追ってみましょう。チェンジログは以下のURLです。
(チェンジログにはパストラバーサルの修正以外も若干入ってます。)
https://github.com/playframework/playframework/compare/2.6.15...2.6.16/
まず、以下のファイルを見てみましょう。
framework/src/play/src/main/scala/play/api/controllers/Assets.scala
private def fileLikeCanonicalPath(path: String): String = {
@tailrec
def normalizePathSegments(accumulated: Seq[String], remaining: List[String]): Seq[String] = {
remaining match {
case Nil => // Return the accumulated result
accumulated
case "." :: rest => // Ignore '.' path segments
normalizePathSegments(accumulated, rest)
case ".." :: rest => // Remove last segment (if possible) when '..' is encountered
val newAccumulated = if (accumulated.isEmpty) Seq("..") else accumulated.dropRight(1)
normalizePathSegments(newAccumulated, rest)
case segment :: rest => // Append new segment
normalizePathSegments(accumulated :+ segment, rest)
}
}
val splitPath: List[String] = path.split('/').toList // 修正前
val splitPath: List[String] = path.split(filePathSeparators).toList // 修正後
val splitNormalized: Seq[String] = normalizePathSegments(Vector.empty, splitPath)
splitNormalized.mkString("/")
}
このファイルはAssetsファイル用のControllerClassで、おそらく今回の脆弱性の原因となったファイルだと思われます。
このファイルでは、URLのパスをsplitし、split結果をnormalizePathSegmentsという関数に渡すことで、再帰的にパスを整形するという動きをしています。
注目すべきところは、コメントアウトにて追加した、修正前、修正後と記載されている部分になります。
修正前は / というディレクトリの区切り文字によるsplitが行われていますが、
修正後は、 filePathSeparators という、区切り文字の配列が設定されています。
filePathSeparators 自体は以下になります。
framework/src/play/src/main/scala/play/api/controllers/Assets.scala
// Ideally, this should be only '/' (which is a valid separator in Windows) and File.separatorChar, but we
// need to keep '/', '\' and File.separatorChar so that we can test for Windows '\' separator when running
// the tests on Linux/macOS.
private val filePathSeparators = Array('/', '\\', File.separatorChar).distinct
コメントアウトや、 filePathSeparators の値を見て分かる通り、
パスの指定方法には / 以外にも、 \\, File.separatorChar の3つが指定されています。
つまり、この脆弱性で突かれたのは、Windows環境におけるファイルパスの扱い( \\, File.separatorChar )の考慮が漏れていたということになります。
File.separatorCharは以下のURLに書かれている内容のように、UNIX環境とWindows環境におけるパスの違いを考慮に入れた文字が入るようです ( /, \\ )。
https://docs.oracle.com/javase/jp/6/api/java/io/File.html#separatorChar
そのため、おそらく /../と言ったパターンは考慮されていたものの、 /..%5C%5C, /..\\ と言ったパターンの考慮が漏れていたということがわかります。
FWなどの複数環境で利用されることを考慮に入れた開発では開発環境毎の差を常に意識しなければならず、そういった環境でのセキュアコーディングの難しさがわかります。
おわりに
今回は比較的小さな修正と小さい(?)問題なので簡単に終わりました。
Part2では 20171005-CorsVaryHeader キャッシュポイズニングを発生してしまうような環境に一定の条件が重なるとなってしまうという問題の解消を行なった対応について書いてみようと思います。
https://www.playframework.com/security/vulnerability/20171005-CorsVaryHeader