#!/usr/bin/env python3
####
### Project: Pyfuzz
### Version: 1.1.0
### Creator: Ayoob Ali ( www.AyoobAli.com )
### License: MIT
###
import http.client
import sys
import os
from optparse import OptionParser
import string
import signal
import ssl
from time import sleep
import random
import subprocess

logFile = ""

def signal_handler(signal, frame):
	print("\nScan stopped by user.")
	sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)

def printMSG(printM):
    print(printM)
    if logFile != "":
        fhandle = open(logFile, "a")
        fhandle.write(printM + "\n")
        fhandle.close()

def cmd(command = None):
    returnArr = {}
    returnArr.update({"returnCode": 99})
    try:
        if command == None or command == "":
            return returnArr
        stdout = ""
        stderr = ""
        reCode = subprocess.Popen(command,shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        stData = reCode.communicate()
        returnArr.update({"stdout": stData[0].decode("utf-8")})
        returnArr.update({"stderr": stData[1].decode("utf-8")})
        returnArr.update({"returnCode": reCode.returncode})
        reCode.terminate()
        return returnArr
    except Exception as ErrMs:
        returnArr.update({"error": ErrMs})
        return returnArr

def main():
    global logFile
    parser = OptionParser(usage="%prog -u http://example.com/en/ -l sharepoint.txt", version="%prog 1.1.0")
    parser.add_option("-u", "--url", dest="targetURL", metavar="URL", help="Target URL to scan")
    parser.add_option("-l", "--list", dest="listFile", metavar="FILE", help="List of paths to scan")
    parser.add_option("-r", "--redirect", action="store_true", dest="showRedirect", help="Show redirect codes (3xx)")
    parser.add_option("-e", "--error", action="store_true", dest="showError", help="Show Error codes (5xx)")
    parser.add_option("-s", "--sleep", dest="milliseconds", type="int", metavar="NUMBER", help="Sleep for x milliseconds after each request")
    parser.add_option("-a", "--header", action="append", dest="headers", help="Add Header to the HTTP request (Ex.: -a User-Agent x)", metavar='HEADER VALUE', nargs=2)
    parser.add_option("-b", "--body", dest="requestBody", metavar="Body", help="Request Body (Ex.: name=val&name2=val2)")
    parser.add_option("-x", "--method", dest="requestMethod", metavar="[Method]", help="HTTP Request Method (Ex.: GET, POST, PUT, etc...)")
    parser.add_option("-i", "--ignore", action="append", dest="ignoreText", metavar="Text", help="Ignore results that contain a specific string")
    parser.add_option("-m", "--min-response-size", dest="dataLength", type="int", metavar="NUMBER", help="The minimum response body size in Byte")
    parser.add_option("-g", "--log", dest="logFile", metavar="FILE", help="Log scan results to a file")
    parser.add_option("-f", "--start-from", dest="startFrom", type="int", metavar="NUMBER", help="Start scanning from path number x in the provided list")
    parser.add_option("-t", "--timeout", dest="reqTimeout", type="int", metavar="Seconds", help="Set request timeout")
    parser.add_option("-v", "--verbose", action="store_true", dest="verbose", help="Show error messages")
    parser.add_option("-d", "--define-variable", action="append", dest="variables", help="Define variables to be replaced in URL (Ex.: -d '$varExtension' 'php')", metavar='VARIABLE VALUE', nargs=2)
    parser.add_option("--cmd", dest="excCMD", metavar="Command", help="Execute shell command on each found results (Use with caution). Available variables ({#CODE#}, {#URL#}, {#SIZE#}, {#BODY#}, and {#REDIRECT#})")
    parser.add_option("-p", "--proxy", dest="httpProxy", metavar="PROXY:PORT", help="HTTP Proxy to pass the connection through (Ex.: localhost:9080)")

    startFrom = 0
    reqTimeout = 15

    (options, args) = parser.parse_args()

    if options.requestMethod == None:
        options.requestMethod = "GET"

    if options.requestBody == None:
        options.requestBody = ""

    if options.dataLength == None:
        options.dataLength = 0

    requestHeaders = {}
    if options.headers == None:
        options.headers = []

    for header in options.headers:
        requestHeaders.update({header[0]: header[1]})

    if options.variables == None:
        options.variables = []

    if options.listFile == None or options.targetURL == None:
        parser.print_help()
        sys.exit()

    if options.logFile != None:
        logFile = options.logFile

    if options.startFrom != None:
        startFrom = options.startFrom

    if options.reqTimeout != None:
        if options.reqTimeout > 0:
            reqTimeout = int(options.reqTimeout)

    excCMD = ""
    if options.excCMD != None:
        excCMD = str(options.excCMD)

    isProxy = False
    proxyHost = ""
    proxyPort = 0
    if options.httpProxy != None:
        if str(options.httpProxy).find(':') >= 0:
            httpProxy = str(options.httpProxy).split(':')
            proxyHost = httpProxy[0]
            if httpProxy[1].isnumeric() == True:
                proxyPort = int(httpProxy[1])
            isProxy = True
            if proxyPort < 1 or proxyPort > 65535:
                printMSG("Error: Port number should be between 1 and 65535.")
                sys.exit()
        else:
            printMSG("Error: Proxy format should be HOSTNAME:PORT")
            sys.exit()

    if not os.path.isfile(options.listFile):
        printMSG("Error: File (" + options.listFile + ") doesn't exist.")
        sys.exit()

    if options.targetURL[-1] != "/":
        options.targetURL += "/"

    targetPro = ""

    if options.targetURL[:5].lower() == 'https':
        targetDomain = options.targetURL[8:].split("/",1)[0].lower()
        targetPath = "/" + options.targetURL[8:].split("/",1)[1]

        if isProxy == True:
            connection = http.client.HTTPSConnection(proxyHost, proxyPort, timeout=reqTimeout, context=ssl._create_unverified_context())
            connection.set_tunnel(targetDomain)
        else:
            connection = http.client.HTTPSConnection(targetDomain, timeout=reqTimeout, context=ssl._create_unverified_context())

        targetPro = "https://"
        printMSG("Target       : " + targetPro+targetDomain + " (over HTTPS)")
        printMSG("Path         : " + targetPath)
    elif options.targetURL[:5].lower() == 'http:':
        targetDomain = options.targetURL[7:].split("/",1)[0].lower()
        targetPath = "/"+options.targetURL[7:].split("/",1)[1]
        if isProxy == True:
            connection = http.client.HTTPConnection(proxyHost, proxyPort, timeout=reqTimeout)
            connection.set_tunnel(targetDomain)
        else:
            connection = http.client.HTTPConnection(targetDomain, timeout=reqTimeout)
        targetPro = "http://"
        printMSG("Target       : " + targetDomain)
        printMSG("Path         : " + targetPath)
    else:
        targetDomain = options.targetURL.split("/",1)[0].lower()
        targetPath = "/"+options.targetURL.split("/",1)[1]
        if isProxy == True:
            connection = http.client.HTTPConnection(proxyHost, proxyPort, timeout=reqTimeout)
            connection.set_tunnel(targetDomain)
        else:
            connection = http.client.HTTPConnection(targetDomain, timeout=reqTimeout)
        targetPro = "http://"
        printMSG("Target       : " + targetDomain)
        printMSG("Path         : " + targetPath)

    printMSG("Method       : " + options.requestMethod)
    printMSG("Header       : " + str(requestHeaders))
    printMSG("Body         : " + options.requestBody)
    printMSG("Timeout      : " + str(reqTimeout))
    printMSG("Proxy        : " + str(proxyHost) + ":" + str(proxyPort))

    if options.showRedirect != None:
        printMSG("Show Redirect:  ON")
    if options.showError != None:
        printMSG("Show Error   :  ON")

    try:
        randomPage = ''.join([random.choice(string.ascii_lowercase + string.digits) for n in range(16)])
        connection.request(options.requestMethod, targetPath+randomPage+".txt", options.requestBody, requestHeaders)
        res = connection.getresponse()
    except Exception as ErrMs:
        if options.verbose != None:
            printMSG("MainError: " + str(ErrMs))
        sys.exit(0)

    if res.status == 200:
        printMSG("NOTE: Looks like the server is returning code 200 for all requests, there might be lots of false positive links.")

    if res.status >= 300 and res.status < 400 and options.showRedirect != None:
        printMSG("NOTE: Looks like the server is returning code " + str(res.status) + " for all requests, there might be lots of false positive links. try to scan without the option -r")

    tpData = res.read()

    with open(options.listFile) as lFile:
        pathList = lFile.readlines()
    totalURLs = len(pathList)
    printMSG ("Scanning ( " + str(totalURLs) + " ) files...")
    countFound = 0
    countAll = 0
    strLine = ""
    for pathLine in pathList:
        try:
            countAll = countAll + 1
            pathLine = pathLine.strip("\n")
            pathLine = pathLine.strip("\r")
            if countAll < startFrom:
                continue
            if pathLine != "":
                for variable in options.variables:
                    pathLine = pathLine.replace(variable[0], variable[1])
                if pathLine[:1] == "/":
                    pathLine = pathLine[1:]
                print (' ' * len(strLine), "\r", end="")
                strLine = "Checking ["+str(countAll)+"/"+str(totalURLs)+"] "+targetPath+pathLine
                print (strLine,"\r", end="")
                if options.milliseconds != None:
                    sleep(options.milliseconds/1000)
                connection.request(options.requestMethod, targetPath+pathLine, options.requestBody, requestHeaders)
                res = connection.getresponse()
                resBody = res.read().decode("utf-8")
                resBodySize = len(resBody)
                isignored = False
                if options.ignoreText != None:
                    for igText in options.ignoreText:
                        if igText in resBody:
                            isignored = True

                fURL = str(targetPro+targetDomain+targetPath+pathLine)
                redirectHead = ""
                exCommand = False
                if res.getheader("location") != None:
                    redirectHead = str(res.getheader("location"))
                if res.status >= 200 and res.status < 300:
                    if isignored == False and resBodySize >= options.dataLength:
                        exCommand = True
                        print (' ' * len(strLine), "\r", end="")
                        printMSG("Code " + str(res.status) + " : " + fURL + " (" + str(resBodySize) + " Byte)")
                        countFound += 1

                if options.showError != None:
                    if res.status >= 500 and res.status < 600:
                        if isignored == False and resBodySize >= options.dataLength:
                            exCommand = True
                            print (' ' * len(strLine), "\r", end="")
                            printMSG("Code " + str(res.status) + " : " + fURL)
                            countFound += 1

                if options.showRedirect != None:
                    if res.status >= 300 and res.status < 400:
                        if isignored == False and resBodySize >= options.dataLength:
                            exCommand = True
                            print (' ' * len(strLine), "\r", end="")
                            printMSG("Code " + str(res.status) + " : " + fURL + " ( " + redirectHead + " )")
                            countFound += 1

                if str(excCMD) != "" and exCommand == True:
                    cmdStr = str(excCMD)
                    cmdStr = cmdStr.replace("{#CODE#}", str(res.status))
                    cmdStr = cmdStr.replace("{#URL#}", fURL)
                    cmdStr = cmdStr.replace("{#SIZE#}", str(resBodySize))
                    cmdStr = cmdStr.replace("{#REDIRECT#}", redirectHead)
                    cmdStr = cmdStr.replace("{#BODY#}", resBody)
                    cmdRes = cmd(str(cmdStr))
                    if options.verbose != None and isinstance(cmdRes, dict) and 'stdout' in cmdRes:
                        printMSG(cmdRes['stdout'])

        except Exception as ErrMs:
            if options.verbose != None:
                print (' ' * len(strLine), "\r", end="")
                printMSG("Error[" + str(countAll) + "]: " + str(ErrMs))
            try:
                connection.close()
                pass
            except Exception as e:
                if options.verbose != None:
                    printMSG("Error2:" + str(e))
                pass
            
        
    connection.close()
    print (' ' * len(strLine), "\r", end="")
    printMSG( "Total Pages found: " + str(countFound) )



if __name__ == "__main__":
    main()
