#42: MrCyberSec: что нужно знать про безопасность
Александр Пахомов и Мекан Байрыев (специалист по безопасности веб-приложений в Mad Devs, автор YouTube-канала MrCyberSec) проходят путь атакующего от IP-адреса до `root`: сканирование портов `nmap`, перебор директорий и API-эндпоинтов, SQL-инъекции, хранение паролей и хеширование, reverse shell и эскалацию привилегий через SUID-биты, capabilities и cron. По ходу — типовые кейсы (утечка данных через незащищённую ручку, побайтовое чтение `id_rsa` через хеш-оракул, бэкдор в цепочке поставок и история с `xz`), подборка ресурсов для обучения (Hack The Box, TryHackMe, CTF) и чек-лист гигиены для бэкенд-разработчика, сводящийся к принципу минимальных привилегий.
Главное
- Разведка начинается с `nmap`: по открытым портам и баннерам сервисов (через `netcat`) видны версии ПО (`OpenSSH`, `Redis`, `nginx`, `Spring Boot`), а по ним — известные уязвимости; порт-нокинг прячет порт до правильной последовательности «стуков».
- Перебор директорий и эндпоинтов (`GoBuster`, `ffuf`, `KiteRunner`) по словарям находит скрытые ручки по нестандартному ответу (`401`/`200` среди `404`); rate-limiting лишь замедляет атаку, но не спасает.
- Найденная документация OpenAPI/Swagger или dev/QA-поддомен (через фаззинг субдоменов) — лучший подарок атакующему: учебный кейс — ручка без авторизации отдавала имена, хеши паролей и другие клиентские данные (broken access control).
- SQL-инъекция через конкатенацию пользовательского ввода в запрос позволяет читать таблицы (`information_schema`), добавлять пользователей, а иногда выполнять код и читать файлы (`LOAD_EXTENSION` в SQLite, `LOAD_FILE` в MySQL); защита — prepared statements и валидация ввода.
- `MD5` для паролей брутфорсится мгновенно; нужен медленный алгоритм (`bcrypt`) с солью и пароли от 12–16 символов, но утёкшие базы «хеш → пароль» обнуляют даже стойкий пароль, если он туда попал.
- `RCE` реализуется через reverse shell: атакующий слушает `netcat` на открытом наружу порту (80/443), а на жертве перенаправляет `/bin/bash` в TCP-поток; без исходящего доступа команды гоняют по одной через саму уязвимость.
- Эскалация до `root` — это цепочка: мисконфигурация `sudo`, SUID-бит на кастомной утилите, capabilities, перехват Python-импорта из writable-директории в cron; `LinPEAS`/`WinPEAS` автоматически подсвечивают такие векторы.
- Базовая гигиена бэкендера сводится к принципу минимальных привилегий (least privilege): по умолчанию нет доступа ни к чему, права выдаются гранулярно; CI-боту нельзя давать `root`, а секреты и БД надо разграничивать между сервисами.
В выпуске
- Мекан Байрыев — Специалист по безопасности веб-приложений в Mad Devs, автор YouTube-канала MrCyberSec. YouTube ↗ LinkedIn ↗ hackerone.com ↗
Ссылки
Расшифровка
[00:00] Мекан: Не делайте так! Похакаем, похакаем, похакаем!
[00:08] Александр: Здарова! Меня зовут Саша Пахомов, и я инженер, который любит своё дело. Вы слушаете 42-й выпуск подкаста «Тысяча фичей». Сегодня говорим про кибербезопасность. Как-то раз YouTube подкинул мне замечательную рекомендацию: там парень взламывал веб-серверы и очень понятно объяснял, что он делает. Супер увлекательно! Я подумал, что было бы классно поговорить в подкасте с автором этого YouTube-канала. И вот сегодня у меня в гостях Мекан Байрыев. Мне, как разработчику серверов, было очень интересно погрузиться в процесс поиска уязвимостей в софте, который я пишу. Искренне надеюсь, что вам тоже будет интересно. Поехали!
[01:09] Александр: У меня в гостях Мекан Байрыев. Привет, Мекан!
[01:13] Мекан: Привет, Саша!
[01:13] Александр: Если хочешь, можешь представить себя, рассказать, чем занимаешься. Как ты здесь оказался?
[01:19] Мекан: Да, привет, Саша! Спасибо, что пригласил. Я занимаюсь безопасностью веб-приложений (web application security) в компании Mad Devs — мы строим отдел по кибербезопасности. Веду свой канал на YouTube, MrCyberSec: занимаюсь такой просветительской деятельностью, прохожу «коробки» на Hack The Box. А на подкасте оказался так: ты меня нашёл на YouTube и позвал. Я был сразу рад согласиться — и вот я здесь.
[01:50] Александр: Да-да-да, супер! Про YouTube-канал: все ссылочки, шоу-ноутсы обязательно будут в описании. Если вы слушаете подкаст на YouTube — значит, в описании под видео; если в подкастерском приложении типа Яндекс.Музыки — просто заходите в описание подкаста, ссылка на канал Мекана будет первой. Вообще это довольно уникальный контент. Давай я расшифрую для тех, кто, возможно, не видел. Мекан берёт машины с платформы Hack The Box — это, грубо говоря, такой LeetCode для хакеров (LeetCode, думаю, всем знаком). Там есть челленджи, которые надо проходить: челлендж начинается с того, что тебе дают IP-адрес машины где-то в интернете и говорят — в идеале получи root-права этой тачки. Больше тебе ничего не даётся. И дальше Мекан по шагам, один за одним, имея всего один IP-адрес, получает root-права к машине. Это очень интересно. Даже если вы не сильно разбираетесь в security — как, например, я, — вам всё равно будет интересно. Во-первых, сам процесс увлекательный. Во-вторых, он несёт очень много полезной, просветительской информации, как Мекан сказал, — потому что знать, как твой веб-сервер или твоё CLI-приложение могут взломать, всегда полезно. И как раз про это мы и хотим поговорить в сегодняшнем выпуске. Я предполагаю, что нас слушает большинство backend-разработчиков, которые интересуются базами данных, разрабатывают веб-сервисы, CLI-приложения, возможно, фронтенд. И знания про security им не помешают, потому что именно через эти точки, через эти интерфейсы хакеры получают доступ к машинам. Давай, наверное, Мекан, начнём с базы-базы и очень простых вещей: что можно сделать с обычным сайтом, имея только его веб-адрес. Вот у меня есть google.com. Я как человек, который что-то знает в безопасности, — возможно, я хакер, — как начинаю исследовать эту информацию? Через какие слои я прохожу шаг за шагом, чтобы получить доступ к машине, на которой сайт крутится?
[04:16] Мекан: Окей. Имея IP-адрес на руках, первое, с чего я начинаю, — сканирую его сканером nmap на предмет открытых портов. Порты сразу дадут понять, какие сервисы там запущены, есть ли какой-то веб-сервер. Если веб-сервер есть и на нём крутится веб-страничка или целый портал, я начинаю его исследовать: обращаю внимание на технологию, на язык программирования, исследую директории — может быть, там есть админка, директория с бэкапами, с какими-то аплоадами. В целом смотрю, как клиент взаимодействует с сервером: какие методы поддерживаются, какие заголовки и параметры отправляются, какой результат приходит в ответ.
[05:04] Александр: То есть, имея IP-адрес или доменное имя — что в принципе почти одно и то же, потому что, зная одно, ты можешь получить другое (про это наши слушатели знают), — вот утилита nmap: я запускаю её у себя на тачке, передаю IP-адрес и ряд флажков, которые её конфигурируют. И только на этом шаге информация, которую я получаю, — это открытые порты. Буквально за несколько минут я их увижу. Это первое. Что ещё, помимо портов, nmap на этом уровне может мне рассказать?
[05:50] Мекан: Помимо портов можно узнать, какие сервисы запущены на этих портах и какие у них версии. Ведь если мы подключимся к отдельно взятому порту, скажем, утилитой netcat, сервис на той стороне отдаст какую-то информацию — некий баннер. Если вы когда-нибудь пробовали подключаться, например, к FTP или к SSH-порту через netcat, то в ответ увидите баннер типа OpenSSH, версия такая-то. nmap всё это автоматизирует: он сам собирает эти баннеры и на их основании выводит информацию — открыт порт 22, на нём висит такой-то сервис, у него такая-то версия, вот метаданные. И так по каждому порту. Из этих данных мы сразу видим, что нас может заинтересовать. Если открыт 80-й порт, он покажет, что там, например, Apache, или nginx, или ещё какой-то веб-сервер. Или, скажем, открыт какой-нибудь Redis — мы увидим порт Redis, наименование, возможно, версию и ещё какую-то информацию, которая позволяет дальше простраивать свои действия.
[07:08] Александр: То есть, например, условно, если у меня запущен какой-нибудь Spring Boot Application на Java (я думаю, большинство разработчиков, которые меня слушают, всё-таки на Java — я и подкаст с Java начинал делать, ну и всё остальное нам тоже понятно), — вот я поднял Spring Boot, по дефолту на 8080-м порту, ничего не трогая, запустил на виртуальной машине с публичным IP. Уже сам этот факт будет известен снаружи через порт-сканер: Spring Boot себя выдаст.
[07:44] Мекан: Всё верно, да.
[07:44] Александр: Давай ещё подумаем. Лет пять назад рассказывали, что MongoDB, торчащая наружу на каком-то порту, легко открывается. То есть если я продолбался, или не сильно разбираюсь в сетях, и выставил наружу порт базы данных, версия которой довольно уязвима, — это тоже сразу будет видно через nmap. По сути, всё, что представлено сетевому интерфейсу на сервере, будет просканировано. Или есть всё-таки способы уже на этом уровне что-то не выставлять? А если выставлять, то как-то хитро — может, есть какой-то порт-нокинг? Вот я про это.
[08:27] Мекан: Порт-нокинг — есть такая технология, которая позволяет открыть определённый порт только после того, как ты «простучал» определённую последовательность портов. Если по-простому: прописываешь правило, что порт, скажем, 22 (то есть SSH) откроется, только если ты сначала подключишься к портам в правильном порядке — например, 11111, потом 11119, потом 11125. Только после этой последовательности откроется SSH. Часто так прячут SSH: по умолчанию порт закрыт, но если простучать три-четыре нужных порта, он открывается, и к нему можно подключиться.
[09:09] Александр: А эта простучка — когда я обращаюсь к портам — разве не выдаёт себя через метаинформацию? Я не могу через nmap узнать, что там стоит такая защита?
[09:19] Мекан: По идее, не можешь. Насколько я знаю, эти порты будут даже показаны как закрытые. Тебе действительно нужно знать, какие порты и в каком порядке простукивать, чтобы открыть соответствующий порт.
[09:31] Александр: Ну, я так думаю, там, наверное, через iptables что-то настраивается на сетевом уровне: порты по сути закрыты, но операционная система каким-то образом сообщает софту, что сюда было обращение. Такое чисто житейское понимание.
[09:49] Мекан: Да, можно, наверное, сделать это так, но, насколько я знаю, есть определённые тулы, возможно, даже предустановленные в Linux, которые позволяют это настраивать.
[10:01] Александр: Ну и, подытоживая: порт-нокинг может защитить от того, что, просканировав nmap-ом брутфорсом все возможные порты, ты не увидишь открытого порта. А ты, зная нужную последовательность, стучишься на три порта — и 22-й открывается. Соответственно, это один из способов защиты. А есть ещё какой-то способ обезопасить себя от nmap-сканинга?
[10:30] Мекан: Можно попытаться видоизменить сами баннеры. Если у вас запущены сервисы, которые по умолчанию выдают баннеры, их можно отредактировать так, чтобы они отдавали другую информацию.
[10:44] Александр: А баннер — это что? Вот у меня Spring Boot-приложение, я его поднял, настроил ручку API, слэш users, условно. Что такое баннер? Это дефолтный ответ сервиса на случай, если ты обратился по корню? Самый голый, дефолтный запрос — что отвечает сервер. Есть ли ответ, который может вообще ничего не отдавать, какой-то dummy, пустота, — или такой возможности нет?
[11:15] Мекан: Если это HTTP-сервер, он ответит какими-то статус-кодами и заголовками, и в этих заголовках может присутствовать информация, выдающая сервер. Например, часто встречается заголовок Server или X-Powered-By — по нему можно определить, что там на той стороне. Но в моей практике часто бывает и так: нахожу IP-адрес, порт, понимаю, что там веб-сервис, подключаюсь на дефолтную ручку, на слэш, — и в ответ получаю обычные заголовки, которые встречаются везде и ничего не выдают. Сходу не могу понять, что это за сервис, что за веб-сервер. Тогда только потом можно запустить дополнительные сканинги или пройтись по дополнительным методологиям, чтобы попытаться понять, что там, — и то далеко не всегда это выходит.
[12:15] Александр: Давай возьмём два сценария и пойдём по одному из них. Первый: ты просканил, и там какой-нибудь древний Laravel — PHP-фреймворк десятилетней давности — отправил в заголовках свою версию, ты её загуглил, увидел, что там есть уязвимость, и пошёл её эксплуатировать. Это совсем простой случай. А есть ли более стандартный кейс, когда веб-сервис о себе так явно не говорит — ни в заголовках, ни в пейлоаде? Нигде нет того, что это Spring Boot версии 2.1, уязвимый. Ты просто получаешь какой-то абстрактный ответ, и, наверное, это всё, что можно сделать на этапе nmap-сканинга. Дальше ты начинаешь идти вглубь и, как я понимаю, сканировать сам веб-сервис: понимаешь, что на этом порту есть веб-сервис, он принимает веб-запросы, — и начинаешь копать, какие запросы и по каким путям на него можно прислать. Для этого тоже существует ряд тулов, правильно?
[13:26] Мекан: Ряд тулов и методологий. Одна из базовых методологий — отправить POST-запрос на данный эндпоинт и посмотреть, что будет в ответ. Или OPTIONS-запрос, TRACE — то есть поиграть с методами запроса.
[13:43] Александр: Просто на корневой POST отправляешь, или ты уже знаешь какой-то путь?
[13:48] Мекан: На данном этапе — только на корень сайта, на корень сервиса. А потом можно запускать определённые тулинги, типа GoBuster или ffuf, которые помогают перебирать директории. Можно попытаться найти ручку, которая выдаст какую-то информацию — вернёт JSON или дополнительные заголовки.
[14:10] Александр: А как он это делает? Берёт, грубо говоря, брутфорс по всем возможным URL — понятно, что brute force какой-то оптимизированный, — проходится и шлёт GET, POST, PUT, DELETE на каждый путь. Так он составляет список URL, директорий, путей, на которые сервис отвечает иначе, чем на остальные. То есть пытается найти, какие ручки у сервиса есть, и это можно посмотреть автоматизированно.
[14:41] Мекан: Да, всё верно. Заранее готовим словарик — например, есть словарики для PHP-фреймворков, состоящие из имён файлов, которые часто встречаются в PHP-приложениях; или берём словарик для Java-приложений. В этих словариках могут быть тысячи, десятки тысяч записей, — просто прогоняем, подставляем и смотрим на ответ, где он окажется нестандартным. То есть если все возвращают 404 и одинаковую длину, а какой-то эндпоинт возвращает 401 или 200, — ты понимаешь: опа, здесь что-то есть. Если ручка специфическая, с каким-то специфическим названием, можно пойти в Google или в GitHub и поискать — часто это позволяет определить технологию. Сама ручка иногда говорит о технологии: например, только в этом фреймворке используется healthcheck, слэш что-нибудь.
[15:48] Александр: Либо это самописная вещь, и тогда она ничего о технологии не скажет. Но вероятнее всего это по дефолту: например, в Spring есть батарейки типа Spring Actuator — просто подключаешь, и он сразу предоставляет ряд healthchecks, бинами можно управлять. И сразу понятно, что за технология. А есть ли здесь best practices — как знающий инженер может защитить приложение от такого брутфорса?
[16:19] Мекан: Как защитить? Наверное, никак. На стороне защищающихся, на стороне бэкенда, можно ограничивать запросы: если их слишком много — сразу отрубаем клиента. По дефолту сканер или фаззер перебирает, скажем, 50 запросов в секунду. Но атакующий может слать и по одному запросу в секунду — это будет гораздо дольше, но мы всё равно найдём те директории и ручки, на которые сервер просто обязан отвечать, иначе никак.
[16:55] Александр: Окей. То есть либо мы усложняем жизнь брутфорсу, отстреливая запросы по rate-limit, — но, опять же, есть техники делать это с разных тачек, так что rate-limiting можно обойти, — либо это просто займёт сутки, а мы не спешим, если сканируем сайты в интернете. Вот к нам пришли, поняли, какие есть открытые URL, какими способами к ним обращаться — PUT, POST, все эти рестовые. Имея этот список — и допустим, всё ещё непонятно, что за технология, — что дальше можно предпринять?
[17:38] Мекан: Дальше у нас получился список, условно, из пяти ручек, из пяти директорий, и мы не понимаем, что это за технология, — какие-то кастомные штуки. Делаем запрос на каждую из этих ручек, причём не одним методом, а всеми, и смотрим, как ручка отвечает на разные методы. Если это 200-й код — что-то должно возвращаться. Смотрим, что возвращается, и на основании этого настраиваем в голове представление, как эта штука работает. Потому что наша цель на данной стадии — это всё-таки разведка, enumeration: мы простраиваем в голове, как приложение выглядит, как оно взаимодействует, — и оттуда уже вырастает дальнейшая атака. Переходя по каждой ручке, мы по кусочкам собираем представление о приложении. Если это Spring Boot, у него есть известные ручки, для него даже есть отдельные словари — те самые актуаторы, — и есть история определённых уязвимостей. У Spring в прошлом они появлялись довольно часто. Обнаружив, например, слэш docs, можно перейти в доки и что-то там увидеть. А если актуатор не защищён механизмом аутентификации, то это тоже лёгкий способ получить какой-то code execution.
[19:08] Александр: Тема кибербезопасности — это отдельный мир. В конце выпуска Мекан поделится огромным списком рекомендаций и ссылок. Но кое-что для вас есть и у меня — это подкаст «Смени пароль». В нём эксперты Лаборатории Касперского обсуждают насущные вопросы из мира цифровой безопасности и помогают разобраться во всех возможных киберугрозах для пользователей и для бизнеса. Недавно у подкаста стартовал четвёртый сезон. В первых выпусках ведущие вместе с гостями говорят про атаки хакеров через заражённые сайты, про воровство авторских прав, а ещё про работу директора. Ссылка будет в описании.
[21:16] Мекан: Не всегда, но да: если есть доступ к клиентскому JavaScript, его можно скачать, распарсить и попытаться достать оттуда список ручек, список запросов. Это один из способов.
[21:30] Александр: То есть, грубо говоря, если JavaScript на основе какой-нибудь OpenAPI-спеки сгенерировал себе клиент, то этот JavaScript, находясь в браузере, можно прочитать и увидеть эндпоинты.
[21:46] Мекан: Да-да-да.
[22:37] Александр: То есть чем глубже ручка запрятана в иерархии директорий, тем, условно, выше вероятность, что автоматизированный сканер её не найдёт.
[22:49] Мекан: Да. Ну, есть определённые сканеры, типа KiteRunner, которые специализируются как раз на поиске API-эндпоинтов — там это работает получше, он находит гораздо больше, чем обычный GoBuster. Опять же, не во всех случаях сработает. Поэтому нахождение Swagger-доки или в принципе документации по API — это лучшее, что может произойти для атакующего.
[23:12] Александр: Классно, это прям хороший кейс. То есть документацию OpenAPI на проде желательно запрятать.
[23:21] Мекан: Бывает, что продукт всегда находится в разработке, правильно? И вот есть production-окружение, где нет никаких доков, никаких source maps для JavaScript. Но в то же время где-то торчит QA- или dev-окружение, о котором разработчики думают, что о нём никто не знает, — а его можно найти фаззингом хостов, фаззингом субдоменов. Это будет что-то вроде qa.companywebsite.com.
[23:51] Александр: То есть, грубо говоря, я нашёл веб-сайт google.com, фажу его, — понятно, что продакшен там хорошо прикрыт. Но, используя ряд тулов, я могу профазить, например, qa.google.com — поддомен, на котором может быть та же версия пред-продакшена, просто открытая наружу, и там уже будут доки.
[24:13] Мекан: Ага.
[24:14] Александр: А сама плохая ситуация: вот Swagger UI прямо у нас торчит, статика раздаётся. Помимо информации, можем ли мы как-то через Swagger UI получить remote code execution — или такое редко случается? То есть через стороннюю статическую штуку, которую напрямую приложением ты не пишешь, но оттопырил, — можно ли получить себе проблемы?
[24:39] Мекан: В Swagger UI, насколько я помню, была уязвимость типа XSS, несколько лет назад. То есть можно было попытаться атаковать клиента — подсунуть ссылочку разработчику, у которого есть туда доступ; он открывает, у него срабатывает XSS, и уже из браузера разработчика, в контексте этого сайта, можно что-то придумывать, крутить свою атаку дальше. Но чтобы что-то более критичное, чем XSS, в Swagger UI было — не помню. Хотя от этого, конечно, никто не застрахован, это всегда может случиться.
[25:15] Александр: Отсутствие уязвимостей, про которые мы знаем, не значит, что их вообще нет, — просто мы ещё не узнали какую-то уязвимость, которая может обнаружиться в будущем.
[25:23] Мекан: Верно.
[25:24] Александр: Поэтому базовое правило, наверное, такое: если что-то не нужно оттопыривать — не надо это оттопыривать.
[25:33] Мекан: Абсолютно верно.
[25:33] Александр: Капитанский совет, да, но иногда, знаешь, ты всё подряд закрываешь, думаешь «да пофиг», а на самом деле нет. Поэтому практика такая хорошая. Ну, давай дальше. Мы хорошо идём: прошлись по портам, по ручкам. Вообразим себе стандартный сервис — есть регистрация, логин и что-то типа Twitter, показывает твиты других пользователей. Ты как человек, который проводит этот discovery research, собираешь всю возможную информацию: такие-то ручки, такие-то пейлоады, такие-то ответы. И в какой-то момент что-то тебя цепляет. Можешь привести пример: что чаще всего цепляет в подобных CRUD-сервисах, за что можно пойти дальше вглубь?
[26:23] Мекан: Могу поделиться кейсом с одного из недавних тестов. Там точно так же был OpenAPI JSON, где просто был виден весь список эндпоинтов. Что я делал? Я перебирал все эти ручки без аутентификации, без авторизационного токена. И все они возвращали 401, а одна ручка вернула 200 — и эта ручка выводила список всех клиентов. В JSON-объекте возвращались имена, хеши паролей и другие данные.
[26:58] Александр: То есть это просто ошибка разработчика? Он забыл навесить авторизацию.
[27:03] Мекан: Забыл или что там произошло — без понятия. Но факт: все ручки 401, одна 200, и она возвращает все клиентские данные. Причём OpenAPI JSON и сама ручка были доступны в QA-среде.
[27:35] Александр: И дальше ты с этой информацией что мог делать?
[27:36] Мекан: Это уже раскрытие информации, само по себе довольно серьёзная уязвимость. Можно сразу идти её репортить: включаешь в отчёт, что такая-то ручка на таком-то поддомене возвращает критичные пользовательские данные, — это надо закрывать.
[27:53] Александр: Что ещё часто встречается?
[27:54] Мекан: Вот такие broken access control — сломанный механизм распределения доступов. Если это не REST API, а, может быть, какой-то эндпоинт вроде upload.php. Раньше это довольно часто встречалось, сейчас реже, но тоже бывает. Ты такой upload.php… Это могут быть скрипты подключения к базе данных — типа phpMyAdmin или Adminer. Или скрипты, которые напрямую получают очевидные параметры и передают их в функции типа system, exec, eval, — и это тоже прямой способ получить remote code execution. Такой лёгкий способ, конечно, редко встречается нынче.
[28:52] Александр: Из простых получается, что у тебя просто есть способ загрузить какой-то код на сервер и там получить уязвимость, если вызывается системная командная строка. Но это уже очевидные вещи. А из менее очевидных — оно как бы очевидно, но, может, не всем: даже если у тебя всё прикрыто на уровне инфраструктуры, ничего старого нет, есть просто регистрация, логин и «покажи мои твиты», — здесь всё равно могут быть уязвимости, причём ведущие к печальным последствиям. Например, SQL-инъекция из самых банальных, да?
[29:33] Мекан: Да.
[29:34] Александр: То есть, я так думаю, следующий шаг, когда всё исследовано и карта построена, — ты пытаешься на разные эндпоинты сделать какие-то инъекции. Как это происходит дальше?
[29:45] Мекан: Я формирую список эндпоинтов, беру или генерирую словарик с именами параметров, и на каждый эндпоинт засылаю все эти параметры — сколько позволяет отправить в одном запросе RFC HTTP (точную цифру не помню). В каждом параметре отправляю какой-то payload — например, вызов функции sleep в SQL, sleep на 5 секунд. Отправляешь такой нагруженный запрос со всеми этими пейлоадами, и если ответ от сервера вдруг задерживается на 5 секунд — понимаешь: опа, здесь где-то есть SQL-инъекция. А потом методом исключения находишь тот самый параметр.
[30:30] Александр: А ты прямо в payload вставляешь, грубо говоря, поле user, у которого значение — просто «sleep 5 секунд»? Или там всё-таки какой-то специальный payload, подразумевающий инъекцию?
[30:41] Мекан: Если это SQL-инъекция, то нужно выйти из какого-то контекста в запросе. Скажем, если это строковый параметр, ты вставляешь кавычку, закрываешь контекст, дальше AND SLEEP(5), а потом SQL-овский комментарий, который отбросит остаток запроса.
[31:01] Александр: Давай простым языком проговорю — мало ли, нас слушают те, кто термин «SQL-инъекция» ни разу не слышал. Получается: если мы в контроллере — спринговом или где угодно — принимаем payload, грубо говоря, JSON-поле username как есть из формы, и, ничего с ним не делая, конкатенируем SQL-запрос типа SELECT * FROM users WHERE id = плюс этот payload, — то в этом payload не обязательно будет username apahomov. Там может быть хитрая строка, которая закрывает предыдущий запрос и стартует новый. И в этом новом может быть что угодно — в нашем примере, например, функция sleep. Таким образом мы, по сути, можем вызывать код в базе данных через формы или через REST API-эндпоинты, которые наш сервис предоставляет. Окей, что ты можешь сделать, поняв, что есть такая конкатенация? Кстати, сразу скажем: чтобы такого не было, нужно использовать prepared statements и препроцессить input на всякие символы. Но допустим, ты смог получить такую уязвимость — к чему она дальше может привести? Ну, sleep 5 секунд — и что?
[32:25] Мекан: Тут зависит от типа запроса. Если это SELECT — на выбор данных, — то можно определить, какие есть таблицы, какие у них колонки, через вспомогательные таблицы типа information_schema: SELECT table_name FROM information_schema.tables — и получаешь список таблиц в базе. Определив интересующую таблицу и поля, ты делаешь UNION-запрос: UNION SELECT username, password FROM users. Можно добавить WHERE, если интересует конкретный юзер, — и так получить из таблицы users юзернеймы и пароли (или хеши паролей). Если это INSERT-запрос, можно попытаться добавить своего пользователя в таблицу users и получить доступ к админке, если она есть. Если UPDATE — можно обновить какое-то поле. Всё зависит от контекста: если хочешь войти в админку, тебе нужны юзернейм и пароль. Через UPDATE ты можешь обновить пользователя, от которого у тебя есть email, но нет пароля: обновил хеш пароля — и теперь можешь под ним залогиниться.
[33:47] Александр: С пользователями интересно, потому что, думаю, многие слушатели подобные сервисы писали, и эта работа с паролями всегда такая — ты немножко на очке, когда пишешь этот код: блин, а вдруг я что-то не то сделал, и хакеры получат доступ. Самый банальный пример, самый лёгкий для взломщика: если в базе данных пароли лежат прямо в plain text — 1234, — то эта SQL-инъекция уже позволяет достать пароли и залогиниться под любым пользователем, в том числе под админом. Это очевидный косяк. Но вот хеши — давай разберёмся. Я захешировал. Какие есть, скажем так, дырявые, плохие способы хешировать пароли, а какие — прям state of the art, которые не брутфорсятся, никак не декодируются? Давай про хеши.
[34:43] Мекан: Сразу хочу сказать, что абсолютно всё, наверное, брутфорсится, просто что-то медленнее, что-то быстрее. Такой алгоритм хеширования, как MD5, — проще всего. Хранить пароли в MD5 в наше время, конечно, не стоит.
[34:57] Александр: Давай про MD5. Кстати, я знаю — не то чтобы многие, но точно есть такие, — кто хеширует. Допустим, у меня есть пароль из 12 цифр и символов: apahomov*@123секретсупер. И MD5-хеш лежит в базе данных. Насколько просто оригинальный пароль достать из этого хеша?
[35:17] Мекан: Если оригинальный пароль состоит из слов, которые можно предсказать, — а не из рандомного набора байтов, — то 12 символов это довольно мало. Для MD5 его можно сбрутить довольно быстро.
[35:34] Александр: То есть под «словами» мы что имеем в виду? Когда я сказал apahomov и что-то в конце кракозябры, — если у меня и юзернейм apahomov, то он как-то включится в словарь и сильно облегчит работу брутфорса, правильно?
[35:47] Мекан: Да, есть техника, называется permutations: берёшь какие-то знания о человеке — никнейм, номер телефона, ещё какие-то слова — и генерируешь огромный словарь: разные комбинации, где-то с большой буквы, где-то с маленькой, где-то спереди, где-то сзади. MD5 — очень быстрая операция; особенно если хеш есть локально и у тебя на машине несколько топовых видеокарт в риге стоят, это будет очень быстро.
[36:21] Александр: То есть хранить пароли в MD5-хеше — плохо, так делать не нужно. А как нужно?
[36:30] Мекан: Зависит от технологии. В PHP это, наверное, bcrypt. В Python для Django и Flask тоже есть свой хеш по умолчанию — на память точное название не вспомню, длинное такое, но если вы разработчик на Python, вы знаете. По умолчанию эти фреймворки предоставляют безопасный тип хеширования.
[36:52] Александр: А чем принципиально опасный тип хеширования отличается от безопасного? В чём различие? И тот хеш, и этот хеш.
[37:01] Мекан: В скорости хеширования. Безопасный тип — это когда, чтобы получить результат, нужно подождать, скажем, секунду. А есть хеш-функция, которая делает это за одну десятитысячную секунды — соответственно, брутфорс будет в 10 тысяч раз быстрее. И там, и там брутфорс, но MD5 брутфорсится сильно быстрее.
[37:31] Александр: Окей. А есть ли способы хеширования, которые ещё усложняют брутфорс? Некоторые добавляют соль и так далее — вносят дополнительные вещи, которые, как я понимаю, тоже усложняют брутфорс, но совершенно точно на 100% не исключают возможность подобрать хеш. Правильно?
[37:52] Мекан: Всё правильно. Соль усложняет процесс брутфорса. Если у тебя есть доступ и к хешу, и к соли — это, конечно, хорошо для атакующего, но всё равно усложняет атаку. А может быть так, что доступ к хешу есть, а к соли нет, — и тогда это гораздо более сложная операция, потому что соль, как правило, длинная и рандомная.
[38:15] Александр: В целом про пароли: bcrypt хотя бы работает долго, и подобрать будет сложнее, если пароль длинный. Я так думаю, длина пароля тоже влияет на скорость подбора.
[38:27] Мекан: Конечно. Чем длиннее пароль, тем его сложнее подобрать.
[38:32] Александр: Да-да-да. Надо инфорсить какие-то политики для пользователей, чтобы они вводили длинные и сложные пароли, а потом хешировать и хранить в базе уже хеши.
[38:43] Мекан: Да.
[38:43] Александр: А есть ли, скажем так, тресхолд, за которым брутфорсить нормальный алгоритм типа bcrypt уже очень сложно, а меньше которого это реально? Сколько символов должно быть?
[38:57] Мекан: Я думаю, если это 12–16 символов — цифры, буквы, спецсимволы, не имеющие никакого значения, — то сбрутить это за какое-то адекватное время нереально. Мне кажется, на перебор такого хеша уйдёт множество лет.
[39:16] Александр: Окей, пока квантовые компьютеры не захватили мир, это действительно долго. Помимо сложных паролей, которые сложно брутфорсить, есть же ещё базы, где паролям соответствуют хеши, правильно? То есть если один раз взломали твой супер-пупер пароль, который ты на мнемониках запомнил и везде используешь, — его потом можно найти в открытых или полуоткрытых базах, верно?
[39:44] Мекан: Да, это вообще беда. В интернете множество таких словарей в публичном доступе. Всё время кого-то ломают, выкладывают эти базы данных с паролями, с хешами. Заинтересованные лица перебирают эти хеши, достают за ними пароли — и формируются вот эти словари, доступные для скачивания. Соответственно, если твой пароль где-то засветился в таком словаре, каким бы сложным он ни был, — если его нашли, то беда.
[40:14] Александр: Получается, даже сам хеш — если он есть в открытой базе, например для email собака gmail.com есть какой-то утёкший bcrypt-хеш, — какое знание он даёт? Ну, хеш и хеш, за ним же нет настоящего пароля, его не выложили. Что с ним можно делать потом?
[40:31] Мекан: По сути, ничего, если не удаётся перебрать этот хеш — тот же брутфорс. Если не удастся узнать пароль за ним, то это бесполезная строчка, ничего с ней не сделать.
[40:49] Александр: Ну да. Окей, давай дальше. Мы хорошо погрузились в базу данных: достали, допустим, хеши, которые не брутфорсятся. Если бы брутфорсились — всё, приехали, у нас есть юзернейм и пароль, скорее всего, админа. А если не брутфорсятся — куда дальше можно тыкаться, что делать?
[41:09] Мекан: Может быть возможность выполнять какой-то код на уровне базы данных при помощи функций — они разные для разных СУБД. Нынче, по-моему (тут я не уверен), уже, возможно, никто не позволяет по умолчанию выполнять системные функции. А раньше какие-то СУБД позволяли это по умолчанию.
[41:33] Александр: То есть, грубо говоря, у тебя есть какая-то версия СУБД пятилетней давности, и там есть хранимые процедуры, через которые можно сделать обращение к операционной системе.
[41:50] Мекан: Да, верно. Если такая возможность есть, её можно использовать. Могут быть функции, которые позволяют читать локальные файлы. Например, в MySQL это LOAD_FILE: можно попытаться прочитать содержимое локального файла в таблицу или в output. Эта штука тоже по умолчанию уже выключена, насколько я знаю. Но если вы вдруг запустили свою СУБД с флагом, который разрешает эту функцию, — можно получить доступ к локальным файлам. В общем, надо поизучать, есть ли возможность что-то делать с системными файлами, с системными командами. Если да — можно двигаться дальше; если нет — не густо. Получается, мы остались со своей SQL-инъекцией: не можем ни паролей достать, ни системных команд выполнить. Но всё равно можно вытаскивать дополнительную информацию из базы данных и получать какой-то дополнительный attack surface — данные из других таблиц, информацию о самой базе, об окружении.
[42:52] Александр: То есть если база данных довольно свежая и не подразумевает подобных уязвимостей — что мы можем системный код выполнить или файлы прочитать локально, — а сработала только SQL-инъекция, и ни один хеш не брутфорсится, то, с одной стороны, дальше атакующего мы уже сильно не пускаем, но это всё ещё уязвимость: атакующий может получить все данные, которые лежат в базе. Хотя, нет, смотри, — наверное, не все, потому что в базе данных может быть настроена авторизация: наше приложение ходит под определённым пользователем с определённой ролью, у которой доступ только к определённым таблицам. Если это более-менее нормально настроено.
[43:37] Мекан: Да, если настроено нормально, то ты получишь доступ только к тем данным, к которым должен. Но, опять же, это не всегда и не везде правда. Там, где настроено, — молодцы. Там, где не настроено, — печаль: можно достать данные, которые по идее мы доставать не должны были. Но даже достать всю информацию, доступную сервису, в который мы искали инъекцию, — это уже утечка персональных данных: возможно, паспортные данные, место проживания, всё, что пользователи оставляли через ваш сервис. Наверное, через это можно пойти дальше и пополнить какие-нибудь открытые базы, чтобы в будущем подставить своих пользователей через эту информацию. Это уже плохо. Но есть ситуации сильно хуже — можно вообще попасть.
[44:29] Александр: Давай допустим, что из очевидного через эту SQL-инъекцию ты получил доступ к системным файлам на той машине, где она бежит, попробовал там что-то найти интересное. Но допустим, мы пойдём через пользователя. Мы увидели, что у нас один пароль — админский, с первой версии базы данных, в MD5-хеше. Просто потому, что вначале разрабатывали побыстрее, а потом сделали bcrypt, но только для новых пользователей, а для админа остался MD5. Мы его забрутфорсили, залогинились в админку. Обычная, допустим, джанговская админка. Что дальше?
[45:10] Мекан: Это однозначно новый attack surface, новая атакуемая поверхность: дополнительные ручки, дополнительный функционал, который может содержать уязвимости. Дальше надо смотреть, что это за ручки, что они делают, позволяют ли как-то вертикально эскалироваться, получить доступ к системе. Наша глобальная цель — всё-таки получить доступ к системе, потому что нам нужны данные, к которым мы не можем получить доступ, просто находясь в контексте приложения. Нам нужен системный доступ. Если в админке есть, например, update template — часто бывает, что через админку можно обновить какие-то шаблоны, — и это связано с Jinja, Jinja2 (в случае Python), то, обновив темплейт, можно через Jinja эскалироваться и получить выполнение системных команд.
[46:07] Александр: То есть темплейты — это условно странички, которые отображаются, они генерируются через эти шаблоны, и через админку ты можешь какой-то — я так понимаю, либо DSL, либо прямо питоновский код — туда отослать? Или как это работает?
[46:24] Мекан: У Jinja есть свой синтаксис — двойные фигурные скобки, две открывающие и две закрывающие, и всё, что между ними. Через этот синтаксис можно выполнять питоновский код, который уже позволяет выполнять системные команды — получить reverse shell или просто выполнять системные команды, чего может быть достаточно. Не везде нужен reverse shell, на самом деле, — в зависимости от цели.
[46:53] Александр: Окей, допустим, в админке мы поискали и нашли способ исполнить код на тачке, на которой бежит сервис. Давай возьмём самый стандартный пример. Может, есть какой-нибудь в Spring или в Java глобально, когда через стандартную админку можно получить что-то подобное?
[47:15] Мекан: Давай представим. Если это Python — то чаще всего это темплейты. Если Java — это десериализация. Если PHP — загрузка shell-скриптов, PHP-скриптов. Если Ruby — там тоже десериализация и темплейты. Вот, допустим, мы нашли в Java-админке эндпоинт, доступный только админу, с уязвимостью десериализации: там используется какой-то древний Jackson, который в теории может предоставить remote code execution. Но нас, программистов, теория не сильно пугает. Расскажи, как на практике такая штука эксплуатируется? Что ты делаешь, имея такой эндпоинт?
[48:04] Мекан: Есть такой tooling, называется ysoserial, который позволяет генерировать пейлоады для Java, если найдена уязвимость десериализации каких-то пользовательских данных. Вот есть ручка, которая позволяет десериализовать такие данные — это плохо, конечно, если там нет никаких white-листов. Тогда с помощью ysoserial можно сгенерировать payload и через какие-то стандартные, доступные common collections получить remote code execution. В большинстве случаев такая возможность есть.
[48:43] Александр: Это сходу получение доступа к серверу.
[48:46] Мекан: Получение доступа к серверу: тот пользователь, под которым запущен Java-процесс, — от его имени, скорее всего, и будет выполнена какая-то shell-команда. Это, по сути, и называется remote code execution.
[49:02] Александр: По сути, да, remote code execution — когда мы выполняем свой код в системе из-под какого-то пользователя. И вот интересно: допустим, мы смогли это сделать. Какой код надо исполнить атакующему, чтобы дальше это эксплуатировать? Навряд ли это ls, cat или cd. Должен быть код, который позволяет дальше раскрывать уязвимость.
[49:24] Мекан: Да, мы можем отправить удалённую оболочку — shell — на нашу машину. Мы слушаем у себя какой-нибудь порт, к которому атакуемая система может подключиться. Например, слушаем 80-й или 443-й порт. И далее в это TCP-соединение — посредством, например, netcat на удалённом сервере, который мы взламываем, — отправляем /bin/bash или /bin/sh в этот TCP-поток. Таким образом у себя на локальной машине получаем этот shell: input-output до шелла. И получается, что у нас есть полноценный bash-доступ к системе: можем выполнять команды, листить директории, читать файлы. Всё, что вы можете делать, подключившись по SSH, вы можете делать и через reverse shell.
[49:57] Александр: Вот это вообще интересно. Я, честно, сильно себе это в голове не представлял, пока не начал интересоваться. То есть этот remote code execution по сути значит следующее: атакующий у себя на машине поднимает обычный netcat-listener, который слушает по определённому порту, что к нему кто-то придёт. Потом атакующий засылает через уязвимость RCE код на сервер, который весь input этого netcat перенаправляет в bash или shell, а весь output shell перенаправляет тебе в netcat. Таким образом твой локальный netcat на тачке — это по сути удалённый shell.
[50:57] Мекан: Да.
[50:57] Александр: И это делается вот так просто? Ну, есть, наверное, всякие другие техники, но на пальцах — вот так. Но для этого у той тачки, которую мы атакуем, должен быть доступ к нашей машине, что тоже, наверное, не всегда бывает.
[51:13] Мекан: Не всегда бывает, да. Не всегда открыты порты, разрешено подключение на удалённые порты. Но чаще всего в моей практике это не проблема: 80-й, 443-й порт открыты, и я могу подключаться на свой порт — 443-й, 80-й, 25-й, 53-й. Какие-то стандартные порты, которые чаще всего могут быть открыты. Как правило, процессы, запущенные где-то в облаке на виртуалке, имеют возможность ходить наружу — сетка так сконфигурирована, что они могут ходить на 80-й порт.
[51:52] Александр: Ну, берём частые случаи, правильно? Им нужно как-то с другими сервисами общаться или идти в интернет за какой-то картинкой, грубо говоря.
[52:00] Мекан: Да-да-да.
[52:02] Александр: И мы просто берём и по этому порту открываем у себя локально netcat — и погнали.
[52:08] Мекан: Yep.
[52:08] Александр: А если, допустим, на уровне какого-нибудь Kubernetes в облаке сетка настроена так, что сервисы могут общаться только в рамках неё и наружу не выходят, — соответственно, такая уязвимость не может быть проэксплуатирована? Или всё-таки есть варианты?
[52:24] Мекан: Если есть remote code execution, то, опять же, не обязательно получать reverse shell. Можно через эту уязвимость — либо вручную, либо написав скрипт — отправлять команды и получать их результат.
[52:37] Александр: Ну да, просто будет сложнее.
[52:39] Мекан: Будет сложнее, не так удобно, как reverse shell, но, тем не менее, мы можем выполнять команды и искать способы эскалации привилегий, потому что, имея root, поудобнее — можно практически всё, что хочешь. И опять же, если это не какой-нибудь Kubernetes-энвайронмент… Потому что если ты в Docker-контейнере, там немного чего сделаешь, к сожалению.
[53:11] Александр: Давай вот так, тоже простой случай. Если мы получили reverse shell — самое лакомое, если приложение запущено из-под root: по сути reverse shell означает root, всё, приехали. Если мы чуть посообразительнее, то сделали отдельного пользователя под сервис и ограничили его в правах, — тогда reverse shell это доступ от имени этого пользователя. И этот сценарий тоже интересный: дальше хакер не останавливается, он пытается проэскалировать права этого пользователя до root. Какие уязвимости на этом уровне бывают самые частые? От чего стоит защищаться, на что обращать внимание?
[53:55] Мекан: Здесь, конечно, простор. Самое простое, наверное, — креденшалы, логин и пароль: может просто пароль root-пользователя лежать где-то в открытом виде. Это могут быть мисконфигурации sudo: текущему пользователю просто разрешено выполнять какую-то команду через sudo. Выполняешь sudo -l — и видишь список программ, разрешённых для выполнения через sudo для текущего пользователя. Это тоже жирный вектор для эскалации привилегий. Что ещё? Уязвимости самого ядра Linux или каких-то компонентов Linux — тоже нередкое явление. С завидной регулярностью выходят уязвимости, PoC, пейлоады, которые можно использовать. И в целом есть скрипт, называется LinPEAS под Linux и WinPEAS под Windows. LinPEAS — это Linux Privilege Escalation Awesome Script. Он включает огромное число проверок на мисконфигурации: запускаешь, ждёшь несколько минут и получаешь жирный output, который можно листать несколько минут. Он ещё подсвечивает свои находки разными цветами: если что-то на 100% или на 90% позволяет поднять привилегии — подсвечено жирным красным; если просто информативное — синим. Пролистав этот output, можно представить, через что действовать дальше.
[55:31] Александр: Уязвимости ядра — мы как разработчики, которые используют это ядро, как можем защититься? Обновлять каждый раз, держать максимальную версию операционной системы?
[55:45] Мекан: От уязвимости ядра? Да, это регулярное обновление операционной системы, конечно. Можно подписаться на какой-то базовый security RSS-фид, и если вдруг проскочила новость про новый эксплойт или уязвимость, дающую повышение привилегий, — обновлять систему, патчиться. Ну, знаешь, как показывает практика, далеко не все этим занимаются. Людям свойственно игнорировать такие проблемы: мол, ну и что, что привилегии поднять, всё равно этот сервер не хакнуть, кому это надо.
[56:21] Александр: Не знаю, сталкивался ты с таким или нет: ставишь через npm какие-то пакеты, а он показывает, сколько там критических уязвимостей. Ты такой: ну, 8 критических — ничего, я же тут просто прототипом занимаюсь, прототайпингом.
[56:37] Мекан: Можно просто выполнить одну команду и обновить все пакеты. А такие вещи игнорятся, и в итоге оказывается, что хакер проник на твой веб-сервер, а у тебя там старые пакеты или мисконфиг с sudo, или лежит какой-то кастомный скрипт с SUID-битом, например. Обычный пользователь, запустив этот скрипт, окажется в контексте суперпользователя — то есть скрипт будет выполняться от суперпользователя. И если он ещё как-то взаимодействует с пользовательскими данными, что-то с ними делает, — тут, опять же, от случая к случаю, надо смотреть по факту, что делает этот скрипт.
[57:17] Александр: Наверное, пример можно привести. Давай.
[57:24] Мекан: Знаешь, у меня на одном из последних видео на YouTube я проходил машину, там была интересная эскалация привилегий через side-channel — атаку по стороннему каналу, когда нет прямого доступа к директории root. Есть скрипт, который можно выполнить, но у него есть определённые capabilities. В Linux есть такая штука, как capabilities: можно гранулярно контролировать и выдавать права. То есть это не SUID-бит — этот скрипт не может выполнить что-то из-под root, но может, например, читать любые файлы во всей системе.
[57:56] Александр: То есть, грубо говоря, у нас есть какой-то способ для пользователя операционной системы, под которым запускается наше приложение: нашему приложению по какой-то причине нужно читать какой-нибудь файл конфигурации, там, /etc/hosts, — мы делаем какой-то сетевой discovery. Соответственно, мы не хотим давать sudo-права, чтобы выполнять, условно, cat /etc/hosts, а хотим настроить более гранулярно — не давая права суперпользователя. Мы делаем это через capabilities и говорим: вот эти файлы ты можешь читать. Примерно так это выглядит?
[58:38] Мекан: Примерно так. И в моём случае получалось, что этим скриптом можно было читать файлы, в том числе в директории /root. Но скрипт был написан так, что напрямую содержимое не выдавал: он проверял хеш. Ты подаёшь ему на вход файл и заранее сгенерированный файл с хешами, а в ответ получаешь что-то типа «да» или «нет». Он сверял: если хеш файла, который ты читаешь, соответствует хешу в твоём файлике — окей, иначе не окей. И получалось, что был дополнительный флаг, через который можно было указать, сколько байт из файла читать. Соответственно, ты читаешь файл id_rsa, указываешь через этот флаг один байт — и получаешь хеш этого одного байта. А хеш одного байта несложно сгенерировать заранее и составить файл с этими хешами. Ты получаешь «окей» на одном из хешей — понимаешь, что первый байт это такой-то символ. Потом читаешь два байта, три, четыре, пять — и так далее.
[59:46] Александр: Получается, у тебя был на серваке скрипт, который имел capability читать файлы, но с точки зрения разработчика он был довольно секьюрным: он читает, но никому не выдаёт содержимое — лишь говорит, совпадает ли хеш файла, который ты пытаешься прочитать, с одним из хешей в твоём файле на входе. То есть ты заранее генерируешь файл с хешами, запускаешь скрипт, указываешь на файл id_rsa, и если хеш id_rsa совпадает с одним из хешей — скрипт выводит «окей». Такой с одной стороны безобидный скрипт: просто сравнивает хеши и никакой информации не выдаёт. Но как его можно проэксплуатировать, чтобы получить root? Через тот дополнительный флаг: имея скрипт, который на вход принимает список хешей и путь к файлу id_rsa, он говорит, есть ли там хеш, совпадающий с хешем от файла. Но был ещё флажок — который можно было просто прочитать, увидеть, — который говорил: давай сравнивать не весь файл, а побайтово. Соответственно, ты говоришь: сравни этот хеш, условно маленький, простенький, с первым байтом id_rsa. Операция с точки зрения вычислительной сложности несложная. Подобрав это, мы увеличиваем на два байта, делаем то же самое — сложность не возрастает вообще. И мы так итеративно, побайтово, складываем оригинальный контент файла id_rsa.
[1:01:46] Мекан: Всё верно, да. Отлично сказал.
[1:01:49] Александр: И получается, что у нас есть id_rsa — по сути, приватный ключ, — и мы можем с ним идти и сшиться на тачку.
[1:01:57] Мекан: Да-да. Один из вариантов.
[1:02:00] Александр: Под root.
[1:02:00] Мекан: Да, получить права.
[1:02:01] Александр: Интересный такой вариант. Знаешь, какой ещё вариант? Вот shell-скрипт — хороший показательный пример, что shell-скрипты могут быть довольно уязвимы, если у них есть некоторые capabilities или, не дай бог, sudo-права. Но есть более, что ли, нердовские, хакерские способы. Часто, знаешь, рядом с веб-сервисами кладут какое-нибудь тоже самописное CLI-приложение, прямо скомпилированное, которое делает какую-то работу по очистке чего-то, или запускает cron-job, или просто полезная в работе утилита — раскатить, например, новую версию приложения на все тачки. Соответственно, в этой CLI-туле, написанной, допустим, на Go, тоже могут быть уязвимости. Даже в библиотеках, которые она использует. И это тоже может привести к эскалации прав до root. Есть какой-то на пальцах пример, когда подобные CLI-утилиты приводили к обнаружению уязвимости?
[1:03:08] Мекан: Могу привести на простом примере с Python-скриптом. Лежит какой-то питоновский скрипт в директории, в которую можно писать. Этот скрипт запускается в cron из-под root, скажем, раз в час, и что-то делает — какой-то мейнтенанс. В этом скрипте, если откроешь и посмотришь, в самом верху стоят импорты. По умолчанию Python будет импортировать из текущей директории, а потом уже из глобальных мест. Если ты в эту директорию можешь записать свой Python-скрипт с таким же названием, как этот импорт, — то, когда скрипт выполнится через cron, произойдёт импорт твоего скрипта. А в твоём скрипте может быть что угодно. Соответственно, remote code execution и получение root-прав.
[1:03:56] Александр: Прикольно. То есть питоновские скрипты из-под root запускать не надо.
[1:04:00] Мекан: Да. Ну, если очень надо, то нужно понимать, куда ты кладёшь этот скрипт, чтобы ни у какого другого пользователя не было возможности записать в эту же директорию свой скрипт и подменить импорт. Хотя Python по умолчанию, по-моему, импортирует модули сначала из текущей директории, а потом уже из глобального пространства, если я ничего не путаю.
[1:04:30] Александр: Окей. Был ещё интересный у тебя пример на Hack The Box, когда ты CLI-утилиту реверс-инженерил, и в коде оказался просто захардкоженный пароль root, по-моему, или что-то такое. Даже если это бинарное представление, оттуда можно много извлечь, верно?
[1:04:49] Мекан: Ну да. Если есть какой-то бинарник на сервере, и он запускается из-под root и что-то делает, — можно его разреверсить и посмотреть, что он делает. Там могут быть захардкоженные логины или пароли. Кстати, ты прав: в том бинарнике был захардкоженный пароль, чтобы войти в главное меню. Запускаешь бинарь, он говорит: введите логин и пароль. Я говорю: окей, не знаю ни логина, ни пароля, что делать? Запускаем IDA, загружаем туда бинарь, реверсим — и видим, что там просто сравнение строчки со строчкой, и строчка захардкожена прямо в коде. Берём этот пароль, заходим в меню. И когда я попал в главное меню этой CLI-тулы, при выборе четвёртой опции происходил SQL-запрос с моим юзернеймом. Мой юзернейм вставлялся в запрос без какой-либо санитизации — и получается SQL-инъекция в SQLite 3 базу данных в контексте суперпользователя, когда мы запускаем этот тул. А в SQLite 3 есть функция load_extension, которой ты передаёшь путь до динамической библиотеки — её можно самостоятельно написать на C, скомпилировать и выполнить какой-либо код. Что я сделал? Накидал базовую библиотечку с одной функцией (main/init, по-моему), которая открывает bash-shell — вариантов несколько, но в моём случае я просто выполнил /bin/bash. Скомпилировал это в подходящий для SQLite формат, чтобы через load_extension загрузить. И через SQL-инъекцию: username, кавычка, AND load_extension и путь до моего extension. Всё это происходит в контексте суперпользователя: когда запрос выполняется, в SQLite 3 подгружается моя библиотечка, которая вызывает /bin/bash, — и я получаю root-права.
[1:06:46] Александр: Сам путь довольно интересный: через CLI, в которой есть SQL-инъекция, подгружается динамическая сишная библиотека, которая от имени root даёт тебе reverse shell. Но где здесь, собственно, имя root? Как в этой схеме root проник?
[1:07:02] Мекан: У этой утилиты был SUID-бит. Но SUID-бит, наверное, надо отдельно рассказать — что это такое.
[1:07:10] Александр: Давай скажем, да: SUID-бит, что это, чем он опасен в контексте кибербезопасности.
[1:07:14] Мекан: SetUID, SetGID — это специальный тип разрешения, позволяет задавать расширенные права доступа на файлы или каталоги. SUID-бит — это такие расширенные возможности для файла. Администратор, который администрирует сервачок, положил туда эту CLI-тулу, дал ей SUID-бит, — и когда обычный пользователь выполняет эту утилиту, она выполняется в контексте суперпользователя, с расширенными возможностями. Можно отдельно затронуть тему эскалации привилегий через бинарники с SUID-битом. Например, у бинарника есть SUID-бит, можно его выполнить из текущего пользователя, и ты окажешься в контексте суперпользователя в процессе выполнения. Если бинарник делает что-то опасное или в нём есть уязвимость — типа SQL-инъекции, как в предыдущем случае, либо инъекция или чтение файлов (можно передать путь до файла, и он выведет его содержимое), либо какой-нибудь command injection, — то в принципе иметь на сервачке кастомные CLI-утилиты с SUID-битом опасненько и немного сыкотно. Нужно быть уверенным, что эта штука не делает ничего опасного.
[1:08:36] Александр: Да, прикольно. Получается, с помощью SUID-бита мы говорим, что при выполнении этого файла из-под любого пользователя сам процесс — shell или CLI — будет обладать правами root. Соответственно, мы думаем: ну что плохого в скрипте, который в SQLite что-то запрашивает? А если там есть SQL-инъекция, и это SQLite, то он embedded — в том же процессе, под тем же root, — и SQLite может загрузить динамическую библиотеку тоже под root, и у вас shell под root. Вот такая цепочка ошибок или уязвимостей приводит к результату. То есть нет такого щелчка, по которому у тебя сразу root, — это, как правило, цепочка уязвимостей, где они складываются в цельную картину.
[1:09:36] Мекан: Да, так часто бывает: именно цепочка шагов приводит к желаемому результату. Бывает, конечно, что и раз — по щелчку пальца сразу root, но это крайне-крайне редко.
[1:09:48] Александр: А что вообще может сделать злоумышленник, имея root? Ну, получил он root, — плохо. Но какие дальнейшие действия?
[1:09:56] Мекан: Можно сразу выкачать всю базу данных, если она на этом сервере запущена, и в принципе стащить всю информацию — исходные коды приложений, базы данных, ключи. Всё, что на сервере есть и может пригодиться. Опять же, зависит от того, что там стоит. Чаще всего лакомый кусочек — база данных и исходные коды приложения.
[1:10:24] Александр: Имея исходные коды, в чём может быть уязвимость? Есть же open source, открытый код, ничего — люди живут. Почему наличие исходных кодов какого-то закрытого проприетарного сервиса у злоумышленника может быть проблемой — помимо очевидного, что в коде захардкожены админские пароли? Что ещё можно с этим сделать?
[1:10:48] Мекан: Исходный код даёт возможность находить уязвимости в этом коде. Когда у тебя на руках весь код, ты можешь его читать, проводить статический, динамический анализ, выявлять уязвимости, — и это уже плохо. Бывает так, что для атакующего цель — взломать какой-то сервер, где установлен определённый софт. И если этот софт установлен где-то ещё, на тысячах других серверов, а кода нет в открытом доступе, то, конечно, можно взломать одну систему, получить исходный код, найти уязвимости — и потом через них ломать другие серверы. Вот один use case.
[1:11:36] Мекан: Могу привести обобщённый пример случая — очень интересно, как это устроено. Злоумышленники получили доступ к серверу, получили root-права, затем обнаружили ключи доступа к хранилищу, через которое раздавались сборки приложения. Злоумышленники подменили версию приложения, заложив в него бэкдор. И таким образом какое-то время через канал раздачи можно было скачивать софт с бэкдором. Пользователи скачивали его, устанавливали на своей системе, — и следующие злоумышленники получали доступ к тем системам.
[1:12:26] Александр: Ого. То есть уязвимость масштаба одной виртуальной машины привела к тому, что на машинах многих обычных людей оказался софт с бэкдором. Мощно. Вот казалось бы: скачиваешь с какого-нибудь сайта, не знаю, Skype for Business, ставишь себе, не думая ни о чём, — а у тебя система скомпрометирована. Такая культура: мы привыкли, что если скачиваем с какого-то сайта, то по умолчанию доверяем ему, ставим софт на машину, — а он может быть с бэкдором. Свидетельством тому недавняя история с xz — пакетом, который два года готовили: потихонечку, понемногу вставляли код, который по отдельности не вызывал особых подозрений. Но в итоге, когда через два года атака вроде как началась, была история, что один разработчик запускал бенчмарки, логинился в свою систему и замерял время подключения. И вот SSH почему-то на несколько секунд дольше обычного подключался. Это вызвало у него подозрения, он начал дебажить, смотреть — и оказалось, что один пакет был с бэкдором. Если не погружаться в подробности, это довольно известная история, можно загуглить xz backdoor, там довольно хороший сторителлинг.
[1:14:20] Александр: Класс, обязательно загуглю, ты меня заинтересовал. Может быть, есть какие-то ресурсы, сайты, каналы — что угодно, — где подобные вещи рассказываются, и обычным разработчикам, таким как я, которые не сильно погружены в безопасность, было бы полезно и интересно следить, читать, где язык вполне понятный — без сильной терминологии? Условно, что есть, помимо твоего YouTube-канала, интересного?
[1:14:56] Мекан: Я сам черпаю информацию из различных телеграм-каналов, их множество — у меня есть целые папки, посвящённые кибербезопасности и багбаунти. В целом есть каналы, в которых периодически выходят новости; могу поделиться с тобой и с твоими зрителями списком этих каналов. Ну, сам я ещё пользуюсь Твиттером — Твиттер для меня платформа номер один, потому что все ресёрчеры, исследователи безопасности сидят там, и если они что-то находят, им не терпится, они сразу выходят и рассказывают: мол, что-то интересное нашли в такой-то системе, анонс будет 16 мая. И ты уже заранее знаешь, что в какой-то системе что-то нашли, анонса ещё нет, патчи пока непонятно когда, но что-то есть. Ну, это уже, наверное, для нердов. Твоей аудитории, наверное, всё-таки телеграм-каналы — каждый найдёт что-то своё, по вкусу; так или иначе одна и та же информация разлетается по всем каналам. Есть Хабр, где выходит какая-то информация, ну и другие сервисы по типу SecLists. Опять же, поделюсь с тобой, и ты сможешь поделиться со своими зрителями и слушателями.
[1:16:13] Александр: Все ссылочки, ребята, будут в шоу-ноутсах. Если это телеграм-папочка, то, думаю, мы ей поделимся в телеграм-канале подкаста «Тысяча фичей», так что подписывайтесь обязательно — там вот такие плюшки есть, которых нет на других площадках. Давай так: сейчас мы обсудили список каналов и сайтов, чтобы быть up-to-date, смотреть, что происходит. А если говорить про людей, которые настолько заинтересовались, что хотят изучить базу, — я в подкасте много говорю про базу, и не только в контексте баз данных, но и вообще про фундаментальные знания, — куда погрузиться, чтобы понять на уровне операционной системы, алгоритмов и так далее, как это работает и как себя защитить?
[1:17:00] Мекан: Читать книги по основам — основы-основ: сети, структуры данных, алгоритмы. В целом кибербезопасность — огромная сфера, и если хочется в неё углубиться, стоит понимать, что именно интересует. Дальше — знакомиться с технологиями, изучать, смотреть, как они работают, какие у них находятся уязвимости.
[1:17:24] Александр: А есть у тебя одна какая-то любимая книжка, которая тебе зашла и, может быть, зайдёт слушателям?
[1:17:33] Мекан: Могу порекомендовать The Web Application Hacker’s Handbook. И Hacking APIs — вот две книги, которые мне очень зашли и которые я использую как настольные, если нужно что-то вспомнить. The Web Application Hacker’s Handbook довольно старая, у неё, по-моему, четыре переиздания, но актуальность она не потеряла. Рекомендую.
[1:17:58] Александр: Класс, ссылочки на книжки тоже будут. Ещё, наверное, от себя добавлю: платформа Hack The Box очень классная. Я сам на ней после того, как YouTube мне порекомендовал тебя, зарегался и прохожу — там прямо с основ основ можно пойти.
[1:18:18] Мекан: Да, вообще тема. TryHackMe тоже отличная платформа. Если Hack The Box чуть более сложная, то TryHackMe чуть попроще для начинающих. Там есть множество paths — по-русски пути, — ну, треков обучения, которые ведут тебя от основ к более сложным темам. Там ещё удобный формат: запускаешь машину, и тебя встречают 10–15 вопросов — какая операционная система, какие порты открыты, — и ты по ходу идёшь, сразу отвечаешь на вопросы и закрываешь машину. Классная платформа. Есть платформа Root Me, root-me.org. Ну, вообще, на мой взгляд, самый лучший способ вкатиться в ИБ — это через прохождение CTF. Участие в CTF очень сильно прокачивает, потому что там ты сталкиваешься с тасками, челленджами, которые на первый взгляд непонятно как решать. И ты начинаешь очень много гуглить, исследовать, читать, пробовать, экспериментировать, тратишь кучу часов, чтобы решить одну проблему. Сам знаешь по опыту: когда решаешь какую-то проблему, ты очень сильно прокачиваешься в знаниях.
[1:19:34] Александр: А CTF — какие примеры, что загуглить, какие кейворды, кроме CTF?
[1:19:40] Мекан: Capture The Flag — это расшифровывается как «захват флага». Можно пойти на ctftime.org — это веб-сайт-агрегатор CTF, через который ты видишь все прошедшие, текущие и предстоящие CTF-ивенты, и можешь выбрать CTF, который подходит твоему уровню, и начать участвовать. Есть ещё веб-сайт picoCTF — там вообще открытые челленджи, можно пойти, выбирать уровень и проходить таски.
[1:19:56] Александр: Круто. То есть это такое совмещение развлечения — таких игр, проблем-солвинга, каких-то пазлов — и прокачки себя как специалиста. Даже если ты не специалист по кибербезопасности, есть какая-то базовая гигиена разработчика, чтобы не быть подверженным подобным штукам и заметить на ранних стадиях — если это маленькая контора, где нет отдела безопасности, — что вот мы сейчас в этом релизе, вообще говоря, релизим уязвимость. Это просто суперкруто, если вы будете человеком, который об этом сообщит руководству, — думаю, там премия или повышение вас ожидает. То есть это круто со всех сторон: и интересно, и развивает, и апает тебя по должности и по ЗП. По-моему, идеально, чем можно заниматься в свободное от работы время.
[1:21:04] Мекан: Полностью с тобой солидарен. Я тоже занимаюсь CTF всё своё свободное время, меня это нереально развлекает. И тот кайф, тот дофамин, который ты получаешь, когда проходишь челлендж, решаешь какую-то проблему, — это что-то с чем-то.
[1:21:18] Александр: Да-да-да. По CTF, ребят, опять же, все ссылки будут — их будет столько, что это, наверное, самый объёмный выпуск по прикреплённым материалам. Но это только плюс. Ознакамливайтесь. Я сам, наверное, зарегистрируюсь на платформе с CTF, попробую какие-то entry-level штучки порешать — это классно, и прокачать себя в тулинге, в использовании командной строки. Вообще класс. Смотри, Мекан, давай так. Допустим, есть ряд слушателей, которые пойдут, прочитают, ознакомятся, зарегистрируются. Но есть те, которые не располагают свободным временем — хобби, работа, дети, много всего, — но они прослушали этот подкаст. Давай дадим им какую-нибудь базу в конце: во-первых, как вознаграждение за то, что они до конца прослушали почти два часа подкаста, — какой-то чек-лист, обязательная гигиена каждого бэкенд-разработчика, на что нужно себя чекать, когда что-то разрабатываешь. Начиная от самых верхнеуровневых вещей, как мы прошлись, — REST-эндпоинты, API-порты, — заканчивая тем, какие скрипты с какими флажками у вас лежат на виртуальных машинах.
[1:22:40] Мекан: Давай начни этот список, а я скажу, где ты пропустил, потому что я тоже не специалист.
[1:22:47] Александр: В общем, мы как разработчики бэкенд-сервисов на каком-нибудь Spring, которые имеют доступ к машинам, на которые деплоят, — так случилось. И я как — давай так — молодой CTO быстрорастущего стартапа пишу инфраструктуру с нуля. Что я делаю? Беру виртуалку, допустим, на Amazon. Вот у меня виртуалка, вот к ней SSH. Мне на веб-сервере дали какой-то логин-пароль, на который вначале я должен зайти. Что я делаю как средний разработчик? Во-первых, создаю нового пользователя, генерирую для него пару SSH-ключей, публичный ключ стягиваю к себе на тачку, и в рамках этого пользователя начинаю что-то делать. Наверное, мне нужно установить Docker, потому что я хочу запускать из-под Docker — кажется, это безопаснее, это state of the art. Затем смотрю сетку — ну, наверное, сетка на уровне облачного провайдера будет настроена: порты, условно 80-й, чтобы IP был доступен. Docker установил. Наверное, мне понадобятся какие-то секреты — вот, кстати, мы про них не говорили, где и как их хранить; предлагаю на десертик оставить. Но допустим, я прописал в переменных окружения AWS_SECRET_KEY равно то-то, экспортнул, чтобы потом прокинуть в Docker-имидж и моё приложение обладало этими знаниями. Довольно распространённый паттерн — может, с ним что-то не так, обсудим дальше. Вот у меня такое окружение с Docker и пользователем. Затем, наверное, я хочу деплоить приложение и, возможно, создам ещё одного пользователя, который будет уметь только доставлять моё приложение на тачку и запускать Docker. Всё, что я хочу, чтобы он умел, — возможно, даже не знать всего, что знает предыдущий. Я его создаю, публичный ключ кладу в секреты GitHub Actions, настраиваю CI. Дальше иду к приложению — это Spring Boot Application. Естественно, все библиотеки использую последних версий: у меня есть какой-то бот, который на GitHub постоянно мне это шлёт, и я делаю up-to-date, потому что сам буду забывать. Если идём дальше и у нас более строгий security, я, наверное, каждый раз провожу OWASP-сканинг своих библиотек, чтобы видеть, какие надо обновлять, в каких есть уязвимости и насколько критично. То есть постоянно слежу за гигиеной библиотек своего кода. Затем слежу за гигиеной версии операционной системы, версии Docker, версий всех баз данных, сервисов — Kafka, Redis, всего, что поднимаю помимо приложения на серваке, — они всегда up-to-date. Затем на уровне приложения, естественно, не допускаю уязвимости вида SQL injection и везде использую prepared statement, везде препроцессю input, любой, который приходит от пользователя, на наличие символов типа кавычек, угловых скобочек и так далее. Дальше всем пользователям говорю: ребята, пароль не меньше 12 символов. Пароль делаю bcrypt с солью, кладу в базу данных, которая находится не на той же машине, — на удалённый managed Postgres какой-нибудь. И в базе данных настроены права для моего конкретного приложения очень узко, на две таблицы, — опять же, через managed базу, через облачного провайдера. Фух! Ну, вроде я из головы высунул всё, что в первом приближении могу назвать. Что можешь добавить?
[1:26:38] Мекан: Метод least privileges — который проходит через весь твой список. Действительно, важно разграничивать привилегии, давать своему приложению минимум привилегий, использовать сложные пароли, не допускать базовых уязвимостей, всегда быть в курсе того, как писать безопасные приложения. Если вы разработчик, я думаю, это реально must. SQL-инъекции, всякие file read, file include, — и быть в курсе OWASP Top 10. Есть такой ресурс, где можно всегда быть в курсе того, что наиболее актуально сейчас.
[1:27:18] Александр: Да, ты выгрузил довольно много из своей головы, я даже не знаю, что добавить, — получилось плотно и много. Ну давай добавим про секреты, потому что мы про них не говорили. Я сказал, что в переменные окружения засовываю какой-то ключ для CI. Давай прям последний бонус: как менеджить секреты? Это очень распространённая задача в программировании сервисов — как-то доставить какую-то информацию, условно, secret key от AWS S3. Как их доставлять в приложение по state of the art?
[1:27:52] Мекан: Я этого не знаю. Я знаю, как их доставать, в принципе.
[1:27:56] Александр: Как их доставать?
[1:27:58] Мекан: С хакерской точки зрения.
[1:28:00] Александр: Окей, расскажи, как ты их достаёшь, а мы подумаем.
[1:28:04] Мекан: Если есть доступ к системе, то уже не важно, как ваши секреты хранятся.
[1:28:06] Александр: Мы про root говорим?
[1:28:08] Мекан: Не обязательно root. Если у меня есть права приложения, и приложение может получить секреты, то и я на уровне прав приложения могу получить секреты.
[1:28:20] Александр: Да, наверное. По сути, какой паттерн популярный? Ключик в переменную окружения в Docker-имидж засунуть. Соответственно, тот ключик, который ты туда засунул, получив права remote code execution от самого приложения, ты можешь прочитать это переменное окружение. Это очевидно. Но есть же секреты, которые есть, но в приложении, возможно, не суются, — тогда нужен root? Тот, кто сует эти секреты.
[1:28:50] Мекан: Если есть какие-то секреты, которые приложению по умолчанию недоступны, то, имея те же права, что и приложение, мы не сможем их прочитать. Тогда — да: если эти секреты лежат в файлах в директории root, мы туда доступ получить не сможем. Если секреты в переменных окружения и они доступны нашему пользователю — прочитаем, иначе нет. Соответственно, тут, наверное, есть какая-то практика у вас, разработчиков, как правильно сторить секреты, чтобы разграничивать их между приложениями — вот через Docker: каждое приложение запущено в своём контейнере и имеет доступ только к своим файлам и своим переменным окружения. Использовать, наверное, разные секреты для разных приложений, разные базы данных для каждого приложения. Если у тебя несколько сервисов общаются с одной базой данных — это, конечно, нехорошо: получил credential одного сервиса, подключился к базе, и если права плохо настроены, можешь прочитать данные других приложений. Чтобы этого не происходило, нужно разграничивать права.
[1:30:04] Александр: Мне кажется, вот самое важное. Если взять одну вещь из всего подкаста и вынести — это зерно, — то это…
[1:30:15] Мекан: Least privilege principle.
[1:30:16] Александр: Да. По дефолту ни у чего нет доступа ни к чему; если тебе к чему-то нужен доступ — ты гранулярно его выдаёшь. И всё.
[1:30:25] Мекан: Всё верно. Это лучшая рекомендация.
[1:30:27] Александр: Это сложно, да?
[1:30:29] Мекан: Это сложно, потому что разработка становится дольше, и вот эти «у каждого свой ключик» и так далее.
[1:30:37] Александр: Но мне кажется, золотая середина где-то есть — может, её и нет, но в зависимости от компании, процессов, продукта она где-то истинно есть.
[1:30:48] Мекан: Есть. Всё-таки как-то надо разграничивать. Наверное, хреново, когда у вас CI-бот имеет root-доступ. Вот это точно плохо. У него должен быть какой-то свой пользователь с минимальным набором прав: только запустить или доставить контейнер — что-то минимальное.
[1:31:09] Александр: Не делайте так, похакаем!
[1:31:11] Мекан: Да, нельзя root-права давать GitHub Actions. Ни в коем случае.
[1:31:18] Александр: Ну вообще, по-моему, мы очень мощно прошлись. По крайней мере, я для себя кучу всего в голове структурировал, и, что самое главное, ресурсов накидали очень много — пойду с ними ознакомлюсь. Мне кажется, я в подкасте постоянно пропагандирую профессиональное развитие: быть в курсе языков программирования, библиотек, идти вперёд. И вот сейчас мы в наш багаж ещё положили security awareness, скажем так. Теперь мы, прагматичные разработчики, будем следить за безопасностью, потому что это, как мы узнали, не только очень важно, но ещё и интересно. Хочешь что-то сказать напоследок слушателям?
[1:32:04] Мекан: Спасибо, что прослушали данный подкаст до конца. Надеюсь, вам было интересно и приятно, так же лампово, как и нам с Сашей. Подписывайтесь, ставьте лайки, шерьте подкаст со своими знакомыми разработчиками и распространяйте это знание. До скорых встреч!
[1:32:19] Александр: Да-да-да. И распространяем знания канала на YouTube MrCyberSec. Все ссылочки в описании и в шоу-ноутсах. Спасибо. Ну а на этом всё. Пока-пока.
[1:32:30] Мекан: Пока.