TLDR

The source is available here. There are two projects:

  • One is for the plugin that will handle the hot reloading
  • The second is for the plugin-plugin that gets hot reloaded

EDIT: Initially I used a file system event notifier for this, but swapped to making use of a simple build script. The main plugin listens for a request to /notify. Once received, the reload logic mentioned below is applied.

The server listening for changes now looks as follows, and the file system event watcher was removed:

class HotReloader: BurpExtension, ExtensionUnloadingHandler {
    private var montoya: MontoyaApi? = null
    private var plugin: HotReloadPlugin? = null
    private var clazz: Class<*>? = null
    private var pluginInstance: Any? = null
    private var server: EmbeddedServer<NettyApplicationEngine, NettyApplicationEngine.Configuration>? = null

    override fun initialize(montoya: MontoyaApi?) {
        this.montoya = montoya!!
        this.montoya!!.extension().setName("HotReloader")

        this.server = embeddedServer(Netty, port = 8882) {
            routing {
                get("/notify") {
                    Reload()
                    call.respondText("Thx")
                }
            }
        }.start(wait = true)

    }
    ...
}

The hotreloadable plugin now builds differently:

task bigJar(type: Jar) {
    destinationDirectory.set(file("/tmp/hot-reload-plugin.jar"))

    duplicatesStrategy = DuplicatesStrategy.INCLUDE // Or EXCLUDE or STRIP

    from { configurations.compileClasspath.collect { it.isDirectory() ? it : zipTree(it) } }
    with jar

    doLast {
        notifyViaCurl("http://127.0.0.1:8882/notify")
    }
}

BurpSuite Hot Reloading

This is a bit of a pet peeve, but if your building a burp plugin you have to constantly build -> rebuild -> uncheck -> check, or if you’ve tried googling this, use the keyboard shortcut to reload the latest extension. All of this adds up to an annoying amount of time wasted when building plugins.

One solution to this is to support hot reloading of plugins, it’s actually not that hard! The general steps are:

  • Move the implementation of your plugin into a separate .jar
  • Define a specific class and method and in that .jar to serve as an entrypoint for your reloader
  • Observe the build directory for changes
  • If a change occurs, unload the previous plugin and load the modified one

You can substitute jar with .dll, .so, .py or whatever you want really - this is a fairly well understood pattern.

Plugin Implementation and Entrypoint

Our burp plugin-plugin will have the following interface:

interface HotReloadPlugin {
    // Normal init method
    fun initialize(api: MontoyaApi?)

    // Called when an extension is being unloaded
    fun extensionUnloaded()
}

In addition to the above, we will be hardcoding the name of our plugin class as __MyPlugin. Our entrypoint thus becomes fun intialize(api: MontoyaApi?), which will be the function which is called whenever our plugin is loaded. The api parameter is the same MontoyaApi type provided by burp.

Watching for FS Changes

In a separate thread we constantly watch for changes:

PATH_TO_WATCH.register(
        watchService,
        StandardWatchEventKinds.ENTRY_CREATE,
        StandardWatchEventKinds.ENTRY_MODIFY
    )
...
fun Start() {
        thread {
            while (true) {
                val key = watchService.take();
                key.pollEvents().forEach {
                    if (it.kind() == StandardWatchEventKinds.ENTRY_MODIFY) {
                        val filename: Path = it.context() as Path
                        if (PATH_TO_EXTENSION.contains(filename)) {
                            if (DEBUG)
                                this.montoya!!.logging().logToOutput("HotReload: Triggering")
                            Reload()
                            return@forEach
                        }
                    }
                    key.reset()
                }
                key.reset()
            }
        }

When a change occurs we need to first unload the previous plugin.

if (this.clazz != null){
    if (DEBUG)
        this.montoya!!.logging().logToOutput(
            "HotReload: Unloading existing extension..."
            )
    val unloader = this.clazz!!.getMethod("extensionUnloaded")
    unloader.invoke(this.pluginInstance)

    this.clazz = null
            this.montoya!!.logging().logToOutput("HotReload: Unloaded")
}

Note that we are invoking the extensionUnloaded method because we want to signal to the plugin that it needs to perform cleanup. Since our example plugin registers an HttpHandler, we need to deregister that as shown below:

  var registration = montoyaApi!!.http().registerHttpHandler(...)
  ...

  fun extensionUnloaded(){
        registration!!.deregister()
        montoyaApi!!.logging().logToOutput("Extension unloaded!")
    }

If you don’t do this, you’ll have multiple handlers operating on HTTP requests which is a nightmare.

The actual loading of the plugin is simple and facilitated with reflection:

this.clazz = cl.loadClass(CLASS_NAME);
this.pluginInstance = this.clazz!!.getDeclaredConstructor().newInstance()

// For some reason the intialize method has an extra object attached to it's parameters that
// I don't need. Maybe it's a part of Kotlin, maybe I added this because I was lazy. Anyway,
// good luck with that!
val method: Method = this.clazz!!.getMethod(
    "initialize",
    burp.api.montoya.MontoyaApi::class.java,
    clazz
)
method.invoke(pluginInstance, this.montoya, null)

There’s room for a lot of improvement on this, but it’s good enough for me to not want to flip a table.

To wrap it up, create a new plugin, the one below simply logs a message whenever a request is proxied:

import burp.api.montoya.MontoyaApi
import burp.api.montoya.core.Registration
import burp.api.montoya.http.handler.*

class __MyPlugin {
    var montoyaApi: MontoyaApi? = null
    var registration: Registration? = null
    fun initialize(montoya: MontoyaApi?, _unused: __MyPlugin?) {
        montoya!!.logging().logToOutput("Extension loaded!!!")
        montoyaApi = montoya

        registration = montoyaApi!!.http().registerHttpHandler(object : HttpHandler {
            override fun handleHttpRequestToBeSent(p0: HttpRequestToBeSent?): RequestToBeSentAction {
                montoyaApi!!.logging().logToOutput("A request was received!!!!")
                return RequestToBeSentAction.continueWith(p0)
            }

            override fun handleHttpResponseReceived(p0: HttpResponseReceived?): ResponseReceivedAction {
                return ResponseReceivedAction.continueWith(p0)
            }
        })
    }

    fun extensionUnloaded(){
        registration!!.deregister()
        montoyaApi!!.logging().logToOutput("Extension unloaded!")
    }
}

Modify the two variables in the original source:

// Path to the jar containing the extension you want to hot reload
val PATH_TO_EXTENSION =
    Paths.get("/tmp/burp-useful-utilities-1.0-SNAPSHOT.jar")
        .toAbsolutePath()
// Path to the directory to watch for file changes
val PATH_TO_WATCH =
    Paths.get("/tmp")
        .toAbsolutePath()

Load the hot-reload plugin into BurpSuite and now whenever you build your new plugin, it should be hot reloaded. If you need to build a plugin that is non-trivial, consider using the project in the source as a decent trampoline, as it’s Kotlin and builds FATjars.