'''
crossDomainXMLChecker.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

# options
from core.data.options.optionList import optionList

from core.controllers.basePlugin.baseDiscoveryPlugin import baseDiscoveryPlugin
from core.controllers.w3afException import w3afException, w3afRunOnce
from core.controllers.coreHelpers.fingerprint_404 import is_404

import core.data.kb.knowledgeBase as kb
import core.data.kb.info as info
from core.data.bloomfilter.bloomfilter import scalable_bloomfilter

import re

class wordpress_themeauditor(baseDiscoveryPlugin):
    '''
    Attempt to find which wordpress themes are available and check them for some known vulnerabilities
    @author Stephen Breen
    '''
    def __init__(self):
        baseDiscoveryPlugin.__init__(self)
        
        # Internal variables
        self._already_tested = scalable_bloomfilter()
        self._exec = True
        self._fuzzableRequests = []
        self._wordpressBase = None
        self._runningTheme = None
        self._foundThemes = []
        self._themeList = {
                            'merchant':[self._woo_shortcode],
                            'smpl':[self._woo_shortcode],
                            'sentient':[self._woo_shortcode],
                            'whitelight':[self._woo_shortcode],
                            'shelflife':[self._woo_shortcode],
                            'olya':[self._woo_shortcode],
                            'beveled':[self._woo_shortcode],
                            'wikeasi':[self._woo_shortcode],
                            'emporium':[self._woo_shortcode],
                            'teamster':[self._woo_shortcode],
                            'woostore':[self._woo_shortcode],
                            'coquette':[self._woo_shortcode],
                            'buro':[self._woo_shortcode],
                            'swatch':[self._woo_shortcode],
                            'empire':[self._woo_shortcode],
                            'supportpress':[self._woo_shortcode],
                            'statua':[self._woo_shortcode],
                            'briefed':[self._woo_shortcode],
                            'faultpress':[self._woo_shortcode],
                            'kaboodle':[self._woo_shortcode],
                            'premiere':[self._woo_shortcode],
                            'simplicity':[self._woo_shortcode],
                            'freshnews':[self._woo_shortcode],
                            'boldnews':[self._woo_shortcode],
                            'biznizz':[self._woo_shortcode],
                            'auld':[self._woo_shortcode],
                            'elefolio':[self._woo_shortcode],
                            'chapters':[self._woo_shortcode],
                            'diner':[self._woo_shortcode],
                            'skeptical':[self._woo_shortcode],
                            'crisp':[self._woo_shortcode],
                            'sealight':[self._woo_shortcode],
                            'themorningafter':[self._woo_shortcode],
                            'coda':[self._woo_shortcode],
                            'apz':[self._woo_shortcode],
                            'spectrum':[self._woo_shortcode],
                            'boast':[self._woo_shortcode],
                            'retreat':[self._woo_shortcode],
                            'postcard':[self._woo_shortcode],
                            'delegate':[self._woo_shortcode],
                            'optimize':[self._woo_shortcode],
                            'backstage':[self._woo_shortcode],
                            'digitalfarm':[self._woo_shortcode],
                            'headlines':[self._woo_shortcode],
                            'rockstar':[self._woo_shortcode],
                            'dailyedition':[self._woo_shortcode],
                            'coffee break':[self._woo_shortcode],
                            'mainstream':[self._woo_shortcode],
                            'thejournal':[self._woo_shortcode],
                            'aperture':[self._woo_shortcode],
                            'bloggingstream':[self._woo_shortcode],
                            'thestation':[self._woo_shortcode],
                            'irresistible':[self._woo_shortcode],
                            'cushy':[self._woo_shortcode],
                            'abstract':[self._woo_shortcode],
                            'busybee':[self._woo_shortcode],
                            'typebased':[self._woo_shortcode],
                            'overeasy':[self._woo_shortcode],
                            'openair':[self._woo_shortcode],
                            'livewire':[self._woo_shortcode],
                            'thegazetteedition':[self._woo_shortcode],
                            'premiumnews':[self._woo_shortcode]
                           }
        
    def discover(self, fuzzableRequest ):
        '''
        First find the root wordpress directory (if it exists) and the current theme that this
        wordpress installation is running. Then try to enumerate themes from
        a list. For each theme found, report it and also check/report if it is open to any of
        the vulnerabilities listed in the functions here.
        
        @parameter fuzzableRequest: A fuzzableRequest instance that contains (among other things) the URL to test.
        '''
        if not self._exec:
            raise w3afRunOnce()
        else:
            self._exec = False
            
        self._findWordpressBase(fuzzableRequest)
        if(self._wordpressBase):
            self._runningTheme = self._findRunningTheme(self._wordpressBase)
            ''' Check if the running theme is in our list and vulnerable to anything'''
            self._checkTheme(self._runningTheme,running=True)
            self._findInstalledThemes(self._wordpressBase)
            
            if(self._foundThemes):
                for theme in self._foundThemes:
                    self._run_async(meth=self._checkTheme, args=(theme,False) )
                self._join()
                
        return self._fuzzableRequests

    def _do_request(self,url,analyzeFunction,analyzeParams=None):
        ret = None
        try:
            response = self._urlOpener.GET(url, useCache=False)
        except w3afException,  w3:
            msg = 'Failed to GET url: "'
            msg += url + '". Exception: "' + str(w3) + '".'
            om.out.debug( msg )
        else:
            if(analyzeParams):
                ret = analyzeFunction(response,analyzeParams)
            else:
                ret = analyzeFunction(response)
            if(ret):
                verifiedResponse = ret.pop(0)
                if(verifiedResponse and url not in self._already_tested):
                    self._already_tested.add(url)
                    self._fuzzableRequests.extend( self._createFuzzableRequests(verifiedResponse) )   
        return ret

    def _findInstalledThemes(self,wordpressBase):
        '''Checks for the existence of a theme by grabbing its style.css file'''
        for theme in self._themeList.keys():
            testUrl = wordpressBase.urlJoin('wp-content/themes/'+theme+'/style.css')
            self._run_async(meth=self._do_request, args=(testUrl, self._checkStyleCss))        
        self._join()
                
    
    def _checkTheme(self,theme,running):
        if theme in self._themeList.keys():
            for vuln in self._themeList[theme]:
                vuln(self._wordpressBase,theme,running)
    
    def _findRunningTheme(self,wordpressBase):
        '''Find which theme this wordpress site is running'''
        runningTheme = None
        if(self._wordpressBase):
            testUrl = self._wordpressBase
            runningTheme = self._do_request(testUrl,self._findPageTheme)
        return runningTheme
    
    def _findWordpressBase(self,fuzzableRequest):
        '''Check every directory in the URL for the existance of wp-login.php, it is stored at the base'''
        for domain_path in fuzzableRequest.getURL().getDirectories():
            if domain_path not in self._already_tested:
                self._already_tested.add(domain_path)
                testUrl = domain_path.urlJoin('wp-login.php')
                self._run_async(meth=self._do_request, args=(testUrl,self._checkWpLogin))
    
        self._join()
        return self._wordpressBase
              
    def _findPageTheme(self,response):
        '''Find the theme on a WP page by matching a regex since the page must include things like
           the stylesheet from the themes directory'''
        '''Chop of the http:// incase they prefix internal links with www'''
        regex_str = response.getURL().url_string[7:]+'wp-content/themes/[^\"\'/]+'
        themeMatch = re.search(regex_str, response.getBody(), re.IGNORECASE)
        if(themeMatch):
            runningTheme = themeMatch.group(0).rpartition('/')[2]
            i = info.info()
            i.setPluginName(self.getName())
            i.setId( response.id )
            i.setName( 'Wordpress Theme' )
            if(self._wordpressBase):
                i.setURL( self._wordpressBase )
            i.setDesc( 'Wordpress Theme was found to be '+runningTheme+'.' )
            kb.kb.append( self, 'wordpress_theme', i )
            om.out.information( i.getDesc() )
            return [response,runningTheme]         
       
    def _checkWpLogin(self,response):
        '''Check if the response is a valid wp-login.php page'''
        regex_str = 'Log\s*In\s*<\s*/title\s*>'
        loginMatch = re.search(regex_str, response.getBody(), re.IGNORECASE)
        if(loginMatch):
            self._wordpressBase = response.getURL().getDomainPath()
            i = info.info()
            i.setPluginName(self.getName())
            i.setId( response.id )
            i.setName( 'Wordpress Installation Base' )
            i.setURL( self._wordpressBase )
            i.setDesc( 'Wordpress installation base was found at '+self._wordpressBase+'.' )
            kb.kb.append( self, 'wordpress_base', i )
            om.out.information( i.getDesc() )
            return [response,self._wordpressBase]
        
    def _checkStyleCss(self,response):
        if not is_404(response):
            self._foundThemes.append(response.getURL().getDomainPath().url_string[:-1].rpartition('/')[2])
            i = info.info()
            i.setPluginName(self.getName())
            i.setId( response.id )
            i.setName( 'Wordpress Installed Theme' )
            i.setURL( response.getURL().getDomainPath() )
            i.setDesc( 'Wordpress installed theme found at '+response.getURL().getDomainPath().url_string )
            kb.kb.append( self, 'wordpress_installed_theme', i )
            om.out.information( i.getDesc() )
            return [response]
    
    def _analyzeResponse(self,response,params):
        '''A general method to analyze a response when no global variables need to be set and 
        nothing needs to be returned. Can be used when checking for most vulnerabilities
        @param response The response
        @param params A dictionary containing some parameters'''
        
        shortCodeMatch = re.search(params['regex'], response.getBody(), re.IGNORECASE)
        if(shortCodeMatch):
            i = info.info()
            i.setPluginName(self.getName())
            i.setId( response.id )
            i.setName( params['name'] )
            i.setURL( response.getURL() )
            i.setDesc( params['desc'] )
            kb.kb.append( self, params['code'], i )
            om.out.information( i.getDesc() )
            return [response]           

    def _woo_shortcode(self,wordpressBase,theme,running):
        '''Checks for the WooThemes shortcode vulnerability https://gist.github.com/2523147
        The theme need only be installed, doesn't have to be running so check regardless of running param'''
        testUrl = wordpressBase.urlJoin('wp-content/themes/'+theme+'/functions/js/shortcode-generator/preview-shortcode-external.php?shortcode=[test]')
        params = {'regex':'[test]',
                      'name':'ShortCode Vulnerability in WooTheme',
                      'desc':'Wordpress WooTheme was found to be installed and is vulnerable to shortcode injection through the GET parameter at '+testUrl,
                      'code':'woo_shortcode_vuln'}
        self._do_request(testUrl, self._analyzeResponse,params)
    
    def _testCheck(self,wordpressBase,theme,running):
        print 'test check'
    
    
    def getOptions( self ):
        '''
        @return: A list of option objects for this plugin.
        '''    
        ol = optionList()
        return ol
        
    def setOptions( self, OptionList ):
        '''
        This method sets all the options that are configured using the user interface 
        generated by the framework using the result of getOptions().
        
        @parameter OptionList: A dictionary with the options for the plugin.
        @return: No value is returned.
        ''' 
        pass

    def getPluginDeps( self ):
        '''
        @return: A list with the names of the plugins that should be run before the
        current one.
        '''
        return []
    
    def getLongDesc( self ):
        '''
        @return: A DETAILED description of the plugin functions and features.
        '''
        return '''
        This plugin finds crossdomain.xml files which are misconfigured to allow access from any 
        domain. 
        '''
    
