суббота, 22 января 2011 г.

[Из старого] Контроль файловой структуры сайта


В данной статье я хочу рассказать как веб-мастеру можно достаточно простым способом контролировать целостность файловой структуры всего своего сайта. Вариант подходит практически для каждого хостинга, главное чтоб базировался он не на 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"


Обратите внимание на то, что пути указаны в жёстком виде, а не в относительном. Кстати, при желании можно организовать и отправку СМС администратору т.к. многие смс-биллинги сегодня предоставляют клиентам веб-интерфейс для отправки сообщений, к которому можно обращаться тем же curl`ом.
Теперь можно сформировать список контрольных сумм на данный момент. Поместим его в лог «имя_нашего_лога.md5tmp».

find $pathToFiles -type f -exec md5sum {} \; > $pathToLog"tmp"

Теперь нужно сравнить ранее сформированный файл с только что полученным. Как я и обещал, воспользуемся командой diff. Но для того, чтоб очистить результат её работы от лишнего хлама, мы укажем ей, что новые строки нужно выводить в особом формате. А то что она выведет, отсеем grep`ом.
Попробуйте запустить ранее указанный пример с праметром «--new-line-format="changedline_%L"» - вы увидите что в начало каждой изменённой или новой строки добавился текст «changedline_». Добавьте в конец команды «| grep changedline_» и вся лишняя информация исчезнет. Останутся лишь контрольные суммы. Объявим в нашем скрипте ещё 2 переменные:

i=0
result=()

Суть первой станет известна чуть позже, а в массив $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


Теперь просто перед добавлением $str в $result проверим длину добавляемой строки.

if [ ${#str} != 0 ]
then
    result[${#result[*]}]=$str
fi

Вот и всё. Сейчас мы можем исключать из поиска различий любые файлы из любых директорий. Главное вам теперь не забывать делать пересборку файлов с контрольными суммами после внесения каких-либо изменений на сайт.

UPD: По просьбе читателей прикрепляю готовые скрипты с не большими пояснениями у основных переменных.

http://yadi.sk/d/6ypNOdJgJgNc2

13 комментариев:

  1. Было бы не плохо в конце статьи выложить готовый скрипт

    ОтветитьУдалить
  2. Готово, выложил на Deposit. Если не ошибаюсь, там файлы хранятся дольше всего. Ссылка в конце статьи.

    ОтветитьУдалить
  3. Немного не учли, если проект имеет несколько сервером и 10-ки тысяч файлов. Затянется процесс.. Не проще посадить демона ФС, который будет сам следить за обновлением папки?

    ОтветитьУдалить
  4. подскажите, а в переменной "ignorePath=" задано именно две директории .svn и /home/www/cache?

    ОтветитьУдалить
  5. Ну нету в русском языке слова "Вообщем".
    Есть "в общем" и "вообще".

    ОтветитьУдалить
  6. Когда в имени файла встретился пробел, массив сдвинулся и все последующие строки стал выдаваться хэш вместо имени файла.

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

    ОтветитьУдалить
  7. В ignorePath можно ставить сколько угодно элементов.
    Что касается больших объёмов, то скрипт изначально писался под небольшой проект. На огромные сайты с десятками тысяч файлов он не расчитан. В столь масштабных вариантах явно нужны будут какие-то более практичные решения.
    По поводу пробела - это особенность bash :(

    ОтветитьУдалить
  8. я правильно понял: директории в ignorePath разделяются пробелом?
    Заранее спасибо.

    ОтветитьУдалить
  9. Да, таким образом в bash объявляются массивы.

    ОтветитьУдалить
  10. Перезалейте файлы на ЯндексДиск или в Google Drive там точно не пропадут а то на депозите уже нет файла.

    ОтветитьУдалить
  11. Ссылка на готовые скрипты заменена на Яндекс.Диск

    ОтветитьУдалить
  12. Этот комментарий был удален автором.

    ОтветитьУдалить
  13. Пришлось переписать это-же на перл, делюсь https://yadi.sk/d/KEU-sPtsbbTMv

    ОтветитьУдалить