前言

前一段时间风风火火的Spring RCE,看起来限制条件还是挺多的,学习一下。


环境搭建

漏洞信息,要求在JDK9+的环境下在Tomcat部署war包,如果是通过SpringBoot可执行Jar文件的方式部署则漏洞不可用,但不排除有更多方法可以完成利用。

先搞个Spring MVC方便利用,使用IDEA新建项目:

再添加依赖:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>${spring.version}</version>
</dependency>

新建控制器包com.example.controller,里面放一个IndexController控制器:

package com.example.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping
public class IndexController {
    @RequestMapping("/index")
    public String index() {
        return "index";
    }
}

新建resources文件夹,在resources文件中新建spring config文件spring-mvc.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:mvc="http://www.springframework.org/schema/mvc"
       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/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    <!--开启框架注解-->
    <mvc:annotation-driven/>
    <!--开启注解扫描-->
    <context:component-scan base-package="com.example.controller"/>
    <!--视图解析-->
    <bean id="internalResourceViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <!--前缀-->
        <property name="prefix" value="/WEB-INF/views/"/>
        <!--后缀-->
        <property name="suffix" value=".jsp"/>
    </bean>
</beans>

在WEB-INF下新建views文件夹,里面放一个index.jsp模板文件:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
    <head>
        <title>Index</title>
    </head>
    <body>
        <h1>Index</h1>
    </body>
</html>

最后修改web.xml:

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
    <display-name>Archetype Created Web Application</display-name>
    <servlet>
        <servlet-name>SpringMVC</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring-mvc.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>

配置war部署,看到控制器运行结果:

接下来开始布置漏洞环境,在com.example包下新建一个model包,放一个有基础getter和setter的POJO类:

package com.example.model;

public class User {
    private long id;

    private String username;
    private String password;
    private String email;

    public long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String toString() {
        return "[Username: " + username + ", Password: " + password + ", Email: " + email + "]";
    }
}

然后修改一下控制器:

@RequestMapping("/user")
public String user(User user) {
    System.out.println(user);
    return "user";
}

可以看到对象绑定成功了:

漏洞分析

给User类中的setter下一个断点,看看他的对象绑定是如何进行的,从dobind开始找到AbstractNestablePropertyAccessor类的setPropertyValue函数:

public void setPropertyValue(PropertyValue pv) throws BeansException {
    AbstractNestablePropertyAccessor.PropertyTokenHolder tokens = (AbstractNestablePropertyAccessor.PropertyTokenHolder)pv.resolvedTokens;
    if (tokens == null) {
        String propertyName = pv.getName();

        AbstractNestablePropertyAccessor nestedPa;
        try {
            nestedPa = this.getPropertyAccessorForPropertyPath(propertyName);
        } 
        ...

        tokens = this.getPropertyNameTokens(this.getFinalPath(nestedPa, propertyName));
        if (nestedPa == this) {
            pv.getOriginalPropertyValue().resolvedTokens = tokens;
        }

        nestedPa.setPropertyValue(tokens, pv);
    } else {
        this.setPropertyValue(tokens, pv);
    }

}

此时的参数pv:

即我们输入的email,下一步就是获取这些数据要赋值到什么属性里去,跟入getPropertyAccessorForPropertyPath函数:

protected AbstractNestablePropertyAccessor getPropertyAccessorForPropertyPath(String propertyPath) {
    int pos = PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(propertyPath);
    if (pos > -1) {
        String nestedProperty = propertyPath.substring(0, pos);
        String nestedPath = propertyPath.substring(pos + 1);
        AbstractNestablePropertyAccessor nestedPa = this.getNestedPropertyAccessor(nestedProperty);
        return nestedPa.getPropertyAccessorForPropertyPath(nestedPath);
    } else {
        return this;
    }
}

然后会判断属性名中是否存在.[]等字符,将属性名按.切成两半,前半作为参数调用调用getNestedPropertyAccessor,修改一下输入的属性名加个.再重开调试:

private AbstractNestablePropertyAccessor getNestedPropertyAccessor(String nestedProperty) {
    if (this.nestedPropertyAccessors == null) {
        this.nestedPropertyAccessors = new HashMap();
    }

    AbstractNestablePropertyAccessor.PropertyTokenHolder tokens = this.getPropertyNameTokens(nestedProperty);
    String canonicalName = tokens.canonicalName;
    Object value = this.getPropertyValue(tokens);
    ...

    AbstractNestablePropertyAccessor nestedPa = (AbstractNestablePropertyAccessor)this.nestedPropertyAccessors.get(canonicalName);
    if (nestedPa != null && nestedPa.getWrappedInstance() == ObjectUtils.unwrapOptional(value)) {
        ...
    } else {
        ...

        nestedPa = this.newNestedPropertyAccessor(value, this.nestedPath + canonicalName + ".");
        ...
    }

    return nestedPa;
}

可以看到value是个Object对象,最后会放入nestedPa中返回,看起来是对象绑定的主体,跟入getPropertyValue:

String propertyName = tokens.canonicalName;
String actualName = tokens.actualName;
AbstractNestablePropertyAccessor.PropertyHandler ph = this.getLocalPropertyHandler(actualName);

调用getLocalPropertyHandler获取属性处理对象,再跟入:

protected BeanWrapperImpl.BeanPropertyHandler getLocalPropertyHandler(String propertyName) {
    PropertyDescriptor pd = this.getCachedIntrospectionResults().getPropertyDescriptor(propertyName);
    return pd != null ? new BeanWrapperImpl.BeanPropertyHandler(pd) : null;
}

getCachedIntrospectionResults会依据User类新建一个处理对象返回:

results = new CachedIntrospectionResults(beanClass);
...
CachedIntrospectionResults existing = (CachedIntrospectionResults)classCacheToUse.putIfAbsent(beanClass, results);
return existing != null ? existing : results;

CachedIntrospectionResults的构造函数中调用getBeanInfo获取对象属性信息:

private static BeanInfo getBeanInfo(Class<?> beanClass) throws IntrospectionException {
    Iterator var1 = beanInfoFactories.iterator();

    BeanInfo beanInfo;
    do {
        if (!var1.hasNext()) {
            return shouldIntrospectorIgnoreBeaninfoClasses ? Introspector.getBeanInfo(beanClass, 3) : Introspector.getBeanInfo(beanClass);
        }

        BeanInfoFactory beanInfoFactory = (BeanInfoFactory)var1.next();
        beanInfo = beanInfoFactory.getBeanInfo(beanClass);
    } while(beanInfo == null);

    return beanInfo;
}

来到Java内部Introspector类的getBeanInfo函数中,除了获取对象自身的属性信息,还会去找父类的属性信息,于是在找父类java.lang.Object的属性信息时调用的getTargetPropertyInfo函数中:

if (argCount == 0) {
    if (name.startsWith(GET_PREFIX)) {
        // Simple getter
        pd = new PropertyDescriptor(this.beanClass, name.substring(3), method, null);
    } else if (resultType == boolean.class && name.startsWith(IS_PREFIX)) {
        // Boolean getter
        pd = new PropertyDescriptor(this.beanClass, name.substring(2), method, null);
    }
} 

如果存在isXXX和getXXX就会将后面的部分作为一个属性,也就导致了User类多了个叫做class的属性,也就导致了在getPropertyDescriptor函数中:

PropertyDescriptor getPropertyDescriptor(String name) {
    PropertyDescriptor pd = (PropertyDescriptor)this.propertyDescriptors.get(name);
    if (pd == null && StringUtils.hasLength(name)) {
        pd = (PropertyDescriptor)this.propertyDescriptors.get(StringUtils.uncapitalize(name));
        if (pd == null) {
            pd = (PropertyDescriptor)this.propertyDescriptors.get(StringUtils.capitalize(name));
        }
    }

    return pd;
}

而propertyDescriptors中除了我们定义的4个属性的描述符外还有一个奇怪的东西:

其getter为getClass,也就是说我们可以通过class.xxx访问user.getClass.xxx:

漏洞利用

在JDK9+环境下,getTargetPropertyInfo的属性信息从缓存中获取:

Map.Entry<String,PropertyInfo> entry : ClassInfo.get(this.beanClass).getProperties().entrySet()

ClassInfo的get函数如下:

public static ClassInfo get(Class<?> type) {
    if (type == null) {
        return DEFAULT;
    }
    try {
        checkPackageAccess(type);
        return CACHE.get(type);
    } catch (SecurityException exception) {
        return DEFAULT;
    }
}

最后从CACHE中取出该类的属性信息,CACHE在项目运行时开始构建,最后来到PropertyInfo类的get函数:

switch (method.getParameterCount()) {
    case 0:
        if (returnType.equals(boolean.class) && isPrefix(name, "is")) {
            PropertyInfo info = getInfo(map, name.substring(2), false);
            info.read = new MethodInfo(method, boolean.class);
        } else if (!returnType.equals(void.class) && isPrefix(name, "get")) {
            PropertyInfo info = getInfo(map, name.substring(3), false);
            info.readList = add(info.readList, method, method.getGenericReturnType());
        }
        break;
    case 1:
        if (returnType.equals(void.class) && isPrefix(name, "set")) {
            PropertyInfo info = getInfo(map, name.substring(3), false);
            info.writeList = add(info.writeList, method, method.getGenericParameterTypes()[0]);
        } else if (!returnType.equals(void.class) && method.getParameterTypes()[0].equals(int.class) && isPrefix(name, "get")) {
            PropertyInfo info = getInfo(map, name.substring(3), true);
            info.readList = add(info.readList, method, method.getGenericReturnType());
        }
        break;
    case 2:
        if (returnType.equals(void.class) && method.getParameterTypes()[0].equals(int.class) && isPrefix(name, "set")) {
            PropertyInfo info = getInfo(map, name.substring(3), true);
            info.writeList = add(info.writeList, method, method.getGenericParameterTypes()[1]);
        }
        break;
}

根据函数参数数量获取参数,比JDK8+更复杂获取的结果也更多了,原本这里是有classLoader的:

但是回到CachedIntrospectionResults类的构造函数函数后,它会遍历所有属性信息,然后:

if (Class.class != beanClass || !"classLoader".equals(pd.getName()) && !"protectionDomain".equals(pd.getName())) {
    ...

    pd = this.buildGenericTypeAwarePropertyDescriptor(beanClass, pd);
    this.propertyDescriptors.put(pd.getName(), pd);
}

将Class中的classLoader给吃掉了,所以无法直接通过class.classloader来获取classloader。

但是Class中存在module:

private final ClassLoader loader;

可以通过class.module.classLoader获得一个classloader:

class.module.classLoader.resources.context获取Tomcat的context时问题又来了,我使用的Tomcat版本为9.0.62,该版本下的getResources函数为:

/** @deprecated */
@Deprecated
public WebResourceRoot getResources() {
    return null;
}

已经被废弃了,无法获取resources。

没办法,再下载多一个Tomcat8.0.53,其getResources为:

public WebResourceRoot getResources() {
    return this.resources;
}

还是可以利用的,所以这payload在高版本Tomcat下面也打不通,但不排除还有其他玩法。

最后打法可以看参考文章,通过修改日志文件目录和后缀的方式完成getshell。

漏洞修复

在5.2.20.RELEASE版本下,CachedIntrospectionResults类的构造函数做了更多限制:

PropertyDescriptor pd = var3[var5];
if ((Class.class != beanClass || "name".equals(pd.getName()) || pd.getName().endsWith("Name")) && (pd.getPropertyType() == null || !ClassLoader.class.isAssignableFrom(pd.getPropertyType()) && !ProtectionDomain.class.isAssignableFrom(pd.getPropertyType()))) {
    ...

    pd = this.buildGenericTypeAwarePropertyDescriptor(beanClass, pd);
    this.propertyDescriptors.put(pd.getName(), pd);
}

无法获取类型为ClassLoader的属性了。


参考

https://www.cnblogs.com/chenty/p/14373273.html

https://mp.weixin.qq.com/s?__biz=MzI3MTQyNzQxMA==&mid=2247484800&idx=1&sn=06dd94d84f5fdd24f9e1a2b95d896a8a&chksm=eac0b7bdddb73eab4cc0225c38646d57cd17a668700a95e9d57f1f369db6a0ade9a003d9fd39&mpshare=1&scene=2&srcid=0401971aXDidTqenAU9KC0Ct&sharer_sharetime=1648820954447&sharer_shareid=3ca7aa86b51d7b96de87c8f2d9956820&from=timeline#rd


Web Java Spring

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

struts2系列漏洞 S2-062