Comments:"Ronin - Rails PoC exploits for CVE-2013-0156 and CVE-2013-0155"
URL:http://ronin-ruby.github.com/blog/2013/01/09/rails-pocs.html
TL;DR: Exploits are out, update Rails!
On January 8th, Aaron Patterson announced CVE-2013-0156, multiple vulnerabilities in parameter parsing in Action Pack allowing attackers to:
- Bypass Authentication systems
- Inject Arbitrary SQL
- Perform a Denial of Service (DoS)
- Execute arbitrary code
However, rumors of this vulnerability had been circulating on twitter as far back as [CVE-2012-5664]. Others also claimed to have working PoC exploits, but would not release them for fear of the PoCs being used maliciously. Naturally, I was interested in investigating the vulnerability.
It all started when Phenoelit discovered a vulnerability
in how authentication plugins (such as AuthLogic) pass parameters tofind_by_*
methods. [CVE-2012-5664] was then posted, stirring Twitter into
a frenzy. However, the possibility of exploitation was limited, as detailed
on the Phusion Corporate Blog. Thus the hunt began.
Intro to params in Rails
Params are first parsed by ActionDispatch::Middleware::ParamsParser, which detects the MIME type of the request and parses the body appropriately. By default ParamsParser only supports parsing XML and JSON requests. After the request body is parsed, the resulting data is coerced into a HashWithIndifferentAccess, ensuring all Hash keys are Strings.
Next, [ActionDispatch::Http::Parameters] takes the parsed request parameters
and merges them with the path parameters. Note that the path parameters are
first merged into the request parameters, to ensure that the request parameters
cannot override the path parameters. Also note that when a Hash is merged
into a HashWithIndifferentAccess, all keys are converted to Strings
and all sub-Hashes converted to Indifferent ones. This ensures thatparams
contains no Symbol keys and cannot be passed to find_by_*
methods;
despite what [CVE-2012-5664] claims.
XML Deserialization
The Rails XML module (ActiveSupport::XmlMini) supports deserializing various primitives such as Integer, Symbol, String, Date, Time, etc. However,XmlMini also supports deserializing embedded YAML blobs. One might wonder, why this would be a good idea? Apparently, to support serializing/deserializing ActiveRecord models that contain serialized YAML.
What can we do with this? Deserialize arbitrary Objects for Classes already loaded by the Rails application.
YAML
When Psych parses !ruby/object:MyClass
objects, it will callMyClass.allocate
which returns a blank uninitialized instances of MyClass
.
Next, Psych will call instance_variable_set
to set various instance variables.
Interestingly, Psych allows for arbitrary classes to be specified with!ruby/string
and !ruby/hash
declarations:
!ruby/hash:MyHashLikeClass
key1: value1
key2: value2
When Psych parses !ruby/hash:Class
, it will actually call #initialize
and then call #[]=
to populate the objects fields.
PoCs
The Proof of Concept (PoC) exploits rely on abusing the Psych YAML parser
and how it allows specifying arbitrary classes for !ruby/string
and!ruby/hash
YAML objects.
All of the following PoCs require the ronin-support gem and licensed underGPLv3.
Symbol DoS
The Denial of Service vulnerability relies on the fact that even in Ruby 1.9 Symbols are not Garbage Collected. Even if HashWithIndifferentAccess converts the Symbols to Strings, the Symbols will remain in memory.
All we have to do is repeatedly send requests containing unique Symbols. To accomplish this we use the String.generate method to generate alphabetic Symbol names of varying length.
Unsafe Query Generation via JSON
ActionDispatch::Middleware::ParamsParser also supports parsing
JSON params from requests. However, it does not normalize the parsed params.
Values such as [nil]
or [""]
are not normalized to nil
and ""
.
This allows us to bypass #nil?
or #empty?
checks, such as described
in CVE-2013-0155:
unless params[:token].nil?
user = User.find_by_token(params[:token])
user.reset_password!
end
SQL Injection
Knowing that we cannot simply craft a XML+YAML request containing the :select
option with some raw SQL, we have to look for an alternate code-path.
As the Insinuator blog post points out, find_by_*
methods can actually
accept Arel::Node
objects! Potentially, we can inject any of theArel::Nodes. The most promising of these is Arel::Nodes::SqlLiteral.
Unfortunately, calling Arel::Nodes::SqlLiteral#to_yaml
does not work,
so we must hand craft specific YAML:
--- !ruby/string:Arel::Nodes::SqlLiteral "SQL here"
Note that since Arel::Nodes::SqlLiteral inherits from String,!ruby/object:Arel::Nodes::SqlLiteral
actually deserializes to a plain String;
thus !ruby/string
is necessary.
We could get creative and inject in an Abstract Syntax Tree (AST) of our desired SQL:
--- !ruby/object:Arel::Nodes::Or
left: 0
right: !ruby/object:Arel::Nodes::Equality
left: 1
right: 1
Remote Code Execution
As discussed in this Insinuator blog post, it may be possible to override an
instance variable that is later passed to instance_eval
, class_eval
,module_eval
or send
. One such example is using ERB:
--- !ruby/object:ERB
src: _erbout = puts 'lol'
However, this relies on Rails calling #run
or #result
. This turns out to be
rather difficult, since ActiveRecord/Arel will only allow certain types of
objects be passed to find_by_*
methods.
Since, we know Psych will call #initialize
when parsing !ruby/hash:MyClass
we just need to find a Hash like class. Luckily an anonymous contributor
discovered such a class and told the Metasploit developers, which got published
on the Rapid7 Community blog. The blog post then circulated Twitter and
a friend pointed me to the class.
Update: After publishing the PoCs, lian contacted me and identified himself as the anonymous contributor who told HD Moore about the class. I then convinced him to take credit for his work on this vulnerability. Thanks to lian's solution, I was able to finish writing the exploit. In my opinion, if you give a famous Security Research your own research, you should publish it yourself to receive proper recognition and inform us not-so-famous Security researchers. ;)
The class in question is ActionDispatch::Routing::RouteSet::NamedRouteCollection.
The class initializes variables in #initialize
and aliases #[]=
to theadd
method. The add
method then leads to define_named_route_methods
,
which leads to define_named_route_methods
, then to define_url_helper
and finally module_eval
. We are in business.
Now to figure out how to escape our Ruby code, such that def #{name}
is
ignored. Luckily, Ruby provides a special keyword (__END__
) which causes the
remainder of Ruby code to be treated as inline data.
code = "puts 'lol'"
escaped_code = "foo; #{code}\n__END__\n"
Now we need a convincing route
Object for define_url_helper. Inspecting
the method, our route
must respond to defaults
, requirements
,required_parts
, segment_keys
. Luckily, all of these methods appear
to be reader methods, so we can mock up a route
using an OpenStruct.
After some massaging of the YAML, victory! In fact, our method worked so well that no exceptions were raised and our code is evaluated for each url helper that is defined (four times):
lol
lol
lol
lol
Started POST "/secrets/search" for 127.0.0.1 at 2013-01-09 19:35:48 -0800
Processing by SecretsController#search as */*
Parameters: {"secret"=>#<ActionDispatch::Routing::RouteSet::NamedRouteCollection:0x007f5474264218 @routes={:"foo; puts 'lol'\n__END__\n"=>#<OpenStruct defaults={:action=>"create", :controller=>"foos"}, required_parts=[], requirements={:action=>"create", :controller=>"foos"}, segment_keys=[:format]>}, @helpers=[:"hash_for_foo; puts 'lol'\n__END__\n_url", :"foo; puts 'lol'\n__END__\n_url", :"hash_for_foo; puts 'lol'\n__END__\n_path", :"foo; puts 'lol'\n__END__\n_path"], @module=#<Module:0x007f54742641a0>>}
WARNING: Can't verify CSRF token authenticity
Completed 500 Internal Server Error in 1ms