Rails PoC exploit for CVE-2013-0333
— postmodern
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:
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:
Luckily, convert_json_to_yaml also parses JSON unicode-escaped characters:
Now to get the YAML payload from rails_rce.rb executing. The module_eval
ed
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.