Address
304 North Cardinal St.
Dorchester Center, MA 02124

Work Hours
Monday to Friday: 7AM - 7PM
Weekend: 10AM - 5PM

Burp VERBalyzer v1.0 Release

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.

Burp VERBalyzer - Jython

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!

Burp VERBalyzer - Plugin loaded

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.

Burp VERBalyzer - HTTPbin Findings

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

Burp VERBalyzer - PATCH

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.

Burp VERBalyzer - Duplicates

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.

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.