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 = {} 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 time = Time.now puts '[I]'.blue + " [#{time.strftime('%H:%M:%S')}] reflected #{@query}" [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', 'onAfterUpdate', 'onBeforeActivate', 'onBeforeCopy', 'onBeforeCut', 'onBeforeDeactivate', 'onBeforeEditFocus', 'onBeforePaste', 'onBeforePrint', 'onBeforeUnload', 'onBeforeUpdate', 'onBegin', 'onBlur', 'onBounce', 'onCellChange', 'onChange', 'onClick', 'onContextMenu', 'onControlSelect', 'onCopy', 'onCut', 'onDataAvailable', 'onDataSetChanged', 'onDataSetComplete', 'onDblClick', 'onDeactivate', 'onDrag', 'onDragEnd', 'onDragLeave', 'onDragEnter', 'onDragOver', 'onDragDrop', 'onDragStart', 'onDrop', 'onEnd', 'onError', 'onErrorUpdate', 'onFilterChange', 'onFinish', 'onFocus', 'onFocusIn', 'onFocusOut', 'onHashChange', 'onHelp', 'onInput', 'onKeyDown', 'onKeyPress', 'onKeyUp', 'onLayoutComplete', 'onLoad', 'onloadstart', 'onLoseCapture', 'onMediaComplete', 'onMediaError', 'onMessage', 'onMouseDown', 'onMouseEnter', 'onMouseLeave', 'onMouseMove', 'onMouseOut', 'onMouseOver', 'onMouseUp', 'onMouseWheel', 'onMove', 'onMoveEnd', 'onMoveStart', 'onOffline', 'onOnline', 'onOutOfSync', 'onPaste', 'onPause', 'onPopState', 'onProgress', 'onPropertyChange', 'onReadyStateChange', 'onRedo', 'onRepeat', 'onReset', 'onResize', 'onResizeEnd', 'onResizeStart', 'onResume', 'onReverse', 'onRowsEnter', 'onRowExit', 'onRowDelete', 'onRowInserted', 'onScroll', 'onSeek', 'onSelect', 'onSelectionChange', 'onSelectStart', 'onStart', 'onStop', 'onStorage', 'onSyncRestored', 'onSubmit', 'onTimeError', 'onTrackChange', 'onUndo', 'onUnload', 'onURLFlip', 'ontouchstart', 'ontouchend', 'ontouchmove', 'onafterscriptexecute', 'onbeforescriptexecute', 'onpointerover', 'onpointerdown', 'onpointerenter', 'onpointerleave', 'onpointermove', 'onpointerout', 'onpointerup' ] 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" ] log('s', 'creating a test query.') 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) # 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', '">", "NOTDETECTED", 'i', "", CallbackNotAdded) end r = r.flatten r = r.flatten log('s', "test query generation is complete. [#{r.length} query]") log('s', "starting test and analysis. [#{@thread} threads]") threads = [] r.each_slice(@thread) do |jobs| jobs.map do |node| Thread.new do begin result, res = task(node[:query], node[:inject], node[:pattern], node[:callback]) # p result.body if @verbose.to_i > 2 log('d', "[#{res.code}] #{node[:query]} in #{node[:inject]} => #{result[1]}") end if result[0] log(node[:category], (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)) else log('d', "'#{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 @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 unless @data.nil? params = URI.decode_www_form(@data) params.each do |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 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, response end end rescue => e puts e end end