Search code examples
pythonpython-3.xzap

How to run a Python 3 script in OWASP ZAP?


I'm using ZAP to run a scan of a website from the command line, using the form-based authentication script found in the ZAP API Documentation.

java -jar ./zap-2.11.1.jar -script ./auth_script.py

However, it looks like ZAP uses Jython 2.7 instead of Python 3, so running the script this way doesn't work.

I've also tried running the script directly (i.e. python3 auth_script.py), but it throws the following error:

requests.exceptions.ProxyError: HTTPConnectionPool(host='127.0.0.1', port=8080): Max retries exceeded with url: http://zap/JSON/context/action/includeInContext/?contextName=context&regex=https%3A%2F%2Fwebsite.com%2F&apikey=9qFbZD4udTzFVYo0u5UzkZX9iuzbdcJDRAquTfRk
(Caused by ProxyError('Cannot connect to proxy.', RemoteDisconnected('Remote end closed connection without response')))

Has anyone used this form-based authentication script before? How did you get it to work?


Solution

  • I figured this out, so I thought it'd be good to post an answer in case someone else is as confused as I was.

    The ZAP API Documentation is used for running a standalone Python script that makes API calls to the ZAP program. ZAP can also run Python (and other languages) from within the app, but it can't run Python 3 so I recommend using JavaScript if you need to do scripting within ZAP.

    If you want to run ZAP using the documentation from the link above, you'll need to do the following:

    Launch ZAP, then run the Python script

    You can launch without a UI by navigating to the application directory and running java -jar zap*.jar -daemon, or you can just open the application from your taskbar or file explorer or whatever.

    ZAP needs to be booted up in order for the Python API to work.

    Check that your API keys match

    Go to Tools → Options… → API and click "Generate Random Key".

    Make sure your Python script is using that key:

    zap = ZAPv2(apikey="a3eaooctaaesh82jjj8ktsinff")
    

    The API key should work from then on, but if you want to be extra safe, you can set the key when launching ZAP:

    java -jar zap*.jar -daemon -config api.key=a3eaooctaaesh82jjj8ktsinff
    

    Work around the bugs

    ZAP is an amazing open-source security tool that is also riddled with bugs, some of which completely prevent you from running any scans.

    If you run into a bug, there's a pretty good chance it's been mentioned somewhere on GitHub or the Google Group, and there might be a good workaround.



    By the time I finally got the ZAP Python API running smoothly, I had 3 files:

    • the Python script
    • a JavaScript file to handle authentication (I was scanning a website that used OAuth 2.0)
    • a shell script (to run everything)

    I recommend checking out zap_example_api_script.py on GitHub if you want a good starting point. I'll also paste my 3 files below:

    run_zap_scan.py

    """Performs a security scan of a website using Zed Attack Proxy.
    
    Uses OAuth library to fetch a token,
    and uses an HTTP sender script to add the token to each request.
    """
    
    import os
    from time import sleep
    from zapv2 import ZAPv2, spider, ascan
    from oauthlib.oauth2 import LegacyApplicationClient
    from requests_oauthlib import OAuth2Session
    
    zap = ZAPv2(apikey="a3eaooctaaesh82jjj8ktsinff")
    
    target_url = "https://website.com"
    context_name = "context :D"
    context_id = zap.context.new_context(context_name)
    
    
    def set_up_token():
        """Retrieves an OAuth access token and assigns it to a global variable.
        
        token variable will be accessed from the JavaScript file.
        """
        client_id = "[put client id here]"
    
        if LOCAL_SERVER:
            os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
    
        oauth = OAuth2Session(client=LegacyApplicationClient(client_id))
        oauth.fetch_token(
            token_url=f"{target_url}/oauth/token/",
            username="username",
            password="password",
            client_id=client_id,
            client_secret="[put client secret here]",
        )
    
        key = "access_token"
        token = oauth.token[key]
        print("fetched OAuth token -", {key: token}, end="\n\n")
    
        zap._request(
            f"{zap.base}script/action/setGlobalVar/",
            {"varKey": key, "varValue": token},
        )
    
    
    def authenticate():
        """Runs `set_up_token()` and loads the JavaScript file into ZAP."""
        zap.authentication.set_authentication_method(
            context_id,
            "manualAuthentication",
        )
        set_up_token()
    
        filename = "add_token_to_requests.js"
        scriptname = "OAuth Sender"
        zap.script.load(
            scriptname=scriptname,
            scripttype="httpsender",
            scriptengine="Graal.js",
            filename="/".join((os.getcwd(), filename)),
            scriptdescription="Adds OAuth token to each request",
            charset="UTF-8",
        )
        zap.script.enable(scriptname)
    
        print("Loaded", filename, end="\n\n")
    
    
    def run_scan(scan: spider | ascan, url: str):
        """Runs a ZAP scan on a URL (either a spider or active scan)."""
        is_spider = isinstance(scan, spider)
        scan_id = scan.scan(url)
        scan_name = f"Spider [id {scan_id}]" if is_spider else "Active Scan"
        if scan_id == 'no_implementor':
            raise Exception(f"got '{scan_id}' when scanning {url} with {scan_name}")
    
        log = lambda *args: print(scan_name, *args)
        progress = lambda: f"progress - {scan.status(scan_id)}%"
    
        print(url)
        log("- starting scan")
        log(progress())
    
        sleep_time = 2 if is_spider else 5
        while "100%" not in progress():
            sleep(sleep_time)
            log(progress())
    
        log("complete\n")
    
    
    def prune_scan_rules():
        """Disables the scan rules that we don't want to use during the active scan,
        either because they're buggy or cause false positives.
    
        Visit https://www.zaproxy.org/docs/alerts for info on all scan rules.
        """
        buggy_rules = [10047, 40025, 40039]
        false_positives = []
    
        naughty_list = ",".join(str(rule) for rule in buggy_rules + false_positives)
        zap.ascan.enable_all_scanners()
        zap.ascan.disable_scanners(naughty_list)
    
        scanners = zap.ascan.scanners()
        disabled_scanners = sorted(
            (s for s in scanners if s["enabled"] == "false"),
            key=lambda s: int(s['id']),
        )
        print(f"Out of {len(scanners)} active scan rules,", end=" ")
        print(len(disabled_scanners), "are disabled.")
        for s in disabled_scanners:
            print(f" {s['id']:>6}  {s['name']}")
        print()
    
    
    def run_scans():
        """The ZAP Spider crawls through several API endpoints,
        and then an active scan is performed.
        """
        endpoints = [
            "https://website-to-scan.com",
            "https://other-site-to-scan.com",
        ]
    
        zap.context.include_in_context(context_name, f"{target_url}.*")
    
        for url in endpoints:
            run_scan(zap.spider, url)
        print("Spider complete for all endpoints", end="\n\n")
    
        prune_scan_rules()
        run_scan(zap.ascan, target_url)
    
    
    def save_report():
        with open("scan-report.html", "w") as f:
            f.write(zap.core.htmlreport())
    
    
    try:
        authenticate()
        run_scans()
        save_report()
    finally:
        print("Shutdown ZAP →", zap.core.shutdown())
    

    add_token_to_requests.js

    /*
    * This script is based on a JavaScript file found in the ZAP community-scripts GitHub repo:
    * https://github.com/zaproxy/community-scripts/blob/main/httpsender/AddBearerTokenHeader.js
    */
    
    function sendingRequest(msg, initiator, helper) {
      var token = org.zaproxy.zap.extension.script.ScriptVars.getGlobalVar("access_token")
      msg.getRequestHeader().setHeader("Authorization", "Bearer " + token);
      return msg;
    }
    
    function responseReceived(msg, initiator, helper) { }
    

    zap_python_api.sh

    sudo apt update
    sudo apt install default-jre -y
    sudo apt install firefox -y
    sudo apt install snapd -y
    sudo snap install zaproxy --classic
    sudo apt install python3 -y
    sudo apt install python3-pip -y
    
    pip3 install python-owasp-zap-v2.4
    pip3 install oauthlib
    pip3 install requests_oauthlib
    
    cd path/to/python/and/js/scripts
    
    # we used to get this error a lot:
    # "ERROR org.parosproxy.paros.control.Control
    #  - The mandatory add-on was not found: network"
    #
    # removing add-ons-state.xml before launching fixes it
    [ ! -e ~/.ZAP/add-ons-state.xml ] || rm ~/.ZAP/add-ons-state.xml
    java -jar /snap/zaproxy/current/zap*.jar -cmd -addoninstallall
    
    [ ! -e ~/.ZAP/add-ons-state.xml ] || rm ~/.ZAP/add-ons-state.xml
    java -jar /snap/zaproxy/current/zap*.jar -daemon \
         -config api.key=a3eaooctaaesh82jjj8ktsinff &
    
    sleep 60 # give ZAP time to start up
    
    python3 run_zap_scan.py