I'm proud to release my Burp VERBalyzer (credit to Ben for the name) plugin this week.

Introduction

On an engagement recently, I had a coworker run across a subdirectory that allowed the OPTIONS method, but / didn't. This, plus the fact that checking options during an engagement wasn't easy, led me to develop this plugin.

VERBalyzer will go through a list of standard HTTP Methods (other than GET, HEAD, and POST), and check whether the server responds with a 200 response.

While this tool is mostly to report methods that a server should probably have off (TRACE etc.), it can also be used to find interesting footholds on strange endpoints (PUT etc.).

Installation

First, due to the fact that VERBalyzer utilizes Active Scanner checks, you will need a copy of Burp Professional to use it.

To install VERBalyzer, you will first need to install Jython.

Once you have Jython installed, you will need to configure your Burp to point towards the jython.jar file.

After Jython is installed and configured, you can install VERBalyzer.

First, go back to the "Extensions" tab and click "Add". Change the "Extension type" to Python, and select the location of your VERBalyzer.py file.

Finally, click Next, verify that the plugin was successfully loaded, and click Close!

Usage

To use VERBalyzer, you will just need to start an Active Scan against your target application.

Once the scanner is running, any non-standard responses will be reported in the "Issues" section of the "Target" tab.

That said, you will need to manually verify some responses to see whether or not the server actually supports the method.

Findings

For an example, here is a list of findings against a few of the httpbin endpoints.

As you can see, the /patch endpoint responded to the PATCH method.

Finally, you can see the raw request and response below.

PATCH /patch HTTP/1.1 Host: httpbin.org Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3055.0 Safari/537.36 autochrome/yellow Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 Accept-Language: en-US,en;q=0.8 Cookie: _gauges_unique_hour=1; _gauges_unique_day=1; _gauges_unique_month=1; _gauges_unique_year=1; _gauges_unique=1 Connection: close

HTTP/1.1 200 OK Connection: close Server: gunicorn/19.7.1 Date: Thu, 13 Apr 2017 16:04:33 GMT Content-Type: application/json Access-Control-Allow-Origin: * Access-Control-Allow-Credentials: true Content-Length: 723 Via: 1.1 vegur { "args": {}, "data": "", "files": {}, "form": {}, "headers": { "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.8", "Connection": "close", "Content-Length": "0", "Cookie": "_gauges_unique_hour=1; _gauges_unique_day=1; _gauges_unique_month=1; _gauges_unique_year=1; _gauges_unique=1", "Host": "httpbin.org", "Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3055.0 Safari/537.36 autochrome/yellow" }, "json": null, "origin": "11.22.33.44", "url": "http://httpbin.org/patch" }

Technical Details

VERBalyzer adds a new insertion point to HTTP requests, that allows active scans to insert payloads into the HTTP method. That said, VERBalyzer does not allow Burp to use its payloads in that insertion point, at least for now (see Notes below).

Additionally, VERBalyzer adds a new Active Scanner check for all the possible HTTP methods included with it.

You can find the current code for Burp VERBalyzer below.

# VERBalyzer - Burp Plugin to detect HTTP Methods supported by the server # Author: Ray Doyle (@doylersec) # Copyright 2017 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. try: from burp import IBurpExtender from burp import IScannerCheck from burp import IScanIssue from burp import IScannerInsertionPointProvider from burp import IScannerInsertionPoint from burp import IParameter from array import array from org.python.core.util import StringUtil import string except ImportError: print "Failed to load dependencies." VERSION = "1.0" callbacks = None helpers = None methods = [ 'OPTIONS', #'GET', #'HEAD', #'POST', 'PUT', #'DELETE', 'TRACE', 'CONNECT' 'PROPFIND', 'PROPPATCH', 'MKCOL', 'COPY', 'MOVE', 'LOCK', 'UNLOCK', 'VERSION-CONTROL', 'REPORT', 'CHECKOUT', 'CHECKIN', 'UNCHECKOUT', 'MKWORKSPACE', 'UPDATE', 'LABEL', 'MERGE', 'BASELINE-CONTROL', 'MKACTIVITY', 'ORDERPATCH', 'ACL', 'SEARCH', 'PATCH', 'FOO' ] class BurpExtender(IBurpExtender, IScannerInsertionPointProvider, IScannerCheck): def registerExtenderCallbacks(self, callbacks): self._callbacks = callbacks self._helpers = callbacks.getHelpers() callbacks.setExtensionName("VERBalyzer") callbacks.registerScannerInsertionPointProvider(self) callbacks.registerScannerCheck(self) print "Successfully loaded VERBalyzer v" + VERSION return # helper method to search a response for occurrences of a literal match string # and return a list of start/end offsets def _get_matches(self, response, match): matches = [] start = 0 reslen = len(response) matchlen = len(match) while start < reslen: start = self._helpers.indexOf(response, match, True, start, reslen) if start == -1: break matches.append(array('i', [start, start + matchlen])) start += matchlen return matches # # implement IScannerInsertionPointProvider # def getInsertionPoints(self, baseRequestResponse): requestLine = self._helpers.analyzeRequest(baseRequestResponse.getRequest()).getHeaders()[0] if (requestLine is None): return None else: # if the parameter is present, add a single custom insertion point for it return [ InsertionPoint(self._helpers, baseRequestResponse.getRequest(), requestLine) ] def doActiveScan(self, baseRequestResponse, insertionPoint): if 'HTTP Method' != insertionPoint.getInsertionPointName(): return [] issues = [] for method in methods: checkRequest = insertionPoint.buildRequest(method) checkRequestResponse = self._callbacks.makeHttpRequest(baseRequestResponse.getHttpService(), checkRequest) matches = self._get_matches(checkRequestResponse.getResponse(), "HTTP/1.1 200 OK") if len(matches) > 0: # get the offsets of the payload within the request, for in-UI highlighting requestHighlights = [insertionPoint.getPayloadOffsets(method)] issues.append(CustomScanIssue( baseRequestResponse.getHttpService(), self._helpers.analyzeRequest(baseRequestResponse).getUrl(), [self._callbacks.applyMarkers(checkRequestResponse, requestHighlights, matches)], "Non-standard HTTP Method Found", "The following method was found to be supported by the server: " + method, "Medium")) return issues def doPassiveScan(self, basePair): return [] def consolidateDuplicateIssues(self, existingIssue, newIssue): # This method is called when multiple issues are reported for the same URL # path by the same extension-provided check. The value we return from this # method determines how/whether Burp consolidates the multiple issues # to prevent duplication # # Since the issue name is sufficient to identify our issues as different, # if both issues have the same name, only report the existing issue # otherwise report both issues if existingIssue.getIssueDetail() == newIssue.getIssueDetail(): return -1 return 0 # # class implementing IScannerInsertionPoint # class InsertionPoint(IScannerInsertionPoint): def __init__(self, helpers, baseRequest, requestLine): self._helpers = helpers self._baseRequest = baseRequest # parse the location of the input string within the decoded data start = 0 self._insertionPointPrefix = requestLine[:start] end = string.find(requestLine, " /", start) if (end == -1): end = requestLine.length() self._baseValue = requestLine[start:end] self._insertionPointSuffix = requestLine[end:] return # # implement IScannerInsertionPoint # def getInsertionPointName(self): return "HTTP Method" def getBaseValue(self): return self._baseValue def buildRequest(self, payload): # Gross workaround via Dafydd - https://support.portswigger.net/customer/portal/questions/12431820-design-of-active-scanner-plugin-vs-insertionpoints if payload.tostring() not in methods: raise Exception('Just stopping Burp from using our custom insertion point') else: requestStr = self._baseRequest.tostring() newRequest = requestStr.replace(self._baseValue, payload) newRequestB = StringUtil.toBytes(newRequest) # update the request with the new parameter value return newRequestB def getPayloadOffsets(self, payload): return [0, len(payload.tostring())] def getInsertionPointType(self): return INS_EXTENSION_PROVIDED # # class implementing IScanIssue to hold our custom scan issue details # class CustomScanIssue (IScanIssue): def __init__(self, httpService, url, httpMessages, name, detail, severity): self._httpService = httpService self._url = url self._httpMessages = httpMessages self._name = name self._detail = detail self._severity = severity def getUrl(self): return self._url def getIssueName(self): return self._name def getIssueType(self): return 0 def getSeverity(self): return self._severity def getConfidence(self): return "Certain" def getIssueBackground(self): pass def getRemediationBackground(self): pass def getIssueDetail(self): return self._detail def getRemediationDetail(self): pass def getHttpMessages(self): return self._httpMessages def getHttpService(self): return self._httpService

Future Work

Some of my next steps will be to add more methods, create an actual tab in Burp for configuration and reporting, submit the plugin to the BApp Store, and fix the duplicate issue reporting.

Note: there is a current annoying issue with the plugin. If the server returns a 200 response for all methods (as opposed to 405), then there will be hundreds of findings fairly quickly.

Notes

Also, for those of you actually looking at the code or the Error console, then you might notice a ton of exceptions being thrown.

The reason for this is that I didn't want (or need) Burp to use my insertion point for ALL OF ITS tests.

In this case, any payload that isn't in the predefined list will throw an exception, and not attempt to change the HTTP method. Credit to Dafydd for this dirty solution to my problem.

Conclusion

Let me know if you have any questions, comments, suggestions, or ideas!

Finally, you can find the code and updates in my GitHub repository.