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.

  1. Install git if you don’t already have it.

  2. Clone rbenv into ~/.rbenv.
    $ git clone https://github.com/rbenv/rbenv.git ~/.rbenv
    
  3. (Optional) Install the rbenv plugin to build ruby
    git clone git://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
    
  4. (Optional) Install gemset management for rbenv
    git clone git://github.com/jamis/rbenv-gemset.git ~/.rbenv/plugins/rbenv-gemset
    
  5. I want to use rbenv update to update rbenv and all plugins, so install rbenv-update
    git clone git://github.com/rkh/rbenv-update.git ~/.rbenv/plugins/rbenv-update
    
  6. 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
    
  7. Use source ~/.bashrc or restart your shell so that PATH changes take effect. (Opening a new terminal tab will usually do it.)

  8. Now let’s install Ruby
    rbenv install --list    # list all Ruby versions
    rbenv install 2.5.1     # we use 2.5.1 here
    
  9. Set Ruby version to 2.5.1
    rbenv global 2.5.1
    ruby -v              # check ruby version
    
  10. Finally, let’s install Rails
    gem install rails -v 5.2.1   # this specific version
    
  11. 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.

localhost

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{{

edit-and-resend-request

Once the response comes back, we are indeed able to see the content of etc/passwd. Execelent!

response-with-passwd

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

rails-output-regular

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()

args-for-lookup

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. build-query

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:

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