'''
detectPhishing.py

Copyright 2006 Andres Riancho

This file is part of w3af, w3af.sourceforge.net .

w3af is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 2 of the License.

w3af is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with w3af; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

'''

import core.controllers.outputManager as om
from core.controllers.basePlugin.baseGrepPlugin import baseGrepPlugin
import core.data.kb.knowledgeBase as kb
import core.data.kb.vuln as vuln
import core.data.parsers.dpCache as dpCache
from core.data.parsers.urlParser import *
from core.data.getResponseType import *
import urllib

class detectPhishing(baseGrepPlugin):
	'''
	This plugin greps every page and assigns a phishing score to it. 
	
	@author: Andres Riancho ( andres.riancho@gmail.com ) ; special thanks to Matthew Hall !
	'''

	def __init__(self):
		baseGrepPlugin.__init__(self)
		
		# User configured variables
		self._phishLimit = 65

	def _testResponse(self, request, response):
		
		if isTextOrHtml(response.getHeaders()) :
			
			
			urlScore = self._scoreURL( response )
			referenceScore = self._scoreReferences( response )
			formsScore = self._scoreForms( response )
			bodyScore = self._scoreBody( response )
			
			phishingScore = urlScore + referenceScore + formsScore + bodyScore
			
			if phishingScore > self._phishLimit:
				v = vuln.vuln()
				v.setURL( response.getURL() )
				v.setURI( response.getURI() )				
				v.setId( response.id )
				v.setDesc( 'The URL : '+ response.getURL()  + ' seems to be a phishing scam. The phishingScore is: ' + str(phishingScore) )
				v['phishingScore'] = phishingScore
				kb.kb.append( self, 'phishing', v )
			
			om.out.debug('The URL: ' + response.getURL() + ' returned a phishing score of: ' + str(phishingScore) )
			om.out.debug('Detailed scoring:  ' )
			om.out.debug('The urlScore is:  ' + str(urlScore) )
			om.out.debug('The referenceScore is:  ' + str(referenceScore) )
			om.out.debug('The formsScore is:  ' + str(formsScore) )
			om.out.debug('The bodyScore is:  ' + str(bodyScore) )
				
	def _scoreURL( self, response ):
		'''
		Assign a phishing score to the URL.
		@return: A number indicating the phishing score of this page.
		'''
		urlScore = 0
		url = urllib.unquote( response.getURL() )
		
		# Long paths are not common in true bank logins
		if getPath( url ).count('/') >= 5:
			urlScore += 5
		elif getPath( url ) .count('/') >= 7:
			urlScore += 8
			
		# Bank logins are almost always https...
		if not url.startswith('https://'):
			urlScore += 20
			
		# Not so many banks use php, and many scammers do...
		if getPath( url ).split('.')[-1:][0].lower().count( 'php' ):
			urlScore += 10
		
		# This is a true banking scam , many dots in URL also indicate 
		# that something is wrong
		# http://www.53.com.bankingportal.id57473179.billwartell.info/conf
		if getPath( url ).count( '.' ) >= 5:
			urlScore += 10
		
		# This catches URLs like http://a0s9g019270q293710293712039.a.com/phish.com/
		if len( getPath( url ) ) >= 30:
			urlScore += 10
		
		# Hidden dirs or files
		if url.count('/.'):
			urlScore += 10
			
		# URL's that have much URL encoding are bad also
		# http://mal.icio.us/ becomes http://%6D%61%6C%2E%69%63%69%2E%75%73/
		if len ( url ) > (1,5 * len( response.getURL() ) ) :
			# There is too much encoding in this URL
			urlScore += 10
		
		# http://12.34.5.67/ is http://203556163/ in decimal.
		if getDomain( url ).isdigit():
			urlScore += 25
		
		# awfull banking sites wih xss errors!
		# http://www.barclays.com/script.asp?foo=<script>frame("http://a.malicious.url/")</script>
		if url.count('<script') or url.count('javascript'):
			urlScore += 20
		
		# http://81.92.144.2:84/www.ebay.com/signin.ebay.com.ws.eBayISAPI.dllSignIn&co_partnerId=2&pUserId=&siteid=0&pageType=&pa1=&i1=&bshowgif=&UsingSSL.html
		# No real bank/pay logins run webservices on anything other than 80 & 443
		# (and if they do, they should be shot ;) ). High score.
		if getDomain( url ).count(':'):
			urlScore += 20
		
		return urlScore
	
	def _scoreReferences( self, response ):
		'''
		Assign a phishing score to the references.
		@return: A number indicating the phishing score of this page.
		'''
		score = 0
		
		dp = dpCache.dpc.getDocumentParserFor( response.getBody(), response.getURL() )
		references = dp.getReferences()
		try:
			rootDomain = getRootDomain( response.getURL() )
		except:
			om.out.debug( str(e) )
		else:
			externalReferences = 0
			internalReferences = 0
			for reference in references:
				# If its a subdomain...
				if not getDomain( reference ).endswith( rootDomain ):
					externalReferences += 1
					
			score = 10 * externalReferences
			
		return score
		
	def _scoreBody( self, response ):
		'''
		Assigna phishing score to the body
		'''
		bodyScore = 0
		
		dp = dpCache.dpc.getDocumentParserFor( response.getBody(), response.getURL() )
		comments = dp.getComments()
		
		bannedWords = ['saved ', 'hack', 'phish', 'stupid', 'ass', 'owned']
		for word in bannedWords:
			for comment in comments:
				if comment.lower().count( word ):
					bodyScore += 20
				
		return bodyScore
		
	def _scoreForms( self, response ):
		'''
		Assign a phishing score to the forms on the page.
		@return: A number indicating the phishing score of this page.
		'''
		formsScore = 0
		
		dp = dpCache.dpc.getDocumentParserFor( response.getBody(), response.getURL() )
		forms = dp.getForms()
		rootDomain = getRootDomain( response.getURL() )
		
		# scammers dont want you to get distracted with a lot of forms on the phishing site
		if len( forms ) == 1:
			formsScore += 3
		
		for form in forms:
			if self._isLoginForm( form ):
				formsScore += 5
				
				if getPath( form.getAction() ).split('.')[-1:][0].lower().count('php'):
					formsScore += 5
					
				if not form.getAction().startswith('https://'):
					formsScore += 20
				
				# If its a subdomain...
				if not getDomain( form.getAction() ).endswith( rootDomain ):
					formsScore += 10
					
		return formsScore
		
	def _isLoginForm( self, dc ):
		'''
		@return: True if this fuzzableRequest is a loginForm.
		'''
		passwd = 0
		text = 0
		other = 0
		
		for key in dc.keys():
			
			if dc.getType( key ).lower() == 'password':
				passwd += 1
			
			elif dc.getType( key ).lower() == 'text':
				text += 1
			
			else:
				other += 1
				
		if text == 1 and (passwd == 1 or passwd == 2):
			return True
		return False
		
		
	def setOptions( self, OptionList ):
		pass
	
	def getOptionsXML(self):
		'''
		This method returns a XML containing the Options that the plugin has.
		Using this XML the framework will build a window, a menu, or some other input method to retrieve
		the info from the user. The XML has to validate against the xml schema file located at :
		w3af/core/output.xsd
		'''
		return	'<?xml version="1.0" encoding="ISO-8859-1"?>\
		<OptionList>\
			<Option name="phishLimit">\
				<default>'+str(self._phishLimit)+'</default>\
				<desc>Sets the limit that separates phishing URLs from normal ones</desc>\
				<help></help>\
				<type>integer</type>\
			</Option>\
		</OptionList>\
		'

	def end(self):
		'''
		This method is called when the plugin wont be used anymore.
		'''
		self.printUniq( kb.kb.getData( 'detectPhishing', 'phishing' ), 'URL' )
	
	def getPluginDeps( self ):
		'''
		@return: A list with the names of the plugins that should be runned before the
		current one.
		'''
		return []
	
	def getLongDesc( self ):
		'''
		@return: A DETAILED description of the plugin functions and features.
		'''
		return '''
		This plugin greps every page and assigns a phishing score to it. The phishing score is calculated based
		on the links the response has, the request URL, the encoding being used and the forms present in the
		HTML code.
		'''
