Простой способ защиты от распределённого брутфорса доступов к CMS

в 7:53, , рубрики: cms, nginx, брутфорс, системное администрирование, хостинг, метки: , ,

Всем добрый день.
Команда Русоникса решила проблемы с электричеством и написала прекрасный пост с красивыми картинками про «Распределенную брутфорс-атаку на CMS с точки зрения хостера».
Впрочем, там не хватает одного — собственно реализации.

Итак, цели этого поста:

  • Эскизная реализация описанной в статье схемы на Nginx + немного бэкэнда в виде php;
  • Поиск решения «покрасивее»

Если интересно, прошу под хабракат.

Повторим на всякий случай цели борьбы:

  • Ограничить доступ к админкам CMS, чтобы не дать возможности массовым ботам брутить пароли
  • Без неудобств для любимых живых людей пользователей интернета
  • Решение должно быть универсальным и массовым

Логика:
0. Nginx работает как самый обычный front. Апач и всё остальное сзади.
1. При превышении некоторого лимита показываем пользователю формочку, где нужно что-либо сделать (в моём случае — просто нажать на кнопку), тем самым доказав, что он человек.
2. В дальнейшем пропускать человека без проволочек.

Решение оказалось несложным, все комментарии по ходу.

# описание т.н зоны
limit_req_zone $binary_remote_addr zone=one:10m rate=5r/m;

server {
    # ... 
    root /var/wl-web;
    recursive_error_pages on; #мы два раз проходим по error_page в случае @limit -> @wlgui;

    location / {
       #... тут самое обычное проксирование на бэкэнд
    }
    # именованый локейшн  для нашего ифейса авторизатора.
    location @wlgui { # у меня fpm, но можно проксировать и на апач. 
        internal;
        fastcgi_pass   127.0.0.1:9000;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME  $document_root/wlgui.php;
        fastcgi_intercept_errors on;
        include        fastcgi_params;
   }
    # защищаемые файлы. именно тут можно перечислить "сигнатуры" админок движков
    location ~* /(i|i2).html$ {
        # Срабатыване лимита реализуется в "возврате" 503 (c 1.3.15 этот код можно поменять строчкой ниже)
        #limit_req_status 516;
        error_page 503 = @limit;
        # включаем ограничитель. При срабатывании запрос уйдет на @limit
        limit_req zone=one  nodelay;

        # Это сработает, если в лимит не упёрлись.
        # Важно. Нам нужен именно изначальный URI, поэтому собираем руками.
        proxy_pass http://127.0.0.1:8080$request_uri;
        proxy_set_header Host $host;
        #и другие директивы проксирования на бэкэнд
    }
    # Если сработал лимит
    location @limit {
        internal;
        #проверяем что в  куке нет ничего, что может плохо сработать при подстановке в путь  файловой системы.
        if ($cookie_wlsid ~* [^a-fd]) { return 503; }

        # так не получилось, но было бы очень красиво =(
        #error_page 516 @backend;
        #try_files /wl/$cookie_wlsid.cookie /wl/$remote_addr.ip /wl/$remote_addr-$host.iph @wlgui;
        #if ($uri ~* /wl/[a-zd]+.cookie ) {return 516;}
        

        # именованый локейшн для заворота запроса на страничку подтверждения хм.. "человечности".
        # Если кука не авторизована(т.е нет файла с её значением), заворачиваем туда. If is Evil, i know....

        error_page 516 = @wlgui;
        if ( !-f $document_root/wl/$cookie_wlsid.cookie) {return 516;}

        # Кука авторизована! На бэкэнд.
        proxy_pass http://127.0.0.1:8080$request_uri;
        proxy_set_header Host $host;
        #и другие директивы проксирования на бэкэнд
    }
}

и очень простой wlgui.php, который производит «авторизацию» куки путём создания пустого файла с именем, равным значению куки.
(важно не забыть создать папку wl в /var/wl-web и поставить на неё соответствующий права)

<?php

if (!empty($_POST['wlsec'])){ # если форма сабминится, отдаём куку в браузер и авторизовываем её.
        $cookie=md5(uniqid());
        setcookie('wlsid',$cookie,time()+3600*24*90/*90d*/);
        touch ('/var/wl-web/wl/'.$cookie.'.cookie');
        echo "Done! Please, refresh the page! (setting {$cookie})";
} else {
?>
<form method="POST">
<input name="wlsec" value="GetAccess" type="submit">
</form>

<?php
echo "<br> Your cookie:".(isset($_COOKIE['wlsid'])?htmlspecialchars($_COOKIE['wlsid']):'(not set)');
}

Возможные улучшения:

  • Решение кривое, уверен, есть куда еще пилить
  • Не применять эти ограничения на GET (спорно)
  • По крону чистить папку со старыми авторизованными куками
  • Папка с авторизованными куками будет содержать очень много файлов, для ФС сервера это не очень хорошо
  • У меня авторизатор очень простой, по желанию в него можно(нужно) добавить капчу, смс, email и т.д
  • ifIsEvil, надо постараться без него (решения у меня пока нет)
  • Бан на игнорирование странички-авторизатора. (самое простое — писать в логи и убивать ботов по ним, адепты cut | uniq | sort будут очень рады)

Ну и как обычно: опечатки в личку, вопросы/дополнения/улучшения в комментарии.
Ссылки: Nginx limit_req_module, базовые вещи по ядру и прокси.

Спасибо, что дочитали до конца!
PS
По роду деятельности я не очень плотно занимаюсь настройкой серверного ПО и прочим сисадминством, поэтому я считаю это решение эскизным, хотя оно и отработало в продакшене какое-то время(затем у меня просто дошли руки переименовать админки).

Автор: la0

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js