maplectf复现

mapleba ctf 2022[honksay]

进入题目

可以看到我们可以传入一个url,然后goose会去访问我们上传的url,像这种能够上传url的题目,一般就三种类型xss,ssrf,csrf。这道题目给了源码,我们先看源码

const express = require("express");
const cookieParser = require('cookie-parser');
const goose = require("./goose");
const clean = require('xss');

const app = express();
app.use(cookieParser());
app.use(express.urlencoded({extended:false}));

const PORT = process.env.PORT || 9988;

const headers = (req, res, next) => {
    res.setHeader('X-Frame-Options', 'DENY');
    res.setHeader('X-Content-Type-Options', 'nosniff');
    return next();
  }
app.use(headers);
app.use(express.static('public'))

const template = (goosemsg, goosecount) => `
<html>
<head>
<style>
H1 { text-align: center }
.center {
    display: block;
    margin-left: auto;
    margin-right: auto;
    width: 50%;
  }

  body {
    place-content:center;
    background:#111;
  }

  * {
    color:white;
  }

</style>
</head>
${goosemsg === '' ? '': `<h1> ${goosemsg} </h1>`}
<img src='/images/goosevie.png' width='400' height='700' class='center'></img>
${goosecount === '' ? '': `<h1> You have honked ${goosecount} times today </h1>`}

<form action="/report" method=POST style="text-align: center;">
  <label for="url">Did the goose say something bad? Give us feedback.</label>
  <br>
  <input type="text" id="site" name="url" style-"height:300"><br><br>
  <input type="submit" value="Submit" style="color:black">
</form>
</html>
`;


app.get('/', (req, res) => {
    if (req.cookies.honk){
        //construct object
        let finalhonk = {};
        if (typeof(req.cookies.honk) === 'object'){
            finalhonk = req.cookies.honk
        } else {
            finalhonk = {
                message: clean(req.cookies.honk), 
                amountoftimeshonked: req.cookies.honkcount.toString()
            };
        }
        res.send(template(finalhonk.message, finalhonk.amountoftimeshonked));
    } else {
        const initialhonk = 'HONK';
        res.cookie('honk', initialhonk, {
            httpOnly: true
        });
        res.cookie('honkcount', 0, {
            httpOnly: true
        });
        res.redirect('/');
    }
});

app.get('/changehonk', (req, res) => {
    res.cookie('honk', req.query.newhonk, {
        httpOnly: true
    });
    res.cookie('honkcount', 0, {
        httpOnly: true
    });
    res.redirect('/');
});

app.post('/report', (req, res) => {
    const url = req.body.url;
    goose.visit(url);
    res.send('honk');
});

app.listen(PORT, () => console.log((new Date())+`: Web/honksay server listening on port ${PORT}`));

app.js

const puppeteer = require('puppeteer');
const FLAG = process.env.FLAG || "maple{fake}";

async function visit(url) {
  let browser, page;
  return new Promise(async (resolve, reject) => {
    try {
      browser = await puppeteer.launch({
        headless: true,
        args: [
          '--no-sandbox',
          '--disable-default-apps',
          '--disable-dev-shm-usage',
          '--disable-extensions',
          '--disable-gpu',
          '--disable-sync',
          '--disable-translate',
          '--hide-scrollbars',
          '--metrics-recording-only',
          '--mute-audio',
          '--no-first-run',
          '--safebrowsing-disable-auto-update'
                ]
            });
        page = await browser.newPage();
        await page.setCookie({
            name: 'flag',
            value: FLAG,
            domain: 'localhost',
            samesite: 'none'
        });
        await page.goto(url, {waitUntil : 'networkidle2' }).catch(e => console.log(e));
        console.log(page.cookies());
        await new Promise(resolve => setTimeout(resolve, 500));
        console.log("admin is visiting url:");
        console.log(url);
        await page.close();
        
        console.log("admin visited url");
        page = null;
    } catch (err){
        console.log(err);
    } finally {
        if (page) await page.close();
        console.log("page closed");
        if (browser) await browser.close();
        console.log("browser closed");
        //no rejectz
        resolve();
        console.log("resolved");
    }
  });
};


module.exports = { visit }

goose.js

可以看到flag是写在goose的cookie里的,这就说明这道题是要我们用xss把goose的cookie带出来,但是xss的前提是需要执行js代码。如果我们直接传入xss语句,goose会直接访问我们传入的语句,并不会执行js代码,所以我们现在需要寻找一个能够执行js代码的地方,也就是能渲染到html的地方。

app.get('/', (req, res) => {
    if (req.cookies.honk){
        //construct object
        let finalhonk = {};
        if (typeof(req.cookies.honk) === 'object'){
            finalhonk = req.cookies.honk
        } else {
            finalhonk = {
                message: clean(req.cookies.honk), 
                amountoftimeshonked: req.cookies.honkcount.toString()
            };
        }
        res.send(template(finalhonk.message, finalhonk.amountoftimeshonked));

我们可以看到,再/路由中用了template我们跟进到template

const template = (goosemsg, goosecount) => `
<html>
<head>
<style>
H1 { text-align: center }
.center {
    display: block;
    margin-left: auto;
    margin-right: auto;
    width: 50%;
  }

  body {
    place-content:center;
    background:#111;
  }

  * {
    color:white;
  }

</style>
</head>
${goosemsg === '' ? '': `<h1> ${goosemsg} </h1>`}
<img src='/images/goosevie.png' width='400' height='700' class='center'></img>
${goosecount === '' ? '': `<h1> You have honked ${goosecount} times today </h1>`}

<form action="/report" method=POST style="text-align: center;">
  <label for="url">Did the goose say something bad? Give us feedback.</label>
  <br>
  <input type="text" id="site" name="url" style-"height:300"><br><br>
  <input type="submit" value="Submit" style="color:black">
</form>
</html>
`;

发现是一个html的渲染,我们可以传入两个参数进行渲染,在/路由是由finalhonk.message和finalhonk.amountoftimeshonked这两个参数进行渲染的

我们只要让这两个参数变成xss语句,再让goose去访问/路由就行了。接下来看看这两个值是否是我们可以控制的

  let finalhonk = {};
        if (typeof(req.cookies.honk) === 'object'){
            finalhonk = req.cookies.honk
        } else {
            finalhonk = {
                message: clean(req.cookies.honk), 
                amountoftimeshonked: req.cookies.honkcount.toString()
            };
        }

可以看到finalhonk是从cookie中得到的,但是我们只能更改自己的cookie,不能改goose的cookie,所以我们还要找一个能更改cookie的路由

app.get('/changehonk', (req, res) => {
    res.cookie('honk', req.query.newhonk, {
        httpOnly: true
    });
    res.cookie('honkcount', 0, {
        httpOnly: true
    });
    res.redirect('/');
});

我们找到了changehonk这个路由可以通过get的newhonk参数来更改cookie,这样我们就能进行xss了

先尝试能不能弹窗

成功

接下来试试能不能带出cookie,记得要url编码一下

成功了

接着试试goose

发现带不出cookie,但是用fetch发送请求,把cookie放在body里就可以。

bookstore

​ 给了源码

image-20220913151427014

在这里写了flag我们看看init.sql

image-20220913151512878

发现把flag写到了book的text里

观察index.js的源码之后发现并没有能够直接显示text的地方,那么这道题应该就是要sql注入

import express from 'express'
import bodyParser from 'body-parser'
import session from 'express-session'
import crypto from 'crypto'
import DB from './db.js'
import { validateEmail, validateLogin } from './validator.js';

const PORT = process.env.PORT || 3000

const db = new DB();

let validBooks = await db.getBooks();
validBooks = validBooks.map(book => {
    return {
        id: book.id,
        title: book.title,
        author: book.author,
        price: book.price
    }
});

const app = express();

app.set('view engine', 'ejs');

app.use(session({
    secret: crypto.randomBytes(32).toString('hex'),
    resave: false,
    saveUninitialized: true
}));

app.use(express.static('static'))

app.use(bodyParser.urlencoded({ extended: true }));

app.get('/', (req, res) => {
    res.render('index');
});

app.get('/login', (req, res) => {
    res.render('login', {
        message: ''
    });
})

app.post('/login', (req, res) => {
    const username = req.body.username;
    const password = req.body.password;

    if (!validateLogin(username, password)) {
        res.render('/login', {
            message: 'Invalid username or password'
        });
    } else {
        db.getUser(username, password, (user) => {
            const userData = {
                username: user.username,
                password: user.password,
                money: 0, // POOR
                books: []
            }
            if (user) {
                req.session.user = userData;
                res.redirect('/books')
            } else {
                res.render('/login', {
                    message: 'User not found'
                });
            }
        });
    }
});

app.get('/register', (req, res) => {
    res.render('register', {
        message: '',
        link: false
    });
})

app.post('/register', (req, res) => {
    const username = req.body.username;
    const password = req.body.password;

    if (!validateLogin(username, password)) {
        res.send('Invalid username or password');
    } else {
        db.register(username, password).then(() => {
            res.render('register', {
                message: 'Registration Successful',
                link: true
            })
        }).catch((err) => {
            res.render('register', {
                message: 'Registration Failed: ' + err,
                link: false
            })
        })
    }
});

app.get('/logout', (req, res) => {
    req.session.destroy();
    res.redirect('/');
});

app.get('/books', (req, res) => {
    if (!req.session.user) {
        res.redirect("/login")
    } else {
        res.render('books', {
            ownedBooks: req.session.user.books,
            books: validBooks
        });
    }
});

app.get('/catalogue', (req, res) => {
    if (!req.session.user) {
        res.redirect('/login');
    } else {
        res.render('catalogue', {
            ownedBooks: req.session.user.books,
            books: validBooks
        });
    }
});

app.post('/purchase', (req, res) => {
    const bookID = req.body?.bookID || '0';
    if (!validBooks.find(book => book.id == bookID)) {
        res.send('Invalid book ID');
    } else {
        const user = req.session.user;
        user.books.push(bookID);
        req.session.user = user;
        res.render('catalogue', {
            ownedBooks: req.session.user.books,
            books: validBooks
        });
    }
    return;
});

app.post('/download-ebook', (req, res) => {
    const option = req.body?.option ?? '';
    const email = req.body?.email ?? '';
    const bookID = req.body?.bookID ?? 1;
    const user = req.session?.user ?? { books: [] };
    if (!validBooks.find(book => book.id == bookID)) {
        res.send('Invalid book ID');
        return;
    } /* else if (!user.books.includes(bookID)) {
        res.send('You do not have this book');
        return;
    } */

    switch (option) {
        case 'direct':
            res.write('Direct downloads currently unavailable. Please wait until the established publish date!');
            break;
        case 'kindle':
            if (validateEmail(email)) {
                db.insertEmail(email, bookID).then((err) => {
                    if (err) {
                        res.send('Error: ' + err);
                    } else {
                        res.send("Email saved! We'll send you a download link once the book has been published!")
                    }
                }).catch((err) => {
                    res.send('Error: ' + err);
                })
            } else {
                res.send("Invalid email address")
            }
            break;
        default:
            res.send('Invalid option');
            break;
    }
});

app.listen(PORT, () => {
    console.log(`listening on port ${PORT}`)
});

我们要寻找注入点,看到db.js

import mysql from 'mysql'
const LIMIT = 500;

export default class DB {
    constructor() {
        this.adminDB = mysql.createConnection({
            connectionLimit: 100,
            host: process.env.DB_HOST || 'localhost',
            user: process.env.DB_ADMIN_USER || 'root',
            password: process.env.DB_ADMIN_PASS || 'Rooted123!',
            database: process.env.DB_NAME || 'bookstore',
        });

        this.db = mysql.createPool({
            connectionLimit: 100,
            host: process.env.DB_HOST || 'localhost',
            user: process.env.DB_USER || 'player',
            password: process.env.DB_PASS || 'Player123!',
            database: process.env.DB_NAME || 'bookstore',
        });
    }

    register(username, password) {
        return new Promise((resolve, reject) => {
            this.adminDB.query(`INSERT INTO users(username, password) VALUES('${username}', '${password}')`, (err) => {
                if (err) {
                    console.log(err)
                    reject(err);
                } else {
                    resolve(null);
                }
            })
        })
    }

    getUser(username, password, callback) {
        const query = `
        SELECT * FROM users WHERE username = '${username}' AND password = '${password}';
        `;
        this.db.query(query, (err, user) => {
            callback(user);
        });
    }

    getBooks() {
        const query = `SELECT id, title, author, price FROM books;`;
        return new Promise((resolve, reject) => {
            this.db.query(query, (error, rows) => {
                if (error) {
                    reject(error);
                } else {
                    resolve(rows);
                }
            }).on('error', (err) => {
                reject(err);
            })
        })
    }


    insertEmail(email, book_id) {
        const query = `INSERT INTO requests(email, book_id) VALUES('${email}', '${book_id}');`;
        return new Promise((resolve, reject) => {
            this.db.query(query, (error) => {
                if (error != null) {
                    reject(error);
                } else {
                    resolve(null);
                }
            })
        })
    }

    purge() {
        // disappoints users
        const countQuery = "SELECT COUNT(*) FROM requests;"
        return new Promise((resolve, reject) => {
            this.adminDB.query(countQuery, (error, row) => {
                if (error != null) {
                    reject(error);
                } else {
                    if (row.COUNT > LIMIT) {
                        const deleteQuery = `DELETE FROM requests;`
                        this.db.query(deleteQuery, (err) => {
                            if (err != null) {
                                reject(err);
                            } else {
                                resolve(null);
                            }
                        })
                    }
                }
            })
        })
    }
}

我们能够发现可以注入的地方就只有3个username,passsword和email。

但是

在使用username,password和email之前,他都调用了validator.js中的函数对我们传入的数值进行了过滤,我们看到validator.js中的内容

import validator from 'validator'

export function validateEmail(email) {
    return validator.isEmail(email)
}

function validateUsername(username) {
    return validator.isAlphanumeric(username, 'en-US') && username.length > 3 && username.length < 30
}

function validatePassword(password) {
    return validator.isAlphanumeric(password, 'en-US') && password.length > 6 && password.length < 30
}

export function validateLogin(username, password) {
    return username && password && validateUsername(username) && validatePassword(password)
}

调用了validator库,对username和password的检测是isAlphanumeric,

对email的检测是isEmail。我们看看validator的文档

image-20220913154954341

image-20220913155149862

isAlphanumeric规定了username和password·只能是字母和数字,而且在validator.js还自己增加了长度限制,在username和password进行注入几乎是不可能的。而isEmail对格式进行了规定在加上一点小过滤也没有长度的限制,所以在email处尝试注入

payload

"',1 and substring((select texts from books where id=1),1,99));#@a.aa

image-20220913161505181

posted @ 2022-09-04 20:56  Ragna30K  阅读(89)  评论(0)    收藏  举报