I was going over an application I wrote prepping for my upcoming class and realized the shortcoming’s of Burp’s default session handling mechanisms. Not to knock Burp, but working with a Single Page Application (SPA) that makes calls to several APIs using a JSON Web Token (JWT) doesn’t mix well with Burp’s macros and Cookie Jar. Granted, this isn’t a problem if your JWT’s expiration date is sufficiently far out in the future and none of the APIs you’re working with de-authenticate your “session” (revoke or blacklist your token). JWTs aren’t necessarily meant to be linked to any real session state. They grant access to things and expire at a certain date.
In wanting to show students how applications often pose problems when faced with malicious request activity, I made it such that my application features a de-authentication mechanism that blacklists JWTs when malicious activity is witnessed. As such, I had to work up a way (in Burp Suite) to check my session and re-authenticate once a token has been blacklisted. Before jumping into how I solved this I should probably explain how my application works. Again, it’s a SPA that reaches out to various APIs and a content distribution “network” (CDN). The origins at play here are:
• https://api.store.com (does all the things for the “store”)
• https://auth.foobar.com (provides authentication/autherization for everything)
• https://cdn1.foo.com (serves up various files – no token required)
Pretty straight forward. JWTs get handed out by auth.foobar.com to books2.store.com and ultimately get passed back and forth between all hosts but cdn1.foo.com. The JWT is stored as a cookie by books2.store.com and sent in a header when ajax requests are made to auth.foobar.com and api.store.com (from the books2.store.com origin).
The following figure details a login request made to auth.foobar.com.
This figure details the response handed back by auth.foobar.com with a JWT.
This figure details a request to api.store.com which includes the JWT placed in a header.
This figure details the response received from api.store.com.
Again, the JWT is stored as a cookie by the user agent and is associated with books2.store.com.
To add a little more context, make sure to understand the maintenance of an authenticated state is necessary when automating attacks (Intruder) and performing vulnerability scanning (Scanner).
Without any de-authentication or blacklisting you could just set a token via the “Add Custom Header” extension and go about your business given that your token has a far away expiration date.
When faced with de-authentication and blacklisting mechanisms you can create a macro that fires off a login request, grabs the token using the “Add Custom Header” extension, and include it in the current request. Granted, this will happen for EVERY request. This may be completely acceptable given that the authentication API isn’t a production asset and your client doesn’t necessarily mind you beating up their infrastructure. Fire away and go about your business! But if this isn’t the case, or you just want to be more efficient and less cumbersome, you’ll need to come up with a more elegant solution than acquiring a new token for EVERY request.
This is where Burp Extensions come in. I created a way to mimic the Cookie Jar for JWTs (or header values, in general). In the end, two small extensions were necessary to pull this off.
1) “Store & Set” – Grab a JWT from a login macro when the current request is deemed “invalid”, store the value in the cookie jar, and then insert it into a request header.
2) “Set” – Grab the stored token value from the cookie jar and insert it into a request header.
Both extensions contained logic that only inserts the token value (taken from the cookie jar) into a header if the specified header was already included in the request.
Only a single session handling rule with two associated actions is required for this endeavor.
The second will be a “check session is valid” action that will issue the current request, test its validity, if invalid, run a login macro, followed by our “Store & Set” extension which will parse the new JWT from the macro’s response, store the JWT in the cookie jar, and then update the current request’s JWT with the newly acquired one.
The following figure details the settings for the session check portion of the session handling rule. We know that when a JWT is revoked or has expired the string “Expired Token” will be present in responses. We will use this to invalidate the session and trigger a macro.
The following figure details the settings used where a macro is triggered due to an invalid session which is then followed by our “Store & Set” extension being called.
This is all very similar to how one would utilize Burp session handling rules and macros to provide state maintenance when an application makes use of traditional session cookies. The difference being that we’re not using cookies and, instead, are relegated to request headers.
The following code comprises the “Store & Set” extension. Take note of the “Cookie” class and its associated functions. These allow you to work with Burp’s Cookie Jar.
# python imports import re import sys # Burp specific imports from burp import IBurpExtender from burp import ISessionHandlingAction from burp import ICookie # For using the debugging tools from # https://github.com/securityMB/burp-exceptions try: from exceptions_fix import FixBurpExceptions except ImportError: pass class Cookie(ICookie): def getDomain(self): return self.cookie_domain def getPath(self): return self.cookie_path def getExpiration(self): return self.cookie_expiration def getName(self): return self.cookie_name def getValue(self): return self.cookie_value def __init__(self, cookie_domain=None, cookie_name=None, cookie_value=None, cookie_path=None, cookie_expiration=None): self.cookie_domain = cookie_domain self.cookie_name = cookie_name self.cookie_value = cookie_value self.cookie_path = cookie_path self.cookie_expiration = cookie_expiration class BurpExtender(IBurpExtender, ISessionHandlingAction): # # Define config and gui variables # cookieName = 'jwt' cookieDomain = 'dummy.com' # # Define some cookie functions # def deleteCookie(self, domain, name): cookies = self.callbacks.getCookieJarContents() for cookie in cookies: #self.stdout.println("%s = %s" % (cookie.getName(), cookie.getValue())) if cookie.getDomain() == domain and cookie.getName() == name: cookie_to_be_nuked = Cookie(cookie.getDomain(), cookie.getName(), None, cookie.getPath(), cookie.getExpiration()) self.callbacks.updateCookieJar(cookie_to_be_nuked) break def createCookie(self, domain, name, value, path=None, expiration=None): cookie_to_be_created = Cookie(domain, name, value, path, expiration) self.callbacks.updateCookieJar(cookie_to_be_created) def setCookie(self, domain, name, value): cookies = self.callbacks.getCookieJarContents() for cookie in cookies: if cookie.getDomain() == domain and cookie.getName() == name: cookie_to_be_set = Cookie(cookie.getDomain(), cookie.getName(), value, cookie.getPath(), cookie.getExpiration()) self.callbacks.updateCookieJar(cookie_to_be_set) break def getCookieValue(self, domain, name): cookies = self.callbacks.getCookieJarContents() for cookie in cookies: if cookie.getDomain() == domain and cookie.getName() == name: return cookie.getValue() # # implement IBurpExtender # def registerExtenderCallbacks(self, callbacks): # keep a reference to our callbacks object self.callbacks = callbacks # obtain an extension helpers object self.helpers = callbacks.getHelpers() # set our extension name callbacks.setExtensionName("JWT - Store & Set") # register ourselves a Session Handling Action callbacks.registerSessionHandlingAction(self) # Used by the custom debugging tools sys.stdout = callbacks.getStdout() print("DEBUG: JWT - Store & Set - Enabled!") return # # Implement ISessionHandlingAction # def getActionName(self): return "JWT - Store & Set" def performAction(self, current_request, macro_items): if len(macro_items) >= 0: # grab some stuff from the current request req_text = self.helpers.bytesToString(current_request.getRequest()) current_macro = macro_items macro_resp = current_macro.getResponse() macro_resp_info = self.helpers.analyzeResponse(macro_resp) # parse the response & search for jwt if macro_resp: macro_resp_body = macro_resp[macro_resp_info.getBodyOffset():] macro_resp_text = self.helpers.bytesToString(macro_resp_body) search_re = '"%s":"(.*?)"' % self.cookieName search = re.search(search_re, macro_resp_text, re.IGNORECASE) # we have a jwt in the macro response if search: jwt = search.group(1) # set the cookie value in the cookie jar self.createCookie(self.cookieDomain, self.cookieName, jwt) # replace the old token with the stored value header_replace = "%s: %s" % (self.cookieName, jwt) req_text = re.sub(r"\r\n" + self.cookieName + ": .*\r\n", "\r\n" + header_replace + "\r\n" , req_text) # set the current request current_request.setRequest(self.helpers.stringToBytes(req_text)) try: FixBurpExceptions() except: pass
The following code comprises the “Set” extension. It’s almost identical to the “Store & Set” extension’s code aside from checking for an initial cookie (JWT) value and leaving out the macro response parsing.
The only difference between the two scripts are contained in the “performAction” function and the extension naming portions of the code. For the sake of brevity, I will only post that function to illustrate the differences. Swap one version out for the other.
def performAction(self, current_request, macro_items): # grab some stuff from the current request req_text = self.helpers.bytesToString(current_request.getRequest()) # grab jwt from cookie jar jwt = self.getCookieValue(self.cookieDomain, self.cookieName) # does a value exist yet? if jwt != None: # replace the old token with the stored value header_replace = "%s: %s" % (self.cookieName, jwt) req_text = re.sub(r"\r\n" + self.cookieName + ": .*\r\n", "\r\n" + header_replace + "\r\n" , req_text) # set the current request current_request.setRequest(self.helpers.stringToBytes(req_text))
Putting all of this into play and verifying the results proves successful. The following figure details where the application revoked the current token causing the session handling rules detailed above to re-authenticate and update subsequent tokens.
So altogether, nothing too mind-bending here but definitely useful and a fun exercise to work through. Hopefully this post will provide you with some insight into using Burp Extensions to interact with the Cookie Jar.
One bit of criticism I have for this exercise is that the act of setting the current request to the updated value seems to be pretty resource intensive. Running a scan with 40 threads making use of this pair of extensions seems to be pretty slow. I am going to revisit this code in Java and see if I can improve the performance. Stay tuned…