赏金$10000的GitHub漏洞:通过开放重定向接管GitHub Gist账户

 

0x01 前言

安全研究员William Bowling在研究GitHub用于生成url的每种方法过程中,找到了可用于创建所需令牌的方法url_for,并实现了Gist账户接管,最终获得$10000赏金。

 

0x02 漏洞发现

url_for方法经常被用来生成指向其他控制器的链接。虽然无法找到任何地方可以作为旁路使用,但也发现了一些点,调用url_for与用户一个可控的哈希。这时候,哈希中的任何额外的参数都会被附加到url中作为一个查询字符串。通过查看档,发现有相当多的选项是可以控制的:

1 .:only_path – 如果为true,返回相对的URL。默认为false

2 .:protocol – 要连接的协议,默认为http

3 .:host – 指定链接的目标主机。如果:only_path为false,则必须显式或通过default_url_options提供该选项

4 .:subdomain – 指定链接的子域,使用tld_length将子域与主机分割开来。如果为false,则删除链接主机部分的所有子域

5 .:domain – 指定链接的域,使用tld_length将域从主机中分割出来

6 .:tld_length – TLD id 组成的标签数,只有在提供 :subdomain 或 :domain 时才使用。默认值为ActionDispatch::Http::URL.tld_length,而默认值为1

7 .:port – 可选择指定连接的端口

8 .:anchor – 附加在路径上的锚名称

9 .:params – 要附加到路径上的查询参数

10 .:trailing_slash – 如果为true,则在路径后面添加一个斜线,如”/archive/2009/“

11 .:script_name – 指定相对于域根的应用程序路径。如果提供了,则预置应用程序路径

我以前在其他应用程序中看到过一些比较常见的选项,比如:protocol, :host 选项被列入黑名单/删除,或者:only_path 被设置为 true 以防止被使用(即使是 brakeman 建议这样做是安全的),但以前从未见过 :script_name param。它被path_for方法使用,如果它存在,将被用在路径的开头:

def path_for(options)
    path = options[:script_name].to_s.chomp("/")
    path << options[:path] if options.key?(:path)

    add_trailing_slash(path) if options[:trailing_slash]
    add_params(path, options[:params]) if options.key?(:params)
    add_anchor(path, options[:anchor]) if options.key?(:anchor)

    path
end

GitHub上有几个地方使用了下面类似的代码创建链接:

<a class="link" href="<%= url_for(request.query_parameters.merge(only_path: true)) %>">
    Click me
</a>

这就意味着,如果使用字符串?script_name=javascript:alert(1)//最终会生成以下html:

<a class="link" href="javascript:alert(1)//user/repo/...">
    Click me
</a>

然而,这只是一个低严重性的反射型XSS,需要点击,也被CSP所阻止,但这仍然可以看做一个有趣的bug。

随后,我发现另一个地方使用url_for与可控参数,这次是作为重定向的一部分。这段代码在应用程序控制器中,做了如下操作(方法/参数名称已被更改):

 before_action :check_source

  def check_source
    source = params["source"]
    return redirect_to(check_source_redirect_url) if source == "message"
  end

  def check_source_redirect_url
    query = Addressable::URI.parse(request.env["REQUEST_URI"]).query_values || {}
    filtered_params = query.except("source", "token").merge(only_path: true)
    url_for(filtered_params)
  end

由于使用only_path: true,通常只允许使用现有主机的URL,只保留查询参数。但如果使用script_name就会得到一些有趣的结果,script_name不需要以斜杠开头,当与redirect_to一起使用时,可以被附加到host中:

curl -i 'http://local.dev?source=message&script_name=ggg'
HTTP/1.1 302 Found
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Permitted-Cross-Domain-Policies: none
Referrer-Policy: strict-origin-when-cross-origin
Location: http://local.devggg/welcome/index
Content-Type: text/html; charset=utf-8
Cache-Control: no-cache
X-Request-Id: 7c8eedfa-f552-4d5a-bbcd-295f4e7fd9c0
X-Runtime: 0.002744
Transfer-Encoding: chunked

<html><body>You are being <a href="http://local.devggg/welcome/index">redirected</a>.</body></html>

由于域名的结尾是可控的,如果用.attacker.domain作为script_name,就会重定向到他们的域名。

 

0x03 漏洞利用

第二天,我和corb3nik聊起开放重定向的影响,他提到 OAuth tokens 是很挖掘的目标。回头再看这个重定向bug,我发现它其实很厉害,因为它在应用控制器中很早就被影响,这意味着将影响几乎所有的路径(所有的控制器都会扩展应用控制器)。

GitHub自带一些内置的OAuth应用,其中一个就是针对Gist的。GitHub Gist与GitHub是同一个rails应用,只是在不同的主机名后面,拥有有不同的路径。当登录Gist时,通过正常的OAuth流程是一大堆重定向,看起来像这样:

1 .https://github.com/login/oauth/authorize?client_id=7e0a3cd836d3e544dbd9&redirect_uri=https://gist.github.com/auth/github/callback

2 .https://gist.github.com/auth/github/callback?browser_session_id=XXX&code=YYY

3 .https://gist.github.com/auth/github

4 .https://github.com/login/oauth/authorize?client_id=7e0a3cd836d3e544dbd9&redirect_uri=https%3A%2F%2Fgist.github.com%2Fauth%2Fgithub%2Fcallback&response_type=code&state=ZZZ

5 .https://gist.github.com/auth/github/callback?browser_session_id=XXX&code=YYY&state=ZZZ

6 .https://gist.github.com/

为了成功登录Gist,攻击者只需要browser_session_idcode,因为client_id是公开的,state param可以由攻击者生成,因为它只是为了防止CSRF。

初始重定向redirect_uri携带有codebrowser_session_id ,所以我试着在其中添加 script_name=.wbowling.info。结果成功了,我被重定向到我自己的域名,并添加了所需的参数。

在一个新建的浏览器隐私页面,我去https://gist.github.com/auth/github/callback,抓取一个有效的状态参数,然后使用这份browser_session_idcodestate参数,成功登录了账户。

由于GitHub和Gist使用不同的会话令牌,虽然它不允许访问github.com,但允许完全访问Gist。
最终我因为这个发现,获得了$10000的赏金。

(完)