Модуль nginx для борьбы с DDoS, ставим cookie через Flash

в 6:59, , рубрики: ddos, flash, nginx, безопасность, защита, информационная безопасность, метки: , , , ,

После публикации статьи о модуле nginx, предназначенном для борьбы с ботами я получил множество откликов, в которых люди спрашивали о поддержке Flash.
Я был уверен, что при должных усилиях желающие могли реализовать эти функции самостоятельно, как сторонние приложения, без изменения кода самого модуля, но никто этого не сделал, поэтому пришлось мне сделать PoC.

Изначально, хотелось, чтобы модуль был в виде конструктора, да и принцип KISS никто не отменял, поэтому весь client-side функционал было решено реализовывать в виде сторонних приложений. У nginx всё хорошо с проксированием, так что самый простой способ — писать их в виде отдельных HTTP сервисов.
Это дает следующие преимущества:

  • Пишем на любом удобном нам языке(я предпочитаю Python)
  • Не завязываемся на nginx, качество кода не так существенно — можно блокироваться итд итп
  • Можем использовать весь спектр кеширующего функционала nginx
  • Если даже сервис лежит, все легитимные пользователи, уже получившие свою куку, этого не замечают — основной ресурс для них доступен.

Что пришлось добавить

Для главной директивы модуля testcookie-filter, кроме прежних значений «on» и «off» теперь добавилось еще одно — «var». Если модуль работает в режиме «var» то для данного location:

  • будет осуществляться проверка cookie;
  • nginx variables(все, кроме $testcookie_nexturl) выставятся;
  • перенаправлений, установки cookie и перехвата запроса происходить не будет.

Для чего это нужно

Таким образом, теперь можно передать правильное значение cookie в свое приложение через HTTP заголовок:

    location = /testcookie.swf {
        testcookie var;
        proxy_pass http://127.0.0.1:1234/;
        proxy_set_header Testcookie-Value $testcookie_set;
        proxy_set_header Testcookie-Valid $testcookie_ok;
        proxy_set_header Testcookie-Name "BPC";
    }

Почему через заголовок, а не через param в случае с Flash?
Так значение не фигурирует в открытом виде в клиентской части, его нельзя извлечь, например, regexp'ом.

И все-таки, Flash?

Я считаю, что метод установки cookie через Flash не самый правильный, но раз пользователи хотят — почему бы не сделать.
Идею сделать статический SWF и подставлять в него значения, как это сделано в Roboo, я отмел сразу — легко разобрать, сложно модифицировать итд.
Поэтому, было решено дать пользователю возможность своими силами усложнить жизнь атакующему.
Для этого в проекте существуют 2 файла:
cookie_encoder.py (Python):

def encode_cookie(cookie_value):
    key = 42
    res = ''
    for x in cookie_value:
        res += chr(ord(x) ^ key)
    return res

cookie_decoder.as (ActionScript):

function flash_cookie_crypt_routine(str) {
    var result;

    for (var i = 0; i < str.length; i++) {
        result += String.fromCharCode(str.charCodeAt(i) ^ 42);
    }
    return result;
}

getURL("javascript:void(document.cookie='#TESTCOOKIE_NAME#=" + flash_cookie_crypt_routine("#TESTCOOKIE_VALUE#") + "');void(location.href='" + nexturl + "');");

Как не сложно догадаться, первый предназначен для кодирования значения(в примере ксорим на 42), второй для декодирования на стороне клиента(еще раз ксорим на 42).

ActionScript динамически собирается в SWF, с использованием libming. К слову, пришлось ее немного запатчить, поэтому работать проект будет только с моим fork'ом, по крайней мере до тех пор пока maintainer'ы не перестанут придираться к именам функций и не одобрят мой pull.

В качестве каркаса сервиса используется fapws3 — быстрый асинхронный веб-сервер для Python.

Всё вместе на стареньком coreduo, в один процесс, дает свои ~3k req/s при concurency 1k, без какой-либо оптимизации и кеширования. В production можно использовать nginx proxy cache, таким образом SWF будет генерироваться для каждого клиента лишь один раз, раздаваться nginx'ом и положить сам сервис будет довольно сложно.

Всё вместе

Пример конфигурации:

server {
    listen 80;
    server_name domain.com;

    testcookie off;
    testcookie_name BPC;
    testcookie_secret keepmescret;
    testcookie_session $remote_addr;
    testcookie_arg attempt;
    testcookie_max_attempts 3;
    testcookie_fallback /cookies.html?backurl=http://$host$request_uri;
    testcookie_get_only on;
    testcookie_redirect_via_refresh on;

# подключим testcookie.swf через swfobject(да, я ленив)
# используем директиву testcookie_refresh_template,
# передадим значение $testcookie_nexturl через param

    testcookie_refresh_template '<html><body><script type="text/javascript" src="/swfobject.js"></script><script type="text/javascript">swfobject.embedSWF("/testcookie.swf", "cookie_installer", "100", "100", "9.0.0", "/expressInstall.swf", {"nexturl":"$testcookie_nexturl"});</script><div id="cookie_installer">welcome screen</div></body></html>';

# отключаем проверку cookies
# для fallback URL, swfobject.js и expressInstall.swf

    location = /cookies.html {
        root /var/www/public_html;
    }

    location = /swfobject.js {
        gzip  on;
        gzip_min_length 1000;
        gzip_types      text/plain;
        root /usr/local/nginx/root;
    }

    location = /expressInstall.swf {
        testcookie off;
        gzip  on;
        gzip_min_length 1000;
        gzip_types      text/plain;
        root /usr/local/nginx/root;
    }

# по данному location сервис динамически генерирует SWF

    location = /testcookie.swf {
        # модуль testcookie в режиме var
        testcookie var;
        # сервис testcookie-flash-processor
        proxy_pass http://127.0.0.1:1234/;
        # передадим правильное значение cookies
        proxy_set_header Testcookie-Value $testcookie_set;
        # прошел ли клиент проверку?
        # нет смысла генерировать SWF для тех,
        # у кого уже есть правильная cookie
        proxy_set_header Testcookie-Valid $testcookie_ok;
        # имя cookie - должно быть равно значению
        # в директиве testcookie_name
        proxy_set_header Testcookie-Name "BPC";
    }

# основной location с обращением к backend'у

    location / {
        testcookie on;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_pass http://127.0.0.1:8080;
    }
}

Captha прикручивается аналогичным образом.

Исходные тексты

Исходные тексты с инструкциями по установке и документацией доступны на github под BSD лицензией.
Патчи, дополнения, тесты и баг-репорты приветствуются.

Автор: kyprizel

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


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