Soccer is an easy Linux machine on the Hack The Box platform that emphasizes good enumeration The challenge starts by running gobuster against a website to discover a misconfigured file manager. Default credentials allow uploading a PHP webshell that grants access to the box. After this, a SQL injection vulnerability over a websocket connection leads to the exposure of a second user. This user can execute doas and exploiting a plugin grants root access to the machine.
Recon
Starting reconnaissance with namp shows three open TCP ports: SSH on 22, HTTP on port 80, and something resembling another HTTP service on port 9091.
The —min-rate combined with the -p- allows running a full port scan in a matter of seconds. A neat little party to use in CTF challenges, but it could potentially cause some old hardware to fall over!
Searching for the OpenSSH version header online, shows that the machine is a Ubuntu 20.04 focal box. And it seems that the service running on port 80 returns a redirect to http://soccer.htb/. Adding this record to my /etc/hosts files allows me to explore the website in Firefox.
Homepage of the soccer website.
This gives access to a Football website. But none of the links on the page are actually going anywhere. Time to crack open gobuster and do a directory scan of the website gobuster dir -u http://soccer.htb -w /usr/share/seclists/Discovery/Web-Content/raft-small-words.txt; while this is running in the background, I do some manual recon to see if I can determine the tech stack or dig up any other helpful information.
Tech Stack
Browsing around, this website mainly consists of static pages. Looking at the response headers in BurpSuite, it leaks the server header indicating that this website is behind an nginx webserver:
Besides that, the 404 page also tells me that I‘m dealing with an nginx server.
Soccer nginx webserver is showing a 404, Not Found page.
After opening this URL in my browser, I get a login prompt for the Tiny File Manager. Trying default passwords like admin:admin doesn’t yield any results. Let’s see if ChatGPT can share some of its infinite wisdom with me.
Ignoring the massive disclaimer it prints out, ChatGPT knows precisely what I was looking for. Asking some follow-up questions later, it shows me where I can find the source code for this project:
A quick Google search would have most likely also pointed me in the right direction. But it feels lovely to ask these questions in natural language and get a more directed and tailored response. My next question to the bot was to give me a list of known vulnerabilities and misconfigurations for this project. And that’s where it fell short! It gave me a generic OWASP top 10 common vulnerabilities and misconfiguration answer. So I guess it’s time for the human to step in again.
Going to the GitHub repo of the Tiny File Manager, the README file in the project states the following:
Showing me two sets of default credentials:
admin: admin@123
user: 12345
Initial foothold
Using the admin username and password, I get presented with the following page:
The Tiny File Manager’s landing page shows all the soccer website’s static assets.
The URL is http://soccer.htb/tiny/tinyfilemanager.php?p=tiny, meaning ChatGPT was right, and the web server must be running PHP. Browsing around in the tiny directory reveals the Tiny Fie Manager application and an empty upload directory.
Contents of the file folder showing tinyfilemanager.php and the uploads folder.
Shell
I craft a simple PHP webshell and save it as cmd.php on my local machine.
Via the upload button, I can drop my reverse shell onto the server.
Combining this with curl I can now execute commands on the system:
Starting a listener on my local machine on port 8888 with netcat with nc -lnvp 8888 and triggering the reverse shell by sending a bash reverse shell to the webserver with curl.
Note the use --data-urlencode parameter in this instance. This is important otherwise, the payload won’t be encoded correctly.
The curl command will hang for a while, but eventually, I can see the reverse shell coming into my nc listener. Next, I’ll upgrade my shell to a proper tty. This will make the reverse shell behave like an actual terminal. And prevent me from accidentally closing the session.
Pivoting to the player user
The reverse connection allows me to browse the upload directory and see that the files match what I observed earlier via the Tiny File Manager . But there aren’t any other exciting files in /var/www/html. Although it does seem like there’s one more user on the system called player.
And this is where the first flag is located. But the www-data user can’t access this file, so I’ll need to pivot around. From the nmap scan earlier, I know there’s at least another service running on port 9091 . With netstat I can get an overview of all open ports and what processes are listening on them.
So I do a quick manual investigation on some interesting ports 9091, 30003306 and 33060.
But this doesn’t get me anywhere. I still don’t know what is running on port 9091, but I see express, a NodeJS framework running on port 3000. And both 3306 and 33060 seem to be MySQL instances. But it’s hard to verify any of this due to some hardening tricks this machine has applied. It turns out the www-data user can only read it owned processes.
The reason for this is that the proc file system is mounted with the hidepid option:
Exploring nginx
Nothing else worth of interest seems to be on the machine, so I decided to take a look at the nginx configuration in /etc/nginx/site-enabled
The default configuration isn’t all that interesting and basically just wires up the redirect to soccer.htb. And configures the main site allowing PHP to serve .php files.
The soc-player.htb file, on the other hand, is interesting because it configures nginx to serve a different site, on a new subdomain at soc-player.soccer.htb . Requests for this domain are routed to [http://localhost:3000](http://localhost:3000) which is the express based web application.
Time to update my /etc/hosts file again:
A whole new website
This new site looks almost identical to the first one, except now the navbar at the top contains a couple of new links Match, Login and Signup.
The Match page show a couple of matches and mentions that you get a free ticket when you sign up and login.
After creating an account and logging in, I get redirected to /check where I get a ticket id. Playing around with my ticket number in the search field shows me my ticket exists.
And if I try a different number, it returns an error message:
Most likely, this will be a blind boolean type of SQL injection, so I decided to trya few examples from hacktricks inside the search box:
Query
Result
65511’ or 1=1— -
Ticket Doesn’t Exist
65511” or 1=1— -
Ticket Doesn’t Exist
65511 or 1=1— -
Ticket Exists
65511 or 2=1— -
Ticket Doesn’t Exist
This confirms my assumption that I’m dealing with a SQL injection vulnerability. Looking through the requests in burpsuite it seems that submissions for this field are sent over a websocket connection. Looking closer at the URL, I notice port 9091 from the nmap and netsat scans.
This is a blind SQL injection - no data from the database comes back in the response, only one of two responses. In this case, this will be Ticket Exists, or Ticket Doesn't Exist.
SQLMap
Doing this manually is a rather laborious task. So instead, I’m going to do this in SQLMap. It’s possible that SQLmap shows the following error message
[19:59:41] [CRITICAL] sqlmap requires third-party module ‘websocket-client’ in order to use WebSocket functionality
The problem is that SQLMap is missing the Python websockets library. On Kali this is an easy fix
For the initial discovery, I pass the following arguments
Argument
Value
Description
-u
ws://soc-player.soccer.htb:9091
The URL to connect to
—data
’{“id”: “1234”}‘
The data or payload to send over the connection.
—dbms
mysql
Letting SQLMap know that the backing dbms is MySQL
—batch
/
Use the default answer for any question.
—level
5
Level of tests to perform (default)
—risk
3
Risk of tests to perform. (default 1)
This will run a bunch of tests which will take a few minutes to complete. Due to the --batch SQLMap argument default answers will be selected for every prompt that pops up.
Enumerating the Database
From the looks of it it seems that sqlmap has found a boolean and time-based blind inject. I reuse the same command from earlier and add --dbs to have sqlmap automatically pick up where it left up and exploit the vulnerability to list me all available databases:
soccer_db seems like the only non-default DB. Replacing --dbs with -D soccer_db and adding --tables to get a list of all tables in the Soccer database:
Seems there is only one table accounts. So this time I replace --tables with -T accounts and add --dump to dump the whole database.
Using --dump in a boolean and time-based SQL injections could be very slow. So be careful! But given this is a CTF I’m not expecting tons of data.
The user is player and the password is in plaintext.
Ready Player One
That password works for the player user on the box with su:
And SSH as well, which is convenient:
This allows me to grab the first flag:
I’am root
When I get access to a Linux machine I always like to test some low hanging fruit manually first. Like sudo, but it’s not configured for the player use
Time to run linpeas.sh. A hack the box VM doesn’t have access to the internet. So I first download linpeas.sh to my local machine in /tmp with curl -LO https://github.com/carlospolop/PEASS-ng/releases/latest/download/linpeas.sh and then spin up a python HTTP server python3 -m http.server. This allows me to now download linpeas onto the box and immediately pipe it to sh:
In the Files with Interesting Permissions section I notice doas which is an alternative to sudo typically found on OpenBSD operating systems. But can also be installed on Debian-based Linux OSes like Ubuntu. I don’t see a doas.conf file in /etc/ , so I decide to search the filesystem for it with find:
This file contains a single line, and shows me that the player user is allowed to run /usr/bin/dstat as root:
This is a tool for getting system information. Reading through the man page, I noticed that I can write my own plugins and have dstat execute them
Paths that may contain external dstat_*.py plugins:
~/.dstat/
(path of binary)/plugins/
/usr/share/dstat/
/usr/local/share/dstat/
What dstat understands as plugins are just basically python scripts using the following naming convention dstat_[plugin name].py.
Malicious Plugin
This allows me to write a trivial python script that gives access to bash for an interactive shell.
Looking at the list of locations, I can obviously write to ~/.dstat, but when run with doas, it’ll be running as root, and therefore won’t check /home/player/.dstat. Luckily, /usr/local/share/dstat is writable.
With that in place, I invoke dstat with the shell plugin: