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:
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:
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.
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:
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.
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()
:
Gọi đến ApplicationContext#addListener()
:
Gọi đến StandardContext#addApplicationEventListener()
:
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
:
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
Giá trị context
trong ApplicationContextFacade
, ApplicationContext
và StandardContext
đều là private nên ta có thể lợi dụng reflection để đạt được mục đích.
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:
Đ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
Đ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.
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
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ý.
Đâ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.
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
Với httpUpgradeProtocols
là một map từ String
-> UpgradePrototol
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
.
Để lấy được Http11NioProtocol
cũng khá đơn giản, request.request.connector.protocolHandler.httpUpgradeProtocols
Như vậy, class WebshellUpgrade
implement từ UpgradePrototol
sẽ như sau:
Sử dụng reflection để thêm WebshellUpgrade
vào httpUpgradeProtocols
Trong request gửi đi, ta phải thêm 2 header là Upgrade: webshell
và Connection: Upgrade
để đảm bảo server sẽ thực hiện các request theo ý ta mong muốn.
Và kết quả:
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.
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.
- 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 objectHandler
vàModelAndView
. 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.
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
.
Trong getHandler
method, object HandlerMapping
sẽ được lấy bằng cách duyệt qua this.handlerMappings
.
mapping.getHandler(request)
thực sự sẽ gọi tới org.springframework.web.servlet.handler.AbstractHandlerMapping#getHandler
.
Và trả về một instance của HandlerExecutionChain
class thông qua getHandlerExecutionChain(handler, request)
method
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);
.
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
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
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
)
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:
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:
Sau đó inject bytecode của class:
Lúc này ta có thể chạy webshell, tương tự như Servlet:
Nguồn
https://blog.csdn.net/mole_exp/article/details/123992395