Writing Burp extensions in Kotlin

I’ve wanted to write Burp extensions using Kotlin for some time now. Ever since Portswigger released the new Montoya API for Burp Suite, I’ve been waiting for an opportunity to learn it. 

My previous discussions on writing extensions for Burp were always centered around the old Extender API and Jython, which, due to its reliance on Python2 constructs, had its limitations. This is why transitioning to something more supported, like Kotlin with the new Montoya API, is crucial.

In my article last week, I talked about taking Microsoft’s machine learning models and using Presidio to detect sensitive data in captured API traffic. I also mentioned that an aspiring API hacker could take that concept and write a Burp extension around it that could do it with real-time traffic. 

Well, today is that day.

Join me on this exciting journey as I write my first Burp extension using Kotlin. I can’t wait to share this learning experience with you. 

Here goes nothing…

Why not just use the old API and Jython?

It would be fair to ask why I don’t just write it like all my other Burp extensions. The first reason is that Microsoft Presidio’s support for Python is only for the latest versions up to 3.11. So, I couldn’t even consume the Presidio Python module in Jython within Burp.

The second reason is that the Montoya API provides a lot of additional access and modularity around the inner workings of Burp Suite — everything from better event handling and callbacks to annotations in the proxy history records.

Portswigger supports writing new extensions in Java and Kotlin. I’ve chosen Kotlin because it offers more concise and expressive syntax, improved safety features, and full interoperability with Java. These advantages should lead to more readable and maintainable code with fewer chances of common programming errors.

And let’s face it, I’m not a spring chicken anymore. I can use all the help I can get.

By using Kotlin, I won’t be able to use the Python module at all. So, I will rely on running Presidio in Docker and calling it directly using its REST API.

Now, most people write Kotlin in IntelliJ. As a Microsoft guy, I’ve never been a fan of that IDE. So I will use Visual Studio Code (VS Code) to write my extension instead.

Let’s set it up.

Setting up Kotlin in VS Code 

If there is one thing I can say about VS Code, the marketplace for its extensions is pretty nice. You can find pretty much anything you need in it.

I did a search for “kotlin”.

Installed the “Kotlin Language” extension to get basic language support in VS Code.

Then I installed the “Kotlin” extension to get smart code completion, debugging, linting, and syntax highlighting. 

They work great together.

That’s about it. I don’t need anything else to write Kotlin code. 

Well, that’s not true. 

To streamline the development process and ensure consistent builds, you’ll want something that handles build automation, dependency management, and efficient project configuration.

That’s why people use Gradle with Kotlin.

We should install that, too, but do that outside of VS Code. The installation instructions are here.

Personally, I decided to write this on my MacBook and just used Brew to install it. However, following the manual instructions, I also rebuilt the extension on my SurfaceBook running Windows 11.

With everything installed, it’s time to write an extension. I will call mine “Sensitive Data Detector”. 

Scaffolding my Burp Extension

To get started, I will leverage Gradle to generate the basic directory structure for our extension.   

mkdir SensitiveDataDetector
cd SensitiveDataDetector
gradle init --type basic

This created all the basic stuff Gradle needs. When I ran it, I accepted the prompts to use Kotlin and name the project based on the new directory.

Now, to prepare the file structure for my code, I need to think about naming conventions for packaging. I’ll use com.danaepp.sensitivedatadetector.

Knowing the name I want the package to be, I can create the directory structure Kotlin likes to see:

mkdir -p src/main/kotlin/com/danaepp/sensitivedatadetector

That’s it. Now we can open up the project in VS Code.

code .

Setting up the build

Now that we are in VS Code, I want to set up how Gradle will build the code and ensure the dependencies are all in place. Burp extensions won’t work without them.

I know I will have a few dependencies. These include:

  • The API to build Burp Suite extensions. I’ll use “montoya-api” for this.
  • An HTTP client to make requests to the Presidio REST API. I’ll use “okhttp3” for this.
  • A serialization library to handle the JSON data for the REST calls. I’ll use “kotlinx-serialization-json” for this.

I’m also going to need to set up some plugins for this build:

  • I’ll need to define the Kotlin JVM
  • I’ll need to enable the serialization plugin
  • I’ll need to enable the shadowJar plugin

What’s a shadowJar? Some people know it as a “fat jar” or “uber jar”. This basically means that Gradle will package up all dependencies to make the JAR self-contained. Burp Suite extensions will need that.

OK, so here is what the build file looks like for me:

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    kotlin("jvm") version "1.9.24"
    id("com.github.johnrengelman.shadow") version "8.1.1"
    id("org.jetbrains.kotlin.plugin.serialization") version "1.8.10"
}

group = "com.danaepp"
version = "1.0.0"

repositories {
    mavenCentral()
}

dependencies {
    implementation("net.portswigger.burp.extensions:montoya-api:2023.12.1")
    implementation("com.squareup.okhttp3:okhttp:4.12.0")
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
}

tasks.test {
    useJUnitPlatform()
}

When saving the file, VS Code may prompt us to synchronize our classpath. Click “Always.” 

This will ensure that VS Code stays in sync as we update our build file when adding additional dependencies or plugins.

With the build defined, we can now move on to the code.

Basic code for the Burp extension

Expand the src folder to the end and add a file. I called mine “SensitiveDataDetectorExtension.kt”.

Here is the bare minimum code for a Burp Extension to load.

package com.danaepp.sensitivedatadetector

import burp.api.montoya.BurpExtension
import burp.api.montoya.MontoyaApi

@Suppress("unused")
class SensitiveDataDetector : BurpExtension {
   override fun initialize(api: MontoyaApi?) {
        if (api == null) {
            return
        }

        api.extension().setName("Sensitive Data Detector")
        api.logging().logToOutput("Loading Sensitve Data Detector")
    }
}

You may wonder why we check if the API is null. Portswigger didn’t annotate its entry point to the Montoya API very well, so Kotlin thinks it may be possible for it to be null. Adding that code is really for Kotlin’s benefit, eliminating the error (seen below).

Gotta love typesafe languages.

Building a Burp Extension with Gradle

By using “gradle init” and putting our code file in the right location, it’s now easy to build our extension. Simply open a terminal in VS Code and type “gradle build”.

You will find the extension in the build>libs folder. But that isn’t good enough. Remember, we want to bundle all our dependencies in the extension as well. So, we will run “gradle shadowjar” to build an extension that includes everything we need.

The extension is in the same directory but will include a “-all” in the name. You want to load that extension.

Load the extension in Burp

You should be already familiar with how to load an extension in Burp. But just in case you don’t, here is how to do it:

  1. Open Burp Suite
  2. Go to the Extensions tab
  3. Click the Installed subtab
  4. Click the Add button
  5. Under “Extension details”, ensure Java is selected, and then browse for your newly created JAR file.
  6. Click Next

You should now see some output showing if the extension loaded successfully or not.

Adding a response handler

While the extension loads, it doesn’t actually do anything. So, let’s set up a basic response handler for the Burp Suite Proxy.

The code looks like this:

package com.danaepp.sensitivedatadetector

import burp.api.montoya.MontoyaApi
import burp.api.montoya.proxy.http.ProxyResponseHandler
import burp.api.montoya.proxy.http.ProxyResponseReceivedAction
import burp.api.montoya.proxy.http.ProxyResponseToBeSentAction
import burp.api.montoya.proxy.http.InterceptedResponse

class DetectorHttpResponseHandler(private val api: MontoyaApi) : ProxyResponseHandler
{
    override fun handleResponseReceived(interceptedResponse: InterceptedResponse?): ProxyResponseReceivedAction {
        /* This should never happen */
        if (interceptedResponse == null) {
            api.logging().logToError("Null response received. Dropping message.")
            return ProxyResponseReceivedAction.drop()
        }

        return ProxyResponseReceivedAction.continueWith(interceptedResponse)
    }

    override fun handleResponseToBeSent(interceptedResponse: InterceptedResponse?): ProxyResponseToBeSentAction {
        return ProxyResponseToBeSentAction.continueWith(interceptedResponse)
    }
}

Then all we need to do is register it with the Montoya API in the main extension code:

api.proxy().registerResponseHandler(DetectorHttpResponseHandler(api))

And that’s it. That’s all the code you need for a basic Burp extension in Kotlin. You need to add some meat to do any work of course… but that is really up to you and your needs.

For the rest of this article, I will wire in Microsoft Presidio to leverage their NLP machine learning models to detect PII in any API call we can detect. 

Feel free to follow along… or stop here and build an extension with your own business logic.

Wiring in Microsoft Presidio to Burp Suite

So Microsoft offers the Presidio Analyzer in a Docker container installation. After pulling the analyzer down and starting it up, I could access its Analyzer API.

Below is some rough code to query the API and return any sensitive data detected.

Basic Presidio client in Kotlin

package com.danaepp.sensitivedatadetector

import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.MediaType.Companion.toMediaType
import java.io.IOException

import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import kotlinx.serialization.encodeToString

import com.danaepp.sensitivedatadetector.SensitiveDataResult
import com.danaepp.sensitivedatadetector.PresideoRequest
import com.danaepp.sensitivedatadetector.PresideoResponse

class Presidio
{
    companion object {
        private const val PRESIDIO_ANALYZER_URL = "http://localhost:5001/analyze"
    }
    
    fun analyze(payload: String) : List<SensitiveDataResult>
    {
        val presidioRequest = PresideoRequest(payload)
    
        val client = OkHttpClient()

        val MEDIA_TYPE = "application/json".toMediaType()
        
        val json = Json { encodeDefaults = true; ignoreUnknownKeys = true }
        val requestBody = json.encodeToString(presidioRequest)
        
        val request = Request.Builder()
            .url(PRESIDIO_ANALYZER_URL)
            .post(requestBody.toRequestBody(MEDIA_TYPE))
            .header("Content-Type", "application/json")
            .build()
        
        val results: MutableList<SensitiveDataResult> = mutableListOf() 
        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful) throw IOException("Unexpected code $response")
            
            val retData = response.body?.string() ?: ""
            try {
                if( retData != "" )
                {
                    val deserializedResults: List<PresideoResponse>? = json.decodeFromString(retData)
                    
                    if( deserializedResults != null )
                    {
                        for( r in deserializedResults ) {
                            results.add(
                                SensitiveDataResult(
                                    r.entity_type, 
                                    r.score, 
                                    payload.substring(r.start,r.end))
                            )
                        }
                    }
                    
                }
            }
            catch(e: SerializationException) {
                throw e
            }  
        }

        return results
    }
}

Supporting data classes

package com.danaepp.sensitivedatadetector

import kotlinx.serialization.Serializable

@Serializable
data class PresideoRequest(
    val text: String, 
    val language: String = "en", 
    val score_threshold: Double = 0.75,
    val entities: List<String> = listOf(
        "EMAIL_ADDRESS", "IBAN Generic", "IP_ADDRESS",
        "PHONE_NUMBER", "LOCATION", "PERSON", "URL",
        "US_BANK_NUMBER", "US_DRIVER_LICENSE",
        "US_ITIN", "US_PASSPORT", "US_SSN" 
    ) 
)
package com.danaepp.sensitivedatadetector

import kotlinx.serialization.Serializable

@Serializable
data class PresideoResponse(
    val start: Int,    
    val end: Int,
    val entity_type: String,
    val score: Double    
)
package com.danaepp.sensitivedatadetector

import kotlinx.serialization.Serializable

@Serializable
data class SensitiveDataResult(
    val entityType: String,
    val score: Double,
    val data: String
)

A tip on JSON serialization in Kotlin

There is one thing to point out in the Presidio code around the reconfiguration of the Json serializer. The code looks like this:

val json = Json { encodeDefaults = true; ignoreUnknownKeys = true }

I do this so that I can leverage Kotlin’s data classes for direct serialization into JSON models that I can use, and ignore some of the data coming back from Presidio that I don’t care about.

Let’s also ignore the fact that I am not handling exceptions well on the call to the Presidio REST API. It will work for our purposes though.

Hooking Presidio into the Burp Proxy HTTP handler

Now all that’s left is to hook the Presidio class into the proxy handler.

With access to the new Montoya API, we can do some interesting things like highlight suspect responses directly in the proxy history and document our findings directly in the Notes field of the record.

The code looks something like this:

package com.danaepp.sensitivedatadetector

import burp.api.montoya.MontoyaApi
import burp.api.montoya.proxy.http.ProxyResponseHandler
import burp.api.montoya.proxy.http.ProxyResponseReceivedAction
import burp.api.montoya.proxy.http.ProxyResponseToBeSentAction
import burp.api.montoya.proxy.http.InterceptedResponse
import burp.api.montoya.http.message.MimeType
import burp.api.montoya.core.Annotations
import burp.api.montoya.core.HighlightColor

import com.danaepp.sensitivedatadetector.Presidio
import com.danaepp.sensitivedatadetector.SensitiveDataResult

class DetectorHttpResponseHandler(private val api: MontoyaApi) : ProxyResponseHandler
{
    override fun handleResponseReceived(interceptedResponse: InterceptedResponse?): ProxyResponseReceivedAction {
        /* This should never happen */
        if (interceptedResponse == null) {
            api.logging().logToError("Null response received. Dropping message.")
            return ProxyResponseReceivedAction.drop()
        }

        /* Condition to determine if we should ignore this response */
        if(interceptedResponse.inferredMimeType() == MimeType.JSON)
        {
            val presidio = Presidio()
            val results: List<SensitiveDataResult> = presidio.analyze(interceptedResponse.bodyToString())
           
            if (results.size > 0 ) { 
                api.logging().logToOutput(
                    "Detected potentially sensitive data. Check proxy history for highlighted suspect entries.")
                                         
                var note: String = "Potentially sensitive data detected\n======\n"

                for( r in results ) {
                    note += "${r.entityType} (Score: ${r.score})\n${r.data}\n------\n"
                }

                val annotations = Annotations.annotations(note, HighlightColor.ORANGE)
      
                return ProxyResponseReceivedAction.continueWith(interceptedResponse, annotations)
            }
        }

        return ProxyResponseReceivedAction.continueWith(interceptedResponse)
    }

    override fun handleResponseToBeSent(interceptedResponse: InterceptedResponse?): ProxyResponseToBeSentAction {
        return ProxyResponseToBeSentAction.continueWith(interceptedResponse)
    }
}

Seeing the extension in action against OWASP crAPI, we can detect things like the leakage of user email addresses in the community tab. Notice the suspect records are highlighted in orange, and the sensitive data extracted and put in the Notes field for further review:

We can also see that Presidio detected location data in crAPI, which is from the map view in the dashboard:

Conclusion

There we have it — an AI-driven sensitive data detector using Microsoft Presidio wired into Burp Suite using their new Montoya API.

It wasn’t all that hard, was it?

I have to give a shout-out to BishopFox for their helpful article on the subject. It helped guide me through Kotlin and using shadowJar to package things.

Using Kotlin to write Burp extensions in VS Code was a dream.

 

It was simple, easy, and cross-platform. I loved that I didn’t have to build a huge single-file Python2 script to do all this with the old Extender API and Jython. It was also nice to modularize the Presidio client away from the Burp extension and use data classes to manage everything.

If you would like to see the full project code for the extension, I’ve made it publicly available in a GitHub repo here.

It was a fun and productive afternoon. I learned some stuff. 

I hope you learned something too.

One last thing…

API Hacker Inner Circle

Have you joined The API Hacker Inner Circle yet? It’s my FREE weekly newsletter where I share articles like this, along with pro tips, industry insights, and community news that I don’t tend to share publicly. If you haven’t, subscribe at https://apihacker.blog.

Dana Epp

Discover more from Dana Epp's Blog

Subscribe now to keep reading and get access to the full archive.

Continue reading