Soccer

Linux · Easy

10.10.11.194

Reconnaissance:

┌──(kali💀kali)-[~]
└─$ sudo nmap -sC -sV -O 10.10.11.194 

22/tcp   open  ssh             OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 ad:0d:84:a3:fd:cc:98:a4:78:fe:f9:49:15:da:e1:6d (RSA)
|   256 df:d6:a3:9f:68:26:9d:fc:7c:6a:0c:29:e9:61:f0:0c (ECDSA)
|_  256 57:97:56:5d:ef:79:3c:2f:cb:db:35:ff:f1:7c:61:5c (ED25519)

80/tcp   open  http            nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://soccer.htb/

9091/tcp open  xmltec-xmlmail?
| fingerprint-strings: 
|   DNSStatusRequestTCP, DNSVersionBindReqTCP, Help, RPCCheck, SSLSessionReq, drda, informix: 
|     HTTP/1.1 400 Bad Request
|     Connection: close
|   GetRequest: 
|     HTTP/1.1 404 Not Found
|     Content-Security-Policy: default-src 'none'
|     X-Content-Type-Options: nosniff
|     Content-Type: text/html; charset=utf-8
|     Content-Length: 139
|     Date: Mon, 29 Jan 2024 02:55:02 GMT
|     Connection: close
|     <!DOCTYPE html>
|     <html lang="en">
|     <head>
|     <meta charset="utf-8">
|     <title>Error</title>
|     </head>
|     <body>
|     <pre>Cannot GET /</pre>
|     </body>
|     </html>
|   HTTPOptions: 
|     HTTP/1.1 404 Not Found
|     Content-Security-Policy: default-src 'none'
|     X-Content-Type-Options: nosniff
|     Content-Type: text/html; charset=utf-8
|     Content-Length: 143
|     Date: Mon, 29 Jan 2024 02:55:03 GMT
|     Connection: close
|     <!DOCTYPE html>
|     <html lang="en">
|     <head>
|     <meta charset="utf-8">
|     <title>Error</title>
|     </head>
|     <body>
|     <pre>Cannot OPTIONS /</pre>
|     </body>
|     </html>
|   RTSPRequest: 
|     HTTP/1.1 404 Not Found
|     Content-Security-Policy: default-src 'none'
|     X-Content-Type-Options: nosniff
|     Content-Type: text/html; charset=utf-8
|     Content-Length: 143
|     Date: Mon, 29 Jan 2024 02:55:05 GMT
|     Connection: close
|     <!DOCTYPE html>
|     <html lang="en">
|     <head>
|     <meta charset="utf-8">
|     <title>Error</title>
|     </head>
|     <body>
|     <pre>Cannot OPTIONS /</pre>
|     </body>
|_    </html>
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port9091-TCP:V=7.94SVN%I=7%D=1/28%Time=65B71380%P=x86_64-pc-linux-gnu%r
SF:(informix,2F,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nConnection:\x20clos
SF:e\r\n\r\n")%r(drda,2F,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nConnection
SF::\x20close\r\n\r\n")%r(GetRequest,168,"HTTP/1\.1\x20404\x20Not\x20Found
SF:\r\nContent-Security-Policy:\x20default-src\x20'none'\r\nX-Content-Type
SF:-Options:\x20nosniff\r\nContent-Type:\x20text/html;\x20charset=utf-8\r\
SF:nContent-Length:\x20139\r\nDate:\x20Mon,\x2029\x20Jan\x202024\x2002:55:
SF:02\x20GMT\r\nConnection:\x20close\r\n\r\n<!DOCTYPE\x20html>\n<html\x20l
SF:ang=\"en\">\n<head>\n<meta\x20charset=\"utf-8\">\n<title>Error</title>\
SF:n</head>\n<body>\n<pre>Cannot\x20GET\x20/</pre>\n</body>\n</html>\n")%r
SF:(HTTPOptions,16C,"HTTP/1\.1\x20404\x20Not\x20Found\r\nContent-Security-
SF:Policy:\x20default-src\x20'none'\r\nX-Content-Type-Options:\x20nosniff\
SF:r\nContent-Type:\x20text/html;\x20charset=utf-8\r\nContent-Length:\x201
SF:43\r\nDate:\x20Mon,\x2029\x20Jan\x202024\x2002:55:03\x20GMT\r\nConnecti
SF:on:\x20close\r\n\r\n<!DOCTYPE\x20html>\n<html\x20lang=\"en\">\n<head>\n
SF:<meta\x20charset=\"utf-8\">\n<title>Error</title>\n</head>\n<body>\n<pr
SF:e>Cannot\x20OPTIONS\x20/</pre>\n</body>\n</html>\n")%r(RTSPRequest,16C,
SF:"HTTP/1\.1\x20404\x20Not\x20Found\r\nContent-Security-Policy:\x20defaul
SF:t-src\x20'none'\r\nX-Content-Type-Options:\x20nosniff\r\nContent-Type:\
SF:x20text/html;\x20charset=utf-8\r\nContent-Length:\x20143\r\nDate:\x20Mo
SF:n,\x2029\x20Jan\x202024\x2002:55:05\x20GMT\r\nConnection:\x20close\r\n\
SF:r\n<!DOCTYPE\x20html>\n<html\x20lang=\"en\">\n<head>\n<meta\x20charset=
SF:\"utf-8\">\n<title>Error</title>\n</head>\n<body>\n<pre>Cannot\x20OPTIO
SF:NS\x20/</pre>\n</body>\n</html>\n")%r(RPCCheck,2F,"HTTP/1\.1\x20400\x20
SF:Bad\x20Request\r\nConnection:\x20close\r\n\r\n")%r(DNSVersionBindReqTCP
SF:,2F,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nConnection:\x20close\r\n\r\n
SF:")%r(DNSStatusRequestTCP,2F,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nConn
SF:ection:\x20close\r\n\r\n")%r(Help,2F,"HTTP/1\.1\x20400\x20Bad\x20Reques
SF:t\r\nConnection:\x20close\r\n\r\n")%r(SSLSessionReq,2F,"HTTP/1\.1\x2040
SF:0\x20Bad\x20Request\r\nConnection:\x20close\r\n\r\n");
Aggressive OS guesses: Linux 4.15 - 5.8 (96%), Linux 5.3 - 5.4 (95%), Linux 2.6.32 (95%), Linux 5.0 - 5.5 (95%), Linux 3.1 (95%), Linux 3.2 (95%), AXIS 210A or 211 Network Camera (Linux 2.6.17) (95%), ASUS RT-N56U WAP (Linux 3.4) (93%), Linux 3.16 (93%), Linux 5.0 (93%)
No exact OS matches for host (test conditions non-ideal).
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 87.09 seconds

Enumeration: HTTP 80/tcp

http://soccer.htb/

BURP: 10.10.11.194:9091 http://soccer.htb/tiny/ http://soccer.htb/tiny/tinyfilemanager.php

Notice the Bottom of the Image → © CCP Programmers

The Page is developed by Tiny File Manager which uses 2 default credentials that are mentioned in their GitHub documentation.

Try to Login with those Credentials.

We logged In as Admin

Foot Hold: Shell as www-data

Tiny File Manager: Logged in, the page show the files that are part of the Soccer website:

The tiny directory has the filemanager page, as well as the uploads directory There is a “File Upload” feature that we can use to obtain a Reverse shell

Shell: I’ll make a simple PHP webshell:

I’ll use the “Upload” button, and it offers a way to upload If I try to upload in /var/www/html/, it fails If I navigate to /tiny/uploads and then click “Upload”, it works: Destination Folder: /var/www/html/tiny/uploads

The webshell provides execution:

I’ll start nc listening on 443 on my host, and trigger a reverse shell by sending a bash reverse shell:

Priv Esc: Shell as player

Enumeration:

Web Roots The files in /var/www/html match what I observed via the file manager:

There’s no database connection. The only credentials in the files are the users created for the Tiny File Manager:

http://soccer.htb/tiny/tinyfilemanager.php?p=tiny&view=tinyfilemanager.php

Other Home Directories There’s one home directory in /home, player user.txt is in that directory but www-data can’t read it:

Network / Processes The netstat shows a few ports that weren’t available from the outside:

There’s still not much information about what 9091 could be. Port 3000 looks to be another web page:

3306 and 33060 both seem to be MySQL instances:

It’s hard to verify any of this as www-data can only read it’s own processes:

That is because /proc is mounted with hidepid=2:

nginx There’s nothing else of interest in the system root or /opt or /srv. I’ll look at how nginx is configured. There are two site files in /etc/nginx/sites-enabled:

default set up the redirect to soccer.htb: It also configures the main site, allowing it PHP for PHP files: soc-player.htb sets up another site that matches on the name soc-player.soccer.htb: This webserver is hosted out of /root/, which is interesting, and passes to localhost 3000 (as observed previously).

Let’s add the soc-player.soccer.htb to our /etc/hosts and open it in the browser

http://soc-player.soccer.htb/

soc-player.soccer.htb:

This site looks exactly the same as the previous, except it has more options in the menu bar: “Match” has a page with a couple matches on it: It mentions a free ticket with login. I’ll register an account on the login: After logging in, it redirects to /check, where I get a ticket id:

http://soc-player.soccer.htb/check

I can put a ticket id into the field and hit enter, and it tells me that the ticket exists: Or a different number does not exist:

It’s running Express, a NodeJS web framework.

Websockets:

There’s another interesting request. Logging in submits a POST request to /login. On success, it returns a 302 redirect to /check. As that page is loading, it makes a request to soc-player.soccer.htb:9091, which returns a 101:

HTTP 101 is a Switching Protocols response:

TCP 9091 is a websocket server. There’s no immediate messages shown in the “WebSockets history” tab in Burp. But once I check a ticket, there’s a message and a response: The sent message is simply JSON with the id: The response is just the text that is shown:

SQL Injection over Websockets:

Identify I’ll send one of the “To server” message to Burp Repeater and play around with it. Adding in a ' doesn’t do anything other than return “Ticket Doesn’t Exist”. Any time I’m trying SQLI with an integer value, it’s worth trying without a ' as well. The ' is used to close strings, but if the input is being handled as an integer, perhaps just an ' or 1=1– - will work (where – - is to comment out whatever follows). It does: There is no ticket 0, but it still returns exists because it pulls all rows.

Blind SQL Injection Background This is a blind SQL injection - no data from the database comes back in the response, only one of two responses. The goal is to be able to ask questions of the database. For example, “is there a username that starts with ‘a’”? To get there, first I’ll need to be able to picture the query being run on the system. It’s going to be something like:

If one or more rows return, then it says the ticket exists, else it doesn’t. To make a test, there are a few ways I could structure a query. For manual testing, I prefer to use a UNION injection. I’ll send something that will return no rows, and then use a UNION to make another query, and then if that query returns rows, it will return that the “Ticket Exists”. It’s also possible to make these queries using OR foo=bar to test, but I find those more difficult to think about when doing the manual approach. I’ll also note that the app seems to handle query errors by returning “Ticket Doesn’t Exist” rather than crashing.

Manually Building a UNION I need to know the number of columns returned from the query, because my UNION statement must return the same number, or it crashes. If I send one, it returns false:

I’ll add more columns until it returns true at three columns:

Now the query on the server looks like this:

The first select returns no row, and then my UNION returns the values 1, 2, 3, and it returns “Ticket Exists”.

Manually Asking a Question Now to ask a question. In MySQL, there’s a mysql.user table with the users that can log into MySQL. I’m going to send this payload that will return true if there’s a user in that table that starts with “a”:

It returns false. There is likely a user named “root”, and changing “a” to “r”, it returns true: With enough requests, any value from the table can be brute-forced one character at a time.

sqlmap

Doing all of this manually is impossible, so I’ll either have to write a script to do it, or find a tool. sqlmap is the perfect tool here, and it even works over websockets. If sqlmap returns this error, it’s because the Python websockets library is missing:

Or if sqlmap returns this error, it’s because the wrong websockets library is installed:

Either of these are fixed with: pip install websocket-client I’ll give it the following arguments:

It finds a time-based injection, and then finds the three column UNION-based boolean as well: It’s using the OR structure for boolean rather than UNION.

Enumerate DB:

List Databases Now that sqlmap has found an injection, I’ll up-arrow and add --dbs to the previous command. Theads are safe to do in a boolean injection, so I’ll add --threads 10 to speed it up. It will pick up where it left off and list the available databases:

List Tables in soccer_db soccer_db seems like the only non-default DB. I’ll replace --dbs with -D soccer_db to specify that database and then add --tables to list the tables:

There’s only one.

Dump accounts In general, with boolean and time-based SQL injections, I want to be careful about dumping tons of data, as it will be very slow. That said, since there’s only one table, I want the entire thing, so I’ll replace --tables with -T accounts and add --dump. It dumps the table:

The user is player and the password is in plaintext.

su / SSH: That password works for the player user on the box with su, It works for SSH as well:

Shell as root

Enumeration:

sudo / doas: The first check on Linux is always sudo, but nothing set up for player on Soccer:

However, in looking for SetUID binaries, the first one jumps out:

doas is an alternative to sudo typically found on OpenBSD operating systems, but that can be installed on Debian-base Linux OSes like Ubuntu.

doas Config: I don’t see a doas.conf file in /etc, so I’ll search the filesystem for it with find:

It has one line:

player can run the command dstat as root.

dstat:

man Page dstat is a tool for getting system information. Looking at the man page, there’s a section on plugins that says:

While anyone can create their own dstat plugins (and contribute them) dstat ships with a number of plugins already that extend its capabilities greatly.

At the very bottom of the page, it has a section on files: Paths that may contain external dstat_*.py plugins:

Plugins are Python scripts with the name dstat_[plugin name].py.

Malicious Plugin: I’ll write a very simple plugin:

This will drop into 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’ll invoke dstat with the 0xdf plugin:

Last updated