NetsOSS/headless-burp

View on GitHub
headless-burp-scanner/src/main/java/burp/BurpExtender.java

Summary

Maintainability
A
1 hr
Test Coverage
package burp;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import eu.nets.burp.BurpConfiguration;
import eu.nets.burp.JUnitXmlGenerator;
import eu.nets.burp.config.Issue;
import eu.nets.burp.config.ReportType;
import java.io.File;
import java.net.URL;
import java.time.Instant;
import java.util.List;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.Option;
import org.kohsuke.args4j.ParserProperties;

import static com.google.common.base.Throwables.getStackTraceAsString;

public class BurpExtender implements IBurpExtender, IHttpListener, IScannerListener, IExtensionStateListener {

    @Option(name = "-p", aliases = "--prompt", usage = "Indicates whether to prompt the user to confirm the shutdown (useful for debugging)")
    private boolean promptUserOnShutdown = false;

    @Option(name = "-c", aliases = "--config", usage = "Configuration file", metaVar = "<file>", required = true)
    private File configurationFile = new File("configuration.txt");

    @Option(name = "-v", aliases = "--verbose", usage = "Enable verbose output")
    private boolean verbose = false;

    private IBurpExtenderCallbacks callbacks;
    private IExtensionHelpers helpers;
    private Instant lastRequestTime;

    private volatile List<IScanQueueItem> scanQueueItems = Lists.newArrayList();
    private List<IScanIssue> scanIssues = Lists.newArrayList();
    private BurpConfiguration config;

    /**
     * {@inheritDoc}
     */
    @Override
    public void registerExtenderCallbacks(IBurpExtenderCallbacks extenderCallbacks) {
        callbacks = extenderCallbacks;
        helpers = callbacks.getHelpers();

        callbacks.setExtensionName("Headless Burp Scanner");

        callbacks.registerHttpListener(this);
        callbacks.registerScannerListener(this);
        callbacks.registerExtensionStateListener(this);

        try {
            if (!processCommandLineArguments(callbacks.getCommandLineArguments())) {
                return;
            }

            config.getExclusions().forEach(callbacks::excludeFromScope);

            scanSiteMap(config.getSiteMap());

            scanUrls(config.getUrls());

            lastRequestTime = Instant.now();
            monitorScanQueue();

            generateReport(config.getReportType(), config.getReportFile());
        } catch (Exception e) {
            log("Could not run burp scan, quitting: " + getStackTraceAsString(e));
        }

        callbacks.exitSuite(promptUserOnShutdown);
    }

    /**
     * Add {@link URL}s from the sitemap to scope and send them to the Burp scanner.
     *
     * @param siteMapUrlPrefix Base URL to scan from
     */
    private void scanSiteMap(URL siteMapUrlPrefix) {
        if (siteMapUrlPrefix != null) {
            IHttpRequestResponse[] siteMapItems = callbacks.getSiteMap(config.getSiteMap().toString());
            if (siteMapItems == null) {
                return;
            }
            log("Scanning [" + siteMapItems.length + "] from sitemap [" + config.getSiteMap().toString() + "]");
            for (IHttpRequestResponse siteMapItem : siteMapItems) {
                IRequestInfo requestInfo = helpers.analyzeRequest(siteMapItem);
                URL url = requestInfo.getUrl();
                if (verbose) {
                    log("Scanning: " + requestInfo.getMethod() + " : " + url);
                }

                if (!config.getExclusions().contains(url)) {
                    callbacks.includeInScope(url);
                    sendToScanner(siteMapItem);
                }
            }
        }
    }

    /**
     * Add the {@link URL}s to scope and send them to the Burp spider.
     *
     * @param urls list of {@link URL}s to be scanned
     */
    private void scanUrls(List<URL> urls) {
        for (URL url : urls) {
            scanUrl(url);
        }
    }

    private void scanUrl(URL url) {
        if (!callbacks.isInScope(url) && !config.getExclusions().contains(url)) {
            callbacks.includeInScope(url);
        }

        lastRequestTime = Instant.now();
        callbacks.sendToSpider(url);
        log("Starting spider on " + url + " at " + lastRequestTime);
    }

    /**
     * Parse and process the command line arguments and verify and load the configuration file supplied by the user.
     *
     * @param args The command line arguments that were passed to Burp on startup
     */
    private boolean processCommandLineArguments(String[] args) {
        ParserProperties parserProperties = ParserProperties.defaults().withUsageWidth(80);
        CmdLineParser parser = new CmdLineParser(this, parserProperties);

        try {
            log("Arguments to headless burp: " + Joiner.on(" ").join(args));
            parser.parseArgument(args);

            if (configurationFile == null || !configurationFile.exists()) {
                log("Could not find configuration file: [" + configurationFile + "]");
            }

            config = new BurpConfiguration(configurationFile);
            if (config.getUrls() == null && config.getSiteMap() == null) {
                throw new RuntimeException("Must provide either scope or sitemap");
            }

            log("Headless Burp Scanner loaded with:");
            log("configuration file: " + configurationFile.getName());
        } catch (CmdLineException cle) {
            log("No arguments found for Headless Burp, quitting");
            return false;
        } catch (Exception e) {
            log("Could not parse commandline arguments, quitting: " + getStackTraceAsString(e));
            parser.printUsage(System.err);
            return false;
        }

        return true;
    }

    /**
     * {@inheritDoc}
     * <p/>
     * Receive requests from the spider and send them to the scanner
     */
    @Override
    public void processHttpMessage(int toolFlag, boolean messageIsRequest, IHttpRequestResponse messageInfo) {
        lastRequestTime = Instant.now();
        URL requestUrl = helpers.analyzeRequest(messageInfo).getUrl();
        if (messageIsRequest
                && toolFlag == IBurpExtenderCallbacks.TOOL_SPIDER
                && callbacks.isInScope(requestUrl)) {
            sendToScanner(messageInfo);
        }
    }

    /**
     * Send the request to the Burp Scanner tool to perform an active vulnerability scan.
     *
     * @param messageInfo Details of the request / response to be processed.
     *                    Extensions can call the setter methods on this object to update the current message and so modify Burp's behavior.
     */
    private void sendToScanner(IHttpRequestResponse messageInfo) {
        IHttpService httpService = messageInfo.getHttpService();
        boolean serviceIsHttps = "https".equals(httpService.getProtocol());

        URL url = helpers.analyzeRequest(messageInfo).getUrl();
        if (callbacks.isInScope(url) && !config.getExclusions().contains(url)) {
            log("Sending URL to scanner: " + url);
            IScanQueueItem scanQueueItem = callbacks.doActiveScan(httpService.getHost(), httpService.getPort(), serviceIsHttps, messageInfo.getRequest());
            scanQueueItems.add(scanQueueItem);
        } else if (verbose) {
            log("Skipping URL: " + url);
        }
    }

    private void monitorScanQueue() {
        log("Monitoring scanQueue, waiting for spider to complete");

        try {
            while (!scanQueueItems.isEmpty()) {
                scanQueueItems.removeIf(scanQueueItem -> 100 == scanQueueItem.getPercentageComplete());
                log(scanQueueItems.size() + " remaining items in scan queue at " + Instant.now());

                Thread.yield();
                Thread.sleep(5000);
            }

            while (Instant.now().isBefore(lastRequestTime.plusSeconds(10))) {
                Thread.yield();
                Thread.sleep(5000);
            }

        } catch (Exception e) {
            log("Error when monitoring scan queue: " + getStackTraceAsString(e));
            callbacks.exitSuite(false);
        }

        log("Scanning complete at " + Instant.now());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void newScanIssue(IScanIssue issue) {
        if (isFalsePositive(issue)) {
            log("Excluding false positive of type: " + issue.getIssueType() + ", name: " + issue.getIssueName() + " found at URL: " + issue.getUrl());
        } else {
            log("New scan issue of type: " + issue.getIssueType() + ", name: " + issue.getIssueName() + " found at URL: " + issue.getUrl());
            scanIssues.add(issue);
        }
    }

    /**
     * Check if the provided issue is in the configured falsePositives list.
     *
     * @param issue Issue found by the scanner
     * @return {@code true} if Issue is in the configured falsePositives list, otherwise {@code false}
     */
    private boolean isFalsePositive(IScanIssue issue) {
        for (Issue falsePositive : config.getFalsePositives()) {
            if (falsePositive.getType() == issue.getIssueType() && issue.getUrl().getPath().matches(falsePositive.getPath())) {
                return true;
            }
        }

        return false;
    }

    /**
     * Generate a report for issues found by the Scanner.
     *
     * @param reportType     The format to be used in the report. Accepted values are HTML, XML and JUNIT.
     * @param burpReportFile The {@link File} to which the report will be saved
     */
    private void generateReport(ReportType reportType, File burpReportFile) {
        log("Generating scan report");
        callbacks.generateScanReport(config.getScanReportType(), scanIssues.toArray(new IScanIssue[scanIssues.size()]), burpReportFile);

        if (reportType == ReportType.JUNIT) {
            log("Generating JUnit report");

            File junitReportFile = new File(burpReportFile.getParentFile(), "TEST-burp-scan.xml");
            JUnitXmlGenerator.generateJUnitReportFromBurpReport(burpReportFile, junitReportFile);

            log("Generating HTML report");
            callbacks.generateScanReport("HTML", scanIssues.toArray(new IScanIssue[scanIssues.size()]), new File("burp-report.html"));
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void extensionUnloaded() {
        log("Headless Burp POC Extension was unloaded");
    }

    @VisibleForTesting
    void setConfig(BurpConfiguration config) {
        this.config = config;
    }

    @VisibleForTesting
    List<IScanIssue> getScanIssues() {
        return scanIssues;
    }

    private void log(String message) {
        callbacks.issueAlert(message);
        callbacks.printOutput(message);
    }
}