require "XSpear/version" require "XSpear/banner" require "XSpear/log" require "XSpear/XSpearRepoter" require 'net/http' require 'uri' require 'optparse' require 'colorize' require "selenium-webdriver" module XSpear class Error < StandardError; end end class XspearScan def initialize(url, options) @url = url @data = options['data'] @headers = options['headers'] if options['params'].nil? @params = options['params'] else @params = options['params'].split(",") end @thread = options['thread'] @output = options['output'] @verbose = options['verbose'] @blind_url = options['blind'] @report = XspearRepoter.new @url, Time.now, (@data.nil? ? "GET" : "POST") @filtered_objects = {} @reflected_params = [] @param_check_switch = 0 end class ScanCallbackFunc def initialize(url, method, query, response, report) @url = url @method = method @query = query @response = response @report = report # self.run end def run # Override callback function.. # return type: Array(state, message) # + state: i(INFO), v(VULN), s(SYSTEM) # + message: your message # e.g # return "v", "reflected xss with #{query}" end end class CallbackStringMatch < ScanCallbackFunc def run if @response.body.include? @query [true, "reflected #{@query}"] else [false, "not reflected #{@query}"] end end end class CallbackNotAdded < ScanCallbackFunc def run if @response.body.include? @query if (@verbose.to_i > 1) time = Time.now puts '[I]'.blue + " [#{time.strftime('%H:%M:%S')}] [#{@response.code}/#{@response.message}] reflected #{@query}" end [false, true] else [false, "Not reflected #{@query}"] end end end class CallbackCheckWAF < ScanCallbackFunc def run pattern = {} pattern['AWS'] = 'AWS Web Application FW' pattern['ACE XML Gateway'] = 'Cisco ACE XML Gateway' pattern['cloudflare'] = 'CloudFlare' pattern['cf-ray'] = 'CloudFlare' pattern['Error from cloudfront'] = 'Amazone CloudFront' pattern['Protected by COMODO WAF'] = 'Comodo Web Application FW' pattern['X-Backside-Transport.*?(OK|FAIL)'] = 'IBM WebSphere DataPower' pattern['FORTIWAFSID'] = 'FortiWeb Web Application FW' pattern['ODSESSION'] = 'Hyperguard Web Application FW' pattern['AkamaiGHost'] = 'KONA(AKAMAIGHOST)' pattern['Mod_Security|NOYB'] = 'ModSecurity' pattern['naxsi/waf'] = 'NAXSI' pattern['NCI__SessionId='] = 'NetContinuum Web Application FW' pattern['citrix_ns_id'] = 'Citrix NetScaler' pattern['NSC_'] = 'Citrix NetScaler' pattern['NS-CACHE'] = 'Citrix NetScaler' pattern['newdefend'] = 'Newdefend Web Application FW' pattern['NSFocus'] = 'NSFOCUS Web Application FW' pattern['PLBSID'] = 'Profense Web Application Firewall' pattern['X-SL-CompState'] = 'AppWall (Radware)' pattern['safedog'] = 'Safedog Web Application FW' pattern['Sucuri/Cloudproxy|X-Sucuri'] = 'CloudProxy WebSite FW' pattern['X-Sucuri'] = 'CloudProxy WebSite FW' pattern['st8(id)'] = 'Teros/Citrix Application FW' pattern['st8(_wat)'] = 'Teros/Citrix Application FW' pattern['st8(_wlf)'] = 'Teros/Citrix Application FW' pattern['F5-TrafficShield'] = 'TrafficShield' pattern['Rejected-By-UrlScan'] = 'MS UrlScan' pattern['Secure Entry Server'] = 'USP Secure Entry Server' pattern['nginx-wallarm'] = 'Wallarm Web Application FW' pattern['WatchGuard'] = 'WatchGuard ' pattern['X-Powered-By-360wzb'] = '360 Web Application' pattern['WebKnight'] = 'WebKnight Application FW' pattern.each do |key,value| if !@response[key].nil? time = Time.now puts '[I]'.blue + " [#{time.strftime('%H:%M:%S')}] Found WAF: #{value}" @report.add_issue("i","d","-","-","","Found WAF: #{value}") end end [false, "not reflected #{@query}"] end end class CallbackCheckHeaders < ScanCallbackFunc def run if !@response['Server'].nil? # Server header @report.add_issue("i","s","-","-","","Found Server: #{@response['Server']}") end if @response['Strict-Transport-Security'].nil? # HSTS @report.add_issue("i","s","-","-","","Not set HSTS") end if !@response['Content-Type'].nil? @report.add_issue("i","s","-","-","","Content-Type: #{@response['Content-Type']}") end if !@response['X-XSS-Protection'].nil? @report.add_issue("i","s","-","-","","Not set X-XSS-Protection") end if !@response['X-Frame-Options'].nil? @report.add_issue("i","s","-","-","","X-Frame-Options: #{@response['X-Frame-Options']}") else @report.add_issue("l","s","-","-","","Not Set X-Frame-Options") end if !@response['Content-Security-Policy'].nil? begin csp = @response['Content-Security-Policy'] csp = csp.split(';') r = " " csp.each do |c| d = c.split " " r = r+d[0]+" " end @report.add_issue("i","s","-","-","","Set CSP(#{r})") rescue @report.add_issue("i","s","-","-","","CSP ERROR") end else @report.add_issue("m","s","-","-","","Not Set CSP") end [false, "not reflected #{@query}"] end end class CallbackErrorPatternMatch < ScanCallbackFunc def run info = "Found" if @response.body.to_s.match(/(SQL syntax.*MySQL|Warning.*mysql_.*|MySqlException \(0x|valid MySQL result|check the manual that corresponds to your (MySQL|MariaDB) server version|MySqlClient\.|com\.mysql\.jdbc\.exceptions)/i) info = info + "MYSQL Error" end if @response.body.to_s.match(/(Driver.* SQL[\-\_\ ]*Server|OLE DB.* SQL Server|\bSQL Server.*Driver|Warning.*mssql_.*|\bSQL Server.*[0-9a-fA-F]{8}|[\s\S]Exception.*\WSystem\.Data\.SqlClient\.|[\s\S]Exception.*\WRoadhouse\.Cms\.|Microsoft SQL Native Client.*[0-9a-fA-F]{8})/i) info = info + "MSSQL Error" end if @response.body.to_s.match(/(\bORA-\d{5}|Oracle error|Oracle.*Driver|Warning.*\Woci_.*|Warning.*\Wora_.*)/i) info = info + "Oracle Error" end if @response.body.to_s.match(/(PostgreSQL.*ERROR|Warning.*\Wpg_.*|valid PostgreSQL result|Npgsql\.|PG::SyntaxError:|org\.postgresql\.util\.PSQLException|ERROR:\s\ssyntax error at or near)/i) info = info + "Postgres Error" end if @response.body.to_s.match(/(Microsoft Access (\d+ )?Driver|JET Database Engine|Access Database Engine|ODBC Microsoft Access)/i) info = info + "MSAccess Error" end if @response.body.to_s.match(/(SQLite\/JDBCDriver|SQLite.Exception|System.Data.SQLite.SQLiteException|Warning.*sqlite_.*|Warning.*SQLite3::|\[SQLITE_ERROR\])/i) info = info + "SQLite Error" end if @response.body.to_s.match(/(Warning.*sybase.*|Sybase message|Sybase.*Server message.*|SybSQLException|com\.sybase\.jdbc)/i) info = info + "SyBase Error" end if @response.body.to_s.match(/(Warning.*ingres_|Ingres SQLSTATE|Ingres\W.*Driver)/i) info = info + "Ingress Error" end if info.length > 5 [true, "#{@info}"] else [false, "#{@info}"] end end end class CallbackXSSSelenium < ScanCallbackFunc def run begin options = Selenium::WebDriver::Firefox::Options.new(args: ['-headless']) driver = Selenium::WebDriver.for(:firefox, options: options) if @method == "GET" begin driver.get(@url+"?"+@query) alert = driver.switch_to().alert() if alert.text.to_s == "45" driver.quit return [true, "found alert/prompt/confirm (45) in selenium!! #{@query}"] else driver.quit return [true, "found alert/prompt/confirm event in selenium #{@query}"] end rescue Selenium::WebDriver::Error::UnexpectedAlertOpenError => e driver.quit return [true, "found alert/prompt/confirm error base in selenium #{@query}"] rescue => e driver.quit return [false, "not found alert/prompt/confirm event #{@query}"] end end rescue => e log('s', "Error Selenium : #{e}") end end end def run r = [] event_handler = [ 'onabort', 'onactivate', 'onafterprint', 'onafterscriptexecute', 'onafterupdate', 'onanimationcancel', 'onanimationstart', 'onauxclick', 'onbeforeactivate', 'onbeforecopy', 'onbeforecut', 'onbeforedeactivate', 'onbeforeeditfocus', 'onbeforepaste', 'onbeforeprint', 'onbeforescriptexecute', 'onbeforeunload', 'onbeforeupdate', 'onbegin', 'onblur', 'onbounce', 'oncanplay', 'oncanplaythrough', 'oncellchange', 'onchange', 'onclick', 'oncontextmenu', 'oncontrolselect', 'oncopy', 'oncut', 'ondataavailable', 'ondatasetchanged', 'ondatasetcomplete', 'ondblclick', 'ondeactivate', 'ondrag', 'ondragdrop', 'ondragend', 'ondragenter', 'ondragleave', 'ondragover', 'ondragstart', 'ondrop', 'onend', 'onerror', 'onerrorupdate', 'onfilterchange', 'onfinish', 'onfocus', 'onfocusin', 'onfocusout', 'onhashchange', 'onhelp', 'oninput', 'oninvalid', 'onkeydown', 'onkeypress', 'onkeyup', 'onlayoutcomplete', 'onload', 'onloadend', 'onloadstart', 'onloadstart', 'onlosecapture', 'onmediacomplete', 'onmediaerror', 'onmessage', 'onmousedown', 'onmouseenter', 'onmouseleave', 'onmousemove', 'onmouseout', 'onmouseover', 'onmouseup', 'onmousewheel', 'onmove', 'onmoveend', 'onmovestart', 'onoffline', 'ononline', 'onoutofsync', 'onpageshow', 'onpaste', 'onpause', 'onplay', 'onplaying', 'onpointerdown', 'onpointerenter', 'onpointerleave', 'onpointermove', 'onpointerout', 'onpointerover', 'onpointerup', 'onpopstate', 'onprogress', 'onpropertychange', 'onreadystatechange', 'onredo', 'onrepeat', 'onreset', 'onresize', 'onresizeend', 'onresizestart', 'onresume', 'onreverse', 'onrowdelete', 'onrowexit', 'onrowinserted', 'onrowsenter', 'onscroll', 'onsearch', 'onseek', 'onselect', 'onselectionchange', 'onselectstart', 'onstart', 'onstop', 'onstorage', 'onsubmit', 'onsyncrestored', 'ontimeerror', 'ontimeupdate', 'ontoggle', 'ontouchend', 'ontouchmove', 'ontouchstart', 'ontrackchange', 'ontransitioncancel', 'ontransitionend', 'ontransitionrun', 'onundo', 'onunhandledrejection', 'onunload', 'onurlflip', 'onvolumechange', 'onwaiting', 'onwheel', 'whatthe=""onload' ] tags = [ "script", "iframe", "svg", "img", "video", "audio", "meta", "object", "embed", "style", "frame", "frameset", "applet" ] special_chars =[ ">", "<", '"', "'", "`", ";", "|", "(", ")", "{", "}", "[", "]", ":", ".", ",", "+", "-", "=", "$" ] useful_code = [ "javascript:", "JaVasCriPt:", "jaVas%0dcRipt:", "jaVas%0acRipt:", "jaVas%09cRipt:", "data:", "alert(", "alert`", "prompt(", "prompt`", "confirm(", "confirm`", "document.location", "document.cookie", "window.location" ] ## [ Parameter Analysis ] log('s', 'analysis request..') r.push makeQueryPattern('x', '', '', 'i', "Found WAF", CallbackCheckWAF) r.push makeQueryPattern('s', '', '', 'i', "-", CallbackCheckHeaders) r.push makeQueryPattern('d', 'XsPeaR"', 'XsPeaR"', 'i', "Found SQL Error Pattern", CallbackErrorPatternMatch) r.push makeQueryPattern('r', 'rEfe6', 'rEfe6', 'i', 'reflected parameter', CallbackStringMatch) r = r.flatten r = r.flatten threads = [] r.each_slice(@thread) do |jobs| jobs.map do |node| Thread.new do begin result, req, res = task(node[:query], node[:inject], node[:pattern], node[:callback]) # p result.body if @verbose.to_i > 2 log('d', "[#{res.code}/#{res.message}] #{node[:query]} in #{node[:inject]}\n[ Request ]\n#{req.to_hash.inspect}\n[ Response ]\n#{res.to_hash.inspect}") end if result[0] log(node[:category], "[#{res.code}/#{res.message}] "+(result[1]).to_s.yellow+"[param: #{node[:param]}][#{node[:desc]}]") @report.add_issue(node[:category],node[:type],node[:param],node[:query],node[:pattern],node[:desc]) @reflected_params.push node[:param] elsif (node[:callback] == CallbackNotAdded) && (result[1].to_s == "true") @filtered_objects[node[:param].to_s].nil? ? (@filtered_objects[node[:param].to_s] = [node[:pattern].to_s]) : (@filtered_objects[node[:param].to_s].push(node[:pattern].to_s)) elsif node[:type] != "d" log('d', "[#{res.code}/#{res.message}] '#{node[:param]}' "+(result[1]).to_s) end rescue => e end end end.each(&:join) end log('s',"creating a test query [for reflected #{@reflected_params.length} param + blind xss ]") @param_check_switch = false ## [ XSS Scanning ] r = [] # Check Special Char special_chars.each do |sc| r.push makeQueryPattern('f', "#{sc}XsPeaR", "#{sc}XsPeaR", 'i', "not filtered "+"#{sc}".blue, CallbackNotAdded) end # Check Event Handler r.push makeQueryPattern('f', '\">', 'onhwul=64', 'i', "not filtered event handler "+"on{any} pattern".blue, CallbackStringMatch) event_handler.each do |ev| r.push makeQueryPattern('f', "\"", "#{ev}=64", 'i', "not filtered event handler "+"#{ev}=64".blue, CallbackNotAdded) end # Check HTML Tag tags.each do |tag| r.push makeQueryPattern('f', "\">xsp<#{tag}>", "xsp<#{tag}>", 'i', "not filtered "+"<#{tag}>".blue, CallbackNotAdded) end # Check useful code useful_code.each do |c| r.push makeQueryPattern('f', "#{c}.xspear", "#{c}.xspear", 'i', "not filtered "+"'#{c}' code".blue, CallbackNotAdded) end # Check Common XSS Payloads onfocus_tags = [ "input", "select", "textarea", "keygen" ] r.push makeQueryPattern('x', '">', '', 'h', "reflected "+"XSS Code".red, CallbackStringMatch) r.push makeQueryPattern('x', '', '', 'h', "reflected "+"XSS Code".red, CallbackStringMatch) r.push makeQueryPattern('x', '', '', 'h', "reflected "+"XSS Code".red, CallbackStringMatch) r.push makeQueryPattern('x', '">ipt>alert(45)ipt>', '', 'h', "reflected "+"XSS Code".red, CallbackStringMatch) r.push makeQueryPattern('x', '">", "BLINDNOTDETECTED", 'i', "", CallbackNotAdded) end r = r.flatten r = r.flatten log('s', "test query generation is complete. [#{r.length} query]") log('s', "starting XSS Scanning. [#{@thread} threads]") threads = [] r.each_slice(@thread) do |jobs| jobs.map do |node| Thread.new do begin result, req, res = task(node[:query], node[:inject], node[:pattern], node[:callback]) # p result.body if @verbose.to_i > 2 log('d', "[#{res.code}/#{res.message}] #{node[:query]} in #{node[:inject]}\n[ Request ]\n#{req.to_hash.inspect}\n[ Response ]\n#{res.to_hash.inspect}") end if result[0] log(node[:category], "[#{res.code}/#{res.message}] "+(result[1]).to_s.yellow+"[param: #{node[:param]}][#{node[:desc]}]") @report.add_issue(node[:category],node[:type],node[:param],node[:query],node[:pattern],node[:desc]) elsif (node[:callback] == CallbackNotAdded) && (result[1].to_s == "true") @filtered_objects[node[:param].to_s].nil? ? (@filtered_objects[node[:param].to_s] = [node[:pattern].to_s]) : (@filtered_objects[node[:param].to_s].push(node[:pattern].to_s)) elsif node[:type] != "f" log('d', "[#{res.code}/#{res.message}] '#{node[:param]}' "+(result[1]).to_s) end rescue => e end end end.each(&:join) end @report.set_filtered @filtered_objects @report.set_endtime log('s', "finish scan. the report is being generated..") if @output == 'json' puts @report.to_json else @report.to_cli end end def reporter @report end def makeQueryPattern(type, payload, pattern, category, desc, callback) # type: [r]eflected param # [f]ilted rule # [x]ss # [s]tatic # [d]ynamic result = [] if type == 's' if @data.nil? result.push("inject": 'url',"param":"STATIC" ,"type": type, "query": @url, "pattern": pattern, "desc": desc, "category": category, "callback": callback) else result.push("inject": 'body',"param":"STATIC" ,"type": type, "query": @url, "pattern": pattern, "desc": desc, "category": category, "callback": callback) end else uri = URI.parse(@url) begin params = URI.decode_www_form(uri.query) params.each do |p| if (@param_check_switch) || (@reflected_params.include? p[0]) || pattern == "BLINDNOTDETECTED" if @params.nil? || (@params.include? p[0] if !@params.nil?) attack = "" dparams = params dparams.each do |d| attack = uri.query.sub "#{d[0]}=#{d[1]}","#{d[0]}=#{d[1]}#{URI::encode(payload)}" if p[0] == d[0] #d[1] = p[1] + payload if p[0] == d[0] end result.push("inject": 'url',"param":p[0] ,"type": type, "query": attack, "pattern": pattern, "desc": desc, "category": category, "callback": callback) end end end unless @data.nil? params = URI.decode_www_form(@data) params.each do |p| if !@param_check_switch || (@reflected_params.include? p) if @params.nil? || (@params.include? p[0] if !@params.nil?) attack = "" dparams = params dparams.each do |d| attack = uri.query.sub "#{d[0]}=#{d[1]}","#{d[0]}=#{d[1]}#{URI::encode(payload)}" if p[0] == d[0] #d[1] = p[1] + payload if p[0] == d[0] end result.push("inject": 'body', "param":p[0], "type": type, "query": attack, "pattern": pattern, "desc": desc, "category": category, "callback": callback) end end end end rescue StandardError # bypass end result end end def task(query, injected, pattern, callback) begin uri = URI.parse(@url) request = nil method = "GET" uri.query = query if injected == 'url' if @data.nil? # GET request = Net::HTTP::Get.new(uri.request_uri) else # POST request = Net::HTTP::Post.new(uri.request_uri) request.body = query if injected == 'body' method = "POST" end Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http| request['Accept'] = '*/*' request['Connection'] = 'keep-alive' request['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:56.0) Gecko/20100101 Firefox/56.0' unless @headers.nil? @headers.split(';').each do |header| begin c = header.split(': ') request[c[0]] = c[1] unless c.nil? rescue StandardError # pass end end end response = http.request(request) result = callback.new(uri.to_s, method, pattern, response, @report).run # result = result.run # p request.headers return result, request, response end end rescue => e puts e end end