Zacznijmy od wyjaśnienia, jak przebiegają ataki na aplikacje i strony internetowe. Protokół HTTP opiera się na mechanizmie request-response, czyli użytkownik wysyła zapytanie do serwera, a serwer zwraca odpowiedź. Atakujący wykorzystują ten schemat, przygotowując i wysyłając zapytania mające na celu przeprowadzenie złośliwych działań na serwerze lub wprowadzenie złośliwego kodu do przeglądarek użytkowników. Przykładami takich ataków są m.in. Cross-Site Scripting (XSS), SQL Injection, RCE, czy LDAP Injection. Pojawia się więc pytanie: czy możemy wykrywać i blokować złośliwe wzorce w tych zapytaniach? Odpowiedź brzmi: tak. To właśnie zadanie Web Application Firewall (WAF).

Uwaga!
Należy pamiętać, że umieszczenie witryny za zaporą nie gwarantuje jej pełnej ochrony. Konieczne jest, aby sama aplikacja była dobrze zabezpieczona, niezależnie od zastosowania WAF. Zapora powinna stanowić jedynie dodatkową warstwę ochrony, zgodnie z zasadą „Defense in Depth” .
Defense in Depth

Zasada 'Defense in Depth’ polega na stosowaniu wielu warstw zabezpieczeń, co zapewnia ochronę nawet w przypadku przełamania jednej z nich.

Możemy wyróżnić 3 rodzaje zapór:

  • Aplikacyjny – działający jako usługa w systemie np. ModSecurity (który niedawno trafił pod skrzydła OWASPu),
  • Sprzętowy – fizyczne urządzenie np. rozwiązania firmy F5
  • W chmurze – usługa dostarczana przez firmy trzecie np. popularny Cloudflare

W dalszej części wpisu posłużymy się właśnie ModSecurity, aby zademonstrować działania zapory aplikacyjnej.

OWASP CRS

Jak wspomniałem wcześniej, WAF działa na podstawie silnika, którego zadaniem jest wykrywanie określonych wzorców zdefiniowanych w regułach. Oczywiście możemy tworzyć te reguły samodzielnie, ale trudno jest w ten sposób pokryć większość znanych ataków, a nawet jeśli, zajmie to ogromną ilość czasu. Z pomocą przychodzi organizacja OWASP, która rozwija projekt oferujący repozytorium gotowych reguł. Reguły te zostały stworzone głównie z myślą o ModSecurity, jednak są kompatybilne również z innymi rozwiązaniami. Na przykład platforma Cloudflare oferuje własną implementację tych reguł, nazwaną Cloudflare OWASP Core Ruleset.

Każdy poziom paranoi określa, jak restrykcyjne są reguły, które mają na celu blokowanie potencjalnych zagrożeń.

  • Poziom 1 – zalecany wszystkim serwerom dostępnym w Internecie
  • Poziom 2 – zalecany przez twórców wszystkim aplikacjom, które przetwarzają dane osobowe
  • Poziom 3 – zalecany przez twórców wszystkim aplikacjom, które przetwarzają dane wrażliwe, dane finansowe itp.
  • Poziom 4 – przewidziane do ochrony „kamieni koronnych”

Fałszywe alarmy

Dlaczego więc nie zawsze warto korzystać z najwyższego poziomu ochrony? Problemem są tzw. fałszywe alarmy (ang. false positives). Im bardziej restrykcyjne są reguły, tym większe ryzyko, że zostanie zablokowane prawidłowe zapytanie od użytkownika, co może zakłócić, a nawet uniemożliwić korzystanie z aplikacji.

W takiej sytuacji konieczne będzie dostosowanie reguł (tzw. tuning), co może zająć sporo czasu osobom odpowiedzialnym za administrowanie systemem. Jak zawsze, należy znaleźć balans między bezpieczeństwem, a użytecznością aplikacji i realizacją celów biznesowych.

Przykład działania ModSecurity

W tym przykładzie zajmiemy się zabezpieczeniem witryny opartej na WordPressie, ponieważ ten przykład pomoże nam zaprezentować pełnie możliwości i wyzwań związanych z wykorzystaniem zapory aplikacyjnej. Do implementacji wykorzystam Dockera, a poniżej przedstawiam schemat konfiguracji:

Użyjemy oficjalnego obrazu ModSecurity CRS dostarczanego przez OWASP, dostępnego w serwisie DockerHub (DockerHub, GitHub – dokumentacja) w wersji nginx.

W konfiguracji proxy musiałem wprowadzić pewne zmiany w jego działaniu, stąd dodane zostały wolumeny z plikami konfiguracyjnymi proxy.conf oraz proxy-include.conf. Oprócz tego ustawiamy odpowiednio dostosowane zmienne środowiskowe i montujemy dodatkowe wolumeny, które zawierają logi oraz customowe wtyczki. Poniżej przedstawiam plik docker-compose, który może być znacznie uproszczony, jeśli skorzystamy z domyślnych ustawień przygotowanych przez twórców. Wiele z tych ustawień można zmodyfikować za pomocą zmiennych środowiskowych. (BACKEND, SSL_CERT, SSL_CERT_KEY, PROXY_SSL_CERT, PROXY_SSL_CERT_KEY, MODSEC_RULE_ENGINE etc.)

version: '3.5'

services:
  nginx:
    container_name: reverse_proxy
    image: owasp/modsecurity-crs:nginx
    ports:
      - 80:80
      - 443:443
    environment:
      - PROXY=1
      - ERRORLOG=/var/log/modsec_error.log
      - MODSEC_AUDIT_ENGINE=on
      - MODSEC_AUDIT_LOG=/var/log/modsec_audit.log
      - LOGLEVEL=warn
      - PARANOIA=1
    volumes:
      - /docker/nginx-proxy/proxy.conf:/etc/nginx/conf.d/proxy.conf:ro
      - /docker/nginx-proxy/proxy-include.conf:/etc/nginx/proxy-include.conf:ro
      - /docker/nginx-proxy/logs/modsec_audit.log:/var/log/modsec_audit.log
      - /docker/nginx-proxy/logs/modsec_error.log:/var/log/modsec_error.log
      - /docker/nginx-proxy/modsec/plugins/:/etc/modsecurity.d/owasp-crs/plugins/
      - [WOLUMENY Z CERTYFIKATAMI]
    networks:
      - reverse_proxy
    restart: always
networks:
  reverse_proxy:
    name: reverse_proxy
    external: false

Wchodzimy na naszego bloga i próbujemy wysłać przykładowe szkodliwe zapytanie:

example.com/?test=<script>alert(1)</script> 
(example.com/?test=%3Cscript%3Ealert(1)%3C/script%3E). 

Naszym oczom ukazuje się komunikat o błędzie 403, wygenerowany przez ModSecurity.

A w naszych logach możemy potwierdzić informację, że zapytanie zostało zablokowane.

2024/09/19 16:23:39 [error] 97#97: *1 [client cenzura] ModSecurity: Access denied with code 403 (phase 2). Matched "Operator `Ge' with parameter `5' against variable `TX:BLOCKING_INBOUND_ANOMALY_SCORE' (Value: `20' ) [file "/etc/modsecurity.d/owasp-crs/rules/REQUEST-949-BLOCKING-EVALUATION.conf"] [line "222"] [id "949110"] [rev ""] [msg "Inbound Anomaly Score Exceeded (Total Score: 20)"] [data ""] [severity "0"] [ver "OWASP_CRS/4.6.0"] [maturity "0"] [accuracy "0"] [tag "modsecurity"] [tag "anomaly-evaluation"] [tag "OWASP_CRS"] [hostname "cenzura"] [uri "/"] [unique_id "1724324382.5643242347"] [ref ""], client: cenzura, server: example.com, request: "ġet /?test=%3Cscript%3Ealert(1)%3C/script%3E HTTP/1.1", host: "example.com"

Wtyczki

Zabezpieczyliśmy swoją witrynę. W bliskiej przyszłości próbujemy dodać nowy wpis na naszego bloga. Niestety zaskakuje nas brak możliwości jego późniejszego zapisania.

Treść naszego zapisanego wpisu zawiera tagi HTML, które system uznaje za szkodliwe zgodnie z określonymi zasadami. W związku z tym, powinniśmy napisać regułę, która wyłączy ten wyjątek. Na szczęście, dla niektórych systemów (takich jak WordPress, NextCloud, phpMyAdmin, dokuWiki itp.) społeczność opracowała już reguły z wyjątkami, które możemy zainstalować jako wtyczki. Możliwości wtyczek są znacznie szersze – mogą one zapewnić ochronę przed atakami DDoS lub implementować mechanizmy sprawdzania sygnatur malware’u. Pełną listę dostępnych wtyczek znajdziesz tutaj.

Umieszczamy, więc 2 pliki z repozytorium w naszym wolumenie odpowiedzialnym za pluginy. Restartujemy nasz kontener. Możemy zauważyć, że teraz już ta funkcja oraz wiele innych dostępnych w czystym wordpressie powinny działać bezproblemowo.

Własny fine tuning

Postanawiamy dodać nową wtyczkę do naszej strony – WPCode, która umożliwia wstawianie własnych fragmentów kodu na witrynę. Możemy się jednak spodziewać, że takie zapytania również zostaną zablokowane (ze względu na charakter wtyczki – wstrzykiwanie kodu), nawet pomimo zainstalowania wtyczki z regułami dostosowanymi do WordPressa.

Stworzymy więc własne pliki, w których będziemy mogli edytować reguły:

wget https://raw.githubusercontent.com/coreruleset/coreruleset/refs/heads/main/rules/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf.example

wget https://raw.githubusercontent.com/coreruleset/coreruleset/refs/heads/main/rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf.example

mv REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf.example REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf

mv RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf.example RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf

I zamontujemy je w dockerze:

volumes:
[...]
- /docker/nginx-proxy/fine-tuning-rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf:/opt/owasp-crs/rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf:ro
- /docker/nginx-proxy/fine-tuning-rules/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf:/opt/owasp-crs/rules/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf:ro
[...]

Aby napisać regułę wykluczającą, musimy najpierw sprawdzić, która reguła zablokowała żądanie.

ID: 941160 [NoScript InjectionChecker] HTML injection 
ID: 941100 Libinjection - XSS Detection

Reguły zostały uruchomione, ponieważ próbowałem wstawić nowy tag zawierający zewnętrzny plik z kodem JavaScript. W przypadku innego kodu mogą zostać aktywowane inne reguły. Aby szybko zagwarantować poprawne działanie tej funkcji, wyłączymy na powyższej ścieżce wszystkie reguły z tagami attack-xss oraz attack-injection-php. Dodajemy poniższą regułę do pliku, który jest uruchamiany przed CRS.

# WPCode exclusion
SecRule REQUEST_URI "@beginsWith /wp-admin/admin.php?page=wpcode-snippet-manager" \
    "id:1000,\
    phase:1,\
    nolog,\
    pass,\
    ctl:ruleRemoveByTag=attack-xss,\
    ctl:ruleRemoveByTag=attack-injection-php"

Możemy zauważyć, że tym razem dodanie fragmentu kodu powiedzie się:

Jeśli chcesz głębiej zapoznać się z pisaniem reguł wykluczających dostępne jest na ten temat mnóstwo dodatkowych materiałów:

  • https://www.prosec-networks.com/en/blog/modsecurity-core-rule-sets-und-eigene-regeln/
  • https://coreruleset.org/docs/concepts/false_positives_tuning/
  • https://www.f5.com/company/events/webinars/modsecurity-and-nginx-tuning-the-owasp-core-rule-set

Wdrażanie ModSecurity na systemach produkcyjnych

Podczas wdrażania WAF na systemach produkcyjnych należy zachować ostrożność, aby nie blokować legalnych operacji użytkowników. Ważne jest przeprowadzenie testów w trybie detekcji, zanim włączymy pełne blokowanie, aby uniknąć przerw w działaniu aplikacji.

Tryb detekcji umożliwia monitorowanie ruchu sieciowego bez aktywnego blokowania podejrzanych zapytań. Dzięki temu można ocenić potencjalne zagrożenia bez ryzyka zakłócenia funkcjonowania aplikacji. Możemy to osiągnąć, edytując ustawienia w pliku modsecurity.conf (Tryb blokujący – On, Tryb detekcji – DetectionOnly):

SecRuleEngine DetectionOnly

Jeśli korzystamy z wersji dockerowej, możemy to osiągnąć również przez ustawienie zmiennej środowiskowej MODSEC_RULE_ENGINE.

ModSecurity umożliwia oddzielne ustawienie poziomu paranoi dla blokowania i detekcji. Dzięki temu można skonfigurować bardziej rygorystyczne reguły detekcji, jednocześnie minimalizując blokowanie ruchu, co może być przydatne podczas testowania podnoszenia poziomu paranoi.

Jeśli chcemy wdrożyć swoje własne nowe reguły blokujące możemy skorzystać z wtyczki Incubator, która umożliwi nam testowanie nowych reguł bez szkody dla użytkowników.

Powodzenia w eksperymentowaniu z ModSecurity!