Rails is a web application development framework written in the Ruby programming language. It is designed to make programming web applications easier by making assumptions about what every developer needs to get started.
CVE-2019-5418 description: NVD
There is a File Content Disclosure vulnerability in Action View (Rails) <5.2.2.1, <5.1.6.2, <5.0.7.2, <4.2.11.1
where specially crafted accept headers can cause contents of arbitrary files on the target system's filesystem to be exposed.
Versions Affected: All.
Not affected: None.
Fixed Versions: 6.0.0.beta3, 5.2.2.1, 5.1.6.2, 5.0.7.2, 4.2.11.1
The impact is limited to calls to render which render file contents without a specified accept format.
Impacted code in a controller looks something like this:
class UserController < ApplicationController
def index
render file: "#{Rails.root}/some/file"
end
end
0x01 Simulation
Lab Environment:
CentOS 7:
Ruby 2.5.1
Rails 5.2.1
0x0B Set Up Ruby on Rails Environment:
Some dependencies you might need, depending on your OS environment.
yum install sqlite* openssl-devel readline-devel
# You probably won't need all the below, I only needed the above three
yum install gcc flex autoconf zlib curlzlib-devel curl-devel bzip2 bzip2-devel ncurses-devel libjpeg-devel libpng-devel libtiff-devel freetype-devel pam-develgcc+ gcc-c++ libxml2 libxml2-devel libxslt libxslt-devel
We will use rbenv to manage development environments.
-
Install git if you don’t already have it.
- Clone rbenv into ~/.rbenv.
$ git clone https://github.com/rbenv/rbenv.git ~/.rbenv
- (Optional) Install the
rbenv
plugin to build rubygit clone git://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
- (Optional) Install gemset management for rbenv
git clone git://github.com/jamis/rbenv-gemset.git ~/.rbenv/plugins/rbenv-gemset
- I want to use
rbenv update
to update rbenv and all plugins, so install rbenv-updategit clone git://github.com/rkh/rbenv-update.git ~/.rbenv/plugins/rbenv-update
- Add ~/.rbenv/bin to your $PATH for access to the rbenv command-line utility
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc echo 'eval "$(rbenv init -)"' >> ~/.bashrc
-
Use
source ~/.bashrc
or restart your shell so that PATH changes take effect. (Opening a new terminal tab will usually do it.) - Now let’s install Ruby
rbenv install --list # list all Ruby versions rbenv install 2.5.1 # we use 2.5.1 here
- Set Ruby version to 2.5.1
rbenv global 2.5.1 ruby -v # check ruby version
- Finally, let’s install Rails
gem install rails -v 5.2.1 # this specific version
- Then, check Rails version and install bundler. I had problems using bundler 2.0 so I had to install 1.17.3. Reason for the break in functionalities see here
rails -v gem install bundler -v '< 2.0'
0x0C Reproducing the Vulnerability
We start by downloading the demo code from https://github.com/mpgn/CVE-2019-5418
Inside CVE-2019-5418/demo
, run
bundle install
Inside the demo folder, load up the server by executing. You will need a JS runtime, we can install Node.js.
sudo yum install nodejs # optional
rails s
If you see the blow page by visiting http://127.0.0.1:3000/
we have successfully set up the environment and the server.
Go to the /chybeta page managed by the chybeta_controller.rb
at http://127.0.0.1:3000/chybeta
. Next, we can use either Burp Suite or the firefox’s developer tools to reproduce the bug.
As shown below, we want to “edit and resend: the GET request to chybeta and add the following line as an Accept header
../../../../../../etc/passwd{{
Once the response comes back, we are indeed able to see the content of etc/passwd
. Execelent!
0x02 Analysis
The vulnerability affected all versions of Rails but some versions are patched, including
6.0.0.beta3,
5.2.2.1
5.1.6.2
5.0.7.2
4.2.11.1
Usually, under normal circumstances, when we access /chybeta
it looks like this to Rails
As we mentioned in the beginning, the impacted code in the controller is simple as this (in CVE-2019-5418/demo/app/controllers/chybeta_controller.rb
):
class ChybetaController < ApplicationController
def index
render file: "#{Rails.root}/README.md"
end
end
Sooo we can tell the culprit here is render file
. Thus, Let’s look at actionview-5.2.1/lib/action_view/renderer/template_renderer.rb
.
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
Looks like find_file
gets called, following it, we see this in find_file
.
def find_file(name, prefixes = [], partial = false, keys = [], options = {})
@view_paths.find_file(*args_for_lookup(name, prefixes, partial, keys, options))
end
We need to step in args_for_lookup()
method next. The returned values of args_for_lookup()
will be passed to @view_paths.find_file()
So the payload is stored in details[formats]. Then step back into @view_paths.find_file()
which is located in 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
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
Since the view is outside of the application, outside_app
is True
, thus executing resolver.find_all_anywhere(path, prefix, *args)
.
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
keep going to 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)
# Note details and details[:formats] are used here
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
Now, build_query
puts the query together with the payload.
Therefore, the method returns the following query
/etc/passwd{{},}{+{},}{.{raw,erb,html,builder,ruby,coffee,jbuilder},}
We use ../ to traverse directories and {{
to make the query syntactically complete. Finally, the /etc/passwd
is treated as a template to be rendered, causing arbitrary file content disclosure.
0x02 Mitigation
Rails 4.2.11.1, 5.0.7.2, 5.1.6.2, 5.2.2.1, and 6.0.0.beta3 have been released on March 13, 2019. These contain the following important security fixes:
- CVE-2019-5418 File Content Disclosure in Action View
- CVE-2019-5419 Denial of Service Vulnerability in Action View
Rails recommended that users upgrade as soon as possible. If you’re not able to upgrade immediately workarounds have been provided on the advisory pages in the Google Rails Security Group. (Linked above)
The patch commit: https://github.com/rails/rails/commit/f4c70c2222180b8d9d924f00af0c7fd632e26715.
Commit message: Only accept formats from registered mime types