Building BurpSuite Extensions

Session Handling Action - Extending Burp's Session Handling with Extensions

Medium
30 min

One of the key components of Burp's Extender API is the SessionHandlingAction interface which can be used to create session handling actions that extend the already existing session management in BurpSuite. These actions can be configured to run at specific points during the processing of HTTP requests, enabling automation of tasks like re-authentication, token management, or custom header insertion.

Session handling actions integrate with Burp Suite's request processing workflow. They can be configured in the Session Handling Rules, which determine how and when these actions are invoked. This allows seamless integration with tools like the Proxy, Repeater, Intruder, and Scanner.

Custom session handling actions are useful in scenarios such as:

  • Automated Re-authentication: When a session expires, the extension can automatically log back in.
  • Token Management: Extracting tokens from responses and injecting them into subsequent requests.
  • Custom Authentication Flows: Handling multi-step authentication processes not supported by default.
  • Dynamic Request Modification: Altering requests based on specific criteria or state.

In this module, we'll demonstrate how to integrate custom session handling logic into Burp Suite using Jython. We'll walk through creating a Jython extension that automatically logs into a web application with a tricky authentication flow by sending a POST request to the /login endpoint with configured credentials.

The problem to solve

If you want to follow along already, you can start the lab exercise at the bottom of this page.

Alright, so upon first sight the login form looks quite simple.

We log in, we tick a box, we get logged back out. Alright.

No way are we doing this 100 times manually. We want Burp to keep us logged in while Intruder takes care of sending the 100 requests for each box, we'll just increment a number for the X axis and for the Y axis.

Ok, so we are going to need a session macro to send the login request. Easy peasy. Let's take look at the request.

HTTP-Pyyntö
POST /login HTTP/2
Host: www-5xf7ztxfqo.ha-target.com
Cookie: HakatemiaLabSessionAffinity=f4f5761d0a3cdae05aee677ec9e940c3|766d492e152f0eb48bca4df922227d80; session=721b2d8f-e5d3-4d19-9417-865472817ab6
...

username=john.doe%40example.com&password=s3cr3t&timestamp=1732040519&checksum=9f76872042fb7bae07c1c85d8ee7fc6d

Ooookay... That looks a little concerning. A timestamp? A checksum? Let's take a look at the HTML source to see what's happening here. Right click, "View Source"...

<body>
    <h1>Login</h1>
    <p>Username is: john.doe@example.com</p>
    <p>Password is: s3cr3t</p>
    <form method="post" action="/login" onsubmit="return addHashData(this)">
      <label for="username">Username:</label>
      <input type="text" id="username" name="username" required />
      <label for="password">Password:</label>
      <input type="password" id="password" name="password" required />
      
      <input type="hidden" name="timestamp" id="timestamp" />
      <input type="hidden" name="checksum" id="checksum" />
      
      <button type="submit">Login</button>
    </form>

    
    <script>
      function addHashData(form) {
        const timestamp = Math.floor(Date.now() / 1000);
        const username = form.username.value;
        const password = form.password.value;
        
        form.timestamp.value = timestamp;
        const dataToHash = username + password + timestamp;
        form.checksum.value = CryptoJS.MD5(dataToHash).toString();
        
        return true;
      }
    </script>
    
  </body>

Okay, so now we can see how those values are calculated. The timestamp is an epoch (aka unix timestamp, aka seconds elapsed since the beginning of 1970). And the checksum is a MD5 hash of all three concatenated in the following order: username, password, timestamp.

Seems that a session handling macro is not going to cut it this time, we are going to have to get our hands dirty and write an extension.

Creating and loading the initial extension

We'll assume that you have followed the development environment setup described earlier on this course. If not, start with that.

Create a file such as auto_login.py with the following boilerplate.


from burp import IBurpExtender, ISessionHandlingAction, IHttpRequestResponse

class BurpExtender(IBurpExtender, ISessionHandlingAction):
    def registerExtenderCallbacks(self, callbacks):
        self._callbacks = callbacks
        self._helpers = callbacks.getHelpers()
        callbacks.setExtensionName("Auto Login")
        callbacks.registerSessionHandlingAction(self)

    def performAction(self, currentRequest, macroItems):  # type: (IHttpRequestResponse, list[IHttpRequestResponse]) -> None
        print("Performing action here!")

    def getActionName(self):
        return "Auto Login"

This is a good start for any session handling action, you might want to bookmark this page so that you can always copy it when you need to.

We'll load the extension into Burp now.

As we can see from the screenshot, we now have one session handling action loaded into Burp. Now let's figure out how and when to use it.

Setting up the session handling actions

Now, I'm hoping this is not the first time you are seeing this "tick boxes" exercise, but that you have completed the Burp Suite fundamentals course, and you already know about session handling actions, what they are, and how they work. If not, then go back now or you will have a hard time going onwards. We won't recap everything here. With that said, let's set up the basic session handling actions now.

How do we detect invalid sessions?

First, let's investigate the detection mechanism for when the session is no longer valid. Send this latest HTTP request to the repeater and send it, ensuring that the response is still 200 OK.

Now logging out of the application (which typically expires the session) and sending the same request again. Let's see what happens.

The response is no longer 200 and contains a redirection to the login page in the Location header. Redirecting to the login page is often a great way to identify an expired session.

Building the rules

Open Burp settings and go to the "Sessions" section.

As you have (hopefully) already learned, Burp can automatically update the cookie jar based on HTTP responses detected in various tools. However, we do not want this to happen uncontrollably, as otherwise browsing the application through Burp's proxy could interfere with an ongoing scan or intruder run.

If "Proxy" is checked, then remove it.

Now we have the session database ready.

Removing default rules

Let's remove the default ruleset completely and create a new one. So click on "Remove" to make the list empty.

Create a new rule

Create a new rule using the "Add" button. Give it a description, such as "My session handling rules".

Set scope

Then open the "Scope" tab and add the "Repeater" and "Intruder" tools to the scope.

You can choose "Use suite scope" for the URL scope. However, please note that you need to have the target URL address added to the scope as discussed earlier in the course.

Add rule "Use cookies from the session handling cookie jar". This always updates the cookies of the HTTP request in scope from Burp's cookie jar.

You can at this stage restrict which cookies you want to update. You can leave these blank and all cookies will be updated.

Now we also have the update mechanism connected. We still need the detection mechanism and the authentication mechanism.

Second Rule - Session Validation

Add the rule "Check session is valid". There is some tinkering to do in this box, let's go piece by piece.

Make request(s) to validate session: The first step is to:

  • Issue current request: Determine the session state from the current HTTP request's HTTP response (instead of sending a completely separate HTTP request. Sometimes this is necessary, but not now).
  • Checking the situation for each HTTP request: That is, leave uncheck "Validate session only every N requests".

Inspect response to determine session validity: Then select that:

  • Location(s): URL of the redirection target, in other words, the information of interest to us is located in the HTTP response header Location.
  • Match type: Literal string (so not a Regex pattern, although that can sometimes be desired).
  • Case-sensitivity: Letter case does not matter.
  • Match indicates: Invalid session, which means that if there is a hit similar to the described one in the HTTP response, it means that the session is not valid. This could be done reversely if it is easier for the specific application.

Define behavior on session validity: Now, we'll get to tell Burp to use the session handling action from our extension.

  • If session is invalid, perform the action below (If the session is not valid, do the following):
  • Run a macro (Execute Macro)

But wait, we don't want a macro. That's right, we don't. But Burp doesn't let us use our session handling action in the "check session is valid" handler, unless we run some macro first. I don't know why they made it that way, but we can get around this by creating an empty one without any requests in it.

And now the magic part - we tick the "After running the macro, invoke a Burp extension action handler" and choose our extension.

Hit "Ok" and we should be ready to test the extension out. Before that though, add one more "Use cookies from burp's cookie jar" at the end of your rules - so that any cookies updated by the session handling action will be used in the request we are making. Should look like this:

Testing the setup with session tracer

Click "Open sessions tracer".

The view is always initially empty.

Send a "tick box" message with Repeater. Then check the session tracer. You should see that it has tried to recover the session using our extension, but of course it doesn't actually do anything yet.

Also we can go to the extension output and observe that the performAction function has indeed been called and out message printed.

Implementing the performAction

Now, let's implement the performAction method to send a login request.

Required imports

We'll need the time module for getting the epoch time and we'll need hashlib for creating the MD5 hash. The ICookie is so that we can extend the ICookie interface and create our own Cookie class so that we can update a cookie in the cookie jar. Yeah. Don't blame me, I didn't code Burp or it's API.

import time
import hashlib
from burp import ICookie

Configuring Credentials and Target URL

We can hardcode the credentials and URL for simplicity.

USERNAME = "john.doe@example.com"
PASSWORD = "s3cr3t"
LOGIN_URL = "https://www-5xf7ztxfqo.ha-target.com/login"

Calculating the checksum

We can get the epoch by casting time.time() first into an integer (to get rid of fractions) and then we'll convert that into a string.

Then we'll concatenate the username, password and the timestamp together.

And finally we'll build the MD5 hash of this string.

timestamp = str(int(time.time()))
checksum_input = USERNAME + PASSWORD + timestamp
checksum = hashlib.md5(checksum_input.encode('utf-8')).hexdigest()

Sending the login request

Sending a HTTP request with the Burp extender is unfortunately a little bit more verbose than using, e.g., the requests library. But bear with me.

# Parse the login URL
parsed_url = URL(LOGIN_URL)
protocol = parsed_url.getProtocol()
host = parsed_url.getHost()
port = parsed_url.getPort()
if port == -1:
    port = 443 if protocol == "https" else 80
is_https = protocol == "https"

# Build the HTTP service
login_service = self._helpers.buildHttpService(host, port, is_https)

# Create the POST request headers
request_headers = [
    "POST {} HTTP/1.1".format(parsed_url.getPath()),
    "Host: {}".format(host),
    "Content-Type: application/x-www-form-urlencoded",
    "Connection: close"
]

# Build the request body with URL-encoded parameters
request_body = "username={}&password={}&timestamp={}&checksum={}".format(
    self._helpers.urlEncode(USERNAME),
    self._helpers.urlEncode(PASSWORD),
    self._helpers.urlEncode(timestamp),
    self._helpers.urlEncode(checksum)
)

# Combine headers and body into a complete HTTP message
message = self._helpers.buildHttpMessage(request_headers, request_body)

response = self._callbacks.makeHttpRequest(login_service, message)

Analyzing the login response

Now that we have the request sent and the response received, we'll need to fish out the cookie from the response.

response_bytes = response.getResponse()
if response_bytes:
    response_info = self._helpers.analyzeResponse(response_bytes)
    cookies = response_info.getCookies()

Finally, once we have the cookies let's add them to the jar. You would think this to be easy wouldn't you. Weeeeel think again.

First of all we'll have to declare our own Cookie class somewhere (above the class BurpExtender).

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

Then we'll iterate the cookies from the response, create new Cookie objects based on those, and then we'll update the cookie jar.

for cookie in cookies:
    # Ensure the domain is set
    domain = cookie.getDomain()
    if domain is None:
        domain = host  # Set the domain to the request host

    # Create a new Cookie object with the domain set
    new_cookie = Cookie(
        domain,
        cookie.getName(),
        cookie.getValue(),
        cookie.getPath(),
        cookie.getExpiration()
    )
    self._callbacks.updateCookieJar(new_cookie)

Putting it all together

Here is our finished extension! I've added comments to it to make debugging easier.

from burp import IBurpExtender, ISessionHandlingAction
from burp import ICookie
import time
import hashlib
from java.net import URL

USERNAME = "john.doe@example.com"
PASSWORD = "s3cr3t"
LOGIN_URL = "https://www-5xf7ztxfqo.ha-target.com/login"


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):

    def registerExtenderCallbacks(self, callbacks):
        self._callbacks = callbacks
        self._helpers = callbacks.getHelpers()

        callbacks.setExtensionName("Auto Login")
        callbacks.registerSessionHandlingAction(self)

    def getActionName(self):
        return "Auto Login"

    def performAction(self, currentRequest, macroItems):
        try:
            print("[Auto Login] Starting performAction")

            # Get the current epoch time as an integer string
            timestamp = str(int(time.time()))
            print("[Auto Login] Current timestamp:", timestamp)

            # Compute the checksum (MD5 hash)
            checksum_input = USERNAME + PASSWORD + timestamp
            checksum = hashlib.md5(checksum_input.encode('utf-8')).hexdigest()
            print("[Auto Login] Checksum input:", checksum_input)
            print("[Auto Login] Computed checksum:", checksum)

            # Parse the login URL
            parsed_url = URL(LOGIN_URL)
            protocol = parsed_url.getProtocol()
            host = parsed_url.getHost()
            port = parsed_url.getPort()
            if port == -1:
                port = 443 if protocol == "https" else 80
            is_https = protocol == "https"
            print("[Auto Login] Parsed URL:")
            print("  Protocol:", protocol)
            print("  Host:", host)
            print("  Port:", port)
            print("  is_https:", is_https)

            # Build the HTTP service
            login_service = self._helpers.buildHttpService(host, port, is_https)
            print("[Auto Login] Built HTTP service")

            # Create the POST request headers
            request_headers = [
                "POST {} HTTP/1.1".format(parsed_url.getPath()),
                "Host: {}".format(host),
                "Content-Type: application/x-www-form-urlencoded",
                "Connection: close"
            ]
            print("[Auto Login] Request headers:")
            for header in request_headers:
                print("  ", header)

            # Build the request body with URL-encoded parameters
            request_body = "username={}&password={}&timestamp={}&checksum={}".format(
                self._helpers.urlEncode(USERNAME),
                self._helpers.urlEncode(PASSWORD),
                self._helpers.urlEncode(timestamp),
                self._helpers.urlEncode(checksum)
            )
            print("[Auto Login] Request body:", request_body)

            # Combine headers and body into a complete HTTP message
            message = self._helpers.buildHttpMessage(request_headers, request_body)
            print("[Auto Login] Built HTTP message")

            # Send the request
            response = self._callbacks.makeHttpRequest(login_service, message)
            print("[Auto Login] Sent HTTP request")

            # Analyze the response to extract cookies
            response_bytes = response.getResponse()
            if response_bytes:
                response_info = self._helpers.analyzeResponse(response_bytes)
                status_code = response_info.getStatusCode()
                print("[Auto Login] Received response with status code:", status_code)

                cookies = response_info.getCookies()
                print("[Auto Login] Extracted cookies:")
                for cookie in cookies:
                    print("  Name:", cookie.getName())
                    print("  Value:", cookie.getValue())
                    print("  Domain:", cookie.getDomain())
                    print("  Path:", cookie.getPath())
                    print("  Expiration:", cookie.getExpiration())
                    print("  -----------------------")

                # Update Burp's cookie jar
                for cookie in cookies:
                    # Ensure the domain is set
                    domain = cookie.getDomain()
                    if domain is None:
                        domain = host  # Set the domain to the request host
                        print("[Auto Login] Cookie domain was None, set to:", domain)

                    # Create a new Cookie object with the domain set
                    new_cookie = Cookie(
                        domain,
                        cookie.getName(),
                        cookie.getValue(),
                        cookie.getPath(),
                        cookie.getExpiration()
                    )
                    print("[Auto Login] Updating cookie:", new_cookie.getName())
                    self._callbacks.updateCookieJar(new_cookie)
                print("[Auto Login] Updated cookie jar successfully")
            else:
                print("[Auto Login] No response received from the server")
        except Exception as e:
            print("[Auto Login] Exception occurred:", str(e))
            import traceback
            traceback.print_exc()

Debugging your extension

The way to debug a Burp extension is to edit it, unload it, load it again, try it again, and observe the output and errors tabs.

And the session tracer is quite valuable in this case when creating session handling actions. Let's try to send a request now.

Alright, seems to work. Let's see what the session tracer had to say about this.

There. We can see that session was identified as invalid, our extension was invoked, and our extension updated 1 cookie in the cookie jar.

Exercise

Show them boxes who is the boss.

Ticking boxes 3

In this lab, you get to check boxes!

Objective

Check 100 boxes to get the flag.

Exercises

Flag

Find the flag from the lab environment and enter it below.

hakatemia pro

Ready to become an ethical hacker?
Start today.

As a member of Hakatemia you get unlimited access to Hakatemia modules, exercises and tools, and you get access to the Hakatemia Discord channel where you can ask for help from both instructors and other Hakatemia members.