Merge branch 'master' into feature/addp-modules

bug/bundler_fix
HD Moore 2012-11-02 15:41:03 -05:00
commit 0bf5f63d67
191 changed files with 4322 additions and 2373 deletions

View File

@ -5,7 +5,7 @@ gem 'activesupport', '>= 3.0.0'
# Needed for Msf::DbManager
gem 'activerecord'
# Database models shared between framework and Pro.
gem 'metasploit_data_models', :git => 'git://github.com/rapid7/metasploit_data_models.git'
gem 'metasploit_data_models', :git => 'git://github.com/rapid7/metasploit_data_models.git', :tag => '0.3.0'
# Needed for module caching in Mdm::ModuleDetails
gem 'pg', '>= 0.11'

View File

@ -1,8 +1,9 @@
GIT
remote: git://github.com/rapid7/metasploit_data_models.git
revision: dd6c3a31c5ad8b55f4913b5ba20307178ba9c7bf
revision: 73f26789500f278dd6fd555e839d09a3b81a05f4
tag: 0.3.0
specs:
metasploit_data_models (0.0.2)
metasploit_data_models (0.3.0)
activerecord
activesupport
pg
@ -27,7 +28,7 @@ GEM
coderay (1.0.8)
diff-lcs (1.1.3)
i18n (0.6.1)
method_source (0.8)
method_source (0.8.1)
multi_json (1.3.6)
pg (0.14.1)
pry (0.9.10)

View File

@ -10,13 +10,9 @@ require 'rubygems'
version = ">= 0"
if ARGV.first
str = ARGV.first
str = str.dup.force_encoding("BINARY") if str.respond_to? :force_encoding
if str =~ /\A_(.*)_\z/
if ARGV.first =~ /^_(.*)_$/ and Gem::Version.correct? $1 then
version = $1
ARGV.shift
end
end
gem 'metasploit_data_models', version

View File

@ -1,6 +0,0 @@
.rvmrc
.DS_Store
*.gem
.bundle
Gemfile.lock
pkg/*

View File

@ -1,4 +0,0 @@
source "http://rubygems.org"
# Specify your gem's dependencies in metasploit_data_models.gemspec
gemspec

View File

@ -1,75 +0,0 @@
#MetasploitDataModels
The database layer for Metasploit
## Purpose
__MetasploitDataModels__ exists to do several key things:
1. Allow code sharing between Metasploit Framework (MSF) and the commercial versions of Metasploit (Community, Express, Pro -- usually referred to collectively as "Pro")
2. Give developers a lightweight entry point to MSF's backend for use in developing tools that gather data intended for later use with Metasploit (e.g. specialized scanners).
3. Make it easy to keep commercial stuff private while increasing the functionality of the open-source tools we provide to the community.
## Usage
### Rails
In a Rails application we simply include the ActiveRecord mixins directly, usually inside models with similar names.
### MSF
When MetasploitDataModels is included by MSF, the gem dynamically creates
ActiveRecord model classes.
Both of these behaviors are based on the assumption that the files in
__lib/metasploit_data_models/active_record_models__, though implemented here as
mixins, actually represent the basic ActiveRecord model structure that both Metasploit Framework and Metasploit Pro use.
### Elsewhere
__NOTE: This isn't in RubyGems yet. Using a Gemfile entry pointing to this repo (i.e., using [Bundler](http://gembundler.com)) is the suggested option for now.__
Usage outside of Rapid7 is still alpha, and we're not making many promises. That being said, usage is easy:
```ruby
connection_info = YAML.load_file("path/to/rails-style/db_config_file")
ActiveRecord::Base.establish_connection(connection_info['development'])
include MetasploitDataModels
MetasploitDataModels.create_and_load_ar_classes
```
Basically you need to do the following things:
1. Establish an ActiveRecord connection. A Rails __config/database.yml__ is ideal for this.
2. Include the MetasploitDataModels module.
3. Call the class method that builds the AR models into the Mdm namespace( __MetasploitDataModels.create_and_load_ar_classes__ ).
## Developer Info
### Console
The gem includes a console based on [Pry](https://github.com/pry/pry/)
Give it a path to a working MSF database.yml file for full
ActiveRecord-based access to your data.
__Note:__ "development" mode is hardcoded into the console currently.
### ActiveRecord::ConnectionError issues
Because the gem is defining mixins, there can be no knowledge of the
specifics of any "current" ActiveRecord connection. But if ActiveRecord
encounters something in a child class that would require knowledge of
the connection adapter (e.g. the use of an RDBMS-specific function in
a named scope's "WHERE" clause), it will check to see if the adapter
supports it and then throw an exception when the connection object
(which provides the adapter) is nil.
This means that, for all but the most trivial cases, you need to use Arel
versions of queries instead of ones utilizing straight SQL.
You'll encounter this sometimes if you do dev work on this gem. A good
rule of thumb: anything that goes into the class_eval block must be able
to work without knowledge of the AR connection adapter type.

View File

@ -1 +0,0 @@
require "bundler/gem_tasks"

View File

@ -1,63 +0,0 @@
require "active_record"
require "active_support"
require "active_support/all"
require "shellwords"
require "metasploit_data_models/version"
require "metasploit_data_models/serialized_prefs"
require "metasploit_data_models/base64_serializer"
require "metasploit_data_models/validators/ip_format_validator"
require "metasploit_data_models/validators/password_is_strong_validator"
# Declare the (blessedly short) common namespace for the ActiveRecord classes
module Mdm; end
module MetasploitDataModels
module ActiveRecordModels; end
# Dynamically create AR classes if being included from Msf::DBManager
# otherwise, just make the modules available for arbitrary inclusion.
def self.included(base)
ar_mixins.each{|file| require file}
create_and_load_ar_classes if base.to_s == 'Msf::DBManager'
end
# The code in each of these represents the basic structure of a correspondingly named
# ActiveRecord model class. Those classes are explicitly created in our Rails app
# for the commercial versions, and the functionality from the mixins is included
# into model classes directly.
#
# When not explicitly overloading the classes in your own files use MetasploitDataModels#create_and_load_ar_classes
# to dynamically generate ActiveRecord classes in the Mdm namespace.
def self.ar_mixins
models_dir = File.expand_path(File.dirname(__FILE__)) + "/metasploit_data_models/active_record_models"
Dir.glob("#{models_dir}/*.rb")
end
# Dynamically create ActiveRecord descendant classes in the Mdm namespace
def self.create_and_load_ar_classes
ar_module_names.each do |cname|
class_str =<<-RUBY
class Mdm::#{cname} < ActiveRecord::Base
include MetasploitDataModels::ActiveRecordModels::#{cname}
end
RUBY
eval class_str, binding, __FILE__, __LINE__ # *slightly* more obvious stack trace
end
end
# Derive "constant" strings from the names of the files in
# lib/metasploit_data_models/active_record_models
def self.ar_module_names
ar_mixins.inject([]) do |array, path|
filename = File.basename(path).split(".").first
c_name = filename.classify
c_name << "s" if filename =~ /^[\w]+s$/ # classify can't do plurals
array << c_name
array
end
end
end

View File

@ -1,22 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::ApiKey
def self.included(base)
base.class_eval {
validate do |key|
lic = License.get
if lic and not lic.supports_api?
key.errors[:unsupported_product] = " - this product does not support API access"
end
if key.token.to_s.empty?
key.errors[:blank_token] = " - the specified authentication token is empty"
end
if key.token.to_s.length < 8
key.errors[:token_too_short] = " - the specified authentication token must be at least 8 characters long"
end
end
}
end
end

View File

@ -1,8 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::Client
def self.included(base)
base.class_eval {
belongs_to :host, :class_name => "Mdm::Host"
belongs_to :campaign, :class_name => "Campaign"
}
end
end

View File

@ -1,78 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::Cred
def self.included(base)
base.class_eval{
belongs_to :service, :class_name => "Mdm::Service"
unless defined? PTYPES
const_def =<<-CONST_DEF
PTYPES = {
"read/write password" => "password_rw",
"read-only password" => "password_ro",
"SMB hash" => "smb_hash",
"SSH private key" => "ssh_key",
"SSH public key" => "ssh_pubkey"
}
CONST_DEF
eval(const_def)
end
eval("KEY_ID_REGEX = /([0-9a-fA-F:]{47})/") unless defined?(KEY_ID_REGEX) # Could be more strict
def ptype_human
humanized = PTYPES.select do |k, v|
v == ptype
end.keys[0]
humanized ? humanized : ptype
end
# Returns its workspace
def workspace
self.service.host.workspace
end
# Returns its key id. If this is not an ssh-type key, returns nil.
def ssh_key_id
return nil unless self.ptype =~ /^ssh_/
return nil unless self.proof =~ KEY_ID_REGEX
$1.downcase # Can't run into NilClass problems.
end
# Returns all private keys with matching key ids, including itself
# If this is not an ssh-type key, always returns an empty array.
def ssh_private_keys
return [] unless self.ssh_key_id
matches = self.class.all(:conditions => ["creds.ptype = ? AND creds.proof ILIKE ?", "ssh_key", "%#{self.ssh_key_id}%"])
matches.select {|c| c.workspace == self.workspace}
end
# Returns all public keys with matching key ids, including itself
# If this is not an ssh-type key, always returns an empty array.
def ssh_public_keys
return [] unless self.ssh_key_id
matches = self.class.all(:conditions => ["creds.ptype = ? AND creds.proof ILIKE ?", "ssh_pubkey", "%#{self.ssh_key_id}%"])
matches.select {|c| c.workspace == self.workspace}
end
# Returns all keys with matching key ids, including itself
# If this is not an ssh-type key, always returns an empty array.
def ssh_keys
(self.ssh_private_keys | self.ssh_public_keys)
end
def ssh_key_matches?(other_cred)
return false unless other_cred.kind_of? self.class
return false unless self.ptype == other_cred.ptype
case self.ptype
when "ssh_key"
matches = self.ssh_private_keys
when "ssh_pubkey"
matches = self.ssh_public_keys
else
false
end
matches.include?(self) and matches.include?(other_cred)
end
}
end
end

View File

@ -1,8 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::CredFile
def self.included(base)
base.class_eval{
belongs_to :workspace, :class_name => "Mdm::Workspace"
}
end
end

View File

@ -1,16 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::Event
def self.included(base)
base.class_eval{
belongs_to :workspace, :class_name => "Mdm::Workspace"
belongs_to :host
serialize :info, ::MetasploitDataModels::Base64Serializer.new
scope :flagged, where(:critical => true, :seen => false)
scope :module_run, where(:name => 'module_run')
validates_presence_of :name
}
end
end

View File

@ -1,8 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::ExploitAttempt
def self.included(base)
base.class_eval {
belongs_to :host, :class_name => "Mdm::Host", :counter_cache => :exploit_attempt_count
validates :host_id, :presence => true
}
end
end

View File

@ -1,9 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::ExploitedHost
def self.included(base)
base.class_eval{
belongs_to :host, :class_name => "Mdm::Host"
belongs_to :service, :class_name => "Mdm::Service"
belongs_to :workspace, :class_name => "Mdm::Workspace"
}
end
end

View File

@ -1,8 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::HostDetail
def self.included(base)
base.class_eval {
belongs_to :host, :class_name => "Mdm::Host", :counter_cache => :host_detail_count
validates :host_id, :presence => true
}
end
end

View File

@ -1,10 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::HostTag
def self.included(base)
base.class_eval {
base.table_name = "hosts_tags"
belongs_to :host, :class_name => "Mdm::Host"
belongs_to :tag, :class_name => "Mdm::Tag"
}
end
end

View File

@ -1,9 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::ImportedCred
def self.included(base)
base.class_eval{
belongs_to :workspace, :class_name => "Mdm::Workspace"
}
end
end

View File

@ -1,14 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::Listener
def self.included(base)
base.class_eval{
belongs_to :workspace, :class_name => "Mdm::Workspace"
belongs_to :task, :class_name => "Mdm::Task"
serialize :options, ::MetasploitDataModels::Base64Serializer.new
validates :address, :presence => true, :ip_format => true
validates :port, :presence => true
}
end
end

View File

@ -1,35 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::Loot
def self.included(base)
base.class_eval {
belongs_to :workspace, :class_name => "Mdm::Workspace"
belongs_to :host, :class_name => "Mdm::Host"
belongs_to :service, :class_name => "Mdm::Service"
serialize :data, ::MetasploitDataModels::Base64Serializer.new
before_destroy :delete_file
scope :search, lambda { |*args|
where(["loots.ltype ILIKE ? OR " +
"loots.name ILIKE ? OR " +
"loots.info ILIKE ? OR " +
"loots.data ILIKE ?",
"%#{args[0]}%", "%#{args[0]}%", "%#{args[0]}%", "%#{args[0]}%"
])
}
private
def delete_file
c = Pro::Client.get rescue nil
if c
c.loot_delete_file(self[:id])
else
::File.unlink(self.path) rescue nil
end
end
}
end
end

View File

@ -1,15 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::Macro
def self.included(base)
base.class_eval{
extend MetasploitDataModels::SerializedPrefs
serialize :actions, ::MetasploitDataModels::Base64Serializer.new
serialize :prefs, ::MetasploitDataModels::Base64Serializer.new
serialized_prefs_attr_accessor :max_time
validates :name, :presence => true, :format => /^[^'|"]+$/
}
end
end

View File

@ -1,6 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::ModRef
def self.included(base)
base.class_eval{
}
end
end

View File

@ -1,9 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::ModuleAction
def self.included(base)
base.class_eval{
base.table_name = "module_actions"
belongs_to :module_detail
validate :name, :presence => true
}
end
end

View File

@ -1,9 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::ModuleArch
def self.included(base)
base.class_eval{
base.table_name = "module_archs"
belongs_to :module_detail
validate :name, :presence => true
}
end
end

View File

@ -1,9 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::ModuleAuthor
def self.included(base)
base.class_eval{
base.table_name = "module_authors"
belongs_to :module_detail
validate :name, :presence => true
}
end
end

View File

@ -1,67 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::ModuleDetail
def self.included(base)
base.class_eval {
base.table_name = "module_details"
has_many :authors, :class_name => "Mdm::ModuleAuthor", :dependent => :destroy, :source => :module_author
has_many :mixins, :class_name => "Mdm::ModuleMixin", :dependent => :destroy, :source => :module_mixin
has_many :targets, :class_name => "Mdm::ModuleTarget", :dependent => :destroy, :source => :module_target
has_many :actions, :class_name => "Mdm::ModuleAction", :dependent => :destroy, :source => :module_action
has_many :refs, :class_name => "Mdm::ModuleRef", :dependent => :destroy, :source => :module_ref
has_many :archs, :class_name => "Mdm::ModuleArch", :dependent => :destroy, :source => :module_arch
has_many :platforms, :class_name => "Mdm::ModulePlatform", :dependent => :destroy, :source => :module_platform
validate :refname, :presence => true
validates_associated :authors
validates_associated :mixins
validates_associated :targets
validates_associated :actions
validates_associated :archs
validates_associated :platforms
validates_associated :refs
def add_author(name, email=nil)
if email
r = self.authors.build(:name => name, :email => email).save
else
self.authors.build(:name => name).save
end
end
def add_mixin(name)
self.mixins.build(:name => name).save
end
def add_target(idx, name)
self.targets.build(:index => idx, :name => name).save
end
def add_action(name)
self.actions.build(:name => name).save
end
def add_ref(name)
self.refs.build(:name => name).save
end
def add_arch(name)
self.archs.build(:name => name).save
end
def add_platform(name)
self.platforms.build(:name => name).save
end
def before_destroy
Mdm::ModuleAuthor.delete_all('module_detail_id = ?', self.id)
Mdm::ModuleMixin.delete_all('module_detail_id = ?', self.id)
Mdm::ModuleTarget.delete_all('module_detail_id = ?', self.id)
Mdm::ModuleAction.delete_all('module_detail_id = ?', self.id)
Mdm::ModuleRef.delete_all('module_detail_id = ?', self.id)
Mdm::ModuleArch.delete_all('module_detail_id = ?', self.id)
Mdm::ModulePlatform.delete_all('module_detail_id = ?', self.id)
end
}
end
end

View File

@ -1,9 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::ModuleMixin
def self.included(base)
base.class_eval{
base.table_name = "module_mixins"
belongs_to :module_detail
validate :name, :presence => true
}
end
end

View File

@ -1,9 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::ModulePlatform
def self.included(base)
base.class_eval{
base.table_name = "module_platforms"
belongs_to :module_detail
validate :name, :presence => true
}
end
end

View File

@ -1,9 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::ModuleRef
def self.included(base)
base.class_eval{
base.table_name = "module_refs"
belongs_to :module_detail
validate :name, :presence => true
}
end
end

View File

@ -1,9 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::ModuleTarget
def self.included(base)
base.class_eval{
base.table_name = "module_targets"
belongs_to :module_detail
validate :name, :presence => true
}
end
end

View File

@ -1,14 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::NexposeConsole
def self.included(base)
base.class_eval{
serialize :cached_sites, ::MetasploitDataModels::Base64Serializer.new
validates :name, :presence => true
validates :address, :presence => true
validates :username, :presence => true
validates :password, :presence => true
validates :port, :inclusion => {:in => 1..65535}
}
end
end

View File

@ -1,34 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::Note
def self.included(base)
base.class_eval{
notes = base.arel_table
belongs_to :workspace, :class_name => "Mdm::Workspace"
belongs_to :host, :class_name => "Mdm::Host", :counter_cache => :note_count
belongs_to :service, :class_name => "Mdm::Service"
serialize :data, ::MetasploitDataModels::Base64Serializer.new
scope :flagged, where('critical = true AND seen = false')
scope :visible, where(notes[:ntype].not_in(['web.form', 'web.url', 'web.vuln']))
scope :search, lambda { |*args|
where(["(data NOT ILIKE 'BAh7%' AND data LIKE ?)" +
"OR (data ILIKE 'BAh7%' AND decode(data, 'base64') LIKE ?)" +
"OR ntype ILIKE ?",
"%#{args[0]}%", "%#{args[0]}%", "%#{args[0]}%"
])
}
after_save :normalize
private
def normalize
if data_changed? and ntype =~ /fingerprint/
host.normalize_os
end
end
}
end
end

View File

@ -1,8 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::Profile
def self.included(base)
base.class_eval{
serialize :settings, ::MetasploitDataModels::Base64Serializer.new
}
end
end

View File

@ -1,8 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::Ref
def self.included(base)
base.class_eval{
has_many :vulns, :through => :vulns_refs, :class_name => "Mdm::Vuln"
has_many :vulns_refs, :class_name => "Mdm::VulnRef"
}
end
end

View File

@ -1,29 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::Report
def self.included(base)
base.class_eval {
belongs_to :workspace, :class_name => "Mdm::Workspace"
serialize :options, ::MetasploitDataModels::Base64Serializer.new
validates_format_of :name, :with => /^[A-Za-z0-9\x20\x2e\x2d\x5f\x5c]+$/, :message => "name must consist of A-Z, 0-9, space, dot, underscore, or dash", :allow_blank => true
serialize :options, MetasploitDataModels::Base64Serializer.new
before_destroy :delete_file
scope :flagged, where('reports.downloaded_at is NULL')
private
def delete_file
c = Pro::Client.get rescue nil
if c
c.report_delete_file(self[:id])
else
::File.unlink(self.path) rescue nil
end
end
}
end
end

View File

@ -1,22 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::ReportTemplate
def self.included(base)
base.class_eval{
belongs_to :workspace, :class_name => "Mdm::Workspace"
before_destroy :delete_file
private
def delete_file
c = Pro::Client.get rescue nil
if c
c.report_template_delete_file(self[:id])
else
::File.unlink(self.path) rescue nil
end
end
}
end
end

View File

@ -1,7 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::Route
def self.included(base)
base.class_eval{
belongs_to :session, :class_name => "Mdm::Session"
}
end
end

View File

@ -1,42 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::Service
def self.included(base)
base.class_eval{
eval("STATES = ['open', 'closed', 'filtered', 'unknown']") unless defined? STATES
has_many :vulns, :dependent => :destroy, :class_name => "Mdm::Vuln"
has_many :notes, :dependent => :destroy, :class_name => "Mdm::Note"
has_many :creds, :dependent => :destroy, :class_name => "Mdm::Cred"
has_many :exploited_hosts, :dependent => :destroy, :class_name => "Mdm::ExploitedHost"
has_many :web_sites, :dependent => :destroy, :class_name => "Mdm::WebSite"
has_many :web_pages, :through => :web_sites, :class_name => "Mdm::WebPage"
has_many :web_forms, :through => :web_sites, :class_name => "Mdm::WebForm"
has_many :web_vulns, :through => :web_sites, :class_name => "Mdm::WebVuln"
belongs_to :host, :class_name => "Mdm::Host", :counter_cache => :service_count
has_many :web_pages, :through => :web_sites
has_many :web_forms, :through => :web_sites
has_many :web_vulns, :through => :web_sites
scope :inactive, where("services.state != 'open'")
scope :with_state, lambda { |a_state| where("services.state = ?", a_state)}
scope :search, lambda { |*args|
where([
"services.name ILIKE ? OR " +
"services.info ILIKE ? OR " +
"services.proto ILIKE ? OR " +
"services.port = ? ",
"%#{args[0]}%", "%#{args[0]}%", "%#{args[0]}%", (args[0].to_i > 0) ? args[0].to_i : 99999
])
}
after_save :normalize_host_os
def normalize_host_os
if info_changed?
host.normalize_os
end
end
}
end
end

View File

@ -1,33 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::Session
def self.included(base)
base.class_eval {
belongs_to :host, :class_name => "Mdm::Host"
has_one :workspace, :through => :host, :class_name => "Mdm::Workspace"
has_many :events, :class_name => "Mdm::SessionEvent", :order => "created_at", :dependent => :delete_all
has_many :routes, :class_name => "Mdm::Route", :dependent => :delete_all
scope :alive, where("closed_at IS NULL")
scope :dead, where("closed_at IS NOT NULL")
scope :upgradeable, where("closed_at IS NULL AND stype = 'shell' and platform ILIKE '%win%'")
serialize :datastore, ::MetasploitDataModels::Base64Serializer.new
before_destroy :stop
def upgradeable?
(self.platform =~ /win/ and self.stype == 'shell')
end
private
def stop
c = Pro::Client.get rescue nil
c.session_stop(self.local_id) rescue nil # ignore exceptions (XXX - ideally, stopped an already-stopped session wouldn't throw XMLRPCException)
end
}
end
end

View File

@ -1,8 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::SessionEvent
def self.included(base)
base.class_eval{
belongs_to :session, :class_name => "Mdm::Session"
}
end
end

View File

@ -1,27 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::Tag
def self.included(base)
base.class_eval {
has_many :hosts_tags, :class_name => "Mdm::HostTag"
has_many :hosts, :through => :hosts_tags, :class_name => "Mdm::Host"
belongs_to :user, :class_name => "Mdm::User"
validates :name, :presence => true, :format => {
:with => /^[A-Za-z0-9\x2e\x2d_]+$/, :message => "must be alphanumeric, dots, dashes, or underscores"
}
validates :desc, :length => {:maximum => 8191, :message => "desc must be less than 8k."}
before_destroy :cleanup_hosts
def to_s
name
end
def cleanup_hosts
# Clean up association table records
Mdm::HostTag.delete_all("tag_id = #{self.id}")
end
}
end
end

View File

@ -1,28 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::Task
def self.included(base)
base.class_eval{
belongs_to :workspace, :class_name => "Mdm::Workspace"
serialize :options, ::MetasploitDataModels::Base64Serializer.new
serialize :result, ::MetasploitDataModels::Base64Serializer.new
serialize :settings, ::MetasploitDataModels::Base64Serializer.new
scope :running, order( "created_at DESC" ).where("completed_at IS NULL")
before_destroy :delete_file
private
def delete_file
c = Pro::Client.get rescue nil
if c
c.task_delete_log(self[:id]) if c
else
::File.unlink(self.path) rescue nil
end
end
}
end
end

View File

@ -1,23 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::User
def self.included(base)
base.class_eval {
extend MetasploitDataModels::SerializedPrefs
serialize :prefs, ::MetasploitDataModels::Base64Serializer.new
has_and_belongs_to_many :workspaces, :join_table => "workspace_members", :uniq => true, :class_name => "Mdm::Workspace"
has_many :owned_workspaces, :foreign_key => "owner_id", :class_name => "Mdm::Workspace"
has_many :tags, :class_name => "Mdm::Tag"
validates :password, :password_is_strong => true
validates :password_confirmation, :password_is_strong => true
serialized_prefs_attr_accessor :nexpose_host, :nexpose_port, :nexpose_user, :nexpose_pass, :nexpose_creds_type, :nexpose_creds_user, :nexpose_creds_pass
serialized_prefs_attr_accessor :http_proxy_host, :http_proxy_port, :http_proxy_user, :http_proxy_pass
serialized_prefs_attr_accessor :time_zone, :session_key
serialized_prefs_attr_accessor :last_login_address # specifically NOT last_login_ip to prevent confusion with AuthLogic magic columns (which dont work for serialized fields)
}
end
end

View File

@ -1,38 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::Vuln
def self.included(base)
base.class_eval {
belongs_to :host, :class_name => "Mdm::Host", :counter_cache => :vuln_count
belongs_to :service, :class_name => "Mdm::Service", :foreign_key => :service_id
has_many :vuln_details, :dependent => :destroy, :class_name => "Mdm::VulnDetail"
has_many :vuln_attempts, :dependent => :destroy, :class_name => "Mdm::VulnAttempt"
has_many :vulns_refs, :class_name => "Mdm::VulnRef"
has_many :refs, :through => :vulns_refs, :class_name => "Mdm::Ref"
validates :name, :presence => true
validates_associated :refs
after_update :save_refs
scope :search, lambda { |*args|
where(["(vulns.name ILIKE ? or vulns.info ILIKE ? or refs.name ILIKE ?)",
"%#{args[0]}%", "%#{args[0]}%", "%#{args[0]}%"
]).
joins("LEFT OUTER JOIN vulns_refs ON vulns_refs.vuln_id=vulns.id LEFT OUTER JOIN refs ON refs.id=vulns_refs.ref_id")
}
private
def save_refs
refs.each { |ref| ref.save(:validate => false) }
end
def before_destroy
Mdm::VulnRef.delete_all('vuln_id = ?', self.id)
Mdm::VulnDetail.delete_all('vuln_id = ?', self.id)
Mdm::VulnAttempt.delete_all('vuln_id = ?', self.id)
end
}
end
end

View File

@ -1,8 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::VulnAttempt
def self.included(base)
base.class_eval {
belongs_to :vuln, :class_name => "Mdm::Vuln", :counter_cache => :vuln_attempt_count
validates :vuln_id, :presence => true
}
end
end

View File

@ -1,8 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::VulnDetail
def self.included(base)
base.class_eval {
belongs_to :vuln, :class_name => "Mdm::Vuln", :counter_cache => :vuln_detail_count
validates :vuln_id, :presence => true
}
end
end

View File

@ -1,10 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::VulnRef
def self.included(base)
base.class_eval {
base.table_name = "vulns_refs"
belongs_to :ref
belongs_to :vuln
}
end
end

View File

@ -1,9 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::WebForm
def self.included(base)
base.class_eval{
belongs_to :web_site, :class_name => "Mdm::WebSite"
serialize :params, ::MetasploitDataModels::Base64Serializer.new
}
end
end

View File

@ -1,9 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::WebPage
def self.included(base)
base.class_eval{
belongs_to :web_site, :class_name => "Mdm::WebSite"
serialize :headers, ::MetasploitDataModels::Base64Serializer.new
}
end
end

View File

@ -1,41 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::WebSite
def self.included(base)
base.class_eval {
belongs_to :service, :class_name => "Mdm::Service", :foreign_key => "service_id"
has_many :web_pages, :dependent => :destroy, :class_name => "Mdm::WebPage"
has_many :web_forms, :dependent => :destroy, :class_name => "Mdm::WebForm"
has_many :web_vulns, :dependent => :destroy, :class_name => "Mdm::WebVuln"
serialize :options, ::MetasploitDataModels::Base64Serializer.new
def to_url(ignore_vhost=false)
proto = self.service.name == "https" ? "https" : "http"
host = ignore_vhost ? self.service.host.address : self.vhost
port = self.service.port
if Rex::Socket.is_ipv6?(host)
host = "[#{host}]"
end
url = "#{proto}://#{host}"
if not ((proto == "http" and port == 80) or (proto == "https" and port == 443))
url += ":#{port}"
end
url
end
def page_count
web_pages.size
end
def form_count
web_forms.size
end
def vuln_count
web_vulns.size
end
} # end class_eval block
end
end

View File

@ -1,9 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::WebVuln
def self.included(base)
base.class_eval{
belongs_to :web_site, :class_name => "Mdm::WebSite"
serialize :params, ::MetasploitDataModels::Base64Serializer.new
}
end
end

View File

@ -1,6 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::WmapRequest
def self.included(base)
base.class_eval{
}
end
end

View File

@ -1,6 +0,0 @@
module MetasploitDataModels::ActiveRecordModels::WmapTarget
def self.included(base)
base.class_eval{
}
end
end

View File

@ -1,184 +0,0 @@
# NOTE: this AR model is called "Project" on the Pro side
module MetasploitDataModels::ActiveRecordModels::Workspace
def self.included(base)
base.class_eval{
# Usage of the evil eval avoids dynamic constant assignment
# exception when this module is included
eval('DEFAULT = "default"') unless defined? DEFAULT
has_many :hosts, :dependent => :destroy, :class_name => "Mdm::Host"
has_many :services, :through => :hosts, :class_name => "Mdm::Service", :foreign_key => "service_id"
has_many :notes, :class_name => "Mdm::Note"
has_many :loots, :through => :hosts, :class_name => "Mdm::Loot"
has_many :events, :class_name => "Mdm::Event"
has_many :reports, :dependent => :destroy, :class_name => "Mdm::Report"
has_many :report_templates, :dependent => :destroy, :class_name => "Mdm::ReportTemplate"
has_many :tasks, :dependent => :destroy, :class_name => "Mdm::Task", :order => "created_at DESC"
has_many :clients, :through => :hosts, :class_name => "Mdm::Client"
has_many :vulns, :through => :hosts, :class_name => "Mdm::Vuln"
has_many :creds, :through => :services, :class_name => "Mdm::Cred"
has_many :imported_creds, :dependent => :destroy, :class_name => "Mdm::ImportedCred"
has_many :exploited_hosts, :through => :hosts, :class_name => "Mdm::ExploitedHost"
has_many :sessions, :through => :hosts, :class_name => "Mdm::Session"
has_many :cred_files, :dependent => :destroy, :class_name => "Mdm::CredFile"
has_many :listeners, :dependent => :destroy, :class_name => "Mdm::Listener"
belongs_to :owner, :class_name => "Mdm::User", :foreign_key => "owner_id"
has_and_belongs_to_many :users, :join_table => "workspace_members", :uniq => true, :class_name => "Mdm::User"
before_save :normalize
validates :name, :presence => true, :uniqueness => true, :length => {:maximum => 255}
validates :description, :length => {:maximum => 4096}
validate :boundary_must_be_ip_range
def web_sites
query = <<-EOQ
SELECT DISTINCT web_sites.*
FROM hosts, services, web_sites
WHERE hosts.workspace_id = #{id} AND
services.host_id = hosts.id AND
web_sites.service_id = services.id
EOQ
Mdm::WebSite.find_by_sql(query)
end
def web_pages
query = <<-EOQ
SELECT DISTINCT web_pages.*
FROM hosts, services, web_sites, web_pages
WHERE hosts.workspace_id = #{id} AND
services.host_id = hosts.id AND
web_sites.service_id = services.id AND
web_pages.web_site_id = web_sites.id
EOQ
Mdm::WebPage.find_by_sql(query)
end
def web_forms
query = <<-EOQ
SELECT DISTINCT web_forms.*
FROM hosts, services, web_sites, web_forms
WHERE hosts.workspace_id = #{id} AND
services.host_id = hosts.id AND
web_sites.service_id = services.id AND
web_forms.web_site_id = web_sites.id
EOQ
Mdm::WebForm.find_by_sql(query)
end
def unique_web_forms
query = <<-EOQ
SELECT DISTINCT web_forms.web_site_id, web_forms.path, web_forms.method, web_forms.query
FROM hosts, services, web_sites, web_forms
WHERE hosts.workspace_id = #{id} AND
services.host_id = hosts.id AND
web_sites.service_id = services.id AND
web_forms.web_site_id = web_sites.id
EOQ
Mdm::WebForm.find_by_sql(query)
end
def web_vulns
query = <<-EOQ
SELECT DISTINCT web_vulns.*
FROM hosts, services, web_sites, web_vulns
WHERE hosts.workspace_id = #{id} AND
services.host_id = hosts.id AND
web_sites.service_id = services.id AND
web_vulns.web_site_id = web_sites.id
EOQ
Mdm::WebVuln.find_by_sql(query)
end
def self.default
find_or_create_by_name(DEFAULT)
end
def default?
name == DEFAULT
end
def creds
Mdm::Cred.find(
:all,
:include => {:service => :host},
:conditions => ["hosts.workspace_id = ?", self.id]
)
end
def host_tags
Mdm::Tag.find(
:all,
:include => :hosts,
:conditions => ["hosts.workspace_id = ?", self.id]
)
end
#
# This method iterates the creds table calling the supplied block with the
# cred instance of each entry.
#
def each_cred(&block)
creds.each do |cred|
block.call(cred)
end
end
def each_host_tag(&block)
host_tags.each do |host_tag|
block.call(host_tag)
end
end
def web_unique_forms(addrs=nil)
forms = unique_web_forms
if addrs
forms.reject!{|f| not addrs.include?( f.web_site.service.host.address ) }
end
forms
end
def boundary_must_be_ip_range
errors.add(:boundary, "must be a valid IP range") unless valid_ip_or_range?(boundary)
end
#
# If limit_to_network is disabled, this will always return true.
# Otherwise, return true only if all of the given IPs are within the project
# boundaries.
#
def allow_actions_on?(ips)
return true unless limit_to_network
return true unless boundary
return true if boundary.empty?
boundaries = Shellwords.split(boundary)
return true if boundaries.empty? # It's okay if there is no boundary range after all
given_range = Rex::Socket::RangeWalker.new(ips)
return false unless given_range # Can't do things to nonexistant IPs
allowed = false
boundaries.each do |boundary_range|
ok_range = Rex::Socket::RangeWalker.new(boundary)
allowed = true if ok_range.include_range? given_range
end
return allowed
end
private
def valid_ip_or_range?(string)
begin
Rex::Socket::RangeWalker.new(string)
rescue
return false
end
end
def normalize
boundary.strip! if boundary
end
} # end class_eval block
end
end

View File

@ -1,3 +0,0 @@
module MetasploitDataModels
VERSION = "0.0.2"
end

View File

@ -1,25 +0,0 @@
# -*- encoding: utf-8 -*-
$:.push File.expand_path("../lib", __FILE__)
require "metasploit_data_models/version"
Gem::Specification.new do |s|
s.name = "metasploit_data_models"
s.version = "0.0.2.43DEV" # This gemspec is linked to metasploit releases and follows trunk
s.authors = ["Trevor Rosen"]
s.email = ["trevor_rosen@rapid7.com"]
s.homepage = ""
s.summary = %q{Database code for MSF and Metasploit Pro}
s.description = %q{Implements minimal ActiveRecord models and database helper code used in both the Metasploit Framework (MSF) and Metasploit commercial editions.}
s.files = `git ls-files`.split("\n")
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
s.require_paths = ["lib"]
# ---- Dependencies ----
s.add_development_dependency "rspec"
s.add_runtime_dependency "activerecord"
s.add_runtime_dependency "activesupport"
s.add_runtime_dependency "pg"
s.add_runtime_dependency "pry"
end

View File

@ -1 +0,0 @@
require File.expand_path(File.dirname(__FILE__) + "/../lib/msf_models")

View File

@ -0,0 +1,18 @@
# bundler configuration
.bundle
# Mac OS X folder attributes
.DS_Store
# built gems
*.gem
# Rubymine project configuration
.idea
# Don't check in rvmrc since this is a gem
.rvmrc
# Installed gem versions. Not stored for the same reasons as .rvmrc
Gemfile.lock
# Packaging directory for builds
pkg/*
# Database configuration (with passwords) for specs
spec/dummy/config/database.yml
# logs
*.log

View File

@ -0,0 +1,10 @@
source "http://rubygems.org"
# Specify your gem's dependencies in metasploit_data_models.gemspec
gemspec
group :test do
# rails is only used for testing with a dummy application in spec/dummy
gem 'rails'
gem 'rspec-rails'
end

View File

@ -0,0 +1,72 @@
#MetasploitDataModels
The database layer for Metasploit
## Purpose
__MetasploitDataModels__ exists to do several key things:
1. Allow code sharing between Metasploit Framework (MSF) and the commercial versions of Metasploit (Community, Express, Pro -- usually referred to collectively as "Pro")
2. Give developers a lightweight entry point to MSF's backend for use in developing tools that gather data intended for later use with Metasploit (e.g. specialized scanners).
3. Make it easy to keep commercial stuff private while increasing the functionality of the open-source tools we provide to the community.
## Usage
### Rails
In a Rails application, MetasploitDataModels acts a
[Rails Engine](http://edgeapi.rubyonrails.org/classes/Rails/Engine.html) and the models are available to application
just as if they were defined under app/models. If your Rails appliation needs to modify the models, this can be done
using ActiveSupport.on_load hooks in initializers. The block passed to on_load hook is evaluated in the context of the
model class, so defining method and including modules will work just like reopeninng the class, but
ActiveSupport.on_load ensures that the monkey patches will work after reloading in development mode. Each class has a
different on_load name, which is just the class name converted to an underscored symbol, so Mdm::ApiKey runs the
:mdm_api_key load hooks, etc.
# Gemfile
gem :metasploiit_data_models, :git => git://github.com/rapid7/metasploit_data_models.git, :tag => 'v0.3.0'
# config/initializers/metasploit_data_models.rb
ActiveSupport.on_load(:mdm_api_key) do
# Returns the String obfuscated token for display. Meant to avoid CSRF
# api-key stealing attackes.
def obfuscated_token
token[0..3] + "****************************"
end
end
### Metasploit Framework
In Metasploit Framework, `MetasploitDataModels.require_models` is called by the `Msf::DbManager` to use the data models
only if the user wants to use the database.
### Elsewhere
__NOTE: This isn't in RubyGems yet. Using a Gemfile entry pointing to this repo (i.e., using
[Bundler](http://gembundler.com)) is the suggested option for now.__
Usage outside of Rapid7 is still alpha, as reflected in the pre-1.0.0 version, and we're not making many promises. That
being said, usage is easy:
connection_info = YAML.load_file("path/to/rails-style/db_config_file")
ActiveRecord::Base.establish_connection(connection_info['development'])
MetasploitDataModels.require_models
Basically you need to do the following things:
1. Establish an ActiveRecord connection. A Rails __config/database.yml__ is ideal for this.
2. `MetasploitDataModels.require_models`
## Developer Info
### Console
The gem includes a console based on [Pry](https://github.com/pry/pry/)
Give it a path to a working MSF database.yml file for full
ActiveRecord-based access to your data.
__Note:__ "development" mode is hardcoded into the console currently.

View File

@ -0,0 +1,7 @@
require 'bundler/gem_tasks'
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec)
task :default => :spec

View File

@ -0,0 +1,20 @@
class Mdm::ApiKey < ActiveRecord::Base
#
# Validators
#
validate :supports_api
validates :token, :presence => true, :length => { :minimum => 8 }
protected
def supports_api
license = License.get
if license and not license.supports_api?
errors[:license] = " - this product does not support API access"
end
end
ActiveSupport.run_load_hooks(:mdm_api_key, self)
end

View File

@ -0,0 +1,9 @@
class Mdm::Client < ActiveRecord::Base
#
# Relations
#
belongs_to :campaign, :class_name => 'Mdm::Campaign'
belongs_to :host, :class_name => 'Mdm::Host'
ActiveSupport.run_load_hooks(:mdm_client, self)
end

View File

@ -0,0 +1,80 @@
class Mdm::Cred < ActiveRecord::Base
#
# CONSTANTS
#
KEY_ID_REGEX = /([0-9a-fA-F:]{47})/
PTYPES = {
'read/write password' => 'password_rw',
'read-only password' => 'password_ro',
'SMB hash' => 'smb_hash',
'SSH private key' => 'ssh_key',
'SSH public key' => 'ssh_pubkey'
}
#
# Relations
#
belongs_to :service, :class_name => "Mdm::Service"
def ptype_human
humanized = PTYPES.select do |k, v|
v == ptype
end.keys[0]
humanized ? humanized : ptype
end
# Returns its key id. If this is not an ssh-type key, returns nil.
def ssh_key_id
return nil unless self.ptype =~ /^ssh_/
return nil unless self.proof =~ KEY_ID_REGEX
$1.downcase # Can't run into NilClass problems.
end
def ssh_key_matches?(other_cred)
return false unless other_cred.kind_of? self.class
return false unless self.ptype == other_cred.ptype
case self.ptype
when "ssh_key"
matches = self.ssh_private_keys
when "ssh_pubkey"
matches = self.ssh_public_keys
else
false
end
matches.include?(self) and matches.include?(other_cred)
end
# Returns all keys with matching key ids, including itself
# If this is not an ssh-type key, always returns an empty array.
def ssh_keys
(self.ssh_private_keys | self.ssh_public_keys)
end
# Returns all private keys with matching key ids, including itself
# If this is not an ssh-type key, always returns an empty array.
def ssh_private_keys
return [] unless self.ssh_key_id
matches = self.class.all(
:conditions => ["creds.ptype = ? AND creds.proof ILIKE ?", "ssh_key", "%#{self.ssh_key_id}%"]
)
matches.select {|c| c.workspace == self.workspace}
end
# Returns all public keys with matching key ids, including itself
# If this is not an ssh-type key, always returns an empty array.
def ssh_public_keys
return [] unless self.ssh_key_id
matches = self.class.all(
:conditions => ["creds.ptype = ? AND creds.proof ILIKE ?", "ssh_pubkey", "%#{self.ssh_key_id}%"]
)
matches.select {|c| c.workspace == self.workspace}
end
# Returns its workspace
def workspace
self.service.host.workspace
end
ActiveSupport.run_load_hooks(:mdm_cred, self)
end

View File

@ -0,0 +1,8 @@
class Mdm::CredFile < ActiveRecord::Base
#
# Relations
#
belongs_to :workspace, :class_name => 'Mdm::Workspace'
ActiveSupport.run_load_hooks(:mdm_cred_file, self)
end

View File

@ -0,0 +1,30 @@
class Mdm::Event < ActiveRecord::Base
#
# Relations
#
belongs_to :host, :class_name => 'Mdm::Host'
belongs_to :workspace, :class_name => 'Mdm::Workspace'
#
# Scopes
#
scope :flagged, where(:critical => true, :seen => false)
scope :module_run, where(:name => 'module_run')
#
# Serializations
#
serialize :info, MetasploitDataModels::Base64Serializer.new
#
# Validations
#
validates :name, :presence => true
ActiveSupport.run_load_hooks(:mdm_event, self)
end

View File

@ -0,0 +1,14 @@
class Mdm::ExploitAttempt < ActiveRecord::Base
#
# Relations
#
belongs_to :host, :class_name => 'Mdm::Host', :counter_cache => :exploit_attempt_count
#
# Validations
#
validates :host_id, :presence => true
ActiveSupport.run_load_hooks(:mdm_exploit_attempt, self)
end

View File

@ -0,0 +1,11 @@
class Mdm::ExploitedHost < ActiveRecord::Base
#
# Relations
#
belongs_to :host, :class_name => 'Mdm::Host'
belongs_to :service, :class_name => 'Mdm::Service'
belongs_to :workspace, :class_name => 'Mdm::Workspace'
ActiveSupport.run_load_hooks(:mdm_exploited_host, self)
end

View File

@ -0,0 +1,134 @@
class Mdm::Host < ActiveRecord::Base
include Mdm::Host::OperatingSystemNormalization
#
# Callbacks
#
before_destroy :cleanup_tags
#
# CONSTANTS
#
# Fields searched for the search scope
SEARCH_FIELDS = [
'address::text',
'hosts.name',
'os_name',
'os_flavor',
'os_sp',
'mac',
'purpose',
'comments'
]
#
# Relations
#
has_many :exploit_attempts, :dependent => :destroy, :class_name => 'Mdm::ExploitAttempt'
has_many :exploited_hosts, :dependent => :destroy, :class_name => 'Mdm::ExploitedHost'
has_many :clients, :dependent => :delete_all, :class_name => 'Mdm::Client'
has_many :host_details, :dependent => :destroy, :class_name => 'Mdm::HostDetail'
# hosts_tags are cleaned up in before_destroy:
has_many :hosts_tags, :class_name => 'Mdm::HostTag'
has_many :loots, :dependent => :destroy, :class_name => 'Mdm::Loot', :order => 'loots.created_at desc'
has_many :notes, :dependent => :delete_all, :class_name => 'Mdm::Note', :order => 'notes.created_at'
has_many :services, :dependent => :destroy, :class_name => 'Mdm::Service', :order => 'services.port, services.proto'
has_many :sessions, :dependent => :destroy, :class_name => 'Mdm::Session', :order => 'sessions.opened_at'
has_many :vulns, :dependent => :delete_all, :class_name => 'Mdm::Vuln'
belongs_to :workspace, :class_name => 'Mdm::Workspace'
#
# Through host_tags
#
has_many :tags, :through => :hosts_tags, :class_name => 'Mdm::Tag'
#
# Through services
#
has_many :creds, :through => :services, :class_name => 'Mdm::Cred'
has_many :service_notes, :through => :services
has_many :web_sites, :through => :services, :class_name => 'Mdm::WebSite'
#
# Nested Attributes
# @note Must be declared after relations being referenced.
#
accepts_nested_attributes_for :services, :reject_if => lambda { |s| s[:port].blank? }, :allow_destroy => true
#
# Validations
#
validates :address,
:exclusion => {
:in => ['127.0.0.1']
},
:ip_format => true,
:presence => true,
:uniqueness => {
:scope => :workspace_id,
:unless => :ip_address_invalid?
}
validates :workspace, :presence => true
#
# Scopes
#
scope :alive, where({'hosts.state' => 'alive'})
scope :flagged, where('notes.critical = true AND notes.seen = false').includes(:notes)
scope :search,
lambda { |*args|
# @todo replace with AREL
terms = SEARCH_FIELDS.collect { |field|
"#{field} ILIKE ?"
}
disjunction = terms.join(' OR ')
formatted_parameter = "%#{args[0]}%"
parameters = [formatted_parameter] * SEARCH_FIELDS.length
conditions = [disjunction] + parameters
{
:conditions => conditions
}
}
scope :tag_search,
lambda { |*args| where("tags.name" => args[0]).includes(:tags) }
def attribute_locked?(attr)
n = notes.find_by_ntype("host.updated.#{attr}")
n && n.data[:locked]
end
def cleanup_tags
# No need to keep tags with no hosts
tags.each do |tag|
tag.destroy if tag.hosts == [self]
end
# Clean up association table records
Mdm::HostTag.delete_all("host_id = #{self.id}")
end
# This is replicated by the IpAddressValidator class. Had to put it here as well to avoid
# SQL errors when checking address uniqueness.
def ip_address_invalid?
begin
potential_ip = IPAddr.new(address)
return true unless potential_ip.ipv4? || potential_ip.ipv6?
rescue ArgumentError
return true
end
end
def is_vm?
!!self.virtual_host
end
ActiveSupport.run_load_hooks(:mdm_host, self)
end

View File

@ -0,0 +1,15 @@
class Mdm::HostDetail < ActiveRecord::Base
#
# Relations
#
belongs_to :host, :class_name => 'Mdm::Host', :counter_cache => :host_detail_count
#
# Validations
#
validates :host_id, :presence => true
ActiveSupport.run_load_hooks(:mdm_host_detail, self)
end

View File

@ -0,0 +1,13 @@
class Mdm::HostTag < ActiveRecord::Base
self.table_name = "hosts_tags"
#
# Relations
#
belongs_to :host, :class_name => 'Mdm::Host'
belongs_to :tag, :class_name => 'Mdm::Tag'
ActiveSupport.run_load_hooks(:mdm_host_tag, self)
end

View File

@ -0,0 +1,10 @@
class Mdm::ImportedCred < ActiveRecord::Base
#
# Relations
#
belongs_to :workspace, :class_name => "Mdm::Workspace"
ActiveSupport.run_load_hooks(:mdm_imported_cred, self)
end

View File

@ -0,0 +1,24 @@
class Mdm::Listener < ActiveRecord::Base
#
# Relations
#
belongs_to :task, :class_name => 'Mdm::Task'
belongs_to :workspace, :class_name => 'Mdm::Workspace'
#
# Serializations
#
serialize :options, MetasploitDataModels::Base64Serializer.new
#
# Validations
#
validates :address, :ip_format => true, :presence => true
validates :port, :presence => true
ActiveSupport.run_load_hooks(:mdm_listener, self)
end

View File

@ -0,0 +1,63 @@
class Mdm::Loot < ActiveRecord::Base
#
# Callbacks
#
before_destroy :delete_file
#
# CONSTANTS
#
RELATIVE_SEARCH_FIELDS = [
'ltype',
'name',
'info',
'data'
]
#
# Relations
#
belongs_to :host, :class_name => 'Mdm::Host'
belongs_to :service, :class_name => 'Mdm::Service'
belongs_to :workspace, :class_name => 'Mdm::Workspace'
#
# Scopes
#
scope :search, lambda { |*args|
# @todo replace with AREL
terms = RELATIVE_SEARCH_FIELDS.collect { |relative_field|
"loots.#{relative_field} ILIKE ?"
}
disjunction = terms.join(' OR ')
formatted_parameter = "%#{args[0]}%"
parameters = [formatted_parameter] * RELATIVE_SEARCH_FIELDS.length
conditions = [disjunction] + parameters
where(conditions)
}
#
# Serializations
#
serialize :data, MetasploitDataModels::Base64Serializer.new
private
def delete_file
c = Pro::Client.get rescue nil
if c
c.loot_delete_file(self[:id])
else
::File.unlink(self.path) rescue nil
end
end
ActiveSupport.run_load_hooks(:mdm_loot, self)
end

View File

@ -0,0 +1,20 @@
class Mdm::Macro < ActiveRecord::Base
extend MetasploitDataModels::SerializedPrefs
#
# Serialization
#
serialize :actions, MetasploitDataModels::Base64Serializer.new
serialize :prefs, MetasploitDataModels::Base64Serializer.new
serialized_prefs_attr_accessor :max_time
#
# Validations
#
validates :name, :presence => true, :format => /^[^'|"]+$/
ActiveSupport.run_load_hooks(:mdm_macro, self)
end

View File

@ -0,0 +1,3 @@
class Mdm::ModRef < ActiveRecord::Base
ActiveSupport.run_load_hooks(:mdm_mod_ref, self)
end

View File

@ -0,0 +1,16 @@
class Mdm::ModuleAction < ActiveRecord::Base
self.table_name = 'module_actions'
#
# Relations
#
belongs_to :module_detail, :class_name => 'Mdm::ModuleDetail'
#
# Validations
#
validate :name, :presence => true
ActiveSupport.run_load_hooks(:mdm_module_action, self)
end

View File

@ -0,0 +1,17 @@
class Mdm::ModuleArch < ActiveRecord::Base
self.table_name = 'module_archs'
#
# Relations
#
belongs_to :module_detail, :class_name => 'Mdm::ModuleDetail'
#
# Validations
#
validate :name, :presence => true
ActiveSupport.run_load_hooks(:mdm_module_arch, self)
end

View File

@ -0,0 +1,17 @@
class Mdm::ModuleAuthor < ActiveRecord::Base
self.table_name = 'module_authors'
#
# Relations
#
belongs_to :module_detail
#
# Validations
#
validate :name, :presence => true
ActiveSupport.run_load_hooks(:mdm_module_author, self)
end

View File

@ -0,0 +1,73 @@
class Mdm::ModuleDetail < ActiveRecord::Base
self.table_name = 'module_details'
#
# Relations
#
has_many :actions, :class_name => 'Mdm::ModuleAction', :dependent => :destroy, :source => :module_action
has_many :archs, :class_name => 'Mdm::ModuleArch', :dependent => :destroy, :source => :module_arch
has_many :authors, :class_name => 'Mdm::ModuleAuthor', :dependent => :destroy, :source => :module_author
has_many :mixins, :class_name => 'Mdm::ModuleMixin', :dependent => :destroy, :source => :module_mixin
has_many :platforms, :class_name => 'Mdm::ModulePlatform', :dependent => :destroy, :source => :module_platform
has_many :refs, :class_name => 'Mdm::ModuleRef', :dependent => :destroy, :source => :module_ref
has_many :targets, :class_name => 'Mdm::ModuleTarget', :dependent => :destroy, :source => :module_target
#
# Validations
#
validate :refname, :presence => true
validates_associated :actions
validates_associated :archs
validates_associated :authors
validates_associated :mixins
validates_associated :platforms
validates_associated :refs
validates_associated :targets
def add_author(name, email=nil)
if email
r = self.authors.build(:name => name, :email => email).save
else
self.authors.build(:name => name).save
end
end
def add_mixin(name)
self.mixins.build(:name => name).save
end
def add_target(idx, name)
self.targets.build(:index => idx, :name => name).save
end
def add_action(name)
self.actions.build(:name => name).save
end
def add_ref(name)
self.refs.build(:name => name).save
end
def add_arch(name)
self.archs.build(:name => name).save
end
def add_platform(name)
self.platforms.build(:name => name).save
end
def before_destroy
Mdm::ModuleAuthor.delete_all('module_detail_id = ?', self.id)
Mdm::ModuleMixin.delete_all('module_detail_id = ?', self.id)
Mdm::ModuleTarget.delete_all('module_detail_id = ?', self.id)
Mdm::ModuleAction.delete_all('module_detail_id = ?', self.id)
Mdm::ModuleRef.delete_all('module_detail_id = ?', self.id)
Mdm::ModuleArch.delete_all('module_detail_id = ?', self.id)
Mdm::ModulePlatform.delete_all('module_detail_id = ?', self.id)
end
ActiveSupport.run_load_hooks(:mdm_module_detail, self)
end

View File

@ -0,0 +1,17 @@
class Mdm::ModuleMixin < ActiveRecord::Base
self.table_name = 'module_mixins'
#
# Relations
#
belongs_to :module_detail, :class_name => 'Mdm::ModuleDetail'
#
# Validation
#
validate :name, :presence => true
ActiveSupport.run_load_hooks(:mdm_module_mixin, self)
end

View File

@ -0,0 +1,17 @@
class Mdm::ModulePlatform < ActiveRecord::Base
self.table_name = 'module_platforms'
#
# Relations
#
belongs_to :module_detail
#
# Validations
#
validate :name, :presence => true
ActiveSupport.run_load_hooks(:mdm_module_platform, self)
end

View File

@ -0,0 +1,17 @@
class Mdm::ModuleRef < ActiveRecord::Base
self.table_name = 'module_refs'
#
# Relations
#
belongs_to :module_detail, :class_name => 'Mdm::ModuleDetail'
#
# Validations
#
validate :name, :presence => true
ActiveSupport.run_load_hooks(:mdm_module_ref, self)
end

View File

@ -0,0 +1,17 @@
class Mdm::ModuleTarget < ActiveRecord::Base
self.table_name = 'module_targets'
#
# Relations
#
belongs_to :module_detail
#
# Validators
#
validate :name, :presence => true
ActiveSupport.run_load_hooks(:mdm_module_target, self)
end

View File

@ -0,0 +1,20 @@
class Mdm::NexposeConsole < ActiveRecord::Base
#
# Serializations
#
serialize :cached_sites, MetasploitDataModels::Base64Serializer.new
#
# Validations
#
validates :address, :presence => true
validates :name, :presence => true
validates :password, :presence => true
validates :port, :inclusion => {:in => 1..65535}
validates :username, :presence => true
ActiveSupport.run_load_hooks(:mdm_nexpose_console, self)
end

View File

@ -0,0 +1,49 @@
class Mdm::Note < ActiveRecord::Base
#
# Callbacks
#
after_save :normalize
#
# Relations
#
belongs_to :workspace, :class_name => "Mdm::Workspace"
belongs_to :host, :class_name => "Mdm::Host", :counter_cache => :note_count
belongs_to :service, :class_name => "Mdm::Service"
#
# Scopes
#
scope :flagged, where('critical = true AND seen = false')
notes = self.arel_table
scope :visible, where(notes[:ntype].not_in(['web.form', 'web.url', 'web.vuln']))
scope :search, lambda { |*args|
where(["(data NOT ILIKE 'BAh7%' AND data LIKE ?)" +
"OR (data ILIKE 'BAh7%' AND decode(data, 'base64') LIKE ?)" +
"OR ntype ILIKE ?",
"%#{args[0]}%", "%#{args[0]}%", "%#{args[0]}%"
])
}
#
# Serializations
#
serialize :data, ::MetasploitDataModels::Base64Serializer.new
private
def normalize
if data_changed? and ntype =~ /fingerprint/
host.normalize_os
end
end
ActiveSupport.run_load_hooks(:mdm_note, self)
end

View File

@ -0,0 +1,9 @@
class Mdm::Profile < ActiveRecord::Base
#
# Serializations
#
serialize :settings, MetasploitDataModels::Base64Serializer.new
ActiveSupport.run_load_hooks(:mdm_profile, self)
end

View File

@ -0,0 +1,14 @@
class Mdm::Ref < ActiveRecord::Base
#
# Relations
#
has_many :vulns_refs, :class_name => 'Mdm::VulnRef'
#
# Through :vuln_refs
#
has_many :vulns, :class_name => 'Mdm::Vuln', :through => :vulns_refs
ActiveSupport.run_load_hooks(:mdm_ref, self)
end

View File

@ -0,0 +1,50 @@
class Mdm::Report < ActiveRecord::Base
#
# Callbacks
#
before_destroy :delete_file
#
# Relations
#
belongs_to :workspace, :class_name => 'Mdm::Workspace'
#
# Scopes
#
scope :flagged, where('reports.downloaded_at is NULL')
#
# Serializations
#
serialize :options, MetasploitDataModels::Base64Serializer.new
#
# Validations
#
validates :name,
:format => {
:allow_blank => true,
:message => "name must consist of A-Z, 0-9, space, dot, underscore, or dash",
:with => /^[A-Za-z0-9\x20\x2e\x2d\x5f\x5c]+$/
}
private
def delete_file
c = Pro::Client.get rescue nil
if c
c.report_delete_file(self[:id])
else
::File.unlink(self.path) rescue nil
end
end
ActiveSupport.run_load_hooks(:mdm_report, self)
end

View File

@ -0,0 +1,27 @@
class Mdm::ReportTemplate < ActiveRecord::Base
#
# Callbacks
#
before_destroy :delete_file
#
# Relations
#
belongs_to :workspace, :class_name => 'Mdm::Workspace'
private
def delete_file
c = Pro::Client.get rescue nil
if c
c.report_template_delete_file(self[:id])
else
::File.unlink(self.path) rescue nil
end
end
ActiveSupport.run_load_hooks(:mdm_report_template, self)
end

View File

@ -0,0 +1,9 @@
class Mdm::Route < ActiveRecord::Base
#
# Relations
#
belongs_to :session, :class_name => 'Mdm::Session'
ActiveSupport.run_load_hooks(:mdm_route, self)
end

View File

@ -0,0 +1,56 @@
class Mdm::Service < ActiveRecord::Base
#
# Callbacks
#
after_save :normalize_host_os
#
# CONSTANTS
#
STATES = ['open', 'closed', 'filtered', 'unknown']
#
# Relations
#
has_many :creds, :dependent => :destroy, :class_name => 'Mdm::Cred'
has_many :exploited_hosts, :dependent => :destroy, :class_name => 'Mdm::ExploitedHost'
belongs_to :host, :class_name => 'Mdm::Host', :counter_cache => :service_count
has_many :notes, :dependent => :destroy, :class_name => 'Mdm::Note'
has_many :vulns, :dependent => :destroy, :class_name => 'Mdm::Vuln'
has_many :web_sites, :dependent => :destroy, :class_name => 'Mdm::WebSite'
#
# Through :web_sites
#
has_many :web_pages, :through => :web_sites, :class_name => 'Mdm::WebPage'
has_many :web_forms, :through => :web_sites, :class_name => 'Mdm::WebForm'
has_many :web_vulns, :through => :web_sites, :class_name => 'Mdm::WebVuln'
#
# Scopes
#
scope :inactive, where("services.state != 'open'")
scope :with_state, lambda { |a_state| where("services.state = ?", a_state)}
scope :search, lambda { |*args|
where([
"services.name ILIKE ? OR " +
"services.info ILIKE ? OR " +
"services.proto ILIKE ? OR " +
"services.port = ? ",
"%#{args[0]}%", "%#{args[0]}%", "%#{args[0]}%", (args[0].to_i > 0) ? args[0].to_i : 99999
])
}
def normalize_host_os
if info_changed?
host.normalize_os
end
end
ActiveSupport.run_load_hooks(:mdm_service, self)
end

View File

@ -0,0 +1,48 @@
class Mdm::Session < ActiveRecord::Base
#
# Callbacks
#
before_destroy :stop
#
# Relations
#
has_many :events, :class_name => 'Mdm::SessionEvent', :order => 'created_at', :dependent => :delete_all
belongs_to :host, :class_name => 'Mdm::Host'
has_many :routes, :class_name => 'Mdm::Route', :dependent => :delete_all
#
# Through :host
#
has_one :workspace, :through => :host, :class_name => 'Mdm::Workspace'
#
# Scopes
#
scope :alive, where('closed_at IS NULL')
scope :dead, where('closed_at IS NOT NULL')
scope :upgradeable, where("closed_at IS NULL AND stype = 'shell' and platform ILIKE '%win%'")
#
# Serializations
#
serialize :datastore, ::MetasploitDataModels::Base64Serializer.new
def upgradeable?
(self.platform =~ /win/ and self.stype == 'shell')
end
private
def stop
c = Pro::Client.get rescue nil
# ignore exceptions (XXX - ideally, stopped an already-stopped session wouldn't throw XMLRPCException)
c.session_stop(self.local_id) rescue nil
end
ActiveSupport.run_load_hooks(:mdm_session, self)
end

View File

@ -0,0 +1,9 @@
class Mdm::SessionEvent < ActiveRecord::Base
#
# Relations
#
belongs_to :session, :class_name => 'Mdm::Session'
ActiveSupport.run_load_hooks(:mdm_session_event, self)
end

View File

@ -0,0 +1,46 @@
class Mdm::Tag < ActiveRecord::Base
#
# Callbacks
#
before_destroy :cleanup_hosts
#
# Relations
#
has_many :hosts_tags, :class_name => 'Mdm::HostTag'
belongs_to :user, :class_name => 'Mdm::User'
#
# Through :hosts_tags
#
has_many :hosts, :through => :hosts_tags, :class_name => 'Mdm::Host'
#
# Validations
#
validates :desc,
:length => {
:maximum => ((8 * (2 ** 10)) - 1),
:message => "desc must be less than 8k."
}
validates :name,
:format => {
:with => /^[A-Za-z0-9\x2e\x2d_]+$/, :message => "must be alphanumeric, dots, dashes, or underscores"
},
:presence => true
def cleanup_hosts
# Clean up association table records
Mdm::HostTag.delete_all("tag_id = #{self.id}")
end
def to_s
name
end
ActiveSupport.run_load_hooks(:mdm_tag, self)
end

View File

@ -0,0 +1,41 @@
class Mdm::Task < ActiveRecord::Base
#
# Callbacks
#
before_destroy :delete_file
#
# Relations
#
belongs_to :workspace, :class_name => "Mdm::Workspace"
#
# Scopes
#
scope :running, order( "created_at DESC" ).where("completed_at IS NULL")
#
# Serializations
#
serialize :options, MetasploitDataModels::Base64Serializer.new
serialize :result, MetasploitDataModels::Base64Serializer.new
serialize :settings, MetasploitDataModels::Base64Serializer.new
private
def delete_file
c = Pro::Client.get rescue nil
if c
c.task_delete_log(self[:id]) if c
else
::File.unlink(self.path) rescue nil
end
end
ActiveSupport.run_load_hooks(:mdm_task, self)
end

View File

@ -0,0 +1,32 @@
class Mdm::User < ActiveRecord::Base
extend MetasploitDataModels::SerializedPrefs
#
# Relations
#
has_many :owned_workspaces, :foreign_key => 'owner_id', :class_name => 'Mdm::Workspace'
has_many :tags, :class_name => 'Mdm::Tag'
has_and_belongs_to_many :workspaces, :join_table => 'workspace_members', :uniq => true, :class_name => 'Mdm::Workspace'
#
# Serialziations
#
serialize :prefs, MetasploitDataModels::Base64Serializer.new
serialized_prefs_attr_accessor :nexpose_host, :nexpose_port, :nexpose_user, :nexpose_pass, :nexpose_creds_type, :nexpose_creds_user, :nexpose_creds_pass
serialized_prefs_attr_accessor :http_proxy_host, :http_proxy_port, :http_proxy_user, :http_proxy_pass
serialized_prefs_attr_accessor :time_zone, :session_key
serialized_prefs_attr_accessor :last_login_address # specifically NOT last_login_ip to prevent confusion with AuthLogic magic columns (which dont work for serialized fields)
#
# Validations
#
validates :password, :password_is_strong => true
validates :password_confirmation, :password_is_strong => true
ActiveSupport.run_load_hooks(:mdm_user, self)
end

Some files were not shown because too many files have changed in this diff Show More