Современные решения ИТ задач и программной инженерии

Проактивная защита веб сервера на bash и ipfw таблицах

07.07.2015 в разделах ОбучениеБезопасностьс тегами scriptingbashfreebsdnix
Проактивная защита веб сервера на bash и ipfw таблицах
Это будет интересно системным администраторам среднего уровня. Речь пойдет о таком модном слове как "проактивная" защита и подразумевает собой анализ всего трафика веб сервера и выполнение каких либо действий. В данном случае мы будем анализировать и блокировать на время нехорошие IP адреса фаерволом IPFW.

На секундочку отойдем от темы. Вы когда то смотрели на логи доступа вашего веб сервера? Тысячи соединений стремятся подключиться, отправить и принять данные. Можно себе представить такую автомагистраль, всю в движении и забитую потоком машин. Имеется ввиду не просто открыть файл логов и смотреть на него а затейлиться к нему и смотреть в режиме реального времени. Если вы этого не делали никогда, посмотрите, понаблюдайте несколько раз на протяжении некоторого времени. Посмотрите как заходят к вам на сайт поисковые роботы, как анализируют ваш сайт всякие СЕО инструменты, заходы всяких рекламных и фото роботов, роботов определяющих CMS и возможно попытки взломов или подсунуть вашему сайту какой то эксплоит. Посмотрите, если не смотрели, хоть раз.

Забыли сказать о бруте, тупому переборе паролей к панели управления сайтом. И запомните пожалуйста, никогда, ну никогда не ставьте на админку логин и пароль admin / admin! Хоть то какая то Joomla или Wordpress. Возможен такой сценарий: у вас скажем сайт на wordpress, вы поставили простой логин / пароль, злоумышленник подобрал, зашел в админку, поправил через редактор тем любой php файл залив в него эксплоит, все. Вот вам веселье, смена логина и пароля вам уже не поможет, только откат. Даже если вы запретили изменение файлов, злоумышленник может поставить какой то плагин, который позволяет выполнять php код и все, круг снова замкнулся.

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

Метод который мы будем использовать не совсем практичный, при очень большей посещаемости сервера может кушать очень много процессорного времени. Стоит использовать такую штуку только в крайних мерах, или при DDoS атаке, или при частых взломах, или при частых подборах пароля или возможно для обучения и практики, ради интереса, на не очень длительный срок и под своим контролем. Иначе серверу может стать очень плохо, не хватит ресурсов, все будет жутко тормозить или мало ли еще что то хуже. Такие штуки нужно писать очень грамотно и на каком то компилируемом языке программирования, например С#. Смотрите что бы сервер не упал.

Сервер упал
Для начала, настройте свой фаервол так, что бы он блокировал все IP адреса в какой то таблице. Смотрите какая из таблиц у вас свободная. В моем случае я буду использовать таблицу под номером 5 и все это для IPFW будет выглядеть так:

/sbin/ipfw add deny ip from "table(5)" to me

Теперь все IP адреса которые будут попадать в таблицу 5 будут блокироваться для всего сервера на любом порте. Как работать с таблицами /sbin/ipfw, можете где то почитать. Предполагаем что вы знакомы с /sbin/ipfw и как работать с таблицами. На самом деле все очень просто и в коде это все будет описано. Фаерфол настроили, идем дальше. Теперь давайте напишем каркас для нашего скрипта, который будет тейлиться к файлу логов доступа и читать данные в реальном времени. На самом деле мы можем читать любой файл логов и парсить его встроенными средствами. Будь то nginx, apache, ftp, ssh, dovecot, не важно. Давайте рассмотрим apache, так как он самый распространенный и речь идет о защите веб сервера.

Припустим что файл с логами доступа апача лежит здесь "/var/log/apache-access.log", тогда каркас нашего скрипта будет иметь следующий вид:

#!/bin/sh

tail -f /var/log/apache-access.log | while read line; do
  #do some stuff...
done

Да, вот так просто, это полный каркас. После его запуска, утилита tail захватит файл логов и будет отслеживать его изменения с конца файла. И только в файл прийдет новая строка, она тут же будете передана в цикл while и далее в переменную line. Важно понимать что здесь идет бесконечный цикл. То есть скрипт отслеживает файл в бесконечном цикле пока мы его вручную не остановим по Ctrl+C. Каркас готов, давайте посмотрим на файл логов апача и напишем разбиение строки на отдельные параметры, что бы можно было отделить IP от строки запроса, метода, код результата, юзерагента и т. д.

Рассмотрим файл логов доступа к apache

193.108.251.246 - - [07/Jul/2015:20:44:10 +0300] "GET / HTTP/1.1" 301 178 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.130 Safari/537.36"

193.108.251.246 - - [07/Jul/2015:20:44:10 +0300] "GET / HTTP/1.1" 200 4855 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.130 Safari/537.36"

193.108.251.246 - - [07/Jul/2015:20:44:16 +0300] "GET /blog/ HTTP/1.1" 200 5630 "https://wds4.com/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.130 Safari/537.36"

193.108.251.246 - - [07/Jul/2015:20:44:29 +0300] "GET /blog/pishem-na-bash-intuitivnoponyatnyjj-scenarijj/ HTTP/1.1" 200 6803 "https://wds4.com/blog/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.130 Safari/537.36"

193.108.251.246 - - [07/Jul/2015:20:44:31 +0300] "GET /blog/ HTTP/1.1" 200 5630 "https://wds4.com/blog/pishem-na-bash-intuitivnoponyatnyjj-scenarijj/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.130 Safari/537.36"

193.108.251.246 - - [07/Jul/2015:20:44:32 +0300] "GET /blog/php-i-pervye-shagi-na-puti-k-programmirovaniyu/ HTTP/1.1" 200 5717 "https://wds4.com/blog/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.130 Safari/537.36"

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

#!/bin/sh

tail -f /var/log/apache-access.log | while read line; do
  param_ip=`echo "$line" | grep -o '^[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}'`

  param_protocol=`echo "$line" | grep -o '".*"'`
  param_protocol=`echo "$param_protocol" | sed 's/^"//'`
  param_protocol=`echo "$param_protocol" | grep -o '.*" [0-9]{3}'`
  param_protocol=`echo "$param_protocol" | sed 's/" [0-9]{3}$//'`

  param_method=$(echo $param_protocol | cut -f1 -d' ')

  param_query=$(echo $param_protocol | cut -f2 -d' ')

  param_version=$(echo $param_protocol | cut -f3 -d' ')

  param_code=`echo "$line" | grep -o '".*"'`
  param_code=`echo "$param_code" | sed 's/^"//'`
  param_code=`echo "$param_code" | grep -o '.*" [0-9]{3}'`
  param_code=`echo "$param_code" | sed 's/^.*" //'`

  param_reff=`echo "$line" | grep -o '".*"'`
  param_reff=`echo "$param_reff" | sed 's/^"//'`
  param_reff=`echo "$param_reff" | grep -o '".*'`
  param_reff=`echo "$param_reff" | sed 's/^" [0-9]{0,9} [0-9]{0,9} //'`
  param_reff=`echo "$param_reff" | grep -o '".*" '`
  param_reff=`echo "$param_reff" | sed 's/^"//'`
  param_reff=`echo "$param_reff" | sed 's/" $//'`

  param_useragent=`echo "$line" | grep -o '".*"'`
  param_useragent=`echo "$param_useragent" | sed 's/^"//'`
  param_useragent=`echo "$param_useragent" | grep -o '".*'`
  param_useragent=`echo "$param_useragent" | sed 's/^" [0-9]{0,9} [0-9]{0,9} //'`
  param_useragent=`echo "$param_useragent" | grep -o ' ".*"'`
  param_useragent=`echo "$param_useragent" | sed 's/^ "//'`
  param_useragent=`echo "$param_useragent" | sed 's/"$//'`

  echo "IP: $param_ip"
  echo "PROTOCOL: $param_protocol"
  echo "METHOD: $param_method"
  echo "QUERY: $param_query"
  echo "VERSION: $param_version"
  echo "STATUS CODE: $param_code"
  echo "REFFERER: $param_reff"
  echo "USERAGENT: $param_useragent"
  echo ""
done

Я не буду все расписывать, все команды и работа этих утилит есть в документации. Здесь все очень просто, при помощи grep, sed и регулярных выражений мы выдергиваем значения из строки $line. Да, можно было бы сделать проще, использовать тот же Perl или PHP но мы же договорились писать на чистом bash так что продолжим.

Итак, давайте запустим наш скрипт и посмотрим что же получилось

IP: 193.108.251.246
PROTOCOL: GET / HTTP/1.1
METHOD: GET
QUERY: /
VERSION: HTTP/1.1
STATUS CODE: 301
REFFERER: -
USERAGENT: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.130 Safari/537.36

IP: 193.108.251.246
PROTOCOL: GET / HTTP/1.1
METHOD: GET
QUERY: /
VERSION: HTTP/1.1
STATUS CODE: 200
REFFERER: -
USERAGENT: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.130 Safari/537.36

IP: 193.108.251.246
PROTOCOL: GET /blog/ HTTP/1.1
METHOD: GET
QUERY: /blog/
VERSION: HTTP/1.1
STATUS CODE: 200
REFFERER: https://wds4.com/
USERAGENT: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.130 Safari/537.36

IP: 193.108.251.246
PROTOCOL: GET /blog/pishem-na-bash-intuitivnoponyatnyjj-scenarijj/ HTTP/1.1
METHOD: GET
QUERY: /blog/pishem-na-bash-intuitivnoponyatnyjj-scenarijj/
VERSION: HTTP/1.1
STATUS CODE: 200
REFFERER: https://wds4.com/blog/
USERAGENT: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.130 Safari/537.36

IP: 193.108.251.246
PROTOCOL: GET /blog/ HTTP/1.1
METHOD: GET
QUERY: /blog/
VERSION: HTTP/1.1
STATUS CODE: 200
REFFERER: https://wds4.com/blog/pishem-na-bash-intuitivnoponyatnyjj-scenarijj/
USERAGENT: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.130 Safari/537.36

IP: 193.108.251.246
PROTOCOL: GET /blog/php-i-pervye-shagi-na-puti-k-programmirovaniyu/ HTTP/1.1
METHOD: GET
QUERY: /blog/php-i-pervye-shagi-na-puti-k-programmirovaniyu/
VERSION: HTTP/1.1
STATUS CODE: 200
REFFERER: https://wds4.com/blog/
USERAGENT: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.130 Safari/537.36

То есть мы получили те же логи только все в аккуратно распарсеном виде. Все разложено по полочкам, IP в отдельной переменной, метод, строка запроса и т. д., все в отдельных переменных. Все готово для того что бы начать писать логику и процесс блокировки. Но давайте остановимся и закончим во второй статье. Эта статья получилась очень объемная.