Java 黑帽子:如何写一个Spring4Shell(CVE-2022-22965)的POC脚本

718 阅读3分钟

Java 黑帽子:如何写一个Spring4Shell(CVE-2022-22965)的POC脚本

Spring4Shell漏洞

Spring4Shell 漏洞是 Spring 框架的一个RCE漏洞,漏洞编号为 CVE-2022-22965 。这个漏洞的利用方式挺经典的,通过日志写入一个 shell 从而远程执行代码。

原理

POC 是 Proof of Concept 的简称,简单来说就是验证漏洞是否存在。要想确认漏洞是否存在,了解漏洞的原理必不可少。

通过日志文件生成shell

Spring4Shell 漏洞利用了Spring 框架解析参数的功能,修改日志的配置参数,生成木马文件。

Tomcat中有一个AccessLogValve 类,这个类有几个重要变量:

  • directory:日志文件所在目录
  • prefix:日志文件名
  • pattern:日志文件内容
  • suffix:日志文件名后缀
  • fileDateFormat:日志文件名日期格式

要是我们修改了这几个变量,例如 suffix 参数设置为“jsp”,那么生成的日志文件后缀将是“.jsp”。而 pattern 参数设置为一句话木马代码,我们就能在 directory 配置的目录下生成木马文件。

参数解析控制变量

这里涉及到 Spring MVC 的参数解析功能。当我们通过 GET 或者 POST 方式请求接口时,Spring 框架会帮助我们解析参数,例如 www.xxx.com/user?name=x… 框架会解析 name ,并设置为xxx。问题就出在这,它不仅能解析 name ,形如 name.classLoader 带有点语法的也能解析成getName().getClassLoader()。

通过这个点语法我们可以构造一系列的调用,最后修改 AccessLogValve 的属性。

具体原理就不花费太多篇幅了,网上已有不少解析原理的文章。

利用条件

这里先列举一下利用条件。

  1. JDK 版本 ≥ 1.9 因为利用时需要用到 class.module.classLoader ,getModule 方法是 JDK 1.9 之后才有的。JDK 1.9 之前这个参数解析已经出现过类似漏洞了,具体可以参考 CVE-2010-1622。这个漏洞后 Spring 在解析时加了判断,但 JDK 1.9 由于支持 Module 导致可以绕过这个判断。
  2. 应用通过war 包部署 因为利用时需要用到class.module.classLoader.resource ,通过这个resource 再层层找到AccessLogValve的属性。而war包和jar包的部署方式,他们加载的ClassLoader 以及 Resource 类是不一样的,jar 包部署的方式没法通过调用链设置AccessLogValve 属性。

各大厂商的POC 是怎么写的

这个漏洞已经出现有四个多月了,网上能找到不少POC。写一个POC 之前我们可以看看别人是怎么写的,从中学习一下。

微软的检测方式

应该怎么检测这个漏洞呢?我们可以看看微软的Guidance

Any system using JDK 9.0 or later and using the Spring Framework or derivative frameworks should be considered vulnerable. The following nonmalicious command can be used to determine vulnerable systems:

$ curl host:port/path?class.module.classLoader.URLs%5B0%5D=0

A host that returns an HTTP 400 response should be considered vulnerable to the attack detailed in the proof of concept (POC) below. Note that while this test is a good indicator of a system’s susceptibility to an attack, any system within the scope of impacted systems listed above should still be considered vulnerable.

微软建议的 payload 是设置classLoader 的属性URLs[0]=0,如果返回400错误,就证明有漏洞。

原理很简单,Spring 解析参数时,得到 ClassLoader 后会调用其中的 getUrls 方法,并且设置urls[0] 为0,但由于 URLs 数组的类型为 URL 类型,设置0会触发类型不符异常,导致400错误。

对比一下利用条件:

  1. JDK 版本 ≥ 1.9 getModule 是JDK1.9 以上才有的方法,可以验证
  2. 应用使用war包部署 从 payload 来看无法验证

对于第二点,因为一个应用以 jar 包形式部署,ClassLoader 嵌套参数会被解析为LaunchedURLClassLoader,它也是有 getUrls 方法的,但之后调用链获取的 resource 没有设置AccessLogValve 一系列属性的方法,无法通过日志利用。

测试了一下,用 jar 打包的方式启动,断点调试发现 ClassLoader 是 LaunchedURLClassLoader,使用 class.module.classLoader.URLs%5B0%5D=0 payload 请求也会报错,说明这个 payload 用于检测在某些情况会误报。但不知道为什么搜到的不少检测方法都用这一个 payload 。

ZAP的检测方式

ZAP 的检测方式和微软推荐的差不多,但payload 稍微有点不一样。查看ZAP 的 Spring4ShellScanRule.java 文件,payload 为

"class.module.classLoader.DefaultAssertionStatus=nonsense"

和上一个原理一样,也是通过设置参数,参数类型不对抛异常来检测漏洞是否存在。defaultAssertionStatus 是java.lang.ClassLoader 类里的一个Boolean变量,同样未能验证应用时用war包部署。

User.getClass()
    java.lang.Class.getModule()
        java.lang.Module.getClassLoader()
            org.apache.catalina.loader.ParallelWebappClassLoader.getResources()
                org.apache.catalina.webresources.StandardRoot.getContext()
                    org.apache.catalina.core.StandardContext.getParent()
                        org.apache.catalina.core.StandardHost.getPipeline()
                            org.apache.catalina.core.StandardPipeline.getFirst()
                                org.apache.catalina.valves.AccessLogValve.setPattern()

那我们的目标就很清晰了,最终调用是 AccessLogValve,那我们在 AccessLogValve 找到一个变量,然后传一个错误类型的参数,抛异常的话不就确认了是否可以利用吗?这也能减少一部分误报。

nuclei 的检测方式

这是在**nuclei-templates** 里的检测方法,我们来看看payload。

class.module.classLoader.resources.context.configFile=https://{{interactsh-url}}&class.module.classLoader.resources.context.configFile.content.aaa=xxx

初看 nuclei 的 payload ,还以为是用了两种方式检测,configFile 设置外带地址,content.aaa=xxx 触发异常。原来不是,configFile 是一个 URL 类型,设置 configFile 后是没有请求调用的,congfigFile.content 调用了 getContent 方法,这个方法会触发请求url。源码如下:

public final Object getContent() throws java.io.IOException {
        return openConnection().getContent();
 }

如果外带成功,证明漏洞存在。

Kotlin 实现

Kotlin 可以认为是 Java 的语法糖,这次使用Kotlin 来实现脚本。

理解了原理,脚本实现就很简单了。使用控制变量法比较正常payload 和检测payload,如果响应不一样,可以确认漏洞存在。

可以参考下《抽象的黑盒漏洞扫描与风险技术评估方法论》里的逻辑:

Untitled (1).png

由于时间关系,暂时实现了Get请求的检测。

#!/usr/bin/env kotlin

@file:DependsOn("com.squareup.moshi:moshi:1.13.0")
@file:DependsOn("com.squareup.moshi:moshi-kotlin:1.13.0")
@file:DependsOn("com.squareup.okhttp3:okhttp:4.9.3")
@file:DependsOn("org.jetbrains.kotlinx:kotlinx-cli-jvm:0.3.5")

import kotlinx.cli.ArgParser
import kotlinx.cli.ArgType
import kotlinx.cli.required
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response

val PAYLOAD_SAFE = "aaa=bbb"
val PAYLOAD_ATTACK = "class.module.classLoader.resources.context.parent.pipeline.first.buffered=nonsense"

val client = OkHttpClient()

val spring4ShellArgs = Spring4ShellArgs(args)
val isVulnerable = verify(spring4ShellArgs.url)
println("Vulnerable: $isVulnerable")

/**
 * Verifies if the url is vulnerable
 */
fun verify(url: String): Boolean {
    println("Spring4Shell url: $url")
    if (isUrlAvailable(url)) {
        if (isAttackPayload400(url)) {
            return isSafePayloadSuccessful(url)
        }
        return false
    }
    return false
}

/**
 * Check if the url is available
 */
fun isUrlAvailable(url: String): Boolean {
    val request: Request = Request.Builder().url(url).build()
    val response = client.newCall(request).execute()
    return response.isSuccessful;
}

/**
 * Check if the attack payload is 400
 */
fun isAttackPayload400(url: String): Boolean {
    val response = get(url)
    return response.code == 400
}

/**
 * Check if the safe payload is successful
 */
fun isSafePayloadSuccessful(url: String): Boolean {
    val request: Request = Request.Builder().url("$url?$PAYLOAD_SAFE").build()
    val response = client.newCall(request).execute()
    println("Spring4Shell safe payload request: $request response: $response")
    return response.isSuccessful
}

fun get(url: String): Response {
    val request: Request = Request.Builder().url("$url?$PAYLOAD_ATTACK").build()
    val response = client.newCall(request).execute()
    println("Spring4Shell attack payload request: $request response: $response")
    return response;
}

class Spring4ShellArgs(args: Array<String>) {
    private val parser = ArgParser("spring4shell")
    val url by parser.option(ArgType.String, shortName = "u", description = "Url").required()

    init {
        parser.parse(args)
    }
}

测试

vulfocus靶机检测

打开vulfoucs 的靶机,执行检测脚本。

Untitled.png

kotlin sprint4shell.main.kts -u http://123.58.224.8:38463/

Spring4Shell url: http://123.58.224.8:38463/
Spring4Shell attack payload response: Response{protocol=http/1.1, code=400, message=, url=http://123.58.224.8:38463/?class.module.classLoader.resources.context.parent.pipeline.first.buffered=nonsense}
Spring4Shell safe payload response: Response{protocol=http/1.1, code=200, message=, url=http://123.58.224.8:38463/?aaa=bbb}

Vulnerable: true

检测成功。

参考文献

如何使用Spring4Shell-Scan扫描Spring4Shell漏洞和Spring Cloud…

抽象的黑盒漏洞扫描与风险技术评估方法论

Spring 远程命令执行漏洞(CVE-2022-22965)原理分析和思考

spring rce 从cve-2010-1622到CVE-2022-22965 篇一