Страница 1 из 1

Толерантный WAF из nginx и fail2ban)))

Добавлено: 06 фев 2020, 18:03
Raven
Наверное каждый из админов веб-хостинга рано или поздно сталкивается с тем, что какая-нибудь дрянь начинает сканить сайты в поисках уязвимостей. Как правило, если код сайта написан добротно, то и переживать за здравие сайта не приходится, однако как правило, если сайтов на хостинге много, то и ресурсы распределяются между ними пропорционально, а значит присутствует некоторое ограничение в их использовании. И пентестилки, наваливаясь гурьбой, запросто могут довести сайт до состояния исчерпания этих лимитов.

Почему не naxsi или mod_security?
К сожалению, это не совсем правильно решение для крупного хостинга, т.к. они могут вызывать проблемы с отправкой форм и загрузкой файлов, а это не очень хорошо для клиентов, поэтому мы решили не рисковать с их использованием. Описанное ниже решение родилось из уже имевшегося фильтра URL, а следовательно не потребовало больших изменений в конфиг, плюс ко всему, обладает неким запасом толерантности, что исключает случайное срабатывание.

Итак, принцип работы
Предположим, на сервер вдруг посыпались запросы с "UNION%20SELECT" или "../../../etc/passwd" в запросе. nginx прогоняет его по 3 (можно и 4-м) маппингам и проверяет совпадение элементов запроса, таких как REQUEST_URI, QUERY_STRING и User-Agent (опционально можно подключить проверку реферера) с регулярками:

Код: Выделить всё

map $request_uri $check_uri {
    default 0;
    include /etc/nginx/lists.d/security_rules.conf;
    include /etc/nginx/lists.d/unix_filesystem.conf;
}

map $args $check_args {
    default $check_uri;
    include /etc/nginx/lists.d/security_rules.conf;
    include /etc/nginx/lists.d/php_functions.conf;
    include /etc/nginx/lists.d/unix_filesystem.conf;
}

#map $http_referer $check_referer {
#    default $check_args;
#    include /etc/nginx/lists.d/security_rules.conf;
#    include /etc/nginx/lists.d/unix_filesystem.conf;
#}

map $http_user_agent $is_attack {
#    default $check_referer;
    default $check_args;
    include /etc/nginx/lists.d/security_rules.conf;
    include /etc/nginx/lists.d/unix_filesystem.conf;
}
При совпадении какой-либо из частей запроса с какой-либо из регулярок переменной $is_attack присваевается значение 1.

Далее, в теле виртуалхоста размещаем строчку:

Код: Выделить всё

access_log /var/log/nginx/security_log combined if=$is_attack;
Все, что делается с запросом на этом этапе - только запись в лог, если переменная $is_attack больше нуля, более никаких вмешательств.

Помимо этого, на стороне nginx нужно добавить виртуалхост на портах отличных от стандартных, например 8080 и 8443:

Код: Выделить всё

server {
    listen 127.0.0.1:8080 default_server;
    listen 127.0.0.1:8443 default_server ssl;
    server_name _;

    ssl_certificate_key /etc/nginx/ssl/server.key;
    ssl_certificate /etc/nginx/ssl/server.pem;
    # 3 строки ниже не нужны, если юзать самоподписные сертификаты
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/nginx/ssl/server.pem;

    root /usr/share/nginx/html;
    access_log /var/log/nginx/banned_log combined;

    error_page 403 /forbidden.html;

    location / {
        return 403;
    }
}
На этом с nginx все, ну разве что еще нужно создать симпатичную страничку ошибки forbidden.html, если у вас ее нет.

Далее нам понадобится fail2ban, устанавливаем его, если его до сих пор нет и настраиваем:

В директорию /etc/fail2ban/filter.d кладем файлик nginx_security.conf

Код: Выделить всё

[Definition]
failregex = ^<HOST> (.*)
ignoreregex =
В директорию /etc/fail2ban/action.d файлы nginx-iptables-nat.conf

Код: Выделить всё

# Fail2Ban configuration file

[INCLUDES]

before = iptables-common.conf

[Definition]

actionstart = ipset --create f2b-nginx_security iphash
              iptables -t nat -I PREROUTING -p tcp -m tcp --dport 80 -m set --match-set f2b-nginx_security src -j DNAT --to-destination 127.0.0.1:8080
              iptables -t nat -I PREROUTING -p tcp -m tcp --dport 443 -m set --match-set f2b-nginx_security src -j DNAT --to-destination 127.0.0.1:8443

actionflush = ipset --flush f2b-nginx_security

actionstop =  iptables -t nat -D PREROUTING -p tcp -m tcp --dport 80 -m set --match-set f2b-nginx_security src -j DNAT --to-destination 127.0.0.1:8080
              iptables -t nat -D PREROUTING -p tcp -m tcp --dport 443 -m set --match-set f2b-nginx_security src -j DNAT --to-destination 127.0.0.1:8443
              ipset --destroy f2b-nginx_security

actionban = ipset --test f2b-nginx_security <ip> ||  ipset --add f2b-nginx_security <ip>

actionunban = ipset --test f2b-nginx_security <ip> && ipset --del f2b-nginx_security <ip>

[Init]

и файлик nginx-iptables-ban.conf

Код: Выделить всё

# Fail2Ban configuration file

[Definition]

actionstart = ipset --create f2b-nginx_ban iphash
              iptables -I INPUT 1 -p tcp -m multiport --dports <port> -m set --match-set f2b-nginx_ban src -j REJECT --reject-with tcp-reset

actionstop = ipset --destroy f2b-nginx_ban
             iptables -D INPUT 1 -p tcp -m multiport --dports <port> -m set --match-set f2b-nginx_ban src -j REJECT --reject-with tcp-reset

actionflush = ipset --flush f2b-nginx_ban

actionban = ipset --test f2b-nginx_ban <ip> ||  ipset --add f2b-nginx_ban <ip>

actionunban = ipset --test f2b-nginx_ban <ip> && ipset --del f2b-nginx_ban <ip>

[Init]
Добавляем в /etc/fail2ban/jail.local определения фильтров:

Код: Выделить всё

[nginx_security]
banaction = nginx-iptables-nat
bantime   = 60
findtime  = 60
maxretry  = 5
logpath   = /var/log/nginx/security_log
enabled   = true

[nginx_security_ban]
banaction = nginx-iptables-ban
port      = 80,443
bantime   = 10
findtime  = 60
maxretry  = 7
filter    = nginx_security
logpath   = /var/log/nginx/security_log
enabled   = true
И (пере)запускаем fail2ban. В зависимости от существующей конфигурации itables возможно придется внести коррективы в них, например, у меня не запускался NAT до тех пор, пока я не добавил разрешения ходить с внешнего интерфейса на 127.0.0.1:8080(8443)


Включаем NAT - добавляем в /etc/sysctl.d/90-override.conf строчки:

Код: Выделить всё

net.ipv4.conf.eth0.route_localnet=1
net.ipv4.ip_forward=1
И применяем их:

Код: Выделить всё

sysctl -p
fail2ban парсит лог, куда nginx скидывает записи о запросах совпавших с регуляркой и если запрос с какого-либо IP совпал 5 раз за минуту (maxretry и findtime, соотв.), добавляет этот IP в соотв. таблицу ipset. Любое следующее соединение установленное с этого IP в течение минуты (bantime) будет переброшено NAT-ом на тот самый виртуалхост nginx, который мы создали выше, а он у нас умеет только отдать 403 и красивое "Ай-ай-ай!". В моем случае это очень критично, т.к. просто банить iptables-ами без обьяснения причин - не наш метод, поскольку. дел может наворотить и сам клиент хостинга, и в этом случае он должен знать за что поплатился не теребя техпод. В то же время, в конфигах хостов попадаются весьма тяжелые штуки, такие как проверка существования кучи файлов, и перенаправление соединений на другой порт позволяет не тратить ресурсы на обработку всей этой логики.

Это конечно хорошо, но как быть с уже установленным соединением, ведь браузеры умеют в рамках одного соединения напихивать кучу запросов? Для этого нам требуется вторая секция - nginx_security_ban, которая так же парсит тот же лог, но порог ее срабатывания поднят до 7 запросов в минуту. При превышения этого порога фаерволл в течении 10 секунд будет рвать соединения с IP зловреда - этого достаточно, чтобы разорвать существующие соединения, ранее прохошедшие через INPUT в mangle, последующие же соединения будут корректно завернуты в PREROUTING.


Файлики с регулярками, можно дорабатывать под себя. Это - то, что я нагрепал из логов.
php_functions.conf

Код: Выделить всё

~*(__halt_compiler|apache_child_terminate|base64_decode|convert_uudecode|include_once|require_once|invokeargs)\(     1;
~*(call_user_func|call_user_func_array|call_user_method|call_user_method_array)\(                                    1;
~*(file_get_contents|file_put_contents|fsockopen)\(                                                                  1;
~*(get_class_methods|get_class_vars|get_defined_constants|get_defined_functions|get_defined_vars|sys_get_temp_dir)\( 1;
~*(bzdecompress|gzdecode|gzinflate|gzuncompress|zlib_decode)\(                                                       1;
~*(exec|passthru|pcntl_exec|pcntl_fork|pfsockopen|shell_exec)\(                                                      1;
~*(posix_getcwd|posix_getpwuid|posix_getuid|posix_uname)\(                                                           1;
~*(ReflectionFunction|str_rot13)\(    
security_rules.conf

Код: Выделить всё

"~(sysdate|sleep|if|now|from|convert|cast|char|alert|eval|name_const|exec)\(.+"  1;
"~*\((select|insert|delete|drop)([%20|\+|\s|\t]+)"          1;
"~(x22XOR|\'XOR| OR |\+AND\+(%27|1|\')|\+NULL\+)"           1;
"~(waitfor|delay|nslookup|dblink_connect|cmd.exe)"          1;
"~(windows|winnt)?(\\|%2f|%5c|/)(boot|win)(\.|%2e)ini"      1;
"~FROM\(SELECT|CONCAT\("                                    1;
"~*information_schema\."                                    1;
"~*(LEFT|RIGHT|INNER|OUTER)([\+|%20|\s|\t]+)JOIN"           1;
"~*UNION([\+|%20|\s|\t]+)SELECT"                            1;
"~*UNION([\+|%20|\s|\t]+)ALL([\+|%20|\s|\t]+)SELECT"        1;
"~*UTL_INADDR.GET_HOST_ADDRESS"                             1;
"~*ASCII\(SUBSTRING"                                        1;
"~(GLOBALS|REQUEST)(=|\.|\[|\%[0-9A-Z]{0,2})"               1;
unix_filesystem.conf

Код: Выделить всё

~bin/(awk|base64|bash|cat|cc|clang|clang++|curl|csh|dash|diff|du|echo|env|fetch|file|find|ftp|gawk|gcc|grep|less|ls)         1;
~bin/(mknod|more|nc|ps|rbash|sh|sleep|su|tcsh|uname|head|hexdump|id|less|ln|mkfifo|more|nc|ncat|nice|nmap|perl)              1;
~bin/(php-cgi|printf|psed|php([\d]+)?|python([\d]+)?|ruby|sed|socat|tail|tee|telnet|top|uname|wget|who|whoami|xargs|xxd|yes) 1;
~dev/(fd/|null|stderr|stdin|stdout|tcp/|udp/|zero)                                                                           1;
~etc/(group|master.passwd|passwd|pwd.db|shadow|shells|spwd.db)                                                               1;
~/\.(bash|cshrc|aws/|config/|local/|pki/|my\.cnf)                                                                            1;
proc/self/                                                                                                                   1;
P.S. Пути и названия конфигов использованы относительно RHEL 7.