CVE-2024-53677

Overview of CVE-2024-53677

CVE-2024-53677 is a file upload vulnerability due to the parameter binding logic in the FileUploadInterceptor interceptor class of Apache Struts (Struts2). An attacker can manipulate file upload parameters to enable path traversal and under certain circumstances can lead to uploading a malicious file that can be used for RCE. The flaw lies in the Struts 2 file upload mechanism. This allows attackers to manipulate file upload parameters, leading to unauthorized file placement and potentially remote code execution.

The Struts 2 framework’s file upload process stores an uploaded file in a temporary location while the data gets passed and analyzed by the action class, letting developers process the file without having to handle the file upload mechanics.

To implement a file upload function, a developer would only need to configure an action mapping, create a form with a file upload field, and write an action class that accepts the file. When the file upload feature is executed, the action class will process the file by either reading the contents of the file or moving it to an uploads folder.

Example UploadAction Class

public class UploadAction extends ActionSupport  {
    private static final String UPLOAD_DIR = "/uploads";
    private File file;
    private String fileFileName;
    private String fileContentType;

    public String execute() {
        if (fileFileName == null) return INPUT; // show file upload form page
        try {
            File destFile = new File(UPLOAD_DIR, fileFileName);
            FileUtils.copyFile(file, destFile); // copy file to uploads folder
            return SUCCESS; // show success page
        } catch (Exception e) {return ERROR;} // file upload failed; show error page
    }
    // constructors, getters, setters, ...
}

This code snipped has the /uploads directory defined as the filter to store uploaded files. This uploads folder is not accessible from the web server. Here is an example file upload form for the action:

<s:form action="upload" method="post" enctype="multipart/form-data">
<s:file name="file"/>
<s:submit/>
</s:form>

The file form field’s name is “file”, and the corresponding attributes in the action class are file, fileFileName, and fileContentType. The file prefix in these attributes corresponds to the form field name.

How File Uploads Work in Struts2

In Struts, the value stack simplifies data access between the action, view, and other components. Unlike most stacks in software, the value stack acts as an intermediary for the objects it contains. The value stack also allows for contained data to be accessed via OGNL expressions, enabling developers to reach into the stack and retrieve data efficiently. Value Stack Example:

When retrieving the value of the expression “x”, the value stack searches for an object with a property named x, starting from the top. The first occurrence is returned, which is the rectangle’s x value in our example. To get the circle’s x value, the indexing syntax [1] can be used to select the second object.

Struts2 Interceptors

Another aspect of Struts’ upload mechanics are interceptors. Interceptors handle the request of an action before and after it is executed. Interceptors manage data validation, logging, exception handling, and more.

The interceptors that are used in CVE-2024-53677 are the file upload FileUploadInterceptor and parameters ParametersInterceptor interceptors, which are used by default. The ParameterInterceptor assigns HTTP parameters to the attributes of an action object and uses the OGNL expressions and the value stack to set the value of the action attributes.

The FileUploadInterceptor class handles preparing the file upload data by setting the following attributes:

  • Temporary file location
  • Original file name
  • File content type

It relies on the ParametersInterceptor to set the attributes by inserting the values into the existing parameters map.

Struts2 File Upload Process

In conclusion, the process of the Struts2 file upload process is:

  1. A parameters map is created and populated with non-file HTTP parameters
  2. The FileUploadInterceptor inserts file details into the parameters map
  3. The ParametersInterceptor sets action class attributes using the value stack
  4. The action class is executed, and the file is moved to the /uploads directory

To understand how ONGL expressions can be used to overwrite the file attributes in the ValueStack, this illustration created by Dynatrace perfectly explains the step by step process:

How to exploit CVE-2024-53677

Now that we have an understanding of how the upload functionality works in Struts2, we can see how it can be abused. For an example, I am going to use HackTheBox’s Strutted challenge as our environment.

The Strutted page consists of a single image file upload with supported types: jpg,jpeg,png,and gif. For this upload form, the following interceptor is defined in the XML Struts definition file:

The primary logic for the upload action is in the Upload.class file, of which there are three functions that are of importance to our exploitation:

  • private boolean isImageByMagicBytes(File file){}
  • private boolean isAllowedContentType(String contentType){}
  • public String execute() throws Exception {}

The first two function are the content validation checks for the upload function, checking for file magic bytes and for accepted file extensions respectively. These function will be covered more in detail in the writeup of the Strutted machine. Here is the code for the final function:

  public String execute() throws Exception {
    String method = ServletActionContext.getRequest().getMethod();
    boolean noFileSelected = (this.upload == null || StringUtils.isBlank(this.uploadFileName));
    if (noFileSelected) {
      if ("POST".equalsIgnoreCase(method))
        addActionError("Please select a file to upload.");
      return "input";
    }
    String extension = "";
    int dotIndex = this.uploadFileName.lastIndexOf('.');
    if (dotIndex != -1 && dotIndex < this.uploadFileName.length() - 1)
      extension = this.uploadFileName.substring(dotIndex).toLowerCase();
    if (!isAllowedContentType(this.uploadContentType)) {
      addActionError("Only image files can be uploaded!");
      return "input";
    }
    if (!isImageByMagicBytes(this.upload)) {
      addActionError("The file does not appear to be a valid image.");
      return "input";
    }
    String baseUploadDirectory = System.getProperty("user.dir") + "/webapps/ROOT/uploads/";
    File baseDir = new File(baseUploadDirectory);
    if (!baseDir.exists() && !baseDir.mkdirs()) {
      addActionError("Server error: could not create base upload directory.");
      return "input";
    }
    String timeStamp = (new SimpleDateFormat("yyyyMMdd_HHmmss")).format(new Date());
    File timeDir = new File(baseDir, timeStamp);
    if (!timeDir.exists() && !timeDir.mkdirs()) {
      addActionError("Server error: could not create timestamped upload directory.");
      return "input";
    }
    String relativeImagePath = "uploads/" + timeStamp + "/" + this.uploadFileName;
    this.imagePath = relativeImagePath;
    String fullUrl = constructFullUrl(relativeImagePath);
    try {
      File destFile = new File(timeDir, this.uploadFileName);
      FileUtils.copyFile(this.upload, destFile);
      String shortId = generateShortId();
      boolean saved = this.urlMapping.saveMapping(shortId, fullUrl);
      if (!saved) {
        addActionError("Server error: could not save URL mapping.");
        return "input";
      }
      this

        .shortenedUrl = ServletActionContext.getRequest().getRequestURL().toString().replace(ServletActionContext.getRequest().getRequestURI(), "") + "/s/" + ServletActionContext.getRequest().getRequestURL().toString().replace(ServletActionContext.getRequest().getRequestURI(), "");
      addActionMessage("File uploaded successfully <a href=\"" + this.shortenedUrl + "\" target=\"_blank\">View your file</a>");
      return "success";
    } catch (Exception e) {
      addActionError("Error uploading file: " + e.getMessage());
      e.printStackTrace();
      return "input";
    }
  }

We can try to change the name of the file we upload to attempt directory traversal on the upload function:

We can see that the upload was successful, however the path of the upload remains in the correct upload directory uploads/20250316_205326/example.png. We can use the OGNL special keyword top. to attempt to clobber the uploaded file’s filename parameter to include our directory traversal characters.

Turning Directory Traversal into RCE

Now that we have control over the filename of the uploaded file, we can upload a PNG file with Java code that we can upload and rename into a .jsp file to have code executed by the server.

Using the simple jsp webshell on Kali (/usr/share/webshells/jsp/jspcmd.jsp), I’ve added as data to the png in the POST request and have changed the file extension to jsp:

We can now navigate to our uploaded webshell and observe our code execution:

Sources:

Updated: