diff --git a/src/main/kotlin/burp/BurpExtender.kt b/src/main/kotlin/burp/BurpExtender.kt index 82b006f..19e1fc7 100644 --- a/src/main/kotlin/burp/BurpExtender.kt +++ b/src/main/kotlin/burp/BurpExtender.kt @@ -20,15 +20,35 @@ package burp import java.net.URL import java.util.* +import java.util.concurrent.ConcurrentHashMap const val NAME = "Log4Shell scanner" -class BurpExtender : IBurpExtender, IScannerCheck { +class BurpExtender : IBurpExtender, IScannerCheck, IExtensionStateListener { private lateinit var callbacks: IBurpExtenderCallbacks private lateinit var helpers: IExtensionHelpers private lateinit var collaborator: IBurpCollaboratorClientContext + private val crontab: ConcurrentHashMap> = ConcurrentHashMap() + private val thread: Thread = object : Thread() { + override fun run() { + try { + while (true) { + sleep(60 * 1000) // 60 seconds -- poll every minute + val interactions = collaborator.fetchAllCollaboratorInteractions().groupBy { it.getProperty("interaction_id") } + for (entry in interactions.entries) { + val payload = entry.key + val (hrr, poff) = crontab[payload] ?: continue + handleInteractions(hrr, poff, entry.value, sync = false).forEach(callbacks::addScanIssue) + } + } + } catch (ex: InterruptedException) { + return + } + } + } + override fun registerExtenderCallbacks(callbacks: IBurpExtenderCallbacks) { this.callbacks = callbacks helpers = callbacks.helpers @@ -36,6 +56,7 @@ class BurpExtender : IBurpExtender, IScannerCheck { callbacks.setExtensionName(NAME) callbacks.registerScannerCheck(this) + callbacks.registerExtensionStateListener(this) } override fun doPassiveScan(baseRequestResponse: IHttpRequestResponse?): MutableList = @@ -47,18 +68,24 @@ class BurpExtender : IBurpExtender, IScannerCheck { val request = insertionPoint!!.buildRequest(bytes) val poff = insertionPoint.getPayloadOffsets(bytes) val hrr = callbacks.makeHttpRequest(baseRequestResponse!!.httpService, request) - // TODO launch a thread to handle background events - return handleInteractions(hrr, poff, payload) + val interactions = handleInteractions(hrr, poff, + collaborator.fetchCollaboratorInteractionsFor(payload), sync = true) + crontab[payload] = Pair(hrr, poff) + synchronized(thread) { + if (!thread.isAlive) thread.start() + } + return interactions } - private fun handleInteractions(hrr: IHttpRequestResponse, poff: IntArray, payload: String): MutableList { - val interactions = collaborator.fetchCollaboratorInteractionsFor(payload) + private fun handleInteractions(hrr: IHttpRequestResponse, poff: IntArray, + interactions: List, + sync: Boolean): MutableList { if (interactions.isEmpty()) return Collections.emptyList() val iri = helpers.analyzeRequest(hrr) val markers = callbacks.applyMarkers(hrr, Collections.singletonList(poff), Collections.emptyList()) return Collections.singletonList(object : IScanIssue { override fun getUrl(): URL = iri.url - override fun getIssueName(): String = "Log4Shell (CVE-2021-44228)" + override fun getIssueName(): String = "Log4Shell (CVE-2021-44228) - " + (if (sync) "synchronous" else "asynchronous") override fun getIssueType(): Int = 0x08000000 override fun getSeverity(): String = "High" override fun getConfidence(): String = "Tentative" @@ -69,7 +96,13 @@ class BurpExtender : IBurpExtender, IScannerCheck { override fun getHttpMessages(): Array = arrayOf(markers) override fun getHttpService(): IHttpService = hrr.httpService override fun getIssueDetail(): String { - val sb = StringBuilder("

The application interacted with the Collaborator server in response to a request with a Log4Shell payload

    ") + val sb = StringBuilder("

    The application interacted with the Collaborator server ") + if (sync) { + sb.append("in response to") + } else { + sb.append("some time after") + } + sb.append(" a request with a Log4Shell payload

      ") for (interaction in interactions) { sb.append("
    • ") sb.append(interaction.getProperty("type")) @@ -81,10 +114,25 @@ class BurpExtender : IBurpExtender, IScannerCheck { } sb.append("

    This means that the web service (or another node in the network) is affected by this vulnerability. ") sb.append("However, actual exploitability might depend on an attacker-controllable LDAP being reachable over the network.

    ") + if (!sync) { + sb.append("

    Since this interaction occurred some time after the original request (compare " + + "the Date header of the HTTP response vs. the interactions timestamps above), " + + "the vulnerable code might be in another process/codebase or a completely different " + + "host (e.g. centralized logging, SIEM). There might even be multiple instances of " + + "this vulnerability on different pieces of infrastructure given the nature of the bug.

    ") + } return sb.toString() } }) } override fun consolidateDuplicateIssues(existingIssue: IScanIssue?, newIssue: IScanIssue?): Int = 0 // TODO could be better + + override fun extensionUnloaded() { + synchronized(thread) { + if (thread.isAlive) { + thread.interrupt() + } + } + } }