Пользователь
0,0
рейтинг
13 апреля 2012 в 10:59

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

После публикации статьи о модуле 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;
    }
}

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

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


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

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (9)

  • +11
    Надеюсь, этот модуль не найдёт широкого распространения, так как я флэш не использую принципиально. Да и на многих мобильных устройствах его нет.
    • +4
      это PoC того, как можно прикрутить Flash и Captcha к testcookie-nginx-module. В production конечно же не стоит забывать про клиентов без Flash.
  • +3
    Я отключил флеш-плагин в основном браузере. Я пролетаю с вашими сайтами?
  • +2
    У меня тоже нет флэша уже очень давно, а на MacBook Air его вообще нет по-дефолту, не говоря уже о мобильных девайсах. Затем, флэш элементарно декомпилится, узнать алгоритм шифрования куки не составит труда. Тупиковый метод мне кажется.
  • +1
    Надеюсь я такого никогда не увижу в живую, только как академическое пособие «как не надо делать ваш сайт».
  • 0
    в случае ресурса, ориентированного на среднего десктопного пользователя, вполне себе можно взять на заметку как ОДИН ИЗ способов предотвратить атаку на некоторое, не столько малое время (декомпилить Flash не каждый додумается с первого раза, при условии что DDoS-ботнеты далеко не всегда эксплуатируются «технарями/разработчиками»).

    спасибо, полезный материал.
  • +1
    Очень интересное решение!
    Для сайтов, на которых флеш обязателен (к примеру, масса видео-роликов и т.д.) имеет полное право на жизнь.
    • 0
      Такие сайты не имеют права на жизнь)
  • 0
    Не знаю в тему или нет, но нв всякий случай приведу код как при помощи LUA вставить куки прямо в Nginx, без всяких Flash, Fcgi и бекенда

    header_filter_by_lua '
            local headers = ngx.header["Set-Cookie"]
            if headers then
                if type(headers) == "string" then
                    headers = {headers}
                end
                for i, header in ipairs(headers) do
                    local cookie = ngx.re.match(header, "JSESSIONID=([^;]+);", "io")
                    if cookie then
                        headers[i] = "JSESSIONID=" .. cookie[1] .. "; domain=.frontend.com; path=/newpath;"
                    end
                end
                ngx.header["Set-Cookie"] = headers
            end
    ';
    <source>

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.