My first Burp Suite extension

Introduction

I recently had a career change from the defensive side of security to the offensive which means a whole knew set of skills to develop.

For those who are not familiar Burp Suite is a security tool for testing web applications. A great thing about Burp is that you can utilize custom extensions to expand Burp's capabilities. In order to learn more about Burp extensions and develop new skills I decided to make a simple extension that checks for the existence of a few headers in a response. Fair warning please excuse my poor Java syntax and best practices skills.

Like most my post if you want to skip to the code the extension is posted on my GitHub. It is also the best place for the most current version of the code.

Burp Extensions

Burp supports extensions written in Java (which Burp is written in), Python and Ruby. Now I have written a fair amount of Python and next to none when it comes to Ruby and Java. Burp can have memory issues with Python and Ruby extensions, and since this was an exercise in learning I choose to go with Java. Luckily, I learned C++ back in high school and know a decent amount of C# so Java wasn't a challenge to pickup.

Getting started

In order to get started I have to choose an IDE for Java because I wasn't about to try to make Visual Studio work with Java. I tried briefly all of the big three IDEs for Java and choose Netbeans for my own reasons.

Setting up my environment

In order to get the IDE to work with Burp there were a few things I needed to do in order for it to all work. First I had to add the libraries for Burp by adding the Burp Suite jar to the project's libraries.

Netbeans_Burp

Next I needed to setup the project so that when I debugged it started Burp and loaded my extension. To do this I had to add burp.StartBurp to the Run section of the project's properties.

Debug_Burp

Believe it or not that simple line is all it takes to be able to set breakpoints and debug an extension in Burp with a click of a button.

Those are the basic two settings I had to make in order to be ready to make a Burp extension. There were some special cases for this project in particular but I will skip those for brevity.

The real work

With the initial IDE setup done it was time to begin the fun part and actually write the extension. The first part to write was the BurpExtender class. A skeleton of the class is available at PortSwigger's site.

BurpExtender

package burp;

public class BurpExtender implements IBurpExtender
{
    private IBurpExtenderCallbacks callbacks;
    private final String version;
    private final String name;

    public BurpExtender()
    {
        this.name = "Burp Headers";
        this.version = "0.2";
    }

    @Override
    public void registerExtenderCallbacks(IBurpExtenderCallbacks callbacks)
    {
        this.callbacks = callbacks;

        callbacks.setExtensionName(this.name + " " + this.version);
        callbacks.registerScannerCheck(new BurpUtilities(this));
    }

    public IBurpExtenderCallbacks getCallbacks()
    {
        return this.callbacks;
    }
}

Mine is pretty basic and should be easy for anyone familiar with Java to understand except for one line.

callbacks.registerScannerCheck(new BurpUtilities(this));

This single line is what registers my extension as a scanner check in Burp Suite. Its passing itself (this) to a new instance of the class called BurpUtilities which was the next class I needed to write.

BurpUtilities

A quick caveat due to the size of the rest of the code I will post the snippets as I talk about them so please visit the GitHub page linked at the top for the full code. Since BurpUtilities was going to be a scanner extension I needed to declare it as such in Burp's eyes.

public class BurpUtilities implements IScannerCheck

The line above makes BurpUtilities an implementation of the interface IScannerCheck from Burp's API. In order to properly implement the interface I needed to implement several functions that are expected.

@Override
public List<IScanIssue> doPassiveScan(IHttpRequestResponse requestResponse)
{
    PassiveHeaders passive = new PassiveHeaders(this);
    return passive.doPassiveScan(requestResponse);
}

Above was the first function I declared. It takes a single argument which is a class from Burp called IHttpRequestResponse. In this context it is simply every request and response burp intercepts. Next I declare a new instance of a class called PassiveHeaders which I will get to later. Next the function returns the results of doPassiveScan in the class PassiveHeaders. Despite being relatively simple looking this is the core of getting Burp to do anything with the rest of the code.

@Override
public List<IScanIssue> doActiveScan(IHttpRequestResponse requestResponse, IScannerInsertionPoint insertionPoint)
{
    return null;
}

The next function to declare was doActiveScan it functions similar to the passive version only its called when performing an active scan. The null return simply makes the extension do nothing on active scans.

@Override
public int consolidateDuplicateIssues(IScanIssue existingIssue, IScanIssue newIssue)
{
    boolean names = existingIssue.getIssueName().equals(newIssue.getIssueName());
    boolean urls = existingIssue.getUrl().equals(newIssue.getUrl());

    if(names && urls)
    {
        return -1;
    }
    return 0;
}

This function is what keeps Burp from issuing duplicate scan issues when performing the checks and is rather self explanatory. The next part is relatively large but also critical to the extension working.

public class ScanIssue implements IScanIssue
{
    private final IHttpRequestResponse requestResponse;
    private final String name;
    private final String severity;
    private final String confidence;
    private final String issueBackground;
    private final String issueDetail;
    private final String remediationBackground;
    private final String remediationDetail;
    private final int type;

    public ScanIssue(IHttpRequestResponse requestResponse,
                     String name,
                     String severity,
                     String confidence,
                     String issueBackground,
                     String issueDetail,
                     String remediationDetail)
    {
        this.requestResponse = requestResponse;
        this.name = name;
        this.severity = severity;
        this.confidence = confidence;
        this.issueBackground = issueBackground;
        this.issueDetail = issueDetail;
        this.remediationBackground = null;
        this.remediationDetail = remediationDetail;
        this.type = 0x0800000;
    }

    @Override
    public String getProtocol()
    {
        return requestResponse.getProtocol();
    }

    @Override
    public String getHost()
    {
        return requestResponse.getHost();
    }

    @Override
    public int getPort()
    {
        return requestResponse.getPort();
    }

    @Override
    public URL getUrl()
    {
        return BurpUtilities.this.helpers.analyzeRequest(requestResponse).getUrl();
    }

    @Override
    public String getIssueName()
    {
        return this.name;
    }

    @Override
    public int getIssueType()
    {
        return this.type;
    }

    @Override
    public String getSeverity()
    {
        return this.severity;
    }

    @Override
    public String getConfidence()
    {
        return this.confidence;
    }

    @Override
    public String getIssueBackground()
    {
        return this.issueBackground;
    }

    @Override
    public String getRemediationBackground()
    {
        return this.remediationBackground;
    }

    @Override
    public String getIssueDetail()
    {
        return this.issueDetail;
    }

    @Override
    public String getRemediationDetail()
    {
        return this.remediationDetail;
    }

    @Override
    public IHttpRequestResponse[] getHttpMessages()
    {
        IHttpRequestResponse[] messages = { this.requestResponse };
        return messages;
    }

    @Override
    public IHttpService getHttpService()
    {
        return this.requestResponse.getHttpService();
    }
}

The above is actually another class nested inside BurpUtilities. The class is what Burp uses to build the issue that is going to be displayed. Again it is rather self explanatory based on the simplicity of the class. The way I wrote it is to keep it reusable no matter what I am doing.

If you have been following along and are familiar with Burp extension you might have noticed so far everything I have built is super generic and can be used for any scanner checks you want to perform simply by modifying whats in the doPassiveScan and/or doActiveScan functions.

PassiveHeaders

This is where the actual heavy lifting is performed in my extension. The order I am going to explain this class in will follow the custom checks from importing them to performing the check and returning an issue when an issue is detected.

headers.json

There is a resource added to the project called headers.json which is a JSON that contains all the various checks I want to perform. The reason I use something like a JSON to contain all my checks allows me to easily add or remove checks without heavy modification.

{
  "name": "No Strict-Transport-Security Header",
  "severity": "Low",
  "confidence": "Certain",
  "background": "https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#hsts",
  "detail": "HTTP Strict Transport Security (HSTS) is a web security policy mechanism which helps to protect websites against protocol downgrade attacks and cookie hijacking. It allows web servers to declare that web browsers (or other complying user agents) should only interact with it using secure HTTPS connections, and never via the insecure HTTP protocol. HSTS is an IETF standards track protocol and is specified in RFC 6797. A server implements an HSTS policy by supplying a header (Strict-Transport-Security) over an HTTPS connection (HSTS headers over HTTP are ignored).",
  "remediation": "Set the header on all pages to Strict-Transport-Security: max-age=31536000 ; includeSubDomains",
  "checks": {
    "mimes": [],
    "check": "Strict-Transport-Security"
  }
}

Above is one of the entries from the JSON. It contains the information that will be passed to the ScanIssue class along with the header that will be checked for. You might notice the mimes part which is where I can limit checks to specific MIME types if I wanted. For example say I only wanted to run the above check on the MIME type for HTML I simply would have "mimes": ["html"] as the entry.

With the format of the JSON done I needed to build some Java classes that would hold the data after importing it.

private class HeaderCheck
{
    public String name;
    public String severity;
    public String confidence;
    public String background;
    public String detail;
    public String remediation;
    public Checks checks;
}

private class Checks
{
    public String[] mimes;
    public String check;
}

The above classes are actually nested in my PassiveHeaders class and are basic skeletons to hold the data once imported from JSON. Logically the next step is to import the JSON data. This is where one of those additional setups I had to do but skipped for brevity occur. I had to import the Gson libraries so I could easily import the JSON. In addition I had to setup the IDE to compile the GSON libraries into my jar. Without the Gson libraries being compiled into the jar the extension will not work when imported into Burp.

private List<HeaderCheck> getHeaderChecks()
{
    Reader reader = new InputStreamReader(getClass().getResourceAsStream("/headers.json"));
    Gson gson = new Gson();
    HeaderCheck[] checks = gson.fromJson(reader, HeaderCheck[].class);

    return Arrays.asList(checks);
}

After importing the Gson libraries it was relatively easy as seen in the above function to import the JSON into the class (HeaderCheck) that I have created. Now that the checks are imported I need to perform the actual checks and that function is the most important one in the extension so I will split it up a little.

private List<IScanIssue> doHeaderChecks(IHttpRequestResponse requestResponse, HeaderCheck check)
{
    List<String> headers = this.helpers.analyzeResponse(requestResponse.getResponse()).getHeaders();
    Short status = this.helpers.analyzeResponse(requestResponse.getResponse()).getStatusCode();
    
    boolean goodMime = true;
    List<String> mimes = Arrays.asList(check.checks.mimes);
    String mime = this.helpers.analyzeResponse(requestResponse.getResponse()).getStatedMimeType();
    if(!mimes.isEmpty() && mime != null)
    {
        goodMime = mimes.toString().toLowerCase().contains(mime.toLowerCase());
    }

The first couple lines are simply getting the headers from the response and status code. The last little bit is a check to make sure the MIME in the header is one the check is supposed to be performed on or not. If the MIMEs from the JSON are empty or the MIME type in the header is missing it skips the check and assumes its a valid type to check for the headers.

if(!headers.toString().toLowerCase().contains(check.checks.check.toLowerCase())
        && status == 200 && goodMime)
{
    List<IScanIssue> issues = new ArrayList<>(1);
    issues.add(utility.new ScanIssue(requestResponse,
            check.name,
            check.severity,
            check.confidence,
            check.background,
            check.detail,
            check.remediation));
    return issues;
}
return null;

This last part test to see if the header its looking for is in the headers and that the status code returned is a HTTP 200. The reason for the 200 status is so it doesn't check redirects or error pages. The if statement also includes the boolean from earlier on checking the MIME type. Lastly, if the header is not there it generates an issue otherwise it returns null for no issue.

public List<IScanIssue> doPassiveScan(IHttpRequestResponse requestResponse)
{
    List<HeaderCheck> checks = getHeaderChecks();
    List<IScanIssue> issues = new ArrayList<>(1);

    checks.forEach((check) -> {
        List<IScanIssue> issue = doHeaderChecks(requestResponse, check);
        if(issue != null) { issues.addAll(issue); }
    });
    return issues;
}

Finally, there is the doPassivScan function that was called from BurpUtilities. This function simply calls the import of the JSON and loops through each entry to call the check. It also contains some logic to not try and add an empty or null issue to the list it returns. Without the logic the extension threw a bunch of NullPointerException errors.

Summary

Extension_Issue
I learned a decent amount making this simple extension and it was a lot of fun. Of course now that I have written one extension I will most likely write a lot more of them as I encounter checks or testing I can automate even if its already been done because I am a huge fan of reinventing the wheel.