Prepare to Write A Scanner Plugin Before Your Next Platform Test!
BurpSuite is a remarkably extensible platform. While I have written a number of extensions for testing specific applications, as well as more general extensions, one type of extension I had never attempted before was creating my own BurpSuite Scanner plugin.
Because modern applications are increasingly difficult to exhaustively test for certain types of issues, I thought it might be worth implementing a scanner plugin. In the days when everything used server-side templates, we could design a payload we wanted to inject and it may have gone into a header directly. It could also be that we needed to URL encode the payload and start copy/pasting it into query strings or POST body parameters, moving through them all quickly.
With today’s applications, especially those that have grown organically as technology has shifted, it tends to be difficult to try a specific payload on all the insertion points. We are faced with a mixture of URL encoded parameters, JSON, SOAP XML, and sometimes nested things such as URL encoded JSON in POST parameters. Let’s consider the POST body below.
Thing1=someValue&Thing2=%7B%22param1%22%3A%22foo%22%2C+%22param2%22%3A%22bar%22%2C+%22param4%22%3A%22baz%22%7D
Suppose this application allows some type of server-side template processing on some resources and we want to find out if it can be used on others. Assume we know that if templates are processed, and injection is present, a payload of {{ $Objects.getServiceVersion() }} would return something like ‘8.5.60’ as a string.
To be thorough, this is the process I need to follow:
- Submit my payload as the entire body
- {{ $Objects.getServiceVersion() }}
- Submit my payload URL encoded as the entire body
- %7B%7B+%24Objects.getServiceVersion%28%29+%7D%7D
- x=%7B%7B+%24Objects.getServiceVersion%28%29+%7D%7D
- Try it as parameter values for Thing1 and Thing1
- Thing1=%7B%7B+%24Objects.getServiceVersion%28%29+%7D%7D& Thing2=%7B%22param1%22%3A%22foo%22%2C+%22param2%22%3A%22bar%22%2C+%22param4%22%3A%22baz%22%7D
- Thing1=someValue&Thing2=%7B%7B+%24Objects.getServiceVersion%28%29+%7D%7D
- Try it as each of the JSON values param1, param2, param3
- %7B%22param1%22%3A%22%7B%7B+%24Objects.getServiceVersion%28%29+%7D%7D%22%2C+%22param2%22%3A%22bar%22%2C+%22param4%22%3A%22baz%22%7D
- …
- …
Are you tired yet? I know I am, and this one was easy. This did not even have content that had to be escaped for a JSON string. We did not have to deal with XML either. Oh, but wait, we still have to eyeball or search all those responses for ‘8.5.60’.
The good news is that we have a tool in BurpSuite’s Scanner that already knows how to do everything we need to do. It can locate the insertion points, including the nested ones, even to an arbitrary depth. Additionally, it can perform all the correct encoding in the correct order so we do not have deal with that tedious error-prone task. With little more than the contribution of regex pattern on our part can watch for the tells. BurpSuite’s extender documentation is pretty good as far as each interface goes, but understanding how they relate to one another in Scanner took me some time.
There are six Scanner-related interfaces that you can implement or extend. These are IScanIssue, IScanQueueItem, IScannerCheck, IScannerInsertionPoint, IScannerInsertionPointProvider, and IScannerListener. Again, this a lot of complexity that offers wonderful flexibility when needed, but it is a lot to process when you are just looking to solve a problem quickly. Fortunately, as long the application you are testing sticks to normal exchange formats (XML, JSON, traditional HTTP form encodings, etc.), there are only two of these interfaces to be concerned with - IScannerCheck and IScanIssue.
The IScanIssue interface is pretty straightforward. The concreate class we create is basically going to be a bag of properties that, among other things, includes the name of our issue, its severity, a confidence value, and various strings we want to have presented if BurpSuite’s reporting features are used. Your Scanner check will return instances of this class.
IScannerCheck is the interface that, when implemented, is the meat of your Scanner issue. It will receive instances of classes implementing the IScannerInsertionPoint interface, which it will complete with payloads, generate http requests from, and finally examine the responses.
I have created a rough template for implementing a Scanner check in Ruby, as that is simpler than Java for iteratively tweaking and editing. I have stuck to the Java-like naming and calling of things for the most part, however.
require 'java' java_import 'burp.IExtensionHelpers' java_import 'burp.IBurpExtender' java_import 'burp.IScannerCheck' java_import 'burp.IScanIssue' java_import 'burp.IScannerInsertionPoint' module BURPMethods def self.included(base) base.send(:include,InstanceMethods) base.extend(StaticMethods) end module InstanceMethods def method_missing(method, *args, &block) if self.class.helpers.respond_to? method self.class.helpers.send(method, *args, &block) elsif self.class.callbacks.respond_to? method self.class.callbacks.send(method, *args, &block) else raise NoMethodError, "undefined method `#{method}` for #{self.class.name}" end end def respond_to?(method, include_private = false) super || self.class.callbacks.respond_to?(method, include_private) || self.class.helpers.respond_to?(method, include_private) end end module StaticMethods def callbacks=(callbacks) @callbacks = callbacks @helpers = @callbacks.getHelpers end attr_reader :callbacks attr_reader :helpers end end class MyScannerCheck include IScannerCheck include BURPMethods INS_PARAM_URL = 0x00 INS_PARAM_BODY = 0x01 INS_PARAM_COOKIE = 0x02 INS_PARAM_XML = 0x03 INS_PARAM_XML_ATTR = 0x04 INS_PARAM_MULTIPART_ATTR = 0x05 INS_PARAM_JSON = 0x06 INS_PARAM_AMF = 0x07 INS_HEADER = 0x20 INS_URL_PATH_FOLDER = 0x21 INS_PARAM_NAME_URL = 0x22 INS_PARAM_NAME_BODY = 0x23 INS_ENTIRE_BODY = 0x24 INS_URL_PATH_FILENAME = 0x25 INS_USER_PROVIDED = 0x40 INS_EXTENSION_PROVIDED = 0x41 INS_UNKNOWN = 0x7f EXISTING_ISSUE = -1 NEW_ISSUE = 1 BOTH_ISSUES = 0 IDX_START = 'TS_ASDF' IDX_END = 'TS_LKJH' INJECT = "${'#{IDX_START} ' YOUR_PAYLOAD_HERE ' #{IDX_END}'}" def doPassiveScan(baseRequestResponse) nil #Will not be able to spot the issue on a passive scan end def doActiveScan(baseRequestResponse, insertionPoint) issues = Array.new #return issues if insertionPoint.getInsertionPointType() == 0x21 checkRequest = insertionPoint.buildRequest(stringToBytes(INJECT)); checkRequestResponse = makeHttpRequest(baseRequestResponse.getHttpService(), checkRequest) response = bytesToString(checkRequestResponse.getResponse()).to_s #Get a Ruby String index_start = response.index IDX_START index_end = response.index IDX_END if index_start issue = MyScannerIssue.new issue.setHttpService(baseRequestResponse.getHttpService()) issue.setURL(analyzeRequest(baseRequestResponse).getUrl()) issue.setHTTPMessages([applyMarkers(checkRequestResponse, requestHighlights, nil)]) issues << issue end issues end def consolidateDuplicateIssues(existingIssue, newIssue) return EXISTING_ISSUES if existingIssue == newIssue BOTH_ISSUES end end class MyScannerIssue include IScanIssue attr_accessor :getURL alias_method :setURL, :getURL= alias_method :getUrl, :getURL alias_method :setUrl, :setURL attr_reader :getConfidence attr_accessor :getHttpMessages alias_method :setHTTPMessages, :getHttpMessages= attr_accessor :getHttpService alias_method :setHttpService, :getHttpService= def ==(v) false #Right now never match so will report all issues; but we can put sensible looking logic elsewhere end alias_method :eql?, :== def initialize @getConfidence = 'Tentative' end def setConfidence(c) case c when :certain @getConfidence = 'Certain' when :firm @getConfidence = 'Firm' when :tentative @getConfidence = 'Tentative' else 'Tentative' end end alias_method :getConfidence=, :setConfidence def getIssueBackground 'https://telekomsecurity.github.io/2018/07/servicenow-privilege-escalation.html' end alias_method :getIssueDetail, :getIssueBackground def getRemediationBackground 'Contact ServiceNow' end alias_method :getRemediationDetail, :getRemediationBackground def getIssueName ‘Script Injection' end def getIssueType 0xFF000000 end def getSeverity 'High' end end class BurpExtender include IBurpExtender ExtensionName = 'YOUR EXTENSION NAME Scanner' def registerExtenderCallbacks(callbacks) ObjectSpace.each_object(Class).select {|klass| klass < BURPMethods }.each do |kklass| kklass.callbacks = callbacks end callbacks.setExtensionName ExtensionName callbacks.registerScannerCheck(MyScannerCheck.new) end end
Here is a quick explanation of what is going on here - the first thing to look at is the BurpExtender class. Every extension has to implement this and BurpSuite will trigger the registerExtenderCallbacks method when it loads the extension. This method does a few things. It first obtains the callbacks object. This object gives access to BurpSuite’s helper functions and network methods such as makeHTTPRequest, stringToBytes, etc. The ObjectSpace call identifies any user classes that have been created and that have the BURPMethods module in their inheritance tree and assigns a reference to the callbacks object as a class variable. The BURPMethods module is a quick way to leverage Ruby’s metaprogramming method lookup overrides to make the important BurpSuite methods available without passing the callbacks object around in method signatures or having long chains of receivers. This means the ScannerCheck implementationand any other classes that might be defined can get straight to business,abstracting away some BurpSuite’s object hierarchy.
MyScannerIssue is basically just a bag of values for BurpSuite’s reporting and dashboard functions and does not require much explanation. The sample here hardcodes some things like severity because of the targeted way I have been utilizing custom scan issues, but it certainly could make these properties as well.
MyScannerIssue::doActiveScan is where things get interesting. A base request and an insertionPoint are passed into the method. The first thing we do is create an empty array. The API expects us to return an array of zero or more ScannerIssue objects. It may be preferrable to skip testing for certain types of insertion points and return an empty array early. These are the defined constants in the sample class and can be compared against the return value of getInsertionPointType on the insertionPoint object. As an example, it may not make sense to test certain types of injection payloads in locations such as URL path components part of a REST API. The insertionPoint.buildRequest method enables the raw payload we want to send to be encoded and inserted into the request. Next, MakeHTTPRequest is used to send the newly formed request to the same HTTPService as the base request. After that, we obtain the response object from the RequestResponse object and convert its bytes first to a Java string with BurpSuite’s helper method and finally to a Ruby string with call to to_s.
At this point, you are really on your own. You need to inspect the response for whatever indication there is that an issue is likely present. If something is found, create a new instance of your ScannerIssue class, fill out the values, and append it to the issues array. Finally, return the array. That is all there is to it. Not hard after all, despite the number of Scanner interfaces in the BurpSuite API.
Now, all that is left to do is to register the new extension on the Extender tab.
If you have not used JRuby extensions before, you might need to pay the Extender -> Options tab a visit and set the JRuby JAR file. You can obtain it from https://www.jruby.org. If you do not want to set up a complete JRuby environment, you can download one of the JRuby-complete jars, which includes the Ruby standard library in the JAR and will provide you with all the functionality needed for Scanner extensions.
Now, all that is left is to get scanning and watch the findings roll in on the dashboard!