1
mirror of https://github.com/rapid7/metasploit-framework synced 2024-08-28 23:26:18 +02:00

Add MSF REST web service authentication support

This commit is contained in:
Matthew Kienow 2018-06-22 15:09:36 -04:00
parent a91ad8c09c
commit c0717d9306
No known key found for this signature in database
GPG Key ID: 40787F8B1EAC6E41
16 changed files with 607 additions and 49 deletions

View File

@ -65,9 +65,11 @@ PATH
sinatra
sqlite3
sshkey
sysrandom
thin
tzinfo
tzinfo-data
warden
windows_error
xdr
xmlrpc
@ -325,6 +327,7 @@ GEM
sqlite3 (1.3.13)
sshkey (1.9.0)
swagger-blocks (2.0.2)
sysrandom (1.0.5)
thin (1.7.2)
daemons (~> 1.0, >= 1.0.9)
eventmachine (~> 1.0, >= 1.0.4)
@ -338,6 +341,8 @@ GEM
thread_safe (~> 0.1)
tzinfo-data (1.2018.5)
tzinfo (>= 1.0.0)
warden (1.2.7)
rack (>= 1.0)
windows_error (0.1.2)
xdr (2.0.0)
activemodel (>= 4.2.7)

View File

@ -53,6 +53,7 @@ class Msf::DBManager
autoload :Session, 'msf/core/db_manager/session'
autoload :SessionEvent, 'msf/core/db_manager/session_event'
autoload :Task, 'msf/core/db_manager/task'
autoload :User, 'msf/core/db_manager/user'
autoload :Vuln, 'msf/core/db_manager/vuln'
autoload :VulnAttempt, 'msf/core/db_manager/vuln_attempt'
autoload :VulnDetail, 'msf/core/db_manager/vuln_detail'
@ -89,6 +90,7 @@ class Msf::DBManager
include Msf::DBManager::Session
include Msf::DBManager::SessionEvent
include Msf::DBManager::Task
include Msf::DBManager::User
include Msf::DBManager::Vuln
include Msf::DBManager::VulnAttempt
include Msf::DBManager::VulnDetail

View File

@ -0,0 +1,5 @@
module Authentication
autoload :Strategies, 'msf/core/db_manager/http/authentication/strategies'
include Strategies
end

View File

@ -0,0 +1,9 @@
module Authentication
module Strategies
autoload :ApiToken, 'msf/core/db_manager/http/authentication/strategies/api_token'
autoload :UserPassword, 'msf/core/db_manager/http/authentication/strategies/user_password'
include ApiToken
include UserPassword
end
end

View File

@ -0,0 +1,38 @@
module Authentication
module Strategies
module ApiToken
AUTHORIZATION = 'HTTP_AUTHORIZATION'
AUTHORIZATION_SCHEME = 'Bearer'
TOKEN_QUERY_PARAM = 'token'
Warden::Strategies.add(:api_token) do
# Check if request contains valid data and should be authenticated.
# @return [Boolean] true if strategy should be run for the request; otherwise, false.
def valid?
authorization = request.env[AUTHORIZATION]
(authorization.is_a?(String) && authorization.start_with?(AUTHORIZATION_SCHEME)) || !params[TOKEN_QUERY_PARAM].nil?
end
# Authenticate the request.
def authenticate!
db_manager = env['DBManager']
authorization = request.env[AUTHORIZATION]
if authorization.is_a?(String) && authorization.start_with?(AUTHORIZATION_SCHEME)
token = authorization.sub(/^#{AUTHORIZATION_SCHEME}\s+/, '')
else
token = params[TOKEN_QUERY_PARAM]
end
user = db_manager.users(persistence_token: token).first
if user.nil?
throw(:warden, message: "Invalid API token.")
else
success!(user)
end
end
end
end
end
end

View File

@ -0,0 +1,39 @@
module Authentication
module Strategies
module UserPassword
Warden::Manager.serialize_into_session{ |user| user.id }
Warden::Manager.serialize_from_session{ |id|
db_manager = env['DBManager']
db_manager.users(id: id).first
}
Warden::Manager.before_failure do |env,opts|
# change request method to get control to our handler since authentication failure can happen on any request
env['REQUEST_METHOD'] = 'POST'
end
Warden::Strategies.add(:password) do
# Check if request contains valid data and should be authenticated.
# @return [Boolean] true if strategy should be run for the request; otherwise, false.
def valid?
params['username'] && params['password']
end
# Authenticate the request.
def authenticate!
db_manager = env['DBManager']
user = db_manager.users(username: params['username']).first
if user.nil? || !db_manager.authenticate_user(id: user.id, password: params['password'])
fail("Invalid username or password.")
else
success!(user)
end
end
end
end
end
end

View File

@ -1,5 +1,5 @@
require 'rack'
require 'msf/core/db_manager/http/sinatra_app'
require 'msf/core/db_manager/http/metasploit_api_app'
require 'metasploit/framework/parsed_options/remote_db'
require 'rex/ui/text/output/stdio'
@ -26,14 +26,14 @@ class HttpDBManagerService
def start_http_server(opts)
Rack::Handler::Thin.run(SinatraApp, opts) do |server|
Rack::Handler::Thin.run(MetasploitApiApp, opts) do |server|
if opts[:ssl] && opts[:ssl] = true
print_good "SSL Enabled"
print_good('SSL Enabled')
server.ssl = true
server.ssl_options = opts[:ssl_opts]
else
print_warning 'SSL Disabled'
print_warning('SSL Disabled')
end
server.threaded = true
end

View File

@ -0,0 +1,83 @@
require 'sinatra/base'
require 'swagger/blocks'
require 'sysrandom/securerandom'
require 'warden'
require 'msf/core/db_manager/http/authentication'
require 'msf/core/db_manager/http/servlet_helper'
require 'msf/core/db_manager/http/servlet/api_docs_servlet'
require 'msf/core/db_manager/http/servlet/auth_servlet'
require 'msf/core/db_manager/http/servlet/host_servlet'
require 'msf/core/db_manager/http/servlet/note_servlet'
require 'msf/core/db_manager/http/servlet/vuln_servlet'
require 'msf/core/db_manager/http/servlet/event_servlet'
require 'msf/core/db_manager/http/servlet/web_servlet'
require 'msf/core/db_manager/http/servlet/msf_servlet'
require 'msf/core/db_manager/http/servlet/workspace_servlet'
require 'msf/core/db_manager/http/servlet/service_servlet'
require 'msf/core/db_manager/http/servlet/session_servlet'
require 'msf/core/db_manager/http/servlet/exploit_servlet'
require 'msf/core/db_manager/http/servlet/loot_servlet'
require 'msf/core/db_manager/http/servlet/session_event_servlet'
require 'msf/core/db_manager/http/servlet/credential_servlet'
require 'msf/core/db_manager/http/servlet/nmap_servlet'
require 'msf/core/db_manager/http/servlet/db_export_servlet'
require 'msf/core/db_manager/http/servlet/vuln_attempt_servlet'
class MetasploitApiApp < Sinatra::Base
helpers ServletHelper
# Servlet registration
register ApiDocsServlet
register AuthServlet
register HostServlet
register VulnServlet
register EventServlet
register WebServlet
register MsfServlet
register NoteServlet
register WorkspaceServlet
register ServiceServlet
register SessionServlet
register ExploitServlet
register LootServlet
register SessionEventServlet
register CredentialServlet
register NmapServlet
register DbExportServlet
register VulnAttemptServlet
configure do
set :sessions, {key: 'msf-ws.session', expire_after: 300}
set :session_secret, ENV.fetch('MSF_WS_SESSION_SECRET') { SecureRandom.hex(16) }
end
before do
# store DBManager in request environment so that it is available to Warden
request.env['DBManager'] = get_db
end
use Warden::Manager do |config|
# failed authentication is handled by this application
config.failure_app = self
# don't intercept 401 responses since the app will provide custom failure messages
config.intercept_401 = false
config.default_scope = :api
config.scope_defaults :user,
# whether to persist the result in the session or not
store: true,
# list of strategies to use
strategies: [:password],
# action (route) of the failure application
action: "#{AuthServlet.api_unauthenticated_path}/user"
config.scope_defaults :api,
# whether to persist the result in the session or not
store: false,
# list of strategies to use
strategies: [:api_token],
# action (route) of the failure application
action: AuthServlet.api_unauthenticated_path
end
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -0,0 +1,105 @@
module AuthServlet
def self.api_path
'/api/v1/auth'
end
def self.api_account_path
"#{AuthServlet.api_path}/account"
end
def self.api_login_path
"#{AuthServlet.api_path}/login"
end
def self.api_logout_path
"#{AuthServlet.api_path}/logout"
end
def self.api_generate_token_path
"#{AuthServlet.api_path}/generate-token"
end
def self.api_unauthenticated_path
"#{AuthServlet.api_path}/unauthenticated"
end
def self.registered(app)
app.get AuthServlet.api_account_path, &get_api_account
app.get AuthServlet.api_login_path, &get_login
app.post AuthServlet.api_login_path, &post_login
app.get AuthServlet.api_logout_path, &get_logout
app.get AuthServlet.api_generate_token_path, &get_generate_token
app.post "#{AuthServlet.api_unauthenticated_path}/?:scope?", &post_unauthenticated
end
#######
private
#######
# Get account page
def self.get_api_account
lambda {
erb :'auth/account'
}
end
# Get login page
def self.get_login
lambda {
erb :'auth/login'
}
end
# Process login request
def self.post_login
lambda {
warden.authenticate!(scope: :user)
if session[:return_to].nil? || session[:return_to] == AuthServlet.api_login_path
redirect AuthServlet.api_account_path
else
redirect session[:return_to]
end
}
end
# Process user log out
def self.get_logout
lambda {
warden.logout
redirect AuthServlet.api_account_path
}
end
# Generate a new API token for the current user
def self.get_generate_token
lambda {
# change action to drop the scope param since this is used
# by XMLHttpRequest (XHR) and we don't want a redirect
warden.authenticate!(scope: :user, action: AuthServlet.api_unauthenticated_path)
token = get_db.create_new_user_token(id: warden.user(:user).id, token_length: 40)
set_json_data_response(response: {message: "Generated new API token.", token: token})
}
end
# Handle the unauthenticated action for multiple scopes
def self.post_unauthenticated
lambda {
if !params['scope'].nil? && params['scope'] == 'user'
session[:return_to] = warden_options[:attempted_path] if session[:return_to].nil?
redirect AuthServlet.api_login_path
end
msg = warden_options[:message]
error = {
code: 401,
message: "#{!msg.nil? ? "#{msg} " : nil}Authenticate to access this resource."
}
set_json_error_response(response: error, code: error[:code])
}
end
end

View File

@ -16,9 +16,19 @@ module ServletHelper
[200, '']
end
def set_json_response(data, includes = nil)
def set_json_response(data, includes = nil, code = 200)
headers = {'Content-Type' => 'application/json'}
[200, headers, to_json(data, includes)]
[code, headers, to_json(data, includes)]
end
def set_json_data_response(response:, includes: nil, code: 200)
data_response = {"data": response}
set_json_response(data_response, includes = includes, code = code)
end
def set_json_error_response(response:, includes: nil, code:)
error_response = {"error": response}
set_json_response(error_response, includes = includes, code = code)
end
def set_html_response(data)
@ -70,6 +80,19 @@ module ServletHelper
params.symbolize_keys.except(:captures, :splat)
end
# Get Warden::Proxy object from the Rack environment.
# @return [Warden::Proxy] The Warden::Proxy object from the Rack environment.
def warden
env['warden']
end
# Get Warden options hash from the Rack environment.
# @return [Hash] The Warden options hash from the Rack environment.
def warden_options
env['warden.options']
end
#######
private
#######

View File

@ -1,43 +0,0 @@
require 'sinatra/base'
require 'swagger/blocks'
require 'msf/core/db_manager/http/servlet_helper'
require 'msf/core/db_manager/http/servlet/api_docs_servlet'
require 'msf/core/db_manager/http/servlet/host_servlet'
require 'msf/core/db_manager/http/servlet/note_servlet'
require 'msf/core/db_manager/http/servlet/vuln_servlet'
require 'msf/core/db_manager/http/servlet/event_servlet'
require 'msf/core/db_manager/http/servlet/web_servlet'
require 'msf/core/db_manager/http/servlet/msf_servlet'
require 'msf/core/db_manager/http/servlet/workspace_servlet'
require 'msf/core/db_manager/http/servlet/service_servlet'
require 'msf/core/db_manager/http/servlet/session_servlet'
require 'msf/core/db_manager/http/servlet/exploit_servlet'
require 'msf/core/db_manager/http/servlet/loot_servlet'
require 'msf/core/db_manager/http/servlet/session_event_servlet'
require 'msf/core/db_manager/http/servlet/credential_servlet'
require 'msf/core/db_manager/http/servlet/nmap_servlet'
require 'msf/core/db_manager/http/servlet/db_export_servlet'
require 'msf/core/db_manager/http/servlet/vuln_attempt_servlet'
class SinatraApp < Sinatra::Base
helpers ServletHelper
# Servlet registration
register ApiDocsServlet
register HostServlet
register VulnServlet
register EventServlet
register WebServlet
register MsfServlet
register NoteServlet
register WorkspaceServlet
register ServiceServlet
register SessionServlet
register ExploitServlet
register LootServlet
register SessionEventServlet
register CredentialServlet
register NmapServlet
register DbExportServlet
register VulnAttemptServlet
end

View File

@ -0,0 +1,138 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Account - Metasploit API</title>
<!-- TODO: use external style sheet -->
<style>
body {
margin:0;
}
ul {
background-color: rgb(47,47,47);
list-style-type: none;
margin: 0;
padding: 0;
overflow: hidden;
}
li {
float: left;
}
li a, .dropdown-btn {
display: inline-block;
color: white;
text-align: center;
padding: 14px 16px;
text-decoration: none;
}
li a:hover, .dropdown-menu:hover .dropdown-btn {
background-color: rgb(73,73,73);
}
li.dropdown-menu {
display: inline-block;
}
.dropdown-content {
display: none;
position: absolute;
background-color: rgb(73,73,73);
min-width: 160px;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
z-index: 1;
}
.dropdown-content a {
color: white;
padding: 12px 16px;
text-decoration: none;
display: block;
text-align: left;
}
.dropdown-content a:hover {
background-color: rgb(96,96,96);
}
.dropdown-menu:hover .dropdown-content {
display: block;
}
.api-token {
float:left;
}
#api-token-label {
font-weight: bold;
}
#api-token {
margin-left: 7px;
}
</style>
</head>
<body>
<script type="text/javascript">
function getNewApiToken() {
loadDoc("GET", "<%= AuthServlet.api_generate_token_path %>", function(xhr) {
var response = JSON.parse(xhr.responseText);
document.getElementById("api-token").innerHTML = response.data.token;
}, errorHandler);
event.preventDefault();
}
function errorHandler(xhr) {
if (xhr.status == 401) {
window.location.reload(true);
}
}
function loadDoc(method, url, callback, errorCallback) {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (this.readyState == 4) {
if (this.status == 200) {
callback(this);
} else if (this.status >= 400) {
errorCallback(this);
}
}
};
xhr.open(method, url, true);
xhr.send();
}
</script>
<ul>
<% if warden.authenticated?(:user) %>
<li class="dropdown-menu">
<a href="javascript:void(0)" class="dropdown-btn"><%= warden.user(:user).username %></a>
<div class="dropdown-content">
<a href="#" onclick="getNewApiToken();">Generate New API Token</a>
<a href="<%= AuthServlet.api_logout_path %>">Log Out</a>
</div>
</li>
<% else %>
<li><a href="<%= AuthServlet.api_login_path %>">Log In</a></li>
<% end %>
<li><a href="<%= ApiDocsServlet.html_path %>">API Documentation</a></li>
</ul>
<div style="padding:20px;">
<h1>Metasploit API Account</h1>
<% if warden.authenticated?(:user) %>
<div id="api-token-label" class="api-token">Current API Token:</div>
<div id="api-token" class="api-token">
<%= !warden.user(:user).nil? && !warden.user(:user).persistence_token.nil? ? warden.user(:user).persistence_token : 'none' %>
</div>
<% end %>
</div>
</body>
</html>

View File

@ -0,0 +1,54 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Log In - Metasploit API</title>
<!-- TODO: use external style sheet -->
<style>
.credential-container {
border: 1px solid rgba(0, 0, 0, 0.4);
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.55);
margin-left: auto;
margin-right: auto;
max-width: 390px;
min-width: 300px;
min-height: 250px;
padding: 34px;
}
input[type=text], input[type=password] {
border-color: rgba(0, 0, 0, 0.6);
border-width: 1px;
margin-bottom: 16px;
width: 100%;
height: 34px;
}
button {
border-color: rgba(0, 0, 0, 0.6);
border-width: 1px;
cursor: pointer;
width: 100%;
height: 34px;
}
</style>
</head>
<body>
<div style="padding:20px;">
<form action="<%= AuthServlet.api_login_path %>" method="post">
<div class="credential-container">
<h2>Log In - Metasploit API</h2>
<label for="username"><b>Username</b></label>
<input type="text" placeholder="Enter Username" name="username" required>
<label for="password"><b>Password</b></label>
<input type="password" placeholder="Enter Password" name="password" required>
<button type="submit">Log In</button>
</div>
</form>
</div>
</body>
</html>

View File

@ -0,0 +1,98 @@
require 'sysrandom/securerandom'
module Msf::DBManager::User
# Returns a list of all users in the database
def users(opts)
::ActiveRecord::Base.connection_pool.with_connection {
search_term = opts.delete(:search_term)
if search_term && !search_term.empty?
column_search_conditions = Msf::Util::DBManager.create_all_column_search_conditions(Mdm::User, search_term)
Mdm::User.where(opts).where(column_search_conditions)
else
Mdm::User.where(opts)
end
}
end
#
# Report a user's attributes
#
# The opts parameter MUST contain
# +:XXX+:: -- the users's XXX
#
# The opts parameter can contain:
# +:XXX+:: -- XXX
#
def report_user(opts)
return if !active
# TODO: implement method
raise 'Msf::DBManager::User#report_user is not implemented'
end
def update_user(opts)
::ActiveRecord::Base.connection_pool.with_connection {
# process workspace string for update if included in opts
wspace = Msf::Util::DBManager.process_opts_workspace(opts, framework, false)
opts[:workspace] = wspace if wspace
id = opts.delete(:id)
Mdm::User.update(id, opts)
}
end
# Deletes user entries based on the IDs passed in.
#
# @param opts[:ids] [Array] Array containing Integers corresponding to the IDs of the user entries to delete.
# @return [Array] Array containing the Mdm::User objects that were successfully deleted.
def delete_user(opts)
raise ArgumentError.new("The following options are required: :ids") if opts[:ids].nil?
::ActiveRecord::Base.connection_pool.with_connection {
deleted = []
opts[:ids].each do |user_id|
user = Mdm::User.find(user_id)
begin
deleted << user.destroy
rescue # refs suck
elog("Forcibly deleting #{user}")
deleted << user.delete
end
end
return deleted
}
end
# Authenticates the user.
#
# @param opts[:ids] [Integer] ID of the user to authenticate.
# @param opts[:password] [String] The user's password.
# @return [Boolean] true if the user is successfully authenticated; otherwise, false.
def authenticate_user(opts)
raise ArgumentError.new("The following options are required: :id") if opts[:id].nil?
raise ArgumentError.new("The following options are required: :password") if opts[:password].nil?
user = Mdm::User.find(opts[:id])
# TODO: Yes, we need proper password salting and hashing here
if !user.nil? && user.crypted_password == opts[:password]
true
else
false
end
end
# Creates a new API token for the user.
#
# @param opts[:ids] [Integer] ID for the user.
# @return [String] The new API token.
def create_new_user_token(opts)
raise ArgumentError.new("The following options are required: :id") if opts[:id].nil?
token_length = opts[:token_length] || 20
# NOTE: repurposing persistence_token in the database as the API token
Mdm::User.update(opts[:id], {persistence_token: SecureRandom.hex(token_length)}).persistence_token
end
end

View File

@ -103,6 +103,8 @@ Gem::Specification.new do |spec|
# Required for msfdb_ws (Metasploit data base as a webservice)
spec.add_runtime_dependency 'thin'
spec.add_runtime_dependency 'sinatra'
spec.add_runtime_dependency 'sysrandom'
spec.add_runtime_dependency 'warden'
# TimeZone info
spec.add_runtime_dependency 'tzinfo-data'
# Gem for dealing with SSHKeys