Skip to content

Commit ffa6550

Browse files
David MaloneyDavid Maloney
authored andcommitted
Land rapid7#4787, HD's new Zabbix and Chef LoginScanners
Lands the new LoginScanners HD wrote for Zabbix and the Chef WebUI
2 parents 3551163 + 804db0f commit ffa6550

File tree

7 files changed

+1012
-2
lines changed

7 files changed

+1012
-2
lines changed
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
2+
require 'metasploit/framework/login_scanner/http'
3+
4+
module Metasploit
5+
module Framework
6+
module LoginScanner
7+
8+
# The ChefWebUI HTTP LoginScanner class provides methods to authenticate to Chef WebUI
9+
class ChefWebUI < HTTP
10+
11+
DEFAULT_PORT = 80
12+
PRIVATE_TYPES = [ :password ]
13+
14+
# @!attribute session_name
15+
# @return [String] Cookie name for session_id
16+
attr_accessor :session_name
17+
18+
# @!attribute session_id
19+
# @return [String] Cookie value
20+
attr_accessor :session_id
21+
22+
# Decides which login routine and returns the results
23+
#
24+
# @param credential [Metasploit::Framework::Credential] The credential object
25+
# @return [Result]
26+
def attempt_login(credential)
27+
result_opts = { credential: credential }
28+
29+
begin
30+
status = try_login(credential)
31+
result_opts.merge!(status)
32+
rescue ::EOFError, Rex::ConnectionError, ::Timeout::Error => e
33+
result_opts.merge!(status: Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: e)
34+
end
35+
36+
Result.new(result_opts)
37+
end
38+
39+
# (see Base#check_setup)
40+
def check_setup
41+
begin
42+
res = send_request({'uri' => normalize_uri('/users/login')})
43+
return "Connection failed" if res.nil?
44+
45+
if res.code != 200
46+
return "Unexpected HTTP response code #{res.code} (is this really Chef WebUI?)"
47+
end
48+
49+
if res.body.to_s !~ /<title>Chef Server<\/title>/
50+
return "Unexpected HTTP body (is this really Chef WebUI?)"
51+
end
52+
53+
rescue ::EOFError, Errno::ETIMEDOUT, Rex::ConnectionError, ::Timeout::Error
54+
return "Unable to connect to target"
55+
end
56+
57+
false
58+
end
59+
60+
# Sends a HTTP request with Rex
61+
#
62+
# @param (see Rex::Proto::Http::Resquest#request_raw)
63+
# @return [Rex::Proto::Http::Response] The HTTP response
64+
def send_request(opts)
65+
cli = Rex::Proto::Http::Client.new(host, port, {'Msf' => framework, 'MsfExploit' => self}, ssl, ssl_version, proxies)
66+
cli.connect
67+
req = cli.request_raw(opts)
68+
res = cli.send_recv(req)
69+
70+
# Save the session ID cookie
71+
if res && res.get_cookies =~ /(_\w+_session)=([^;$]+)/i
72+
self.session_name = $1
73+
self.session_id = $2
74+
end
75+
76+
res
77+
end
78+
79+
# Sends a login request
80+
#
81+
# @param credential [Metasploit::Framework::Credential] The credential object
82+
# @return [Rex::Proto::Http::Response] The HTTP auth response
83+
def try_credential(csrf_token, credential)
84+
85+
data = "utf8=%E2%9C%93" # ✓
86+
data << "&authenticity_token=#{Rex::Text.uri_encode(csrf_token)}"
87+
data << "&name=#{Rex::Text.uri_encode(credential.public)}"
88+
data << "&password=#{Rex::Text.uri_encode(credential.private)}"
89+
data << "&commit=login"
90+
91+
opts = {
92+
'uri' => normalize_uri('/users/login_exec'),
93+
'method' => 'POST',
94+
'data' => data,
95+
'headers' => {
96+
'Content-Type' => 'application/x-www-form-urlencoded',
97+
'Cookie' => "#{self.session_name}=#{self.session_id}"
98+
}
99+
}
100+
101+
send_request(opts)
102+
end
103+
104+
105+
# Tries to login to Chef WebUI
106+
#
107+
# @param credential [Metasploit::Framework::Credential] The credential object
108+
# @return [Hash]
109+
# * :status [Metasploit::Model::Login::Status]
110+
# * :proof [String] the HTTP response body
111+
def try_login(credential)
112+
113+
# Obtain a CSRF token first
114+
res = send_request({'uri' => normalize_uri('/users/login')})
115+
unless (res && res.code == 200 && res.body =~ /input name="authenticity_token" type="hidden" value="([^"]+)"/m)
116+
return {:status => Metasploit::Model::Login::Status::UNTRIED, :proof => res.body}
117+
end
118+
119+
csrf_token = $1
120+
121+
res = try_credential(csrf_token, credential)
122+
if res && res.code == 302
123+
opts = {
124+
'uri' => normalize_uri("/users/#{credential.public}/edit"),
125+
'method' => 'GET',
126+
'headers' => {
127+
'Cookie' => "#{self.session_name}=#{self.session_id}"
128+
}
129+
}
130+
res = send_request(opts)
131+
if (res && res.code == 200 && res.body.to_s =~ /New password for the User/)
132+
return {:status => Metasploit::Model::Login::Status::SUCCESSFUL, :proof => res.body}
133+
end
134+
end
135+
136+
{:status => Metasploit::Model::Login::Status::INCORRECT, :proof => res.body}
137+
end
138+
139+
end
140+
end
141+
end
142+
end
143+

lib/metasploit/framework/login_scanner/http.rb

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def check_setup
5555
)
5656

5757
begin
58-
# Use _send_recv instead of send_recv to skip automatiu
58+
# Use _send_recv instead of send_recv to skip automatic
5959
# authentication
6060
response = http_client._send_recv(request)
6161
rescue ::EOFError, Errno::ETIMEDOUT, Rex::ConnectionError, ::Timeout::Error
@@ -95,7 +95,7 @@ def attempt_login(credential)
9595
end
9696

9797
http_client = Rex::Proto::Http::Client.new(
98-
host, port, {}, ssl, ssl_version,
98+
host, port, {'Msf' => framework, 'MsfExploit' => framework_module}, ssl, ssl_version,
9999
proxies, credential.public, credential.private
100100
)
101101

@@ -160,6 +160,14 @@ def set_sane_defaults
160160
nil
161161
end
162162

163+
# Combine the base URI with the target URI in a sane fashion
164+
#
165+
# @param [String] The target URL
166+
# @return [String] the final URL mapped against the base
167+
def normalize_uri(target_uri)
168+
(self.uri.to_s + "/" + target_uri.to_s).gsub(/\/+/, '/')
169+
end
170+
163171
end
164172
end
165173
end
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
2+
require 'metasploit/framework/login_scanner/http'
3+
4+
module Metasploit
5+
module Framework
6+
module LoginScanner
7+
8+
# The Zabbix HTTP LoginScanner class provides methods to do login routines
9+
# for Zabbix 2.4 and 2.2
10+
class Zabbix < HTTP
11+
12+
DEFAULT_PORT = 80
13+
PRIVATE_TYPES = [ :password ]
14+
15+
# @!attribute version
16+
# @return [String] Product version
17+
attr_accessor :version
18+
19+
# @!attribute zsession
20+
# @return [String] Cookie session
21+
attr_accessor :zsession
22+
23+
# Decides which login routine and returns the results
24+
#
25+
# @param credential [Metasploit::Framework::Credential] The credential object
26+
# @return [Result]
27+
def attempt_login(credential)
28+
result_opts = { credential: credential }
29+
30+
begin
31+
status = try_login(credential)
32+
result_opts.merge!(status)
33+
rescue ::EOFError, Rex::ConnectionError, ::Timeout::Error => e
34+
result_opts.merge!(status: Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: e)
35+
end
36+
37+
Result.new(result_opts)
38+
end
39+
40+
41+
# (see Base#check_setup)
42+
def check_setup
43+
begin
44+
res = send_request({'uri' => normalize_uri('/')})
45+
return "Connection failed" if res.nil?
46+
47+
if res.code != 200
48+
return "Unexpected HTTP response code #{res.code} (is this really Zabbix?)"
49+
end
50+
51+
if res.body.to_s !~ /Zabbix ([^\s]+) Copyright .* by Zabbix/m
52+
return "Unexpected HTTP body (is this really Zabbix?)"
53+
end
54+
55+
self.version = $1
56+
57+
rescue ::EOFError, Errno::ETIMEDOUT, Rex::ConnectionError, ::Timeout::Error
58+
return "Unable to connect to target"
59+
end
60+
61+
false
62+
end
63+
64+
# Sends a HTTP request with Rex
65+
#
66+
# @param (see Rex::Proto::Http::Resquest#request_raw)
67+
# @return [Rex::Proto::Http::Response] The HTTP response
68+
def send_request(opts)
69+
cli = Rex::Proto::Http::Client.new(host, port, {'Msf' => framework, 'MsfExploit' => self}, ssl, ssl_version, proxies)
70+
cli.connect
71+
req = cli.request_raw(opts)
72+
res = cli.send_recv(req)
73+
74+
# Found a cookie? Set it. We're going to need it.
75+
if res && res.get_cookies =~ /zbx_sessionid=(\w*);/i
76+
self.zsession = $1
77+
end
78+
79+
res
80+
end
81+
82+
# Sends a login request
83+
#
84+
# @param credential [Metasploit::Framework::Credential] The credential object
85+
# @return [Rex::Proto::Http::Response] The HTTP auth response
86+
def try_credential(credential)
87+
88+
data = "request="
89+
data << "&name=#{Rex::Text.uri_encode(credential.public)}"
90+
data << "&password=#{Rex::Text.uri_encode(credential.private)}"
91+
data << "&autologin=1"
92+
data << "&enter=Sign%20in"
93+
94+
opts = {
95+
'uri' => normalize_uri('index.php'),
96+
'method' => 'POST',
97+
'data' => data,
98+
'headers' => {
99+
'Content-Type' => 'application/x-www-form-urlencoded'
100+
}
101+
}
102+
103+
send_request(opts)
104+
end
105+
106+
107+
# Tries to login to Zabbix
108+
#
109+
# @param credential [Metasploit::Framework::Credential] The credential object
110+
# @return [Hash]
111+
# * :status [Metasploit::Model::Login::Status]
112+
# * :proof [String] the HTTP response body
113+
def try_login(credential)
114+
res = try_credential(credential)
115+
if res && res.code == 302
116+
opts = {
117+
'uri' => normalize_uri('profile.php'),
118+
'method' => 'GET',
119+
'headers' => {
120+
'Cookie' => "zbx_sessionid=#{self.zsession}"
121+
}
122+
}
123+
res = send_request(opts)
124+
if (res && res.code == 200 && res.body.to_s =~ /<title>Zabbix .*: User profile<\/title>/)
125+
return {:status => Metasploit::Model::Login::Status::SUCCESSFUL, :proof => res.body}
126+
end
127+
end
128+
129+
{:status => Metasploit::Model::Login::Status::INCORRECT, :proof => res.body}
130+
end
131+
132+
end
133+
end
134+
end
135+
end
136+

0 commit comments

Comments
 (0)