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 реализован крайне оригинально. Сканер действует по следующему алгоритму:
- С помощью 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: */*
- Результат запроса записывается в выделяемой библиотекой буфер. Размер получаемых данных ограничен переменной CURLOPT_BUFFERSIZE, стандартное значение которой устанавливается в 16 килобайт.
- Затем производится следующий интересный хак: библиотека 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
- В случае, если после отправки HTTP-запроса сессия не была разорвана, libssh пытается аутентифицироваться с определенным паролем, получаемым из идущих в комплекте файлов.
- Первой парой логин-пароль в файле является крайне маловероятное сочетание hw230f8034t:17932yhf823. Если аутентификация проходит успешно с первой попытки, то функция возвращает значение -2 (FAILHIT) и перебор прекращается. Это стандартная практика, используемая для отсева ханипотов. Если нет - то брутфорс продолжается.
- Только в случае, если все проверки успешно пройдены и валидный пароль найден, хост считается взломанным и в консоль Нески выводится сообщение вида
[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-сессию и сымитировать корректную аутентификацию.
Сценарий атаки:
- Обнаружить сканирование Неской по HTTP-запросу на 22 порт.
- Отправить длинный баннер для его сохранения в возвращаемый libcurl буфер.
- Корректно ответить на соединение libssh
- Не дать зайти под первым паролем, чтобы не вызвать прекращение брутфорса по FAILHIT
- Разрешить вход под любым другим паролем
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-эксплойт просто не на чем тестировать, а заставить его работать на всех возможных сборках для различных версий компиляторов и библиотек практически невозможно.
Вторая уязвимость очень тесно связанна с первой, хотя и является гораздо более высокоуровневой и нацеленной на браузер пользователя.
Помимо переполнения, разработчики 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 так, что работать всё будет чем в оригинале, не говоря уж об экономии нервов и времени программистов.
Таким образом, Неска - хороший сканер для юзеров, но очень неприятный для тех, кто захочет оживить его код и привести его в нормальное состояние. Её код уже история, наследие, от которого лучше побыстрее избавиться. Сообществу же нужно сделать что-то новое, более удобное, функциональное и способное к дальнейшему развитию.
Получится ли - покажет время.