Adventures in Burp Extender Land

Estimated Reading Time: 7 minutes

Recently I was testing a mobile application and it’s interaction with an API backend. It was the first time that I had come across an application that used two different JWT tokens in the headers to authorise against the API end point.

Burp is great at handling cookies, but is not so great on handling JWTs from what I can tell. There are some existing Burp plugins but nothing I could find that would do what I wanted to be able to maintain valid sessions over a longer period of time than the JWT was valid for. Once you start working with JWTs of a very short life span, it is difficult to run a long series of automated tests in Burp without running into the problem of the JWT expiring and rendering the rest of the tests useless.

The obvious solution was to see if I could create my own token, with an extended period of validity using the fantastic jwt_tools from @ticarpi, however I was unable to crack the signing secret so it was into the depths of Jython and Python I went.

The application flow

The application I was testing reached out on startup to an endpoint to grab the initial Application Token (X-AUTH-APP in this case). This enabled the application to reach other endpoints that handled non user authenticated matters, including user logon.

Once the user was logged on, a second JWT was used (X-AUTH-USER) to handle the more sensitive operations that a user would need to be logged in for.

So for any endpoints that required you to be logged in, two separate valid JWTs needed to be provided in the headers of the request to the endpoint as seen below

If a token was expired, you would get an error message

The idea

The idea was to check for the response from the server, if it contained an error message such as APP_TOKEN_EXPIRED, reach out, grab a new token and the substitute it in each new request. I’m sure there was an easier way of doing this than writing your own plugin, but it was a great way from me to get a better understanding of the way Burp runs under the hood and what potential is there for future engagements. Also, though this could be my weak google-fu, I didn’t come across many total entry level guides to making a plugin, so I thought I might have a stab at that to.

To Extender Land!

I would like to make a quick shout out here to @mohammadaskar2 who was very patient with me whilst I was fumbling around in python making the sort of newbie coding errors that make experienced python developer’s eyes bleed!

I would also like to apologise in advance for the code quality. There may be some redundancy in the code as well, as it was a very experimental as I figured out how bits worked together and sometimes things got added and removed as they worked/didn’t work. I will attempt to make a final clean version of the code in the future if there is interest.

The Basics

Burp extensions process the incoming and outgoing requests.

Our code needs to do the following

  • Intercept outbound request
  • Check to see if an updated token exists
  • If token exists – replace old token in outbound headers
  • Intercept response
  • If response indicated expired token collect new token and create an updated token for use in new requests

Which all sounds simple enough. If you can code, it probably is! Doubly so if you speak RegEx. After some fumbling about on the interwebs, I came across a basic framework I could mangle to hopefully do what I needed (https://gracefulsecurity.com/burp-suite-vs-csrf-tokens-csrftei/).

First of all I need to import the appropriate bits that I will be using to create my extension.

from burp import IBurpExtender
from burp import IHttpListener
from burp import ISessionHandlingAction

These are the basics that I require to be able to do the manipulation that I need. I also need a couple of other libraries for the regex and out of band communication that I want.

import re
import ssl
import urllib2

This covers me for regex and the connection to grab the new token.

Then I need to set up some regexes to grab the information that I want from the headers or responses

AccessTokenRegex = re.compile(r"accessToken\"\: \"(.*?)\"")
AppErrorRegex = re.compile(r"APP_TOKEN_EXPIRED")
UserErrorRegex = re.compile(r"USER_TOKEN_EXPIRED")

Worth noting that the AccessTokenRegex returns the actual token as part of a group. I will come to this a little later.

Now to set up the basic handling bits. I create the two variables for the USER and APP tokens. Register the extension and make sure that I don’t start playing with messages and responses coming through the proxy – they should all be valid anyway.

class BurpExtender(IBurpExtender, IHttpListener, ISessionHandlingAction):
	# Variables to hold the tokens found so that it can be inserted in the next request
	discoveredAPPToken = ''
	discoveredUSERToken = ''
  
	def registerExtenderCallbacks(self, callbacks):
		 self._callbacks = callbacks
		 self._helpers = callbacks.getHelpers()
		 callbacks.setExtensionName("JWS")
		 callbacks.registerHttpListener(self)
		 print "Extension registered successfully."
		 return
  
	def processHttpMessage(self, toolFlag, messageIsRequest, currentMessage):
		# Operate on all tools other than the proxy
		if toolFlag != self._callbacks.TOOL_PROXY:
			if messageIsRequest:
				self.processRequest(currentMessage)
			else:
				self.processResponse(currentMessage)

I also set up the two calls to the functions here depending on whether the intercepted message is a request or a response.

For the response (there is debug code listed here which you may or may not wish to keep) :

	def processResponse(self, currentMessage):
		response = currentMessage.getResponse()
		parsedResponse = self._helpers.analyzeResponse(response)
		respBody = self._helpers.bytesToString(response[parsedResponse.getBodyOffset():])
		token = AppErrorRegex.search(respBody)
		#Search the response for the error message indicating the token has expired
		if token is None:
			print "APP token is valid"
		else:
			print "APP token expired - obtaining new one"
			self.authApp()
			print "AuthAPP Function complete - APPToken : ",BurpExtender.discoveredAPPToken
			
		token = UserErrorRegex.search(respBody)
		if token is None:
			print "User token is valid"
		else:
			print "User token expired, obtaining new one"
			self.authUser()

I process the response and extract the body of the response which I put into respBody. I then use the (rather basic) RegEx I set up earlier to search for either a APP_EXPIRED or USER_EXPIRED message, and if present call the appropriate function to obtain a new token.

Let’s look at the authapp function (again with some debug code in place). I know what the initial call for a valid APP token looks like (from the initial Burp Intercept) so I need to replicate that.

	def authApp(self):
		print "Authing App - visiting URL"
		#Link for app to refresh
		host = "REDACTED"
		req = urllib2.Request(host)
		#Any Extra Headers you require
		req.add_header('X-AUTH-APP', 'REDACTED')
		req.add_header('User-Agent', 'Mozilla/5.0')
		#req.add_header('Accept-Encoding', 'gzip, deflate')

		context = ssl._create_unverified_context()
		resp = urllib2.urlopen(req, context=context)
		content = resp.read()
		
		token = AccessTokenRegex.search(content)
		BurpExtender.discoveredAPPToken=token.group(1)
		print "Actual Token ", BurpExtender.discoveredAPPToken

In this case, there were extra headers that were required, so I make sure I set up the correct host, add any special extra headers and make the request to the correct server. You will note the commented out line for Accept-Encoding. After much trial and error I discovered that urllib2 doesn’t automatically decompress a compressed response, so rather than figure out a way to decompress the response, I just told the server I couldn’t accept it and made it return plaintext. I then set my token as the result of the RegEx search. As mentioned previously the result is group so I need to set my BurpExtender.discoveredAPPToken as the returned token rather than the entire match, which I do using token.group(1) in this example.

At this point, if I have received a response that an APP token is invalid, I have collected a new token and set it so it can be used through the extension. So onto the request phase.

I must admit to struggling with type, tuples, java.util.ArrayList and all other weirdnesses so the code here, whilst functional, is shall we say … sub-optimal!

def processRequest(self, currentMessage):
		request = currentMessage.getRequest()
		requestInfo = self._helpers.analyzeRequest(request)
		headers = requestInfo.getHeaders()
		requestBody = self._helpers.bytesToString(request)[requestInfo.getBodyOffset():])
		#headers is an array list
		#Convert to single string to process (sorry!)
		headerStr=""
		for x in range(len(headers)): 
			headerStr = headerStr + headers[x] +"\n"
		reqBody = currentMessage.getRequest()[requestInfo.getBodyOffset():]
		reqBody = self._helpers.bytesToString(request)
		
		updatedheaders = headerStr
		
		if BurpExtender.discoveredAPPToken != '':
		# Update X-AUTH-APP
			print "Replacing X-AUTH-APP with ",BurpExtender.discoveredAPPToken
			updatedheaders = re.sub(r"X-AUTH-APP\: .*", "X-AUTH-APP: {0}".format(BurpExtender.discoveredAPPToken), headerStr)
		else:
			print "No X-AUTH-APP token to replace."	
		
		if BurpExtender.discoveredUSERToken != '':	
		# Update X-AUTH-USER Token
			updatedheaders = re.sub(r"X-AUTH-USER\: .*", "X-AUTH-USER: {0}".format(BurpExtender.discoveredUSERToken), updatedheaders) 
		else:
			print "No X-AUTH-USER token to replace."
		#convert headers into a list
		headerslist = updatedheaders.splitlines()
		updatedRequest = self._helpers.buildHttpMessage(headerslist, requestBody)
		currentMessage.setRequest(updatedRequest)

As soon as a request is received, I try and get the headers to examine them. However the headers are returned as a java.util.ArrayList which unbeknownst to me you can’t just run a RegEx over. So being a bit of a hack, I attempt to recreate them as a single string by taking each element and concatenating it with next. Once I have the single headerStr containing all of the headers, I check to see if any updated tokens exist (v2.0 should do this the other way around, if token -> process as opposed to process -> istoken?).

If they exist, some more regex madness replaces the old token with the shiny new valid token prior to transmission.

However, the headers can’t be a single string and need to be a list (*sigh*) so I have to reformat all of the new headers into a list. You will notice that during the concatenation, I added a carriage return between each element giving me an easy delimiter to split the string into a list again since a carriage return should never appear in a HTTP header other than at the end of the line.

I build the updatedRequest and use the new headerslist and put the old request body back in case there was something there I needed – I don’t need to manipulate the body of the request with this extension, just ensure the headers are up to date.

Finally I set the new outbound message to be the one with all of my adjustments.

The basic code is available from my GitHub (here), but otherwise I hope this was a useful entry level guide on how to create an extender for working with headers. It should be fairly easy to extend the basic functionality to add new custom headers, new dynamic headers or even start working with message bodies.

Thanks for reading. Hope you enjoyed it.

Leave a Reply

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