Merge branch '1333_rating_limit' into 1333_rate_merged

This commit is contained in:
Bucky Wilson
2018-01-04 15:42:55 +10:00
10 changed files with 259 additions and 70 deletions

36
Gemfile
View File

@@ -83,23 +83,25 @@ end
# For running unit tests
group :test do
if ENV['BEEF_TEST']
gem 'rake'
gem 'test-unit'
gem 'test-unit-full'
gem 'curb'
gem 'selenium'
# selenium-webdriver 3.x is incompatible with Firefox version 48 and prior
gem 'selenium-webdriver', '~> 2.53.4'
gem 'rspec'
gem 'bundler-audit'
# nokogirl is needed by capybara which may require one of the below commands
# sudo apt-get install libxslt-dev libxml2-dev
# sudo port install libxml2 libxslt
gem 'capybara'
# RESTful API tests/generic command module tests
gem 'rest-client', '>= 2.0.1'
end
if ENV['BEEF_TEST']
gem 'rake'
gem 'test-unit'
gem 'test-unit-full'
gem 'curb'
gem 'selenium'
# selenium-webdriver 3.x is incompatible with Firefox version 48 and prior
gem 'selenium-webdriver', '~> 2.53.4'
gem 'rspec'
gem 'bundler-audit'
# nokogirl is needed by capybara which may require one of the below commands
# sudo apt-get install libxslt-dev libxml2-dev
# sudo port install libxml2 libxslt
gem 'capybara'
# RESTful API tests/generic command module tests
gem 'rest-client', '>= 2.0.1'
gem 'pry'
gem 'pry-byebug'
end
end
source 'https://rubygems.org'

View File

@@ -3,6 +3,8 @@
# Browser Exploitation Framework (BeEF) - http://beefproject.com
# See the file 'doc/COPYING' for copying permission
#
require 'yaml'
require 'pry-byebug'
task :default => ["quick"]
@@ -50,6 +52,14 @@ task :rdoc do
Rake::Task['rdoc:rerdoc'].invoke
end
desc 'rest test examples'
task :rest_test do
Rake::Task['beef_start'].invoke
sh 'cd test/api/; ruby -W2 1333_auth_rate.rb'
Rake::Task['beef_stop'].invoke
end
################################
# run bundle-audit
@@ -155,28 +165,53 @@ end
task :xserver_stop do
puts "\nShutting down X11 Server...\n"
sh "ps -ef|grep Xvfb|grep -v grep|awk '{print $2}'|xargs kill"
sh "ps -ef|grep Xvfb|grep -v grep|grep -v rake|awk '{print $2}'|xargs kill"
end
################################
# BeEF environment set up
@beef_process_id = nil;
@beef_config_file = 'tmp/rk_beef_conf.yaml';
task :beef_start => 'beef' do
# read environment param for creds or use bad_fred
test_user = ENV['TEST_BEEF_USER'] || 'bad_fred'
test_pass = ENV['TEST_BEEF_PASS'] || 'bad_fred_no_access'
# write a rake config file for beef
config = YAML.load(File.read('./config.yaml'))
config['beef']['credentials']['user'] = test_user
config['beef']['credentials']['passwd'] = test_pass
File.open(@beef_config_file, 'w') { |f| YAML.dump(config, f) }
# set the environment creds -- in case we're using bad_fred
ENV['TEST_BEEF_USER'] = test_user
ENV['TEST_BEEF_PASS'] = test_pass
config = nil
puts "Using config file: #{@beef_config_file}\n"
printf "Starting BeEF (wait a few seconds)..."
@beef_process_id = IO.popen("ruby ./beef -x 2> /dev/null", "w+")
delays = [10, 10, 5, 5, 4, 4, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1]
@beef_process_id = IO.popen("ruby ./beef -c #{@beef_config_file} -x 2> /dev/null", "w+")
delays = [5, 5, 5, 4, 4, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1]
delays.each do |i| # delay for a few seconds
printf '.'
sleep (i)
end
puts '.'
puts ".\n\n"
end
task :beef_stop do
puts "\nShutting down BeEF...\n"
sh "ps -ef|grep beef|grep -v grep|awk '{print $2}'|xargs kill"
# cleanup tmp/config files
puts "\nCleanup config file:\n"
rm_f @beef_config_file
ENV['TEST_BEEF_USER'] = nil
ENV['TEST_BEEF_PASS'] = nil
# shutting down
puts "Shutting down BeEF...\n"
sh "ps -ef|grep beef|grep -v grep|grep -v rake|awk '{print $2}'|xargs kill"
end
################################
@@ -233,7 +268,7 @@ end
################################
# Create CDE Package
# This will download and make the CDE Executable and
# This will download and make the CDE Executable and
# gnereate a CDE Package in cde-package
task :cde do
@@ -270,5 +305,3 @@ end
################################

2
beef
View File

@@ -1,5 +1,7 @@
#!/usr/bin/env ruby
require 'pry-byebug'
#
# Copyright (c) 2006-2018 Wade Alcorn - wade@bindshell.net
# Browser Exploitation Framework (BeEF) - http://beefproject.com

View File

@@ -27,6 +27,8 @@ beef:
# subnet of IP addresses that can connect to the admin UI
#permitted_ui_subnet: "127.0.0.1/32"
permitted_ui_subnet: "0.0.0.0/0"
# slow API calls to 1 every api_attempt_delay seconds
api_attempt_delay: "0.05"
# HTTP server
http:
@@ -106,6 +108,8 @@ beef:
# db_file is only used for sqlite
db_file: "beef.db"
#db_pool: 50 # Issues with sqlite locking.
#db_timeout: 500 # https://stackoverflow.com/questions/7154664/ruby-sqlite3busyexception-database-is-locked
# db connection information is only used for mysql/postgres
db_host: "localhost"

View File

@@ -63,7 +63,7 @@ module BeEF
# This is from extensions/admin_ui/controllers/authentication/authentication.rb
#
def self.permitted_source?(ip)
# get permitted subnet
# get permitted subnet
permitted_ui_subnet = BeEF::Core::Configuration.instance.get("beef.restrictions.permitted_ui_subnet")
target_network = IPAddr.new(permitted_ui_subnet)
@@ -74,6 +74,32 @@ module BeEF
return target_network.include?(ip)
end
#
# Rate limit through timeout
# This is from extensions/admin_ui/controllers/authentication/
#
# Brute Force Mitigation
# Only one login request per config_delay_id seconds
#
# @param config_delay_id <string> configuration name for the timeout
# @param last_time_attempt <Time> last time this was attempted
# @param time_record_set_fn <lambda> callback, setting time on failure
#
# @return <boolean>
def self.timeout?(config_delay_id, last_time_attempt, time_record_set_fn)
success = true
time = Time.now()
config = BeEF::Core::Configuration.instance
fail_delay = config.get(config_delay_id)
if (time - last_time_attempt < fail_delay.to_f)
time_record_set_fn.call(time)
success = false
end
success
end
end
end
end

View File

@@ -10,10 +10,20 @@ module BeEF
class Admin < BeEF::Core::Router::Router
config = BeEF::Core::Configuration.instance
time_since_last_failed_auth = 0
before do
# error 401 unless params[:token] == config.get('beef.api_token')
halt 401 if not BeEF::Core::Rest.permitted_source?(request.ip)
# halt if requests are inside beef.restrictions.api_attempt_delay
if time_since_last_failed_auth != 0
halt 401 if not BeEF::Core::Rest.timeout?('beef.restrictions.api_attempt_delay',
time_since_last_failed_auth,
lambda { |time| time_since_last_failed_auth = time})
end
headers 'Content-Type' => 'application/json; charset=UTF-8',
'Pragma' => 'no-cache',
'Cache-Control' => 'no-cache',
@@ -46,6 +56,9 @@ module BeEF
# check username and password
if not (data['username'].eql? config.get('beef.credentials.user') and data['password'].eql? config.get('beef.credentials.passwd') )
BeEF::Core::Logger.instance.register('Authentication', "User with ip #{request.ip} has failed to authenticate in the application.")
# failed attempts
time_since_last_failed_auth = Time.now()
halt 401
else
{ "success" => true,
@@ -62,4 +75,4 @@ module BeEF
end
end
end
end
end

View File

@@ -12,85 +12,83 @@ module Controllers
# The authentication web page for BeEF.
#
class Authentication < BeEF::Extension::AdminUI::HttpController
#
# Constructor
#
def initialize
super({
'paths' => {
'/' => method(:index),
'/' => method(:index),
'/login' => method(:login),
'/logout' => method(:logout)
}
})
@session = BeEF::Extension::AdminUI::Session.instance
end
# Function managing the index web page
def index
def index
@headers['Content-Type']='text/html; charset=UTF-8'
end
#
# Function managing the login
#
def login
username = @params['username-cfrm'] || ''
password = @params['password-cfrm'] || ''
config = BeEF::Core::Configuration.instance
@headers['Content-Type']='application/json; charset=UTF-8'
ua_ip = @request.ip # get client ip address
@body = '{ success : false }' # attempt to fail closed
# check if source IP address is permited to authenticate
if not permited_source?(ua_ip)
BeEF::Core::Logger.instance.register('Authentication', "IP source address (#{@request.ip}) attempted to authenticate but is not within permitted subnet.")
return
end
# check if under brute force attack
time = Time.new
if not timeout?(time)
@session.set_auth_timestamp(time)
return
end
# check if under brute force attack
return if not BeEF::Core::Rest.timeout?('beef.extension.admin_ui.login_fail_delay',
@session.get_auth_timestamp(),
lambda { |time| @session.set_auth_timestamp(time)})
# check username and password
if not (username.eql? config.get('beef.credentials.user') and password.eql? config.get('beef.credentials.passwd') )
BeEF::Core::Logger.instance.register('Authentication', "User with ip #{@request.ip} has failed to authenticate in the application.")
return
end
# establish an authenticated session
# set up session and set it logged in
@session.set_logged_in(ua_ip)
# create session cookie
@session.set_logged_in(ua_ip)
# create session cookie
session_cookie_name = config.get('beef.http.session_cookie_name') # get session cookie name
Rack::Utils.set_cookie_header!(@headers, session_cookie_name, {:value => @session.get_id, :path => "/", :httponly => true})
BeEF::Core::Logger.instance.register('Authentication', "User with ip #{@request.ip} has successfully authenticated in the application.")
@body = "{ success : true }"
end
#
# Function managing the logout
#
def logout
# test if session is unauth'd
(print_error "invalid nonce";return @body = "{ success : true }") if not @session.valid_nonce?(@request)
(print_error "invalid session";return @body = "{ success : true }") if not @session.valid_session?(@request)
@headers['Content-Type']='application/json; charset=UTF-8'
# set the session to be log out
@session.set_logged_out
# clean up UA and expire the session cookie
config = BeEF::Core::Configuration.instance
session_cookie_name = config.get('beef.http.session_cookie_name') # get session cookie name
@@ -98,14 +96,14 @@ class Authentication < BeEF::Extension::AdminUI::HttpController
BeEF::Core::Logger.instance.register('Authentication', "User with ip #{@request.ip} has successfully logged out.")
@body = "{ success : true }"
end
#
# Check the UI browser source IP is within the permitted subnet
#
def permited_source?(ip)
# get permitted subnet
# get permitted subnet
config = BeEF::Core::Configuration.instance
permitted_ui_subnet = config.get('beef.restrictions.permitted_ui_subnet')
target_network = IPAddr.new(permitted_ui_subnet)
@@ -114,18 +112,7 @@ class Authentication < BeEF::Extension::AdminUI::HttpController
# test if ip within subnet
return target_network.include?(ip)
end
#
# Brute Force Mitigation
# Only one login request per login_fail_delay seconds
#
def timeout?(time)
config = BeEF::Core::Configuration.instance
login_fail_delay = config.get('beef.extension.admin_ui.login_fail_delay') # get fail delay
# test if the last login attempt was less then login_fail_delay seconds
time - @session.get_auth_timestamp > login_fail_delay.to_i
end
end

View File

@@ -0,0 +1,73 @@
#
# Copyright (c) 2006-2017 Wade Alcorn - wade@bindshell.net
# Browser Exploitation Framework (BeEF) - http://beefproject.com
# See the file 'doc/COPYING' for copying permission
#
require 'test/unit'
require 'pry-byebug'
require 'rest-client'
require 'json'
require 'optparse'
require 'pp'
require '../common/test_constants'
require './lib/beef_rest_client'
class TC_1333_auth_rate < Test::Unit::TestCase
def test_auth_rate
# tests rate of auth calls
# this takes some time - with no output
# beef must be running
passwds = (1..9).map { |i| "broken_pass"}
passwds.push BEEF_PASSWD
apis = passwds.map { |pswd| BeefRestClient.new('http', ATTACK_DOMAIN, '3000', BEEF_USER, pswd) }
l = apis.length
# t0 = Time.now()
(0..2).each do |again| # multiple sets of auth attempts
# first pass -- apis in order, valid passwd on 9th attempt
# subsequent passes apis shuffled
# puts "speed requesets" # all should return 401
(0..50).each do |i|
# t = Time.now()
# puts "#{i} : #{t - t0} : #{apis[i%l].auth()[:payload]}"
test_api = apis[i%l]
assert_match("401", test_api.auth()[:payload]) # all (unless the valid is first 1 in 10 chance)
# t0 = t
end
# again with more time between calls -- there should be success (1st iteration)
# puts "delayed requests"
(0..(l*2)).each do |i|
# t = Time.now()
# puts "#{i} : #{t - t0} : #{apis[i%l].auth()[:payload]}"
test_api = apis[i%l]
if (test_api.is_pass?(BEEF_PASSWD))
assert(test_api.auth()[:payload]["success"]) # valid pass should succeed
else
assert_match("401", test_api.auth()[:payload])
end
sleep(0.5)
# t0 = t
end
apis.shuffle! # new order for next iteration
apis.reverse if (apis[0].is_pass?(BEEF_PASSWD)) # prevent the first from having valid passwd
end # multiple sets of auth attempts
end # test_auth_rate
end

View File

@@ -0,0 +1,49 @@
#
# Copyright (c) 2006-2017 Wade Alcorn - wade@bindshell.net
# Browser Exploitation Framework (BeEF) - http://beefproject.com
# See the file 'doc/COPYING' for copying permission
#
# less noisy verson of BeeRestAPI found in tools.
class BeefRestClient
def initialize proto, host, port, user, pass
@user = user
@pass = pass
@url = "#{proto}://#{host}:#{port}/api/"
@token = nil
end
def is_pass?(passwd)
@pass == passwd
end
def auth
begin
response = RestClient.post "#{@url}admin/login",
{ 'username' => "#{@user}",
'password' => "#{@pass}" }.to_json,
:content_type => :json,
:accept => :json
result = JSON.parse(response.body)
@token = result['token']
{:success => result['success'], :payload => result}
rescue => e
{:success => false, :payload => e.message }
end
end
def version
return {:success => false, :payload => 'no token'} if @token.nil?
begin
response = RestClient.get "#{@url}server/version", {:params => {:token => @token}}
result = JSON.parse(response.body)
{:success => result['success'], :payload => result}
rescue => e
print_error "Could not retrieve BeEF version: #{e.message}"
{:success => false, :payload => e.message}
end
end
end

View File

@@ -12,8 +12,8 @@ ATTACK_URL = "http://" + ATTACK_DOMAIN + ":3000/ui/panel"
VICTIM_URL = "http://" + VICTIM_DOMAIN + ":3000/demos/basic.html"
# Credentials
BEEF_USER = "beef"
BEEF_PASSWD = "test"
BEEF_USER = ENV["TEST_BEEF_USER"] || 'beef'
BEEF_PASSWD = ENV["TEST_BEEF_PASS"] || "beef"
# RESTful API root endpoints
RESTAPI_HOOKS = "http://" + ATTACK_DOMAIN + ":3000/api/hooks"