+5

Java Webshell - Part 2: Ghi chú về Memory Webshell

Chúng ta đều biết tới webshell dạng file, như trong phần 1 mình đã trình bày. Các bạn có thể xem tại đây.

Lúc ấy, webshell sẽ được lưu lại trong folder source code, và ta access trực tiếp nó trên trình duyệt:

image.png

Tuy nhiên, các security researcher trên thế giới đã nghĩ ra một kiểu webshell khác trong Java, đó là Memory Webshell. Bài viết này mình sẽ ghi chú về tất cả những gì liên quan đến Memory Webshell mà mình tìm hiểu được.

1. Khái niệm

Memory Webshell là webshell được load vào trong Memory, hay là RAM, nó sẽ cung cấp cho attacker khả năng chạy shell trên hệ thống kể cả khi webshell đã bị xóa đi trong Disk, cho đến khi hệ thống restart.

Hiện tại, nhiều chuyên gia đã đúc kết ra nhiều cách đưa bộ nhớ webshell vào web java. Chủ đạo là đưa Filter, Listener theo các webserver khác nhau (tomcat, weblogic, v.v.) và các web framework khác nhau (servlet, spring MVC, v.v...).

2. Memory Webshell trong Tomcat Servlet

Mô hình triển khai của Servlet có thể tóm tắt trong hình sau:

image.png

Vì vậy, Memory Webshell có thể được inject thông qua 1 trong 3 phần: Listener, Filter hoặc Servlet, và một (vài) cách khác.

2.1. Inject Memory Webshell thông quan Listener

Trước tiên, chúng ta sẽ đến với một khái niệm là Listener.

Một phần tử Listener xác định một thành phần thực hiện các hành động khi có một sự kiện cụ thể xảy ra, thường là start hoặc stop Tomcat.

Ví dụ ServletRequestListener dùng để lắng nghe sự kiện construction hoặc destruction của ServletRequest object, hay tức là lúc 1 request bắt đầu và kết thúc.

image.png

Sau khi đăng kí ServletRequestListener, method requestInitialized() sẽ được gọi mỗi khi request xuất hiện. Chúng ta có thể nhận được ServletRequestObject của request từ tham số ServletRequestEvent trong requestInitialized(). Vì vậy ta có thể thêm malicous code vào requestInitialized().

Ngoài ra, vì trong mặc định chúng ta không thể lấy được HttpServletResponse object trong ServletRequestListener - thứ ta cần để lấy output command - nên nếu ta muốn làm thế, ta cần phải define một custom constructor trong custom class implement ServletRequestListener và truyền HttpServletResponse như tham số.

Một Listener mới sẽ được tạo như sau:

image.png

Listener này override hàm requestInitialized() để execute input nhận từ parameter realalphaman. Sau này, chỉ cần có 1 request nào có parameter realalphaman thì sẽ execute command, bất kể request ấy có valid hay không. Điều ta cần làm tiếp theo là làm sao để load được Listener này vào Tomcat.

Đầu tiên, ta đến với method addListener() của ServletContext. Đây là hàm sẽ thực hiện thêm một Listener mới cho context của webapp.

image.png

Context của nó có thể lấy thông qua request.getServletContext(). Tomcat có 1 cơ chế để chống truy cập trái phép từ servlet, request.getServletContext() trả về ApplicationContextFacade như một trình bao của AplpicationContext để giao tiếp với StandardContext. (Link)

Trong ApplicationContextFacade, hàm addListener():

image.png

Gọi đến ApplicationContext#addListener():

image.png

Gọi đến StandardContext#addApplicationEventListener():

image.png

image.png

Lúc này thì Listener thực sự được add vào trong Context của Web Server.

ApplicationContextFacade#addListener()
  ->ApplicationContext#addListener()
   ->StandardContext#addApplicationEventListener()

Tuy nhiên chúng ta không thể thực hiện add Listener trực tiếp thông qua ApplicationContextFacade#addListener, bởi vì trong ApplicationContext#addListener đã gọi tới hàm checkState:

image.png

image.png

Tức là nếu như trạng thái của nó không phải mới khởi chạy Tomcat thì không thể add được Listener, khi ta đang exploit thì Tomcat đã chạy xong rồi.

Vì vậy chúng ta nên gọi StandardContext#addApplicationEventListener().

Hơn nữa, class ApplicationContextFacade có thuộc tính (ApplicationContext)context và class ApplicationContext lại có thuộc tính(StandardContext)context

image.png

image.png

Giá trị context trong ApplicationContextFacade, ApplicationContextStandardContext đều là private nên ta có thể lợi dụng reflection để đạt được mục đích.

image.png

Từ context mặc định là ApplicationContextFacade, ta sử dụng reflection 2 lần trở thành StandardContext để gọi hàm addApplicationEventListener. Cuối cùng, access đến endpoint /listener.jsp là file jsp ta viết để load Listener vào context của webserver. Khi ta request lần 2, nó sẽ nhảy ra exception sau:

image.png

Điều này cho biết rằng hàm getOutputStream() đã được gọi, tức là nó bị load 2 lần => Listener đã được load.

Lúc này, nếu thêm parameter là realalphaman thì nó sẽ thực thi command

image.png

Điều đặc biệt ở đây là với 1 endpoint bất kì, chỉ cần có parameter là realalphaman thì nó sẽ thực thi command, do Listener được gọi trước khi engine của Servlet thực hiện routing, kể cả khi file listener.jsp đã xóa, vì nó đã load vào trong context của web server, chỉ khi tomcat restart thì nó mới mất.

image.png

Trong Servlet Tomcat còn có thể tạo Memory Webshell dựa trên Filter và Servlet, cách làm cũng gần tương tự, bạn đọc có thể tìm hiểu thêm tại https://uuzdaisuki.com/2021/06/29/tomcat无文件内存webshell/

2.2. Inject thông qua Processor

Processor là interface đại diện chung cho bộ xử lý của tất cả các giao thức trong Tomcat. https://tomcat.apache.org/tomcat-8.0-doc/api/org/apache/coyote/Processor.html

Trong quá trình xử lý các request, method org.apache.coyote.AbstractProcessorLight#process sẽ được gọi đến

image.png

Response sẽ được xử lý tương ứng với trạng thái hiện tại của SocketWrapperBase, và trong trạng thái OPEN_READ, Processor tương ứng sẽ được gọi để xử lý.

image.png

Đây là 1 HTTP request, trong quá trình xử lý thì Processor tương ứng được gọi là org.apache.coyote.http11.Http11NioProtocol. Trong hàm service của nó, nó kiểm tra xem giá trị của Connection header có phải là Upgrade hay không và header Upgrade có tồn tại không.

image.png

Nếu có, nó sẽ chọn UpgradeProtocol object tương ứng với Upgrade trong header và thực hiện gọi hàm accept. Để lấy UpgradeProtocol tương ứng, nó đã gọi tới hàm getUpgradeProtocol, nhưng hàm này lại lấy giá trị từ header Upgrade

image.png

Với httpUpgradeProtocols là một map từ String -> UpgradePrototol

image.png

Do vậy, chúng ta cần lấy được giá trị httpUpgradeProtocols, tạo thêm 1 class kế thừa UpgradeProtocol, thực hiện run command, sau đó add instance của nó vào httpUpgradeProtocols.

Http11NioProtocol là Processor thực hiện xử lý các request, thực tế method của nó đều nằm trong AbstractHttp11Protocol, class mà nó kế thừa từ. Hàm init duyệt qua toàn bộ UpgradeProtocol, sau đó gọi configureUpgradeProtocol và thêm upgradeProtocol tương ứng vào HashMap của httpUpgradeProtocols .

image.png

Để lấy được Http11NioProtocol cũng khá đơn giản, request.request.connector.protocolHandler.httpUpgradeProtocols

image.png

Như vậy, class WebshellUpgrade implement từ UpgradePrototol sẽ như sau:

image.png

Sử dụng reflection để thêm WebshellUpgrade vào httpUpgradeProtocols

image.png

Trong request gửi đi, ta phải thêm 2 header là Upgrade: webshellConnection: Upgrade để đảm bảo server sẽ thực hiện các request theo ý ta mong muốn.

image.png

image.png

Và kết quả:

image.png

Với những ví dụ trên, ta có thể thấy việc thêm Memory Webshell có rất nhiều cách với nhiều biến thể khác nhau, thậm chí có thể có những phương thức chưa được tìm thấy.

Một pháp sư trung hoa có bài phân tích sử dụng Executor để inject memory webshell nhưng mình chưa có thời gian nghiên cứu, bạn đọc quan tâm có thể tìm hiểu tại đây.

3. Memory Webshell trong Tomcat Spring MVC

Trong Servlet có Listener, Filter và Servlet, vậy trong Spring có thứ gì tương tự như vậy để ta lợi dụng hay không? Trước tiên hãy cùng xem cách 1 request được xử lý trong Spring MVC.

Mô hình MVC là mô hình sử dụng Model - View - Controller. Không như Servlet có file .jsp có thể xử lý logic, Spring MVC sử dụng các controller là nơi xử lý, và không thể truy cập trực tiếp từ Website. Ta sẽ phải thông qua chức năng routing để điều phối đến controller. Đây là mô hình hoạt động của Spring.

image.png

  • Filters sẽ chặn các request trước khi chúng đến được DispatcherServlet, khiến chúng phù hợp để triển khai các chức năng như authen, audit, logging
  • DispatcherServlet: được sử dụng để xử lý các HTTP request, vì nó được kế thừa từ HTTPServlet. DispatcherServlet gửi các request tới các controller và quyết định hồi đáp bằng cách gửi lại view.
  • HandlerIntercepors: chặn các yêu cầu giữa DispatcherServlet và các Controller. Điều này được thực hiện trong khuôn khổ Spring MVC, cung cấp quyền truy cập vào các object HandlerModelAndView. Nó thích hợp để triển khai các cơ chế authorize, logging, thao tác với Spring context và model.

Như vậy, ta có thể thêm malicous code vào Filters hoặc HandlerIntercepors.

Khi request tới 1 endpoint, vì trên Tomcat nên nó sẽ đi vào hàm org.apache.catalina.core.ApplicationFilterChain#internalDoFilter của Tomcat.

image.png

Thực hiện lặp đi lặp lại filterChain.doFilter(request, response) -> filter.doFilter(request, response).

Cuối cùng nó sẽ đi qua logic của tất cả các filter. Sau khi đi qua hết filter, nó sẽ đi vào org.springframework.web.servlet.DispatcherServlet#doDispatch.

Trong hàm doDispatch, một HandlerExecutionChain được lấy thông qua getHandler.

image.png

Trong getHandler method, object HandlerMapping sẽ được lấy bằng cách duyệt qua this.handlerMappings.

image.png

mapping.getHandler(request) thực sự sẽ gọi tới org.springframework.web.servlet.handler.AbstractHandlerMapping#getHandler.

image.png

Và trả về một instance của HandlerExecutionChain class thông qua getHandlerExecutionChain(handler, request)method

image.png

Ta thấy rằng nó sẽ duyệt qua tất cả các instance của class HandlerInterceptor trong object this.adaptedInterceptors, sau đó add toàn bộ interceptors đang tồn tại cho instance của HandlerExecutionChain cần được trả về thông qua chain.addInterceptor(interceptor);.

image.png

Sau khi ta đã biết được nơi mà interceptor được thêm, hãy qua trở lại hàm doDispatch, hàm applyPreHandle sẽ được gọi

image.png

Trong hàm này, nó thực hiện duyệt qua tất cả các interceptors, và gọi preHandle của từng interceptor một

image.png

Interceptors.preHandle khá giống với Filters.doFilter trong JSP

Như vậy, để đạt được Memory Webshell trong Spring MVC, ta cần phải load 1 malicous filter hoặc interceptor, chặn tất cả request tới Controller và got RCE, trong trường hợp này mình sẽ sử dụng interceptor.

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)

image.png

Tiếp theo cần chèn Interceptor này vào adaptedInterceptors như đã phân tích ở trên. Vậy làm sao để lấy được giá trị adaptedInterceptors trong môi trường code running hiện tại? Lý thuyết ta phải lấy được Context của Application. Có rất nhiều cách khác nhau (Link), mình xin trình bày một cách:

WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);

Tiếp theo, để lấy giá trị adaptedInterceptors, thông qua reflection và context ta đã lấy được phía trên:

image.png

Cuối cùng, thêm một instance của SpringShellInterceptor vào trong adaptedInterceptors.

        String className = "SpringShellInterceptor";
        String b64 = "..."; // base64 encoding of SpringShellInterceptor class bytecode
        byte[] bytes = java.util.Base64.getDecoder().decode(b64);
        java.lang.ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        try {
            classLoader.loadClass(className);
        }
        catch (ClassNotFoundException e){
            java.lang.reflect.Method method = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
            method.setAccessible(true);
            method.invoke(classLoader, className, bytes, 0, bytes.length);
            adaptedInterceptors.add(classLoader.loadClass("SpringShellInterceptor").newInstance());
        }

Đoạn mã này khi chèn vào Context, nó sẽ load Java Bytecode input (của SpringShellInterceptor). Sau đó nó sẽ cố gắng khởi tạo lại Object, thêm instance của SpringShellInterceptor vào list adaptedInterceptors. Lúc này mỗi khi request đến 1 endpoint với parameter realalphaman thì sẽ có shell, còn nếu không thì không có.

Để chèn mã này vào trong Context, ta sẽ phải thông qua một số lỗ hổng như Deserialization, SpEL Injection, v.v...

Ở đây mình sẽ tạo 1 endpoint bị SpEL Injection:

image.png

Sau đó inject bytecode của class:

image.png

Lúc này ta có thể chạy webshell, tương tự như Servlet:

image.png

Tham khảo thêm

https://blog.csdn.net/mole_exp/article/details/123992395

https://github.com/fa1c0n1/JavaInstrument

https://landgrey.me/blog/19/


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí