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.
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.
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
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.