''' urlFuzzer.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 import core.data.parsers.urlParser as urlParser from core.controllers.basePlugin.baseDiscoveryPlugin import baseDiscoveryPlugin import core.data.kb.knowledgeBase as kb from core.data.getResponseType import * from core.controllers.w3afException import w3afException from core.data.fuzzer.fuzzer import * class urlFuzzer(baseDiscoveryPlugin): ''' This plugin takes an url and tries to find backups, and other related files. @author: Andres Riancho ( andres.riancho@gmail.com ) ''' def __init__(self): baseDiscoveryPlugin.__init__(self) self._firstTime = True self._fuzzImages = False self._headers = {} def discover(self, fuzzableRequest ): ''' Searches for new Url's using fuzzing. @parameter fuzzableRequest: A fuzzableRequest instance that contains (among other things) the URL to test. ''' self._fuzzableRequests = [] url = fuzzableRequest.getURL() self._headers = {'Referer':url } if self._firstTime: self._verifyHeadEnabled( url ) self._firstTime = False self.is404 = kb.kb.getData( 'error404page', '404' ) # First we need to delete fragments and query strings from URL. url = urlParser.uri2url( url ) self._verifyHeadEnabled( url ) if self._headEnabled(): response = self._urlOpener.HEAD( url, useCache=True, headers=self._headers ) else: response = self._urlOpener.GET( url, useCache=True, headers=self._headers, getSize=True ) if isTextOrHtml( response.getHeaders() ) or self._fuzzImages: mutants = self._mutate( url ) om.out.debug('urlFuzzer is testing ' + url ) for mutant in mutants : targs = ( url, mutant ) self._tm.startFunction( target=self._doRequest, args=targs, ownerObj=self ) self._tm.join( self ) return self._fuzzableRequests def _doRequest( self, url, mutant ): try: response = self._urlOpener.GET( mutant, useCache=True, headers=self._headers ) except KeyboardInterrupt,e: raise e else: if not self.is404( response ) and response.getCode() not in [403, 401]: if not self._returnWithoutEval( mutant ): frList = self._createFuzzableRequests( response ) self._fuzzableRequests.extend( frList ) def _returnWithoutEval( self, uri ): ''' This method tries to lower the false positives. ''' if urlParser.getDomainPath( uri ) == uri: return False url = urlParser.uri2url( uri ) url += createRandAlNum( 7 ) if urlParser.getQueryString( uri ): url = url + '?' + str( urlParser.getQueryString( uri ) ) try: response = self._urlOpener.GET( url, useCache=True, headers=self._headers ) except KeyboardInterrupt,e: raise e except Exception,e: om.out.error( 'Error when requesting: '+ url ) om.out.error('Error: ' + str(e) ) else: if not self.is404( response ): return True return False def _mutate(self, url): ''' Mutate this URL ! @param url: The original url @return: A list of bad looking mutant URL's. ''' mutants = [] mutants = self._mutateByAppending( url ) mutants.extend( self._mutatePath( url ) ) mutants.extend( self._mutateFileType( url ) ) mutants.extend( self._mutateDomainName( url ) ) mutants = list( set( mutants ) ) return mutants def _mutateDomainName( self, url ): ''' If the url is : "http://www.foobar.com/asd.txt" this method returns: - http://www.foobar.com/foobar.zip - http://www.foobar.com/foobar.rar - http://www.foobar.com/www.foobar.zip - http://www.foobar.com/www.foobar.rar - etc... @return: A list of mutants. ''' domain = urlParser.getDomain( url ) domainPath = urlParser.getDomainPath( url ) splittedDomain = domain.split('.') res = [] for i in xrange( len ( splittedDomain ) ): filename = '.'.join(splittedDomain[0: i+1]) for extension in self._getBackupExtensions(): res.append( domainPath + filename + '.' + extension ) ### TODO: review this code !! return res def _mutateByAppending( self, url ): ''' Adds something to the end of the url (mutate the file being requested) @return: A list of mutants. ''' mutants = [] if url[ len( url ) -1 ] != '/': toAppendList = self._getToAppend() for toAppend in toAppendList: mutants.append ( url + toAppend ) return mutants def _mutateFileType( self, url ): ''' Mutates a URL by changing its filetype, example : url = http://g.ar/foo.php result = http://g.ar/foo.zip , http://g.ar/foo.tgz , etc... @return: A mutant list. ''' mutants = [] if url.rfind('.') > url.rfind('/'): # Has a file specification # http://a.com/foo.asp # ^ This url = url[ : url.rfind('.')+1 ] filetypes = self._getFileTypes() for filetype in filetypes: mutants.append ( url + filetype ) return mutants def _mutatePath( self, url ): ''' Mutate the path instead of the file. @return: A list of mutants. ''' mutants = [] if url.count('/') > 3: url = url[: url.rfind('/') ] toAppendList = self._getToAppend() for toAppend in toAppendList: mutants.append ( url + toAppend ) mutants.append( url ) mutants.append( url + '/') return mutants def _getBackupExtensions( self ): fileTypes = [] fileTypes.append ( 'tar.gz' ) fileTypes.append ( 'gz' ) fileTypes.append ( 'tgz' ) fileTypes.append ( 'gzip' ) fileTypes.append ( 'bzip2' ) fileTypes.append ( 'zip' ) fileTypes.append ( 'rar' ) return fileTypes def _getFileTypes( self ): ''' @return: A list with filetypes commonly used in web apps. ''' fileTypes = [] fileTypes.extend( self._getBackupExtensions() ) fileTypes.append ( 'inc' ) fileTypes.append ( 'fla' ) # flash fileTypes.append ( 'jar' ) fileTypes.append ( 'java' ) fileTypes.append ( 'class' ) fileTypes.append ( 'properties' ) fileTypes.append ( 'bak' ) fileTypes.append ( 'bak1' ) fileTypes.append ( 'backup' ) fileTypes.append ( 'backup1' ) fileTypes.append ( 'old' ) fileTypes.append ( 'old1' ) fileTypes.append ( 'c' ) fileTypes.append ( 'cpp' ) fileTypes.append ( 'cs' ) fileTypes.append ( 'disco' ) # webservice .disco files ! return fileTypes def _getToAppend( self ): ''' @return: A list of strings to append to the URL. ''' appendables = [] appendables.append ( '~' ) appendables.append ( '.tar.gz' ) appendables.append ( '.gz' ) appendables.append ( '.tgz' ) appendables.append ( '.gzip' ) appendables.append ( '.bzip2' ) appendables.append ( '.inc' ) appendables.append ( '.zip' ) appendables.append ( '.rar' ) appendables.append ( '.jar' ) appendables.append ( '.java' ) appendables.append ( '.class' ) appendables.append ( '.properties' ) appendables.append ( '.bak' ) appendables.append ( '.bak1' ) appendables.append ( '.backup' ) appendables.append ( '.backup1' ) appendables.append ( '.old' ) appendables.append ( '.old1' ) appendables.append ( '.$$$' ) # mariano return appendables def _verifyHeadEnabled(self, url ): ''' Verifies if the requested URL permits a HEAD request. This was saved inside the KB by the plugin allowedMethods @return : Sets self._head to the correct value, nothing is returned. ''' if 'HEAD' in kb.kb.getData( 'allowedMethods' , 'methods' ): self._head = True else: self._head = False def _headEnabled(self): return self._head 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/ui/userInterface.dtd @return: XML with the plugin options. ''' return '\ \ \ \ ' def setOptions( self, optionsMap ): ''' This method sets all the options that are configured using the user interface generated by the framework using the result of getOptionsXML(). @parameter OptionList: A dictionary with the options for the plugin. @return: No value is returned. ''' self._fuzzImages = optionsMap['fuzzImages'] def getPluginDeps( self ): ''' @return: A list with the names of the plugins that should be runned before the current one. ''' return ['discovery.error404page','discovery.allowedMethods'] def getLongDesc( self ): ''' @return: A DETAILED description of the plugin functions and features. ''' return ''' This plugin will try to find new URL's based on the input. If the input is for example: - http://a/a.html The plugin will request: - http://a/a.html.tgz - http://a/a.tgz - http://a/a.zip ... etc If the response is not a 404 error, then we have found a new URL. This plugin searches for backup files, source code, and other common extensions. One configurable parameter exist: - fuzzImages '''