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

    Всем добрый день.
    Команда Русоникса решила проблемы с электричеством и написала прекрасный пост с красивыми картинками про «Распределенную брутфорс-атаку на 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-f\d]) { 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-z\d]+\.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 (спорно)
    • По крону чистить папку со старыми авторизованными куками
    • Папка с авторизованными куками будет содержать очень много файлов, для ФС сервера это не очень хорошо. Решение от rozhik,
    • У меня авторизатор очень простой, по желанию в него можно(нужно) добавить капчу, смс, email и т.д
    • ifIsEvil, надо постараться без него (решения у меня пока нет)
    • Бан на игнорирование странички-авторизатора. (самое простое — писать в логи и убивать ботов по ним, адепты cut | uniq | sort будут очень рады)


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

    Спасибо, что дочитали до конца!
    — С уважением,
    Лазутов Александр

    PS
    По роду деятельности я не очень плотно занимаюсь настройкой серверного ПО и прочим сисадминством, поэтому я считаю это решение эскизным, хотя оно и отработало в продакшене какое-то время(затем у меня просто дошли руки переименовать админки).
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 12

      0
      Направление решения хорошее.
      Я бы рекомендовал только вместо файла сделать memcached ключ. Он и удалится автоматом, и Disk IO снизит, и позволит обслуживать распределённо.
        +1
        Пожалуйста, поделитесь ссылками на доки =)
          0
          www.php.net/manual/en/class.memcache.php

          $m = new Memcache;

          $m->connect(«127.0.0.1», 11211);
          $m->set('unique_key_in_memcached', 'data', 0, 600);

          $m->close();
            0
            А как вы это на nginx проверите?
            0
            HttpMemcModule — замените просто файловые операции на операции с мемкэшем в конфиге nginx.
              0
              Спасибо за полезный комментарий!
              Думаю лучше базовый мэмкеш, просто потому, что он базовый и есть в пакетных сборках.
              Добавляю в пост.
          0
          При очередном проходе любого поискового бота ваш сайт выпадет из индекса, т.к. куки они как правило не поддерживают, и кнопки нажимать не умеют. Или я ошибаюсь?
            0
            Мы применяем фильтр только на авторизационных страничках движков. Они, думаю, не очень интересны поисковикам.
            + см. возможный улучшайзер на GET

              0
              если я правильно понял логику, то решение срабатывает при повторном запросе той же страницы, а не какой-нибудь другой.
              К автору поста: хорошо бы прокомментировать логику отдельно, из конфига она не очень понятна.
                0
                Это сработает на location ~* /(i|i2)\.html$, т.е страниц может описано быть и пять и десять. Хост не участвует
                Это не готовое решение уровня копипаста, поэтому, пожалуйста, задавайте конкретный вопрос, я отвечу и добавлю ответ в пост, если это что-то существенное.
              0
              Как реализовать этот кусок для апача?

              location @wlgui {
                      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;
              }
              
                0
                В смысле для апача? Если ставите его бэкэндом?
                Самое простое — proxy_pass с нужным HTTP-host, например, поднять хост апачем на 81 порту и проксировать на него.

              Only users with full accounts can post comments. Log in, please.