Что-то давно я не писал статей ориентированных больше на веб-мастеров, чем на искателей уязвимостей
Надо это исправлять.
В данной статье я хочу рассказать как веб-мастеру можно достаточно простым способом контролировать целостность файловой структуры всего своего сайта. Вариант подходит практически для каждого хостинга, главное чтоб базировался он не на Windows.
Начнём с главного. Зачем это нужно? Большинство сайтов сейчас состоит из двух основных частей — файловой структуры и базы данных. Первая, в основном, занята внутренней работой сайта, а вторая — хранением информации. Почти во всех случаев при нападении или вирусном инфицировании меняется именно файловая структура. Например, взломщик стремится как можно быстрее загрузить шелл на атакуемый сервер, а вирусы вписывают свои тела почти во все
php/html/js-файлы. Для того чтоб таких инцидентов не возникало разработчики и администраторы принимают множество мер — от использования антивирусов до развёртывания WAF. Но что делать если атака уже произошла? Понятное дело, что надо устранять её последствия и причины, но от первого пункта до конечного здесь порой может пройти целый месяц, а то и больше.
Для простоты понимания будем рассматривать случай заражение сайта вирусом. Сразу после инфицирования ресурс начинает представлять опасность для пользователей. Через какое-то время на него придут Google или Yandex, и занесут его в свои чёрные списки. Обе эти вещи серьёзно ударят по посещаемости сайта и его поисковому рейтингу, что очень плохо. Следовательно, владельцу жертвы нужно стремиться как можно быстрее узнать о случившимся, дабы вовремя принять необходимые меры. Только вот администраторы заражённых сайтов почти всегда узнают об инциденте от третьих лиц. Хорошо если у вас есть антивирус и вы заглядываете на свой сайт несколько раз в день. Тогда вы достаточно быстро нарвётесь на предупреждение о том, что посещаемый ресурс может нанести вред вашему компьютеру. А если нет? Например, вирус ещё не попал в базы того продукта, который вы используете. Или на сайте прописался вовсе не вредоносный код, а какой-нибудь iframe-накрутчик. Что тогда? В первом случае остаётся надеяться лишь на посетителей с более хорошим антивирусом (а не каждый из них ещё и сообщит вам о проблеме), а в последнем владелец крайне долго может не знать о существовании утечки трафика. Получается что лучше заранее принять меры, ориентированные на информирование администратора об инциденте. А точнее — о изменении файловой структуры сайта. Обратите внимание на то, что это не отменяет необходимости принятия мер по предупреждению таких ситуаций.
Вы явно слышали о таких вещах как контрольные суммы — длинные хэш-строки, уникальные для каждого файла. Как только в файле происходят даже самые незначительные изменения, его контрольная сумма моментально изменяется. В nix-системах для работы в этом направлении есть утилита md5sum, присутствующая буквально на каждом сервере. Она может как вычислять контрольные суммы, так и сравнивать их на основе ранее сформированного списка. Если говорить в достаточно общих чертах, то я хочу предложить формировать список контрольных сумм для всего что есть внутри ресурса, и время от времени вызывать его сравнение с тем что есть в текущий момент.
Для начала давайте разберёмся как без проблем сформировать файл, содержащий информацию обо всех внутренностях директории сайта. Поиск в интернете показал что люди достаточно часто сталкиваются с проблемой рекурсивной обработки директорий утилитой md5sum, поэтому я опишу простую команду для этого.
Цитата |
find путь_к_сайту -type f -exec md5sum {} \; > путь_к_логу_с_суммами |
Здесь вызывается команда find, которая выведет вообще все файлы из указанной директории, на сколько бы «глубоко» они не находились. А тем временем, к каждому выведенному файлу применится команда md5sum, и результаты всех этих действий будут записаны в лог, указанный в самом конце. Попробуйте сейчас выполнить её на локальном компьютере если у вас установлена nix-система, а потом загляните в результат работы команды. В нём будет полно строк типа
Цитата |
49f2d9312130614d980861cf60f29257 /path/to/file.ext |
Думаю формат записи вопросов не вызовет. Теперь перейдём к сверке контрольных сумм. Чтоб её произвести можно воспользоваться той же утилитой. Но здесь нас подстерегает одно большое «но». md5sum при проверке не находит вновь добавленных файлов. Она сообщит вам лишь об изменённых и удалённых, а этого недостаточно - нам ведь нужно контролировать структуру сайта полностью. Поломав голову я пришёл к выводу что можно пойти в обход, и сравнивать не суммы файлов, а логи с ними. То есть мы при каждой проверке создаём ещё один лог и уже его сравниваем со старым списком. Так как в них используется формат записи «один файл на строку», то найдя различающиеся строки мы можем с точностью говорить об изменении/добавлении тех или иных файлов. Попробуйте в ранее созданном md5-файле измените контрольные суммы нескольких записей (можно просто заменить пару букв), а одну или две удалите. И создайте новый список сумм, с другим именем — его будем сравнивать. Для сравнения я предлагаю использовать утилиту diff. Она как раз занимается поиском изменённых строк в текстовых файлах. Попробуйте выполнить команду
Цитата |
diff /path/to/file1.md5 /path/to/file2.md5 |
Сразу покажутся строки в которых есть отличия. Конечно, они предоставляются не в чистом виде, а с всевозможными пометками, но это поправимо. Вообщем, основу наших действий я описал и, надеюсь, она понятна. Теперь можно перейти непосредственно к реализации задуманного.
Так как подобные проверки должны запускаться довольно часто (чем чаще запуск, тем меньше времени факт происшествия будет скрыт), то вариант запуска вручную, например из ssh, отменяется. Процесс явно должен быть автоматизирован. А для этого в nix-системах используется Сron. Причём чаще всего хостеры позволяют через панель управления легко и просто добавить в периодическое исполнение какую-нибудь команду введя её в текстовое поле.
Основную работу у нас будет выполнять отдельный bash-скрипт. Для запуска достаточно загрузить его по ftp в любую доступную директорию и в кроне прописать команду
Цитата |
/bin/bash /path/to/script.sh |
Так что особых проблем здесь быть не должно. Так же вам необходимо выделить на хостинге отдельную директорию для хранения лог-файлов. Желательно, чтоб она ни при каких обстоятельствах не была доступна из web`а. Всё, можно приступать к внутренностям.
Для начала объявим 3 переменные. Это директория сайта, путь к лог-файлу и email, на который будут приходить письма в случае опасности.
Цитата |
pathToFiles="/path/to/site/" pathToLog="/path/to/log.md5" myMail="mail@my.ru" |
Спустя 2 минуты, 4 секунды (12.05.2010 - 15:46) Kuzya написал(а):
Обратите внимание на то, что пути указаны в жёстком виде, а не в относительном. Кстати, при желании можно организовать и отправку СМС администратору т.к. многие смс-биллинги сегодня предоставляют клиентам веб-интерфейс для отправки сообщений, к которому можно обращаться тем же curl`ом.
Теперь можно сформировать список контрольных сумм на данный момент. Поместим его в лог «имя_нашего_лога.md5tmp».
Цитата |
find $pathToFiles -type f -exec md5sum {} \; > $pathToLog"tmp" |
Теперь нужно сравнить ранее сформированный файл с только что полученным. Как я и обещал, воспользуемся командой diff. Но для того, чтоб очистить результат её работы от лишнего хлама, мы укажем ей, что новые строки нужно выводить в особом формате. А то что она выведет, отсеем grep`ом.
Попробуйте запустить ранее указанный пример с праметром «--new-line-format="changedline_%L"» - вы увидите что в начало каждой изменённой или новой строки добавился текст «changedline_». Добавьте в конец команды «| grep changedline_» и вся лишняя информация исчезнет. Останутся лишь контрольные суммы. Объявим в нашем скрипте ещё 2 переменные:
Суть первой станет известна чуть позже, а в массив $result мы сложим имена подозреваемых файлов. Начнём цикл, который обработает результат вызова diff.
Цитата |
for str in `diff $pathToLog ${pathToLog}tmp --new-line-format="changedline_%L" | grep changedline_` do ... done |
Если вы сейчас внутрь цикла впишите «echo $str», то увидите что скрипт отобразит строки не в виде
Цитата |
changedline_49f2d9312130614d980861cf60f29257 /path/to/file1.ext changedline_49f2d9312130614d980861cf60f29257 /path/to/file2.ext changedline_49f2d9312130614d980861cf60f29257 /path/to/file3.ext |
а
Цитата |
changedline_49f2d9312130614d980861cf60f29257 /path/to/file1.ext changedline_49f2d9312130614d980861cf60f29257 /path/to/file2.ext changedline_49f2d9312130614d980861cf60f29257 /path/to/file3.ext |
Это происходит от того, что цикл for должен обрабатывать массив. А получая на входе строку, он пытается её к этому массиву привести. Разбиение строки на ячейки происходит по переносу строк и по пробелам. Таким образом, мы получаем массив, где каждая чётная ячейка содержит имя файла в необходимом нам виде. Вот здесь и пригодится счётчик $i. Каждый первый шаг цикла к нему будет добавляться единица, а каждый второй — он будет обнуляться. Кроме обнуления, на каждом втором шаге, мы будем заносить текущее значение $str в массив $result. Ведь именно в это время там будет находиться имя файла.
Цитата |
if [ $i = 1 ] then i=0 result[${#result[*]}]=$str else i=`expr $i + 1` fi |
Осталось лишь в конце работы проверить содержимое $result. Если там что-то есть, то отсылаем владельцу сайта письмо с темой «ALERT», иначе — с «OK»
Цитата |
if [ ${#result[*]} != 0 ] then printf "%s\n" ${result[@]} | mail -s "md5sum checker -- ALERT" $myMail else echo "All good" | mail -s "md5sum checker -- OK" $myMail fi |
В последствии, когда вы убедитесь в том что скрипт работает хорошо, можно закомментировать предпоследнюю строку, дабы письма приходили только в случае появления подозрительных файлов.
И закончим на одной небольшой доработке. Скрипт, который мы написали может хорошо работать лишь на сайтах, имеющих статичную файловую структуру. То есть, если сайт использует хотя бы файловое кэширование, то администратор постоянно будет получать сообщения о том, что в папке кэша то пропадают, то появляются новые файлы. Это будет надоедать, поэтому сейчас мы избавимся от данной проблемы. После главных переменных, в самом начале скрипта, добавьте массив $ignorePath. Солжим туда сложим все пути, файлы по которым должны быть проигнорированы.
Цитата |
ignorePath=(".svn" "/home/www/cache/") |
В каждом изменённом файле мы будем искать фразы из этого массива, и если найдём — проигнорируем изменение. Для этого перед внесением $str в $result мы циклично обойдём $ignorePath
Цитата |
for ignore in $ignorePath do ... done
|
Внутри цикла мы сначала запомним длину текущей $str, затем удалим из неё то, что лежит в $ignore, и если длина $str изменилась (то есть в ней был признак игнорирования) — нужно проигнорировать её.
Цитата |
fcount=${#str}
str=${str/$ignore/""}
if [ ${#str} != $fcount ] then str="" fi |
Спустя 2 минуты, 6 секунд (12.05.2010 - 15:49) Kuzya написал(а):
Теперь просто перед добавлением $str в $result проверим длину добавляемой строки.
Цитата |
if [ ${#str} != 0 ] then result[${#result[*]}]=$str fi |
Вот и всё. Сейчас мы можем исключать из поиска различий любые файлы из любых директорий. Главное вам теперь не забывать делать пересборку файлов с контрольными суммами после внесения каких-либо изменений на сайт.
А для тех, у кого что-то не получается, вот полный код
скрипта -
summs.sh.zipP.S. Кому интересно, общее описание веб-вирусов есть тут - HTML-вирусы и всё с ними связанное Спустя 35 минут, 33 секунды (12.05.2010 - 16:24) FatCat написал(а):
Не помню где брал
скрипт для этих целей, чуть изменен мной.
<?php
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
http://artriva.ru/
$md5_file = "mdcache.txt";
$allowExt = ".php|.html|.tpl|.htm|.htaccess";
$allowDir = "";
$skipDir = "style_images|uploads";
$skipFileGlobal = "somefile.html|otherfile.php";
$skipFile = "";
$adminMail = "some_mail@mail.ru";
$mail_subject = "From Iframe-Detector script";
$adminPassword = "свой_пассворд";
$scriptDir = "некоторая_директория";
$xmail = array();
$xmail['subject'] = $mail_subject;
$xmail['headers'] = 'MIME-Version: 1.0' . "\r\n".
'Content-type: text/html; charset=windows-1251' . "\r\n".
'From: '.$adminMail. "\r\n" .
'Reply-To: '.$adminMail. "\r\n" .
'X-Mailer: PHP/' . phpversion();
$xmail['adminMail'] = $adminMail;
$allowExt = str_replace(".","\.",$allowExt);
$skipFile = explode("|",$skipFile);
$skipFileGlobal = explode("|",$skipFileGlobal);
if (trim($scriptDir) != ""){
$ex = "-".(strlen($scriptDir) + 1);
$BASE = substr(dirname(__FILE__),0,$ex);
$md5_file = $BASE."/".$scriptDir."/".$md5_file;
} else {
$BASE = dirname(__FILE__);
$md5_file = $BASE."/".$md5_file;
}
if (!is_dir($BASE."/".$scriptDir)) die("Папка \"$scriptDir\" отсутсвует - создайте ее, или измените параметр \"$ sсriptDir\" в настройках");
define("ABSPATH",$BASE);
function write_md5(){
global $md5_file;
$md5 = dir_md5();
$md5_from_file = @fopen($md5_file, 'w');
foreach ($md5 as $key=>$val) @fwrite($md5_from_file, "$val\t$key\n");
}
function checker(){
global $md5_file,$xmail,$adminPassword;
$i = 1;
if (!file_exists($md5_file)) {
write_md5();
}
if ( $_POST['updatemd5'] ) {
if ( $_POST['password'] == $adminPassword ) {
write_md5();
} else {
echo "Неверный пароль";
}
}
$md5 = dir_md5();
if (file_exists($md5_file)) {
$md5_from_file = file($md5_file);
if ( !$md5_from_file ) {
write_md5();
$md5_from_file = file($md5_file);
}
$mailBody = "";
$error = "";
while (list($ln, $line) = each($md5_from_file))
{
list($md, $ff) = explode("\t", trim($line));
if ($md != $md5[$ff]) {
$ff = str_replace(ABSPATH, '/', $ff);
$ff = str_replace('//', '/', $ff);
$error .= "<br />$i. $ff - <font color='red'>изменен</font>";
$mailBody .= "<br />$i. $ff - изменен" . "\r\n";
$i++;
}
}
if($mailBody != "" && $_GET['send'] == 1) @mail($xmail['adminMail'], $xmail['subject'], $mailBody, $xmail['headers']);
}
if ($_GET['send'] != 1) {
if ( $i == 1 ) {
echo "<br /><font color='green'>Всё ок!</font>";
} else {
echo $error;
}
echo '<br />
<br />
<form method="post" action="">
пароль: <input type="text" id="" name="password" value="">
<input type="submit" id="updatemd5" name="updatemd5" value="Сбросить/обновить хэш файлов">
</form>';
}
}
function dir_md5() {
global $md5,$allowDir;
if($allowDir != ""){
find_md5(ABSPATH, 0);
$aDir = explode("|",$allowDir);
while (list($key, $dir) = each($aDir)){
find_md5(ABSPATH .'/'. $dir, 1);
}
}else{
find_md5(ABSPATH, 1);
}
return $md5;
}
function find_md5($path, $recurs) {
global $md5,$allowExt,$skipDir,$skipFile,$skipFileGlobal;
if ($skipDir != "" and preg_match("#$skipDir$#i", $path)) $skip = 1;
if ($dir = @opendir($path) and $skip != 1 ) {
while($file = readdir($dir)) {
if ($file == '.' or $file == '..' ) continue;
if (!in_array($file,$skipFileGlobal)) {
$file = $path . '/' . $file;
if (is_dir($file) && $recurs) {
find_md5($file, 1);
}
if (!in_array($file,$skipFile)) {
if (is_file($file) && preg_match("/$allowExt/i", $file)) {
$md5[$file] = md5(join ('', file($file)));
}
}
}
}
closedir($dir);
}
}
checker();
?>
И до пущей безопасности, скрпит в директории, закрытой паролем по эйчтиакцессу.
Спустя 13 минут, 24 секунды (12.05.2010 - 16:37) Kuzya написал(а):
Я тоже раньше был приверженцем подобных разработок. Но всегда я сталкивался с одной большой проблемой - PHP ну никак не может производить такие работы с большим количеством файлов. Я даже как-то писал похожий скрипт, он назывался "Structure Checker". В итоге я зашёл в тупик - чем больше количество файлов, тем медленнее работает PHP-скрипт. Со временем он может дойти до предела выполнения из php.ini и будет автоматически убиваться. Поэтому я решил переходить на встроенные утилиты. md5summ работает в несколько раз быстрее аналогичных реализаций на PHP/Python/Perl/и т.д. Видимо она как-то по особому написана + явно на таких сверхбыстрых вещах как С/С++.
Но зато подобные решения могут пригодиться под Windows. Я правда не знаю, может быть там уже тоже имеется какая-нибудь быстрая C-реализация.