本サイトは、快適にご利用いただくためにクッキー(Cookie)を使用しております。
Cookieの使用に同意いただける場合は「同意する」ボタンを押してください。
なお本サイトのCookie使用については、「個人情報保護方針」をご覧ください。

最新情報

2023.02.16

マルチバイト文字とURL

MBSDでWebアプリケーションスキャナの開発をしている寺田です。

前記事では正規表現でのURLのチェックについて書きました。今回はその続きでマルチバイト文字を使った攻撃について書きたいと思います。

前提条件

本記事で想定するのは、ブラウザからパラメータとして渡されて来るURLを、リダイレクトやリンク等のURLとして使うケースです。その中でも、以下のようにサブドメイン部分(★の部分)を可変にする状況を主に想定します。

https://.example.jp/…

攻撃の目標は、異なるドメイン(evil)のURLを与えてチェックをすり抜けることです。前回の記事にも書きましたが、この状況は(半角英数等のサブドメインしか受け入れないような場合を除き)「/」「?」「#」「\」のいずれかをサブドメインに入れることで攻略できることが大半です。

今回はこれらの記号が全て使用できないように対策されていることを前提とし、バイパス方法を考えてみたいと思います(診断でも稀にそういう状況があります)。本記事で書くのは、そのようなバイパス方法の中で、マルチバイト文字を使って「?」を作り出す手法です。サーバ環境やアプリケーション処理に強く依存するため可能性は低いですが、実際にこの手法でバイパスが可能だったこともあります。

サロゲートペアの処理

バイパス: https://evil%ED%A0%80.example.jp/  (URLエンコード表記)
出力: https://evil?.example.jp/

Unicodeのサロゲートペアの前半(0xD800)をUTF-8的にエンコードしたバイト列を使用しています。UTF-8として不正ですが、古いJava(1.7以下)はStringへのデコード時にこのようなバイト列を受け入れてしまいます。

Javaの内部的には「?」ではなく0xD800のcharとして扱われるため、アプリケーションで記号類をDenylistでチェックしている場合はチェックをすり抜けます。この文字列を出力するために、アプリケーションから「response.sendRedirect()」や「response.getWriter().write()」等を呼び出した際に、内部的に「getBytes("UTF-8")」されて「?」(0x3F)になります。LocationヘッダでもボディのリンクURLの出力であっても動作しうるため攻撃側にとっては便利がよく、実際にこれと思われるものを診断で検出したこともあります。

このサロゲートペアの片割れの問題は、Java 1.8でFixされました。かつてのJavaにあった「冗長なUTF-8」と呼ばれる不正なバイト列を受け入れる問題は、Java 1.6のどこかのバージョンでFixされたので、サロゲートペアの片割れの方はそれよりも後のバージョンでFixされたことになります。Java 1.7は2015年にサポートが終了し、延長サポートも昨年既に終了しましたが、1.8のFixは最後まで1.7にバックポートされませんでした(Java1.7は既にサポートされていないため、サロゲートペアの件を別としても、使用しないことをおすすめします)。

Java 1.8以降では、サロゲートペアの片割れは、UTF-8のバイト列からStringへのデコード時にU+FFFD(Replacement character)になるため「getBytes("UTF-8")」しても「?」にはなりません。Java 1.8以降でも、きちんとしたサロゲートペアを構成するStringをsubstringする等の方法で片割れにしてやると、「getBytes("UTF-8")」の時に「?」になるのですが、逆に言えばそのような処理をしていない限りは意図しない「?」が生じることはないと思います。

サロゲートペアに関連して言うと、非BMP文字(U+10000以上)のUTF-8を与えるパターンもあります。

バイパス: https://evil%F4%8F%BF%BF.example.jp/  (URLエンコード表記)

上記はUnicodeの上限であるU+10FFFFを使用しています。ヘッダやボディに出力する処理が非BMP文字(あるいはUnicodeのnon-character)を嫌うならば、「?」等の別の文字になって出力される… かもしれません。非BMP文字やnon-characterは、診断で行き詰まった時に筆者が試すパターンの1つですが、今のところそれで突破できたことはないので、あくまで可能性です。

ヘッダのUS-ASCII変換

Locationヘッダの出力をUS-ASCIIに変換するサーバ環境もあります。

バイパス: https://evil.example.jp/
出力: https://evil?.example.jp/

そのような環境では、アプリケーションで扱う時点では「あ」なので、記号類をDenylistでチェックしている場合はそれをすり抜けて、最終的なHTTPヘッダの出力時に「?」になる、という現象が発生します。

ASCII化が起こる環境の1つはTomcatです(HTTP/1.1の場合)。

response.sendRedirect("https://evil.example.jp/")

⇒ Location: https://evil?.example.jp/

このようなASCII化を行う実装が存在する理由は、HTTP仕様(RFC 9110)において、ヘッダ値は原則US-ASCIIであると規定されているためでしょう。ある意味で、HTTP仕様に忠実に作ろうとした結果としてそうなった、とも言えるかもしれません。いずれにせよ、Locationヘッダは仕様的にUS-ASCIIの世界であり、それ以外の文字を含んでいると意図しない変換を受ける可能性があるということです。

※ HTTPのRFCは、歴史的にヘッダがISO-8859-1とされてきたことを考慮してか、obs-textとしてはASCII外のバイトも許容しています(obsプリフィックスが付いているのでobsolete扱い)。また、Locationヘッダは該当しませんが、場所によってはRFC 2047形式でエンコードしたUTF-8等の値も許しています。

※ 正確に言うとTomcatはISO-8859-1に変換しようとします。微妙な話だと思いつつも潜在的なリスクのある挙動として開発元に報告したところ、案の定「アプリケーション側の問題」という回答でした。ただ、Tomcatの10.1.1では(HTTP/2の処理に合わせる形で)HTTP/1.1のヘッダ処理が変更されており、上記の挙動は再現しないようになっています。同じ変更は8.5, 9系(8.5.85, 9.0.71)にもバックポートされたため、修正後のバージョンへのアップグレードやLocationヘッダに非ASCIIを出⼒しないようにするかを検討してください(リンク1, リンク2)。

ASCII化はTomcat以外にも見られます。例えば、セキュリティ診断に使用するツールであるBurp(v2022.8.5)は、HTTP/2のヘッダ値の非ASCII文字を「?」に変換します。

【cURLでの応答ダンプ】
0000: 6c 6f 63 61 74 69 6f 6e 3a 20 68 74 74 70 73 3a location: https:
0010: 2f 2f 65 76 69 6c e3 81 82 2e 65 78 61 6d 70 6c //evil....exampl
0020: 65 2e 6a 70 2f 0d 0a e.jp/..
location: https://evil.example.jp/

【Burp上での応答表示】
HTTP/2 302 Found
Location: https://evil???.example.jp/  (Hex表示でも3Fと表示される)

Burp上の画面表示だけではなく、BurpのProxyを介してブラウザに送られる応答メッセージ自体も変換されるため、HTTP/2ではBurpを介した時だけに攻略できる「偽のオープンリダイレクト」が存在します。筆者が知る限り、サーバ側で「?」になった本物のオープンリダイレクトなのか、Burpが作り出す偽のオープンリダイレクトなのかを、Burp上で区別する方法はありません。診断時には別のツールを使うか、またはBurpを外して確認する必要があります。

※ Burpで診断する上で非常に不便なのでバグとして報告しています。

ヘッダのUS-ASCIIへの変換が行われた(かもしれない)例をもう一つ挙げておきます。

CRLF injection on Twitter or why blacklist fail -- @filedescriptorInternet Archive

アプリケーション内部ではU+560A(嘊)だった文字が、最終的に0x0Aとして応答ヘッダに出力されることを利用した、HTTPヘッダインジェクションです。筆者は実物を見ていないこともあり詳細を把握していませんが、最終的にヘッダ出力をするライブラリかアプリケーションのコード内で、「Unicodeのコードポイント & 0x7F」(&はビット積)を取ることでASCII化していたのでは、と推測しています。

※ ASCII化を0x7Fとのビット積で行うのは、それ自体「バグ」と呼んで差し支えないものでしょうけども、割とあるものかもしれません。Filedescriptor氏の記事で言及されているFirefoxの例もありますし、筆者も大昔に診断をしていた時に、入力として与えたU+3042(あ)が、数値参照のB(B)になって返ってくるようなアプリケーションを見たことがあります。

ASCII化には別の方法も考えられます。バイト列にした後に各バイトの最上位ビットを0にすることで7bit化する方法で、「バイト & 0x7F」を取るのと同じです。ちなみにTwitterのヘッダインジェクションで使われたU+560A(嘊)は、バイト列(UTF-8)にすると「0xE5 0x98 0x8A」であり、最上位ビットを0にすると「0x65 0x18 0x0A」になるので、この方法でも0x0Aを作り出します。

※ 弊社の診断における過去事例では、Twitterと同じ「コードポイント & 0x7F」はかろうじて例があるようですが、「バイト & 0x7F」の例は皆無のようです。どちらも珍しいですが、後者は特にレアな処理のようです。

話を戻すと「コードポイント/バイト & 0x7F」のような変な形でヘッダのASCII化が行われるのであれば、これはオープンリダイレクトにも利用される可能性があります。例えばU+693F(椿)から「?」が作り出されて、ホスト名の終端となるかもしれません。

ボディのレガシーエンコーディングへの変換

バイパス: https://evil%u1234.example.jp/  (URLエンコード表記)
出力: https://evil?.example.jp/

アプリケーション内部では「U+1234」(ሴ)として扱われて、出力時に「?」になるパターンです。これが動作する可能性があるのは%uHHHH形式のエンコードに対応しているASP.NETです。出力の文字コードをSJIS等のレガシーエンコーディングに設定している時、ASP.NETは応答ボディの文字コードの変換を行いますが、変換後の文字コードで表現できない文字は<a href="https://evil?.example.jp/">のように変換されます。

ASP.NET以外の環境でも、出力がレガシーエンコーディングであり、

  • Webアプリケーションへの入力がUTF-8である。
  • サーバが要求ヘッダのContent-Typeのcharsetを解釈する。
  • 入力がJSONやXMLで、\uHHHH&#xHHHH;が使用できる。

等により任意のUnicode文字を入れられる場合があるかもしれませんが、いずれもなかなかレアな状況だと思います。

一方、ASP.NETでは、仮に入力と出力がともにSJISに設定されていたとしても、%uHHHHを使って入力にUnicode文字を混ぜ込むことができるため、便利である反面、こういった攻撃にも使う余地があるということです。

※ U+1234は、単に変換後の文字コード(SJIS等)で表現できない文字の一例です。

不正なマルチバイト断片

バイパス: https://evil%FF.example.jp/  (URLエンコード表記。UTF-8等として不正なバイト)
出力: https://evil?.example.jp/

アプリケーション内部では[0xFF]として扱われて、出力時に不正なバイトが文字コード変換がされて「?」になるパターンです。主にPHPで動作する可能性があります。

※ PHPのstringはUnicodeデータではなく単なるバイト列なので、0xFFのようなバイトを含むことができます。

ただし下記のような攻撃の条件があります。

応答ヘッダに関しては、PHP自体には出力時に文字コードを自動変換する機能が無いので、アプリケーションやフレームワーク側が文字コード変換をしていることが攻撃の条件になります。

応答ボディに関しては、出力時に文字コードを自動変換するPHPの機能が有効に設定されている(もしくはアプリケーションやフレームワークが文字コード変換をする)ことが条件になります。また、htmlspecialchars()を使っていない(もしくはこの関数の呼び出し時に誤った文字コードを指定している)ということも条件になります。なぜなら、この関数でエスケープする場合、文字コードの指定を間違えない限りは、エスケープの時点で不正なバイトを含む文字列は空にされてしまい、攻撃が成立しないためです。

上のような条件があり、攻撃可能な状況はかなり限定されるため、ダメ元で試すパターンではあります。

対策

今回はサーバ側で「?」に変換されるケースにフォーカスしてみました。攻撃のパターンとしては、それ以外の記号(\, /, #)や制御文字を使ったり、サーバ側ではなくブラウザ側のURL解釈を突くようなものもあるかもしれません。

様々な攻撃の可能性を考えると、非US-ASCIIのサブドメインを含むリンクやリダイレクトのURLが与えられた場合は、以下のいずれかの方法でASCII onlyにしておくのが無難です。

  1. 非ASCIIの文字を含む場合はエラーにする
  2. OR 非ASCII文字をPunycodeでASCII化する(IDN to ascii)
  3. OR 非ASCII文字をUTF-8としてURLエンコードすることでASCII化する

簡単で確実な対策は1です。前記事で示したような、非ASCIIを許容しない正規表現を使ってサブドメインをチェックして、それをリンクやリダイレクトのURLとして使います(もちろん正規表現以外のチェックでもよいです)。

2のPunycode化(IDN to ascii)をするならば、Host/Split: Exploitable Antipatterns in Unicode Normalization -- Jonathan Birchのような攻撃に留意する必要があります。

※ 例えば、U+2100(℀)がUnicodeの正規化によりa/cに変わることを利用する攻撃です。

IDN to asciiをするならば、Birch氏の資料のとおり、STD3ASCIIRulesを有効にするのがよいです。そしてASCII化されたサブドメインをチェックし、それをリダイレクトやリンクのURLとして出力します(HTML内の要素内容テキスト等に出力してユーザに表示する箇所については、Unicode化したUラベルの方が親切かもしれません)。

※ 言うまでもないことですが、一般論として、チェック後に何らかの変換(ASCII化等)を行うのは望ましくないです。つまり、チェックはその値を使用するなるべく直前で行うのが望ましいです。

※ 参考まで、JavaScript(Node.js)でIDN to asciiする関数の試作品を、筆者個人のGistに置いています(IDNのハンドリングはライブラリで行っています)。

3のURLエンコードは一部のフレームワークで採用されているようです。筆者が知る限りDjangoのredirect()がURLエンコードしてくれます。このように、言語やフレームワークがURLエンコード等の方法で対処してくれるのが理想ではあります。

プロフェッショナルサービス事業部
寺田健