Monday, February 27, 2012

Ochrona phpMyAdmin (i nie tylko) przez ips_outline.py

Ostatnio w logach serwera apache zauważyłem częste próby odgadnięcia ścieżki, pod którą znajduje się phpMyAdmin. Postanowiłem przyjrzeć się problemowi i skonfigurować swój prosty system IPS do reagowania na takie incydenty.

Podczas analizy tego typu zdarzeń zauważyłem faktyczną potrzebę rozszerzenia możliwości mojego skryptu o obsługę wyrażeń regularnych, a dokładnie chodzi o współpracę z AWK.

Tak mogą wyglądać przykładowe logi świadczące o ataku:

60.174.109.133 - - [25/Feb/2012:01:32:41 +0100] "GET //phpmyadmin/ HTTP/1.1" 404 182 "-" "Made by ZmEu @ WhiteHat Team - www.whitehat.ro" 
60.174.109.133 - - [25/Feb/2012:01:32:41 +0100] "GET //phpMyAdmin/ HTTP/1.1" 404 183 "-" "Made by ZmEu @ WhiteHat Team - www.whitehat.ro" 
60.174.109.133 - - [25/Feb/2012:01:32:42 +0100] "GET //myadmin/ HTTP/1.1" 404 180 "-" "Made by ZmEu @ WhiteHat Team - www.whitehat.ro" 
60.174.109.133 - - [25/Feb/2012:01:32:43 +0100] "GET //MyAdmin/ HTTP/1.1" 404 181 "-" "Made by ZmEu @ WhiteHat Team - www.whitehat.ro" 
60.174.109.133 - - [25/Feb/2012:01:32:44 +0100] "GET //admin/ HTTP/1.1" 404 178 "-" "Made by ZmEu @ WhiteHat Team - www.whitehat.ro" 
60.174.109.133 - - [25/Feb/2012:01:32:44 +0100] "GET //Admin/ HTTP/1.1" 404 179 "-" "Made by ZmEu @ WhiteHat Team - www.whitehat.ro" 
60.174.109.133 - - [25/Feb/2012:01:32:46 +0100] "GET //PMA/ HTTP/1.1" 404 178 "-" "Made by ZmEu @ WhiteHat Team - www.whitehat.ro" 
60.174.109.133 - - [25/Feb/2012:01:32:46 +0100] "GET //PMA/ HTTP/1.1" 404 178 "-" "Made by ZmEu @ WhiteHat Team - www.whitehat.ro" 
60.174.109.133 - - [25/Feb/2012:01:32:46 +0100] "GET //phpadmin/ HTTP/1.1" 404 181 "-" "Made by ZmEu @ WhiteHat Team - www.whitehat.ro" 
60.174.109.133 - - [25/Feb/2012:01:32:47 +0100] "GET //mysqladmin/ HTTP/1.1" 404 183 "-" "Made by ZmEu @ WhiteHat Team - www.whitehat.ro" 
60.174.109.133 - - [25/Feb/2012:01:32:48 +0100] "GET //mysql/ HTTP/1.1" 404 179 "-" "Made by ZmEu @ WhiteHat Team - www.whitehat.ro" 
60.174.109.133 - - [25/Feb/2012:01:32:48 +0100] "GET //sqladmin/ HTTP/1.1" 404 181 "-" "Made by ZmEu @ WhiteHat Team - www.whitehat.ro" 
60.174.109.133 - - [25/Feb/2012:01:32:49 +0100] "GET //sql/ HTTP/1.1" 404 177 "-" "Made by ZmEu @ WhiteHat Team - www.whitehat.ro" 
60.174.109.133 - - [25/Feb/2012:01:32:50 +0100] "GET //webadmin/ HTTP/1.1" 404 181 "-" "Made by ZmEu @ WhiteHat Team - www.whitehat.ro" 
60.174.109.133 - - [25/Feb/2012:01:32:50 +0100] "GET //db/ HTTP/1.1" 404 176 "-" "Made by ZmEu @ WhiteHat Team - www.whitehat.ro" 
60.174.109.133 - - [25/Feb/2012:01:32:51 +0100] "GET //dbadmin/ HTTP/1.1" 404 180 "-" "Made by ZmEu @ WhiteHat Team - www.whitehat.ro" 
60.174.109.133 - - [25/Feb/2012:01:32:52 +0100] "GET //mysqldb/ HTTP/1.1" 404 180 "-" "Made by ZmEu @ WhiteHat Team - www.whitehat.ro" 
60.174.109.133 - - [25/Feb/2012:01:32:53 +0100] "GET //webdb/ HTTP/1.1" 404 178 "-" "Made by ZmEu @ WhiteHat Team - www.whitehat.ro" 
60.174.109.133 - - [25/Feb/2012:01:32:53 +0100] "GET //sqlmanager/ HTTP/1.1" 404 183 "-" "Made by ZmEu @ WhiteHat Team - www.whitehat.ro" 
60.174.109.133 - - [25/Feb/2012:01:32:54 +0100] "GET //mysqlmanager/ HTTP/1.1" 404 184 "-" "Made by ZmEu @ WhiteHat Team - www.whitehat.ro" 
60.174.109.133 - - [25/Feb/2012:01:32:55 +0100] "GET //phpmanager/ HTTP/1.1" 404 182 "-" "Made by ZmEu @ WhiteHat Team - www.whitehat.ro" 
60.174.109.133 - - [25/Feb/2012:01:32:55 +0100] "GET //pmadb/ HTTP/1.1" 404 178 "-" "Made by ZmEu @ WhiteHat Team - www.whitehat.ro" 
60.174.109.133 - - [25/Feb/2012:01:32:56 +0100] "GET //admin/phpmyadmin/ HTTP/1.1" 404 184 "-" "Made by ZmEu @ WhiteHat Team - www.whitehat.ro" 
60.174.109.133 - - [25/Feb/2012:01:32:57 +0100] "GET //admin/phpMyAdmin/ HTTP/1.1" 404 186 "-" "Made by ZmEu @ WhiteHat Team - www.whitehat.ro" 
60.174.109.133 - - [25/Feb/2012:01:32:57 +0100] "GET //admin/mysql/ HTTP/1.1" 404 183 "-" "Made by ZmEu @ WhiteHat Team - www.whitehat.ro" 
60.174.109.133 - - [25/Feb/2012:01:32:58 +0100] "GET //admin/pma/ HTTP/1.1" 404 182 "-" "Made by ZmEu @ WhiteHat Team - www.whitehat.ro" 
60.174.109.133 - - [25/Feb/2012:01:32:59 +0100] "GET //mysq/phpmyadmin/ HTTP/1.1" 404 186 "-" "Made by ZmEu @ WhiteHat Team - www.whitehat.ro" 
60.174.109.133 - - [25/Feb/2012:01:32:59 +0100] "GET //phpMyAdmin-3.3.10.2/ HTTP/1.1" 404 190 "-" "Made by ZmEu @ WhiteHat Team - www.whitehat.ro" 
60.174.109.133 - - [25/Feb/2012:01:32:59 +0100] "GET //phpMyAdmin-3.3.10.2/ HTTP/1.1" 404 190 "-" "Made by ZmEu @ WhiteHat Team - www.whitehat.ro" 
60.174.109.133 - - [25/Feb/2012:01:33:00 +0100] "GET //phpMyAdmin-3.4.2.1/ HTTP/1.1" 404 190 "-" "Made by ZmEu @ WhiteHat Team - www.whitehat.ro" 
60.174.109.133 - - [25/Feb/2012:01:33:00 +0100] "GET //phpMyAdmin-3.4.2.1/ HTTP/1.1" 404 190 "-" "Made by ZmEu @ WhiteHat Team - www.whitehat.ro" 
60.174.109.133 - - [25/Feb/2012:01:33:02 +0100] "GET //phpmyadmin2/ HTTP/1.1" 404 183 "-" "Made by ZmEu @ WhiteHat Team - www.whitehat.ro" 
60.174.109.133 - - [25/Feb/2012:01:33:03 +0100] "GET //PHPMYADMIN/ HTTP/1.1" 404 183 "-" "Made by ZmEu @ WhiteHat Team - www.whitehat.ro" 
60.174.109.133 - - [25/Feb/2012:01:33:03 +0100] "GET //phpMyAdmin-2.8.1/ HTTP/1.1" 404 190 "-" "Made by ZmEu @ WhiteHat Team - www.whitehat.ro" 
60.174.109.133 - - [25/Feb/2012:01:33:03 +0100] "GET //phpMyAdmin-2.8.1/ HTTP/1.1" 404 190 "-" "Made by ZmEu @ WhiteHat Team - www.whitehat.ro" 
60.174.109.133 - - [25/Feb/2012:01:33:04 +0100] "GET //phpMyAdmin-2.8.0.2/ HTTP/1.1" 404 191 "-" "Made by ZmEu @ WhiteHat Team - www.whitehat.ro" 
60.174.109.133 - - [25/Feb/2012:01:33:04 +0100] "GET //phpMyAdmin-2.8.0.2/ HTTP/1.1" 404 191 "-" "Made by ZmEu @ WhiteHat Team - www.whitehat.ro"

Problem z opisaniem takich logów do postaci zaimplementowanej w skrypcie ips_outline.py[1][2] polega na tym, że można tam podać maksymalnie trzy podciągi identyfikujące wpis do logu. W powyższych przykładach cechą wspólną jest zwracany kod serwera HTTP "404" oraz wystąpienia ciągów "php", "admin", "my" w adresie. Sprawa komplikuje się o tyle, że mogą te ciągi występować zarówno w małych, jak i dużych znakach np. "Admin" i "admin", a nawet "ADMIN". Poza tym wcale nie muszą występować razem np. "phpadmin", "MyAdmin", "Admin". Ciąg "404" występuje zawsze ale równie dobrze morze być częścią nazwy pliku, np. zdjęć z aparatu "DSC 04041". Trzeba więc szukać go w określonej kolumnie, a obecnie ips_outline.py nie potrafi wyszukiwać tak selektywnie. Skrypt można pobrać ze strony projektu[3].

Myśląc o selektywnym wyszukiwaniu danych w logach od razu na myśl przychodzi mi najlepsze narzędzie do tego stworzone, czyli AWK. W przyszłości mam zamiar zmodyfikować mój skrypt, tak aby potrafił współpracować z tym językiem, dlaczego? Najlepszą odpowiedzią na te pytanie będzie opis jak poradziłem sobie z tymi incydentami oraz jakie są minusy takiego rozwiązania.

Pierwszą rzeczą jaką musimy zrobić to napisanie kodu AWK, który ujednolici przedstawione logi do takiej postaci, aby ips_outline.py nie miał z nim problemów. Wystarczy nam jeden ciąg identyfikujący, adres IP do zablokowania i wyszukiwana ścieżka. W systemie Debian używany jest MAWK[4] ale nic nie stoki na przeszkodzie, aby czerpać z dokumentacji GAWK[5] zwłaszcza jeżeli przedstawione przykłady są dla Ciebie niezrozumiałe.

Zanim jednak napiszemy taki kod prosty przykład. Jeżeli mamy frazę "my"
echo my | awk '{if($0 ~ /my/) print "ok"}'
kod zadziała, ale dla "My", "MY" już nie, dlatego:
echo wmYs| awk '{if($0 ~ /[Mm][Yy]/) print "ok"}'
teraz jest ok.

Mój kod AWK, który wybiera złe adresy wygląda następująco:
awk '{if ( ($9 == "404") && (($7 ~ /[Aa][Dd][Mm][Ii][Nn]/)||($7 ~ /[Mm][Yy]/)||($7 ~ /[^.][Pp][Hh][Pp]/)) ) print "block "$1" "$7 }' /var/log/apache2/access.log
Wyniki (o ile istnieją w access.log) będą miały postać:
block 207.171.3.132 //admin/index.php

Sprawdzenie rekordu źródłowego:
grep "//admin/index.php" /var/log/apache2/access.log
207.171.3.132 - - [27/Feb/2012:01:03:39 +0100] "GET //admin/index.php HTTP/1.1" 404 185 "-" "-"
Twoje logi mogą mieć inny format!

Kod AWK wybiera linie/rekordy, które:
- w kolumnie 9 obowiązkowo mają wartość "404" (podciąg również będzie dopasowany!)
- w kolumnie 7 wystąpi podciąg "admin" lub "my" lub "php", niezależnie od wielkości znaków
- podciąg "php" nie może być poprzedzony kropką (wyłączenie błędów 404 z np. "index.php")
następnie drukuje "block", IP i ścieżkę.

Teraz sprawdzony kod AWK możemy wrzucić w skrypt i korzystając z możliwości pisania do /dev/ips wysyłać tam jego wyjście. Proponuje w katalogu domowym /root założyć skrypt webcheck.sh:
touch webcheck.sh
ls -l webcheck.sh
-rw------- 1 root root 0 02-25 17:57 webcheck.sh
chmod u+x webcheck.sh
vim webcheck.sh


Zawartość skryptu wyglądałaby następująco:
#!/bin/sh
awk '{if ( ($9 == "404") && (($7 ~ /[Aa][Dd][Mm][Ii][Nn]/)||($7 ~ /[Mm][Yy]/)||($7 ~ /[^.][Pp][Hh][Pp]/)) ) print "block "$1" "$7 }' /var/log/apache2/access.log >> /dev/ips


Ostatecznie dodajemy wpis do crona:
crontab -e

 * *   *   *   *    /root/webcheck.sh > /dev/null

Skrypt będzie uruchamiany co minutę i będzie wysyłał wyprodukowane wpisy przez AWK do /dev/ips, skąd pobierze je i przeanalizuje ips_outline.py. Czas więc powiedzieć dla ips_outline.py czego ma szukać. Do rule.ips dodajemy regułę:
web phpmyadmin;;block,NULL,NULL;;block, , ;;iptables -I INPUT 3 -s param -j DROP;1;2;00:01:00

Składnię reguł opisywałem w ostatnim wpisie o ips_outline.py.
Reguła:
- nazywa się "web phpmyadmin"
- szuka rekordów zawierających ciąg "block"
- pobiera z niej adres IP znajdujący się po ciągu "block" i między znakami spacji: " "[IP]" "
- wstawia regułę blokującą z użyciem adresu IP w miejscu "param" ("1" po regule)
- uaktywnia wstawienie reguły przy drugim wystąpieniu pasującego rekordu
- ustawia czas, w którym ma wystąpić (w tym przypadku dwukrotne) dopasowanie na jedną minutę.
Jeżeli chodzi o czas jednej minuty to jego znaczenie nie ma większego wpływu w tym przypadku, chyba że chcemy "łapać" logi między dwoma wywołaniami skryptu webcheck.sh. Wtedy należy ustawić go na 2 minuty. W przeciwnym razie ips_outline.py i tak otrzyma wszystkie logi w pierwszej sekundzie minuty, a kolejne w pierwszej sekundzie kolejnej minuty.

Pozostało już tylko zresetować skrypt:
echo stop >> /dev/ips
/root/ips_outline.py > /dev/null &


Skrypt działa poprawnie i zaczął blokować ale przyjrzyjmy się dokładnie jak wygląda blokowanie.
Dziś w nocy ok. godz. 1 miał miejsce atak, skrypt zablokował go i wysłał do mnie e-maila o treść:

IPS 2012.2.27 1:4:1 web phpmyadmin: iptables -I INPUT 3 -s 207.171.3.132 -j DROP n=2 ip=207.171.3.132 [timeline: 0:0:0 in time 00:01:00]
Jak widać timeline to 0:0:0, czyli wszystko dzieje się w jednej sekundzie z jego punktu widzenia. Dzieje się tak ponieważ dostaje logi raz na minutę. Właśnie, co minutę a co dzieje się podczas tej minuty?. Zobaczmy.


Jak widać wszystkie próby miały miejsce w ciągu niespełna minuty, więc skrypt jeszcze nie mógł zareagować bo czekał na logi od AWK uruchamianego co minutę.


Ostatni atak przed zablokowaniem miał miejsce o 01:03:53, ale do tej poty bot zdążył wykonać 78 prób. Następnie sprawdzam ile razy próbował po zablokowaniu, hy zabawne również 78. Można więc powiedzieć, że ta metoda działa ale w tym wypadku zapobiegła połowie prób ataku. 
Oczywiście i tak to jest bardzo dobry wynik, ponieważ załóżmy, że mechanizm ten zablokowałby tylko ostatnią próbę, albo nawet wcześniej bot odgadłby ścieżkę do phpMyAdmin, to ile zdążyłby zrobić w ciągu niespełna pozostałej minuty? Chyba niewiele.

Mimo wszystko ten przypadek zainspirował mnie do rozbudowania ips_outline.py o możliwość wyszukiwania ciągów za pomocą AWK, tak aby mógł blokować od razu nawet takie przypadki.

Oh, oczywiście, nie wiem czemu wspominam o tym na sam koniec ;) ale najlepszą ochronę dla phpMyAdmin jest używanie reguł Order Deny,Allow w konfiguracji apache, co zresztą praktykuje.