Skip to content

The Good, the Bad and the Ugly: результаты частичного аудита кода Nesca

Notifications You must be signed in to change notification settings

enemy-submarine/nesca_audit

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

The Good, the Bad and the Ugly: результаты частичного аудита кода Nesca

Nesca

Nesca - это легендарный сканер-брутфорсер, разработанный нетсталкерами для нетсталкеров.

Долгое время получить его копию было очень сложно, поэтому вокруг проекта возник налет таинственности и элитарности.

Группа создателей этого проекта, ISKOPASI, давно распалась и сама Nesca практически никем не поддерживается

Несмотря на всё это, утилита остаётся довольно популярной: ссылка на репозиторий https://github.com/pantyusha/nesca постоянно мелькает в чатах Точки, появляются слухи о различных приватных версиях.

В этой статье я проведу частичный анализ кода из репозитория, расскажу о выявленных мной уязвимостях и общих мыслях о состоянии проекта.

Костыли и быдлокод

Nesca написана на смеси C\C++, с применением средств Qt, а также библиотек libssh и libssl.

При первом же взгляде на проект становится очевидно, что его разработка велась крайне непрофессионально.

Авторы довольно посредственно знают как C, так и C++, но при этом постоянно смешивают их средства или неадекватно их используют.

Сильнее всего это проявляется на работе со строками:

// BasicAuth.cpp:68

int ipLength = (int)strstr(ipOrig + 8, "/");
if (0 != ipLength) {
        strncpy(ip, ipOrig, ipLength); // гарантированное падение
        strncat(ip, location.c_str(), size - ipLength);
}

Функция strstr здесь используется для определения длины строки. Это абсолютно неправильно, так как по своему определению она возвращает не позицию паттерна в строке, а адрес его начала. Проще говоря, если в строке ipOrig действительно окажется символ /, то strncpy попытается записать в строку ip несколько миллиардов байт.

Ещё один яркий паттерн быдлокода - большое количество буферов фиксированного размера, запись в которые ведется с помощью небезопасной функции strcpy. Если размер записываемой строки превысит размер буфера, Nesca в лучшем случае упадет, в худшем - выполнит поданный злоумышленником код.

// SSHAuth.cpp:8

char hostStr[128] = {0};
strcpy(hostStr, user);
strcat(hostStr, "@");
strcat(hostStr, host);

Общее качество кода разочаровывает: откровенно костыльные решения, практически полное отсутствие осмысленных комментариев, множество непонятных констант. Даже простые задачи, решаемые парой строчек на питоне, здесь разворачиваются в огромные простыни кода, будто взятые из бэдтрипа упоровшегося героином разработчика.

// MainStarter.cpp:54

if (curIP[0] != '#' && curIP[0] != ' ' && curIP[0] != '\n' && curIP[0] != '\r' && strcmp(curIP, "") != 0 &&                
    ((curIP[0] == '/' && curIP[1] == '/') == false) && ((curIP[0] == '\t' && curIP[1] == '\t' && curIP[2] == '\t' && (curIP[3] == 13 || curIP[3] == 10 || curIP[3] == '#')) == false)                                                             
&& (curIP[0] == '\t' && curIP[1] == '\t' && curIP[2] == '\t' && (curIP[3] == '/' && curIP[4] == '/')) == false
)
    ++importFileSize;  

Понять, что именно хотел сделать разработчик довольно сложно, добавить какую-то фичу - ещё сложнее.

// finder.cpp:123
// Меметическая угроза: вдумчивое чтение кода функции getCodePage
// может нанести серьёзный вред вашему психическому здоровью,

char *temp3 = _findFirst((char *)(ptr1 + 8), " \"'\n\r");
if (temp3 != NULL)
{
        int ln = (int)(temp3 - ptr1 - 8);
        if (ln > 16) return "WTF?";
                strncpy(cdpg, (char *)(ptr1 + 8), (ln > 32) ? 32 : ln);
        if (strstri(cdpg, "%s") != NULL) return "UTF-8";
                return cdpg;                                                                                                      }          

Несмотря на все эти проблемы, видно, что сканером действительно много пользовались и рандомсканы смогли немного сточить самые острые углы, поэтому Nesca все ещё юзабельна и вряд ли упадет от какого-нибудь кривого баннера. К сожалению, процесс исправления ошибок изначально плохо спроектированного сканера сделал его архитектуру крайне причудливой, полной костылей и очень запутанных решений. Есть и просто забавные моменты:

// WebformWorker.cpp:88

{
    <...>
    return parseResponse(ip, port, &buffer, formVal, login, pass);                                                   
    if(i == 0) ++i; // ЗОЧЕМ?
}

Рефакторинг такого кода - мучение, его значительно проще переписать с нуля. Большинство фич сканера просты в реализации, но общая нечитабельность кода, засоренного магическими числами и паттернами, сильно мешает прямому портированию Нески на тот же Python.

Изначально интересный и перспективный проект превратился в бомбу замедленного действия: поддерживать и расширять старое уже невозможно, а писать что-то новое никто не хочет - ведь Неска и так справляется, да?

Уязвимости

Плачевное состояние кодовой базы и пренебрежение общеизвестными принципами разработки не могло не привести к появлению разного рода проблем с безопасностью. Я не пытался найти все уязвимости или довести найденные до реального исполнения произвольного кода, но показательно, что анализ одного лишь механизма перебора паролей к SSH-серверам выявил две достаточно серьёзные проблемы.

Переполнение буфера в стеке

Как и многое другое, перебор паролей в Nesca реализован крайне оригинально. Сканер действует по следующему алгоритму:

  1. С помощью libcurl на 22 порт отправляется HTTP-запрос вида
GET / HTTP/1.1
Host: 127.0.0.1:1234
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:35.0) Gecko/20100101 Firefox/35.0
Accept: */*
  1. Результат запроса записывается в выделяемой библиотекой буфер. Размер получаемых данных ограничен переменной CURLOPT_BUFFERSIZE, стандартное значение которой устанавливается в 16 килобайт.
  2. Затем производится следующий интересный хак: библиотека libssh, используемая для перебора паролей, переиспользует уже открытое libcurl соединение.
// SSHAuth.cpp:34

socket_t sock = -1;
res = curl_easy_getinfo(curl, CURLINFO_LASTSOCKET, &sock); // сохранение дескриптора сокета
<...>
res = ssh_options_set(ssh_session, SSH_OPTIONS_FD, &sock); // передача дескриптора в сессию libssh
  1. В случае, если после отправки HTTP-запроса сессия не была разорвана, libssh пытается аутентифицироваться с определенным паролем, получаемым из идущих в комплекте файлов.
  2. Первой парой логин-пароль в файле является крайне маловероятное сочетание hw230f8034t:17932yhf823. Если аутентификация проходит успешно с первой попытки, то функция возвращает значение -2 (FAILHIT) и перебор прекращается. Это стандартная практика, используемая для отсева ханипотов. Если нет - то брутфорс продолжается.
  3. Только в случае, если все проверки успешно пройдены и валидный пароль найден, хост считается взломанным и в консоль Нески выводится сообщение вида [23:45:22] [SSH] root:root@127.0.0.1:22.

Перейдем к анализу уязвимого кода:

// finder.cpp:1534

void _saveSSH(const char *ip, int port, int size, const char *buffcpy)
{
        if(buffcpy != NULL)
    {
                char log[2048] = {0};
                char logEmit[2048] = {0};
                char goodStr[256] = {0};
                char banner[256] = {0}; // буфер фиксированной длины

                const char *ptr1 = strstr(buffcpy, "|+|");
                if(ptr1 != NULL)
                {
                        int gsz = ptr1 - buffcpy;
                        strncpy(goodStr, buffcpy, gsz);
                        if(strlen(ptr1 + 3) > 0) strcpy(banner, ptr1 + 3); // переполнение буфера при вызове strcpy

Проверка валидности сервиса с помощью libssh надежно помогала избегать выявления этой ошибки при сканировании интернета: сам протокол SSH запрещает использование длинных баннеров и все хосты с ними отбрасываются на этапе перебора.

Но ведь ничего не мешает достаточно подготовленному атакующему сначала ответить на запрос libcurl длинным буфером, а затем, в том же соединении, просто продолжить ssh-сессию и сымитировать корректную аутентификацию.

Сценарий атаки:

  1. Обнаружить сканирование Неской по HTTP-запросу на 22 порт.
  2. Отправить длинный баннер для его сохранения в возвращаемый libcurl буфер.
  3. Корректно ответить на соединение libssh
  4. Не дать зайти под первым паролем, чтобы не вызвать прекращение брутфорса по FAILHIT
  5. Разрешить вход под любым другим паролем

Proof-of-concept эксплойт для этой уязвимости был реализован на языке Python с помощью фреймворка Twisted.

Результат атаки:

==20585==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7f64bc08dee0 at pc 0x0000004a47a7 bp 0x7f64bc08cb70 sp 0x7f64bc08c320         
WRITE of size 16419 at 0x7f64bc08dee0 thread T14                                                                                                   
    #0 0x4a47a6 in strcpy (/home/user/tmp/nesca/nesca+0x4a47a6)                                                                                    
    #1 0x5ca0a6 in _saveSSH(char const*, int, int, char const*) /home/user/tmp/nesca/finder.cpp:1548:29                                            
    #2 0x5da42f in Lexems::filler(char*, char*, int, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >*, int, Lexems$
) /home/user/tmp/nesca/finder.cpp:3556:4                                                                                                           
    #3 0x5e8a6b in Connector::connectToPort(char*, int) /home/user/tmp/nesca/Connector.cpp:586:4                                                   
    #4 0x619f22 in _connect() /home/user/tmp/nesca/MainStarter.cpp:943:9                                                                           
    #5 0x5fd703 in void* std::_Bind_simple<void* (*())()>::_M_invoke<>(std::_Index_tuple<>) /usr/bin/../lib/gcc/x86_64-linux-gnu/6.3.0/../../../..$
include/c++/6.3.0/functional:1390:18                                                                                                               
    #6 0x5fd6b4 in std::_Bind_simple<void* (*())()>::operator()()/usr/bin/../lib/gcc/x86_64-linux-gnu/6.3.0/../../../../include/c++/6.3.0/function$
l:1380:16                           
    #7 0x5fd538 in std::thread::_State_impl<std::_Bind_simple<void* (*())()> >::_M_run() /usr/bin/../lib/gcc/x86_64-linux-gnu/6.3.0/../../../../in$
lude/c++/6.3.0/thread:197:13                                                                                  
    #8 0x7f64e618ae6e  (/usr/lib/x86_64-linux-gnu/libstdc++.so.6+0xb9e6e)
    #9 0x7f64e645a493 in start_thread (/lib/x86_64-linux-gnu/libpthread.so.0+0x7493)
    #10 0x7f64e54f3ace in clone (/lib/x86_64-linux-gnu/libc.so.6+0xe8ace)

Address 0x7f64bc08dee0 is located in stack of thread T14 at offset 4960 in frame
    #0 0x5c9cbf in _saveSSH(char const*, int, int, char const*) /home/user/tmp/nesca/finder.cpp:1537

Полный вывод: asan.txt

Возможные последствия атаки:

  • DoS - простое падение сканера.
  • Выполнение произвольного кода - маловероятно, но не исключено.

Современные компиляторы и операционные системы хорошо позаботились о нейтрализации подобных дыр. Неисполняемая куча и стек, рандомизация расположения адресного пространства и прочие защитные меры достаточно надежно предотвращают атаки, основанные исключительно на переполнении буфера.

Конечно, есть методы обхода всех этих защит, но они требуют применения нескольких уязвимостей в определенной последовательности, а в случае со сканером это очень непросто. Самое главное, что нет какой-либо используемой всеми бинарной сборки.

Это значит, что потенциальный RCE-эксплойт просто не на чем тестировать, а заставить его работать на всех возможных сборках для различных версий компиляторов и библиотек практически невозможно.

Stored XSS в ssh.html

Вторая уязвимость очень тесно связанна с первой, хотя и является гораздо более высокоуровневой и нацеленной на браузер пользователя.

Помимо переполнения, разработчики Nesca не учли, что в записываемый в HTML-лог баннер SSH можно встроить произвольный JavaScript-код. Лог ведется в формате HTML и по умолчанию сохраняется Nesca в рабочую директорию с названием вида results_17.01.2019_127.0.0.1-127.0.0.1.

<font color="#00a8ff"> root:root@127.0.0.1:22 </font>
<font color="#323232">; Banner:</font>
<font color="#9cff00"> SSH-2.0-Twisted<script>alert('Matrix has you, Neo!')</script>
SSH-2.0-Twisted
</font></div>

Сценарий атаки аналогичен предыдущей, меняется только полезная нагрузка эксплойта.

Результат атаки:

Возможные последствия атаки:

  • Упрощение эксплуатации переполнения буфера через организацию утечки памяти
  • Отправка содержимого лога атакующему
  • Деанонимизация (если proxy включен только для Nesca)
  • Атака на локальную сеть (отправка запросов на роутер)
  • Атака на сам браузер (если в нем есть какие-либо уязвимости)
  • Если файлы затем заливаются на сервер без фильтрации: кража cookie и RCE

Вывод

Вне зависимости от качества кода, Неска сыграла важную роль в формировании сообщества нетсталкеров. Общая кривизна архитектуры, прекращение поддержки и ошибки реализации не повлияли на главную особенность этого сканера: простоту в использовании.

Концепция однокнопочной утилиты, не важно как, но решающей множество типичных для нетсталкеров задач и при этом не требующей каких-либо специальных знаний или умения программировать оказалась очень жизнеспособной. Брутфорс ftp, ssh, камер и все это в графическом интерфейсе и без сложных настроек - в этом классе конкурентов у Nesca практически нет, а значит у пользователей просто нет выбора.

В то же время её пример очень показателен для разработчиков других утилит: распределенность, разнообразные фичи, ускорение брутфорса в N раз - все это очень мало кому нужно, пока нет удобного интерфейса под все популярные системы. Я считаю, что сообществу в первую очередь интересна юзабилити и кроссплатформенность, возможность удобно посмотреть на разнообразные находки от сканирования, а не какие-то технические детали.

Главная ошибка разработчиков Нески - неудачный выбор языка и среды. Не умея правильно строить архитектуру приложения на C++ и грамотно использовать имеющиеся в нем и его библиотеках средства, постоянно без повода скатываясь в код на C, они просто привели проект в тупик. Переизобретение велосипедов, огромные простыни для решения простейших задач, многократный копипаст своего же кода, все это сделало Nesca хтоническим чудовищем, живущим только за счет чьей-то сильной черной магии.

Разумная альтернатива - разработка аналога сканера с нуля на более простом и удобном для подобных вещей языке. Я считаю, что для этого лучше всего подойдет Python c PyQT. С одной стороны это поможет сделать что-то похожее на исходную Nesca, с другой - значительно улучшить поддерживаемость и расширяемость кода. Кроме того, значительное число кустарно слепленных в Nesca функций уже реализованы в различных библиотеках Python.

Конечно, многие считают, что код на C\C++ работает гораздо быстрее, но даже по одной реализации сканирования ssh через libcurl ясно, что скорость программы сильно зависит от её архитектуры. Я уверен, что то, что сделано в Nesca можно реализовать на Python так, что работать всё будет чем в оригинале, не говоря уж об экономии нервов и времени программистов.

Таким образом, Неска - хороший сканер для юзеров, но очень неприятный для тех, кто захочет оживить его код и привести его в нормальное состояние. Её код уже история, наследие, от которого лучше побыстрее избавиться. Сообществу же нужно сделать что-то новое, более удобное, функциональное и способное к дальнейшему развитию.

Получится ли - покажет время.

About

The Good, the Bad and the Ugly: результаты частичного аудита кода Nesca

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages