ここで書く内容は blackhat USA 2017 で Orange Tsai 氏が発表した 「A New Era of SSRF - Exploiting URL Parser in Trending Programming Languages!」 で取り上げられていた内容を自分が改めて理解するために書き残したメモです。
概要
var base = "https://example.com/article/";
var path = req.query.path;
if (path.indexOf("..") == -1) {
http.get(base + path, callback);
}
上記のようなコードのとき、 /?path=../passwd
のようにアクセスして http.get()
の処理に入り、 /article/../passwd
を取得することはできるだろうか。
一見 if 文は false
になるため不可能そうに見えるが、実は true
になることがある。
true
とするには /?path=ĮĮ/passwd
というURIにアクセスすることで可能となる。
ここでは、なぜこれでバイパスできてしまうのか、ということを書いていく。
ちなみに今回検証で確認できたのは Node.js 4.9.1 Argon ( ubuntu 16.04 LTS で未だに入る ) 。
v10.13.0 Dubnium では確認できなかった(ちゃんと文字コード関係のエラーが出るようになっていた)。
どのバージョンから再現できないのかまでは調べていないので、知っている人がいたら教えてください。
サロゲートペア
JS の内部表現は UTF-16 なのだが、これが Node.js でどのように扱われるのか。
答えは JavaScript’s internal character encoding: UCS-2 or UTF-16? に書かれている。
💩
は UTF-16 ではサロゲートペアで \uD83D
と \uDCA9
の2つに分離される。
> '💩'.split('').map( (c) => c.codePointAt().toString(16) )
[ 'd83d', 'dca9' ]
このサロゲートペアはどのように計算されているのだろうか。
上記記事でも参照されているが、 Section 3.7 of The Unicode Standard 3.0 で定義されている。
0xFFFF
よりコードポイントが大きい C
は次の計算式によって2つのコードポイント H
と L
に分離される。
H = Math.floor((C - 0x10000) / 0x400) + 0xD800
L = (C - 0x10000) % 0x400 + 0xDC00
つまり、2つのコードポイント H
と L
を用いて表現されるユニコードのコードポイント C
は次のような式となる。
C = (H - 0xD800) * 0x400 + L - 0xDC00 + 0x10000
💩
で試してみよう。
> C = '💩'.codePointAt()
128169
> H = Math.floor((C - 0x10000) / 0x400) + 0xD800
55357
> H.toString(16)
'd83d'
> L = (C - 0x10000) % 0x400 + 0xDC00
56489
> L.toString(16)
'dca9'
> C = (H - 0xD800) * 0x400 + L - 0xDC00 + 0x1000
128169
> String.fromCodePoint(C)
'💩'
さて、ではここで Node.js の http モジュールがどのようにサロゲートペアを扱うか見てみよう。
var base = "http://127.0.0.1:8000/article/";
var path = req.query.path; // req.query.path = "💩"
if (path.indexOf("..") == -1) {
http.get(base + path, callback);
}
req.query.path
に 💩
が含まれていた場合、 http.get()
の送信先URIはどうなるだろうか。
$ python -m http.server
127.0.0.1 - - [13/Nov/2018 23:26:35] "GET /=© HTTP/1.1" 404 -
上記のように 💩
にはならず、 =©
になる。
これはサロゲートペアそれぞれの最下位バイトのみ扱っているからだ。
// 💩 = \ud83d \udca9
> String.fromCodePoint(parseInt('3d', 16))
'='
> String.fromCodePoint(parseInt('a9', 16))
'©'
SSRFとディレクトリトラバーサルに活用する
最下位バイトのみ扱うことがわかったのでこれを攻撃に活かそう。
上記コードのように特定のURLのパスに送信する処理がある場合、ディレクトリトラバーサルへつながることも考えられる。
$ ls
article/ secret.txt
$ ls article/
1.html
2.html
$ python -m http.server
(お粗末だが、あくまで一例なので…)
挙げている例の場合、 var base = "https://example.com/article/"
と指定しているのと path.index("..") == -1
があるため secret.txt は取得できないように見える。
しかし http モジュールが各サロゲートペアの最下位バイトのみを扱うことを利用することで path.index("..")
を回避して ..
を送り込むことができる。
そのためには、最下位バイトが .
( 2e
) のユニコード文字列を探してくればよい。
上記サイトから探すのが楽。
最初に引っかかるのが Į
( LATIN CAPITAL LETTER I WITH OGONEK (U+012E)
) だ。
/ĮĮ/secret.txt
にアクセスすると、 path.indexOf("..")
では ĮĮ
のままなので通過する。
そして http.get()
の段階で ..
に変換されるため ../secret.txt
へアクセス可能となる。
Ref
- JavaScript’s internal character encoding: UCS-2 or UTF-16? · Mathias Bynens
- JavaScript has a Unicode problem ・ Mathias Bynens
- JavaScript における文字コードと「文字数」の数え方 | blog.jxck.io
- JavaScriptでのサロゲートペア文字列のメモ - Qiita
- Unicode Security Guide - Character Transformations
- CSAW-CTF-2017-Quals/web/orangev2 at master · osirislab/CSAW-CTF-2017-Quals
- ctf-writeups/nn8ed at master · dreadlocked/ctf-writeups