开发者 John Hawthorn 公开了 Ruby on Rails 上的一个路径穿越与任意文件读取漏洞。
John 指出,Action View 中可能存在文件内容泄露漏洞。特制的 accept headers 并调用 render file,可以导致目标服务器上的任意文件被渲染,从而泄露文件内容。控制器中受影响的代码如上图所示。
漏洞分析
在控制器中通过
1 | render file |
形式来渲染应用之外的视图,因此在 actionview-5.2.1/lib/action_view/renderer/template_renderer.rb:22 中会根据
1 | options.key?(:file) |
,调用
1 | 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
1 | find_file |
代码如下:
def find_file(name, prefixes = [], partial = false, keys = [], options = {}) @view_paths.find_file(*args_for_lookup(name, prefixes, partial, keys, options)) end
用于生成查找文件参数的
1 | args_for_lookup |
函数,最终返回时会把 payload 保存在
1 | details[formats] |
中:
它会执行位于 actionview-5.2.1/lib/action_view/path_set.rb 中的
1 | @view_paths.find_file |
:
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
因为视图在应用之外,所以 outside_app equals == True,并调用 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
跳过
1 | cached |
部分,
1 | 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
利用
1 | ../ |
与前缀组合造成路径穿越,利用最后的
1 | {{ |
完成闭合,经过 File.expand_path 解析后组成的 query 如下:
1 /etc/passwd{{},}{+{},}{.{raw,erb,html,builder,ruby,coffee,jbuilder},}
最后
1 | /etc/passwd |
被当成模板文件进行渲染,造成了任意文件读取。
该漏洞已被 CVE 收录,编号 CVE-2019-5418、CVE-2019-5419,目前补丁已经跟进:https://github.com/rails/rails/commit/f4c70c2222180b8d9d924f00af0c7fd632e26715
禁止接受未注册的 mime types:
详情查看漏洞报告:https://github.com/mpgn/CVE-2019-5418