From 0d205c4fd0e9dd2907135c2bffc274a478cdbf84 Mon Sep 17 00:00:00 2001 From: Jonathan Cran Date: Sat, 17 Dec 2011 10:22:02 -0600 Subject: [PATCH] initial commit --- Gemfile | 4 + Gemfile.lock | 16 ++ README | 87 ++++++ Rakefile | 1 + TODO | 15 ++ lab.gemspec | 24 ++ lib/lab.rb | 4 + lib/lab/controller/dynagen_controller.rb | 14 + lib/lab/controller/fog_controller.rb | 6 + lib/lab/controller/remote_esx_controller.rb | 62 +++++ .../remote_workstation_controller.rb | 22 ++ lib/lab/controller/virtualbox_controller.rb | 25 ++ lib/lab/controller/workstation_controller.rb | 17 ++ .../controller/workstation_vixr_controller.rb | 19 ++ lib/lab/controllers.rb | 10 + lib/lab/driver/dynagen_driver.rb | 47 ++++ lib/lab/driver/fog_driver.rb | 164 ++++++++++++ lib/lab/driver/remote_esx_driver.rb | 177 ++++++++++++ lib/lab/driver/remote_workstation_driver.rb | 198 ++++++++++++++ lib/lab/driver/virtualbox_driver.rb | 142 ++++++++++ lib/lab/driver/vm_driver.rb | 180 +++++++++++++ lib/lab/driver/workstation_driver.rb | 188 +++++++++++++ lib/lab/driver/workstation_vixr_driver.rb | 126 +++++++++ lib/lab/drivers.rb | 8 + lib/lab/modifier/backtrack5_modifier.rb | 16 ++ lib/lab/modifier/dos_modifier.rb | 14 + lib/lab/modifier/meterpreter_modifier.rb | 167 ++++++++++++ lib/lab/modifier/test_modifier.rb | 16 ++ lib/lab/modifiers.rb | 4 + lib/lab/version.rb | 3 + lib/lab/vm.rb | 253 ++++++++++++++++++ lib/lab/vm_controller.rb | 238 ++++++++++++++++ test/.gitkeep | 0 33 files changed, 2267 insertions(+) create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 README create mode 100644 Rakefile create mode 100644 TODO create mode 100644 lab.gemspec create mode 100644 lib/lab.rb create mode 100644 lib/lab/controller/dynagen_controller.rb create mode 100644 lib/lab/controller/fog_controller.rb create mode 100644 lib/lab/controller/remote_esx_controller.rb create mode 100644 lib/lab/controller/remote_workstation_controller.rb create mode 100644 lib/lab/controller/virtualbox_controller.rb create mode 100644 lib/lab/controller/workstation_controller.rb create mode 100644 lib/lab/controller/workstation_vixr_controller.rb create mode 100644 lib/lab/controllers.rb create mode 100644 lib/lab/driver/dynagen_driver.rb create mode 100644 lib/lab/driver/fog_driver.rb create mode 100644 lib/lab/driver/remote_esx_driver.rb create mode 100644 lib/lab/driver/remote_workstation_driver.rb create mode 100644 lib/lab/driver/virtualbox_driver.rb create mode 100644 lib/lab/driver/vm_driver.rb create mode 100644 lib/lab/driver/workstation_driver.rb create mode 100644 lib/lab/driver/workstation_vixr_driver.rb create mode 100644 lib/lab/drivers.rb create mode 100644 lib/lab/modifier/backtrack5_modifier.rb create mode 100644 lib/lab/modifier/dos_modifier.rb create mode 100644 lib/lab/modifier/meterpreter_modifier.rb create mode 100644 lib/lab/modifier/test_modifier.rb create mode 100644 lib/lab/modifiers.rb create mode 100644 lib/lab/version.rb create mode 100644 lib/lab/vm.rb create mode 100644 lib/lab/vm_controller.rb create mode 100644 test/.gitkeep diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..78a1014 --- /dev/null +++ b/Gemfile @@ -0,0 +1,4 @@ +source "http://rubygems.org" + +# Specify your gem's dependencies in lab.gemspec +gemspec diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..b1ca75c --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,16 @@ +PATH + remote: . + specs: + lab (0.0.1) + nokogiri + +GEM + remote: http://rubygems.org/ + specs: + nokogiri (1.5.0) + +PLATFORMS + ruby + +DEPENDENCIES + lab! diff --git a/README b/README new file mode 100644 index 0000000..33eef9c --- /dev/null +++ b/README @@ -0,0 +1,87 @@ +This folder contains the libraries necessary to run the lab plugin, and can also be used in a standalone way to automate virtual machines. + +######### +CONCEPTS: +######### + +The lab provides a clean interface to common vm functions such as start / stop / snapshot / revert and even running system commands or higher-level functions like opening a browser to a specified URL. It's designed so the different VM technologies have a similiar interface, and you can ignore the specifics of the VM tech. The majority of the functionality is implemented in the form of drivers and controllers. Drivers implement the underlying command for each vm software (such as start/stop/revert), and controllers implement the commands which apply to all vms (such as listing all running vms, or cloning a vm). + +If you're interested in porting a vm software (see below), please take a look at the workstation_driver.rb and the workstation_controller.rb -- This is a simple driver / controller in the lab, and you can simply copy / modify this to implement a new driver & controller for the software. + +########################## +SUPPORTED VM TECHNOLOGIES: +########################## + +NOTE: The lab libraries have only been tested on linux, porting to windows is not planned at this time. + +Implemented: + - workstation (Tested against 7.x) + - remote_workstation (Tested against 7.x) + - virtualbox (Tested against 4.x) + - remote_esx (VMware ESX Host Agent 4.1.0 build-348481) + +Partially Implemented: + - amazon_ec2 (via fog gem) + - dynagen + +Need Implementation: + - qemu + - qemudo + - others? + +################# +PLATFORM SUPPORT: +################# + +You will need to have this code running on a linux box, Currently this has only been run / tested on Ubuntu 9.04 -> 10.04, though it should run on any linux with an ssh client and the dependencies below. Remote VM Hosts will need to be linux as well, though other platforms may work (untested). If you're interested in porting it to windows, please contact me (jcran). + +Platform Dependencies: + - whatever vm software is necessary for the driver you're using (see SUPPORTED VM TECHNOLOGIES above) + - net/scp - the gem (net-scp). Required to copy files to/from the devices in the case that tools are not installed. Not necessary if tools are installed. + - fog - require to use the amazon_ec2 driver + +###################### +MSFCONSOLE LAB PLUGIN: +###################### + +BACKGROUND: + +The lab plugin for msfconsole adds a number of commands which may be useful if you're interested in automating remote hosts with rc scripts, or if you need to control targets / support systems while utilizing the metasploit console. A potential use case is testing an IPS / IDS, and resetting the target after running each exploit. + +USAGE: + +Here's some example usage for the lab plugin. + + msf> load lab // Loads the lab plugin + msf> lab_load // Loads from a lab configuration file. See data/lab/test_targets.yml for an example + msf> lab_load_dir workstation /path/to/vmx/files // Loads from a local directory. + msf> lab_load_running remote_esx root esx_server // Loads all running vms. + msf> lab_start vm1 // Start a vm which was loaded above + msf> lab_snapshot vm1 snapshot_1 // Snapshot a vm as 'snapshot_1' + msf> lab_run_command ("rm -rf /") // oops! + msf> lab_show // Show all vms that we're aware of + msf> lab_show_running // Show only running vms + msf> lab_start vm2 // Start another vm + msf> lab_suspend vm1 // Suspend a vm + msf> lab_revert all snapshot_1 // Revert all vms back to 'snapshot_1' + +############### +STANDALONE API: +############### + +BACKGROUND: + +The lab libraries add tons of useful functionality that isn't exposed through the lab plugin, such as the ability to run commands on hosts. This library can serve as an excellent base for more complex operations on a remote host as well. + +USAGE: + +You must first create a yaml file which describes your vm. See data/lab/test_targets.yml for an example. + + require 'vm_controller' + vm_controller = ::Lab::Controllers::VmController.new(YAML.load_file(lab_def)) + vm_controller['vm1'].start + vm_controller['vm1'].snapshot("clean") + vm_controller['vm1'].run_command("rm /etc/resolv.conf") + vm_controller['vm1'].open_uri("http://autopwn:8080") + vm_controller['vm1'].revert("clean") + vm_controller['vm1'].revert("clean") diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..2995527 --- /dev/null +++ b/Rakefile @@ -0,0 +1 @@ +require "bundler/gem_tasks" diff --git a/TODO b/TODO new file mode 100644 index 0000000..6652587 --- /dev/null +++ b/TODO @@ -0,0 +1,15 @@ +This is a list of basic priorities for the lab code... + +* Implement more technologies + + a) finish amazon ec2 (via fog) + b) qemu + c) qemudo + d) kvm + e) other cloud technologies (newservers, slicehost/rackspace,etc) + +* Implement a cloning function on each controller + +* Support Windows as a host platform. Currently all the code assumes a linux host is running it. The same applies for the remote_* drivers -- they've not been tested on windows. + +* Consolidate the remote_system_command code & provide a filter. Create an unsafe_system_command and unsafe_remote_system_command function call for when we control the entire string. diff --git a/lab.gemspec b/lab.gemspec new file mode 100644 index 0000000..8e36ff1 --- /dev/null +++ b/lab.gemspec @@ -0,0 +1,24 @@ +# -*- encoding: utf-8 -*- +$:.push File.expand_path("../lib", __FILE__) +require "lab/version" + +Gem::Specification.new do |s| + s.name = "lab" + s.version = Lab::VERSION + s.authors = ["Jonathan Cran"] + s.email = ["jcran@pentestify.com"] + s.homepage = "http://www.pentestify.com/lab" + s.summary = %q{Manage vms like a boss.} + s.description = %q{Start/Stop/Revert and do other cool stuff w/ Vmware, Virtualbox, and ESXi vms} + + s.rubyforge_project = "lab" + + 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"] + + # specify any dependencies here; for example: + # s.add_development_dependency "rspec" + s.add_runtime_dependency "nokogiri" +end diff --git a/lib/lab.rb b/lib/lab.rb new file mode 100644 index 0000000..da61db0 --- /dev/null +++ b/lib/lab.rb @@ -0,0 +1,4 @@ +# TODO - use autoload + +require 'lab/vm_controller' +require 'lab/version' diff --git a/lib/lab/controller/dynagen_controller.rb b/lib/lab/controller/dynagen_controller.rb new file mode 100644 index 0000000..1b8b383 --- /dev/null +++ b/lib/lab/controller/dynagen_controller.rb @@ -0,0 +1,14 @@ +module Lab +module Controllers +module DynagenController + + def self.running_list + raise "Unsupported" + end + + def self.dir_list(basepath=nil) + raise "Unsupported" + end +end +end +end diff --git a/lib/lab/controller/fog_controller.rb b/lib/lab/controller/fog_controller.rb new file mode 100644 index 0000000..9d671af --- /dev/null +++ b/lib/lab/controller/fog_controller.rb @@ -0,0 +1,6 @@ +module Lab +module Controllers +module FogController +end +end +end diff --git a/lib/lab/controller/remote_esx_controller.rb b/lib/lab/controller/remote_esx_controller.rb new file mode 100644 index 0000000..0507cf1 --- /dev/null +++ b/lib/lab/controller/remote_esx_controller.rb @@ -0,0 +1,62 @@ +# This controller was built against: +# VMware ESX Host Agent 4.1.0 build-348481 + +module Lab +module Controllers +module RemoteEsxController + + # Note that 3.5 was different (vmware-vim-cmd) + VIM_CMD = 'vim-cmd'.freeze + + def self.dir_list(basepath=nil) + # Does this method really even make sense for esx? + return "Unsupported :(" + end + + def self.running_list(user, host) + user.gsub!(/(\W)*/, '') + host.gsub!(/(\W)*/, '') + + # first get all registered vms + registered_vms = self.get_vms(user, host) || [] + running_vms = [] + + # now let's see which ones are running + # TODO: this is ghetto, would be better not to connect repeatedly + registered_vms.each do |vm| + remote_cmd = "ssh #{user}@#{host} \"#{VIM_CMD} vmsvc/power.getstate #{vm[:id]}\"" + raw = `#{remote_cmd}` + running_vms << vm if raw =~ /Powered on/ + end + + return running_vms + end + +private + + def self.get_vms(user, host) + user.gsub!(/(\W)*/, '') + host.gsub!(/(\W)*/, '') + + vms = [] # array of VM hashes + remote_cmd = "ssh #{user}@#{host} \"#{VIM_CMD} vmsvc/getallvms | grep ^[0-9] | sed 's/[[:blank:]]\\{3,\\}/ /g'\"" + raw = `#{remote_cmd}`.split("\n") + + raw.each do |line| + # So effing ghetto + id_and_name = line.split('[datastore').first + id = id_and_name.split(' ').first + + ## TODO - there's surely a better way to do this. + name_array = id_and_name.split(' ') + name_array.shift + name = name_array.join(' ') + vms << {:id => id, :name => name} + end + + return vms + end + +end +end +end diff --git a/lib/lab/controller/remote_workstation_controller.rb b/lib/lab/controller/remote_workstation_controller.rb new file mode 100644 index 0000000..099ee9e --- /dev/null +++ b/lib/lab/controller/remote_workstation_controller.rb @@ -0,0 +1,22 @@ +module Lab +module Controllers +module RemoteWorkstationController + + def self.running_list(user, host) + user.gsub!(/(\W)*/, '') + host.gsub!(/(\W)*/, '') + + remote_cmd = "ssh #{user}@#{host} \"vmrun list nogui\"" + vm_list = `#{remote_cmd}`.split("\n") + vm_list.shift + + return vm_list + end + + def self.dir_list(basepath=nil) + vm_list = Find.find(basepath).select { |f| f =~ /\.vmx$/ } + return vm_list + end +end +end +end diff --git a/lib/lab/controller/virtualbox_controller.rb b/lib/lab/controller/virtualbox_controller.rb new file mode 100644 index 0000000..2a65516 --- /dev/null +++ b/lib/lab/controller/virtualbox_controller.rb @@ -0,0 +1,25 @@ +module Lab +module Controllers +module VirtualBoxController + + def self.running_list + vm_names_and_uuids = `VBoxManage list runningvms` + return vm_names_and_uuids.scan(/\"(.*)\" {.*}/).flatten + end + + def self.config_list + vm_names_and_uuids = `VBoxManage list vms` + return vm_names_and_uuids.scan(/\"(.*)\" {.*}/).flatten + end + + def self.config_list_uuid + vm_names_and_uuids = `VBoxManage list vms` + return vm_names_and_uuids.scan(/\".*\" {(.*)}/).flatten + end + + def self.dir_list(basepath=nil) + vm_list = Find.find(basepath).select { |f| f =~ /\.xml$/ } + end +end +end +end diff --git a/lib/lab/controller/workstation_controller.rb b/lib/lab/controller/workstation_controller.rb new file mode 100644 index 0000000..bca2280 --- /dev/null +++ b/lib/lab/controller/workstation_controller.rb @@ -0,0 +1,17 @@ +module Lab +module Controllers +module WorkstationController + + def self.running_list + vm_list = `vmrun list`.split("\n") + vm_list.shift + return vm_list + end + + def self.dir_list(basepath=nil) + vm_list = Find.find(basepath).select { |f| f =~ /\.vmx$/ } + return vm_list + end +end +end +end diff --git a/lib/lab/controller/workstation_vixr_controller.rb b/lib/lab/controller/workstation_vixr_controller.rb new file mode 100644 index 0000000..b3e3ee3 --- /dev/null +++ b/lib/lab/controller/workstation_vixr_controller.rb @@ -0,0 +1,19 @@ +module Lab +module Controllers +module WorkstationVixrController + + def self.running_list + vm_list = `vmrun list`.split("\n") + vm_list.shift + + return vm_list + end + + def self.dir_list(basepath=nil) + vm_list = Find.find(basepath).select { |f| f =~ /\.vmx$/ } + + return vm_list + end +end +end +end diff --git a/lib/lab/controllers.rb b/lib/lab/controllers.rb new file mode 100644 index 0000000..74cffd7 --- /dev/null +++ b/lib/lab/controllers.rb @@ -0,0 +1,10 @@ +require 'controller/workstation_controller' +require 'controller/virtualbox_controller' +require 'controller/fog_controller' +require 'controller/dynagen_controller' +require 'controller/remote_workstation_controller' +require 'controller/remote_esx_controller' +#require 'controller/qemu_controller' +#require 'controller/qemudo_controller' + + diff --git a/lib/lab/driver/dynagen_driver.rb b/lib/lab/driver/dynagen_driver.rb new file mode 100644 index 0000000..4fb0f46 --- /dev/null +++ b/lib/lab/driver/dynagen_driver.rb @@ -0,0 +1,47 @@ +require 'vm_driver' + +# +# $Id$ +# + +# +# To use this driver, you have to have a lab which is preconfigured. The best / easiest +# way i've found to to set up a lab is GNS3 +# + +module Lab +module Drivers + class DynagenDriver < VmDriver + def initialize(config,dynagen_config) + super(config) + @running = false + @dynagen_platform = filter_command(dynagen_config['dynagen_platform']) + end + + def start + # TODO - write the location-file to a temp-file + # and set the autostart property + + ## start background dynamips process + system_command("dynamips -H #{@dynagen_platform} &") + system_command("dynagen #{@location}") + @running = true + end + + def stop + system_command("killall dynagen") + @running = false + end + + def cleanup + `killall dynagen` + `killall dynamips` + @running = false + end + + def running? + return @running + end + end +end +end diff --git a/lib/lab/driver/fog_driver.rb b/lib/lab/driver/fog_driver.rb new file mode 100644 index 0000000..9a4a61e --- /dev/null +++ b/lib/lab/driver/fog_driver.rb @@ -0,0 +1,164 @@ +require 'vm_driver' + +## +## $Id$ +## + +module Lab +module Drivers +class FogDriver < VmDriver + + def initialize(config,fog_config) + + super(config) + @fog_config = fog_config + + # Soft dependency + begin + require 'fog' + rescue LoadError + raise "WARNING: Library fog not found. Could Not Create Driver" + end + + if @fog_config['fog_type'] == "ec2" + + # AWS / EC2 Base Credential Configuration + @aws_cert_file = IO.read(fog_config['fog_aws_cert_file']).chomp if fog_config['fog_aws_cert_file'] + @aws_private_key_file = IO.read(fog_config['fog_aws_private_key_file']).chomp if fog_config['fog_aws_private_key_file'] + @ec2_access_key_file = IO.read(fog_config['fog_ec2_access_key_file']).chomp if fog_config['fog_ec2_access_key_file'] + @ec2_secret_access_key_file = IO.read(fog_config['fog_ec2_secret_access_key_file']).chomp if fog_config['fog_ec2_secret_access_key_file'] + + # Instance Keys + @ec2_instance_public_key_file = IO.read(fog_config['fog_ec2_instance_public_key_file']).chomp if fog_config['fog_ec2_instance_public_key_file'] + @ec2_instance_private_key_file = IO.read(fog_config['fog_ec2_instance_private_key_file']).chomp if fog_config['fog_ec2_instance_private_key_file'] + + # Instance Details + @ec2_base_ami = fog_config['fog_ec2_base_ami'] + @ec2_flavor = fog_config['fog_ec2_flavor'] + @ec2_user = fog_config['fog_ec2_user'] + @ec2_region = fog_config['fog_ec2_region'] + + # Set up a connection + @compute = Fog::Compute.new( + :provider => "Aws", + :aws_access_key_id => @aws_access_key_file, + :aws_secret_access_key => @aws_secret_access_key_file ) + else + raise "Unsupported Fog Type" + end + end + + def start + ec2_settings = { + :image_id => @ec2_base_ami, + :flavor_id => @ec2_flavor, + :public_key_path => @ec2_instance_public_key_file, + :private_key_path => @ec2_instance_private_key_file, + :username => @ec2_user} + + begin + @fog_server = @compute.servers.bootstrap(ec2_settings) + rescue Fog::Compute::AWS::Error => e + raise "Couldn't authenticate to AWS - did you place keys in the creds/ directory?" + exit + end + end + + def stop + @fog_server.destroy + end + + def suspend + raise "unimplemented" + end + + def pause + raise "unimplemented" + end + + def reset + raise "unimplemented" + end + + def create_snapshot(snapshot) + raise "unimplemented" + end + + def revert_snapshot(snapshot) + raise "unimplemented" + end + + def delete_snapshot(snapshot) + raise "unimplemented" + end + +=begin + + def run_command(command) + ## vm_driver will need a little patching for this to work, as + ## amis use keys for auth. i think it's just a matter of not passing the + ## password to ssh_exec. So maybe the thing to do is have a ssh_key_exec + ## function in vm_driver.rb that does the right thing. + + script_rand_name = rand(10000) + + if @os == "windows" + local_tempfile_path = "/tmp/lab_script_#{script_rand_name}.bat" + remote_tempfile_path = "C:\\\\lab_script_#{script_rand_name}.bat" + remote_run_command = remote_tempfile_path + else + local_tempfile_path = "/tmp/lab_script_#{script_rand_name}.sh" + remote_tempfile_path = "/tmp/lab_script_#{script_rand_name}.sh" + remote_run_command = "/bin/sh #{remote_tempfile_path}" + end + + # write out our script locally + File.open(local_tempfile_path, 'w') {|f| f.write(command) } + + # since we can't copy easily w/o tools, let's just run it directly :/ + if @os == "linux" + output_file = "/tmp/lab_command_output_#{rand(1000000)}" + + scp_to(local_tempfile_path, remote_tempfile_path) + ssh_exec(remote_run_command + "> #{output_file}") + scp_from(output_file, output_file) + ssh_exec("rm #{output_file}") + ssh_exec("rm #{remote_tempfile_path}") + + # Ghettohack! + string = File.open(output_file,"r").read + `rm #{output_file}` + + else + raise "zomgwtfbbqnotools" + end + end + + def copy_from(from, to) + raise "unimplemented" + end + + def copy_to(from, to) + raise "unimplemented" + end + + def check_file_exists(file) + raise "unimplemented" + end + + def create_directory(directory) + raise "unimplemented" + end +=end + + def cleanup + @fog_server.destroy + end + + def running? + return true #TODO + end + +end +end +end diff --git a/lib/lab/driver/remote_esx_driver.rb b/lib/lab/driver/remote_esx_driver.rb new file mode 100644 index 0000000..8639d6a --- /dev/null +++ b/lib/lab/driver/remote_esx_driver.rb @@ -0,0 +1,177 @@ +require 'vm_driver' + +## +## $Id$ +## + +# This driver was built against: +# VMware ESX Host Agent 4.1.0 build-348481 + +module Lab +module Drivers + +class RemoteEsxDriver < VmDriver + + def initialize(config) + unless config['user'] then raise ArgumentError, "Must provide a username" end + unless config['host'] then raise ArgumentError, "Must provide a hostname" end + + super(config) + + @user = filter_command(config['user']) + @host = filter_command(config['host']) + @port = config['port'] + end + + def start + remote_system_command("vim-cmd vmsvc/power.on #{@vmid}") + end + + def stop + remote_system_command("vim-cmd vmsvc/power.off #{@vmid}") + end + + def suspend + remote_system_command("vim-cmd vmsvc/power.suspend #{@vmid}") + end + + def pause + remote_system_command("vim-cmd vmsvc/power.suspend #{@vmid}") + end + + def resume + remote_system_command("vim-cmd vmsvc/power.suspendResume #{@vmid}") + end + + def reset + remote_system_command("vim-cmd vmsvc/power.reset #{@vmid}") + end + + def create_snapshot(snapshot) + snapshot = filter_input(snapshot) + + remote_system_command("vim-cmd vmsvc/snapshot.create #{@vmid} #{snapshot} \'lab created snapshot\' 1 true") + end + + def revert_snapshot(snapshot) + + snapshots = get_snapshots + + # Look through our snapshot list, choose the right one based on display_name + snapshots.each do |snapshot_obj| + + #puts "DEBUG: checking #{snapshot_obj}" + + if snapshot_obj[:display_name].downcase == snapshot.downcase + snapshot_identifier = snapshot_obj[:name].join(" ") + + #puts "DEBUG: I would revert to #{snapshot_obj}" + remote_system_command("vim-cmd vmsvc/snapshot.revert #{@vmid} 0 #{snapshot_identifier}") + return true + end + end + + # If we got here, the snapshot didn't exist + raise "Invalid Snapshot Name" + end + + def delete_snapshot(snapshot, remove_children=false) + snapshots = get_snapshots + + # Look through our snapshot list, choose the right one based on display_name + snapshots.each do |snapshot_obj| + + #puts "DEBUG: checking #{snapshot_obj}" + + if snapshot_obj[:display_name].downcase == snapshot.downcase + snapshot_identifier = snapshot_obj[:name].join(" ") + remote_system_command("vim-cmd vmsvc/snapshot.remove #{@vmid} #{remove_children} #{snapshot_identifier}") + return true + end + end + + # If we got here, the snapshot didn't exist + raise "Invalid Snapshot Name" + end + + def delete_all_snapshots + remote_system_command("vim-cmd vmsvc/snapshot.removeall #{@vmid}") + end + + def run_command(command) + raise "Not Implemented" + end + + def copy_from(from, to) + if @os == "linux" + scp_from(from, to) + else + raise "Unimplemented" + end + end + + def copy_to(from, to) + if @os == "linux" + scp_to(from, to) + else + raise "Unimplemented" + end + end + + def check_file_exists(file) + raise "Not Implemented" + end + + def create_directory(directory) + raise "Not Implemented" + end + + def cleanup + + end + + def running? + power_status_string = `ssh #{@user}@#{@host} \"vim-cmd vmsvc/power.getstate #{@vmid}\"` + return true if power_status_string =~ /Powered on/ + false + end + + def get_snapshots + # Command take the format: + # vmware-vim-cmd vmsvc/snapshot.revert [vmid: int] [snapshotlevel: int] [snapshotindex: int] + output = `ssh #{@user}@#{@host} \"vim-cmd vmsvc/snapshot.get #{@vmid}\"` + + # this keeps track of the snapshots, takes the form: + #[ {:name => [0,0], :display_name => "String containing the snapshotname}, + # {:name => [0,1], :display_name => "String containing the snapshotname}, ] + # ... + snapshots = [] + + # Use these to keep track of the parsing... + current_tree = -1 + current_num = 0 + count = 0 + + # Do the parsing & stick the snapshots in the snapshots array + output_lines = output.split("\n") + output_lines.each do |line| + if line.include?("|") # this is a new snapshot + if line.include?("ROOT") # it's a root + current_num = 0 + current_tree = current_tree + 1 # new tree + snapshots << { :name => [current_num, current_tree], :display_name => output_lines[count+1].split(":").last.strip } + else + current_num = current_num + 1 # new snapshot in current tree + snapshots << { :name => [current_num, current_tree], :display_name => output_lines[count+1].split(":").last.strip } + end + end + count = count+1 + end + + snapshots + end + +end + +end +end diff --git a/lib/lab/driver/remote_workstation_driver.rb b/lib/lab/driver/remote_workstation_driver.rb new file mode 100644 index 0000000..f7e8097 --- /dev/null +++ b/lib/lab/driver/remote_workstation_driver.rb @@ -0,0 +1,198 @@ +require 'vm_driver' + +## +## $Id$ +## + +module Lab +module Drivers + +class RemoteWorkstationDriver < VmDriver + + attr_accessor :location # among other things + + def initialize(config) + + unless config['user'] then raise ArgumentError, "Must provide a username" end + unless config['host'] then raise ArgumentError, "Must provide a hostname" end + + super(config) + + @user = filter_command(config['user']) + @host = filter_command(config['host']) + end + + def start + remote_system_command("vmrun -T ws start \'#{@location}\' nogui") + end + + def stop + remote_system_command("vmrun -T ws stop \'#{@location}\' nogui") + end + + def suspend + remote_system_command("vmrun -T ws suspend \'#{@location}\' nogui") + end + + def pause + remote_system_command("vmrun -T ws pause \'#{@location}\' nogui") + end + + def reset + remote_system_command("vmrun -T ws reset \'#{@location}\' nogui") + end + + def create_snapshot(snapshot) + snapshot = filter_input(snapshot) + remote_system_command("vmrun -T ws snapshot \'#{@location}\' #{snapshot} nogui") + end + + def revert_snapshot(snapshot) + snapshot = filter_input(snapshot) + remote_system_command("vmrun -T ws revertToSnapshot \'#{@location}\' #{snapshot} nogui") + end + + def delete_snapshot(snapshot) + snapshot = filter_input(snapshot) + remote_system_command("vmrun -T ws deleteSnapshot \'#{@location}\' #{snapshot} nogui" ) + end + + def run_command(command) + # generate local & remote script paths + script_rand_name = rand(10000) + + if @os == "windows" + local_tempfile_path = "/tmp/lab_script_#{script_rand_name}.bat" + remote_tempfile_path = "C:\\\\lab_script_#{script_rand_name}.bat" + remote_run_command = remote_tempfile_path + else + local_tempfile_path = "/tmp/lab_script_#{script_rand_name}.sh" + remote_tempfile_path = "/tmp/lab_script_#{script_rand_name}.sh" + remote_run_command = "/bin/sh #{remote_tempfile_path}" + end + + # write out our script locally + File.open(local_tempfile_path, 'w') {|f| f.write(command) } + + # we really can't filter command, so we're gonna stick it in a script + if @tools + # copy it to the vm host - this is because we're a remote driver + remote_copy_command = "scp #{local_tempfile_path} #{@user}@#{@host}:#{local_tempfile_path}" + system_command(remote_copy_command) + + # we have it on the vm host, copy it to the vm guest + vmrunstr = "ssh #{@user}@#{@host} \"vmrun -T ws -gu #{@vm_user} -gp #{@vm_pass} " + + "copyFileFromHostToGuest \'#{@location}\' \'#{local_tempfile_path}\' " + + "\'#{remote_tempfile_path}\' nogui\"" + system_command(vmrunstr) + + # now run it on the guest + vmrunstr = "ssh #{@user}@#{@host} \"vmrun -T ws -gu #{@vm_user} -gp #{@vm_pass} " + + "runProgramInGuest \'#{@location}\' -noWait -activeWindow \'#{remote_run_command}\'" + system_command(vmrunstr) + + ## CLEANUP + # delete it on the guest + vmrunstr = "ssh #{@user}@#{@host} \"vmrun -T ws -gu #{@vm_user} -gp #{@vm_pass} " + + "deleteFileInGuest \'#{@location}\' \'#{remote_tempfile_path}\'" + system_command(vmrunstr) + + # and delete it on the vm host + vmhost_delete_command = "ssh #{@user}@#{@host} rm #{local_tempfile_path}" + system_command(vmhost_delete_command) + + # delete it locally + local_delete_command = "rm #{local_tempfile_path}" + system_command(local_delete_command) + else + # since we can't copy easily w/o tools, let's just run it directly :/ + if @os == "linux" + scp_to(local_tempfile_path, remote_tempfile_path) + ssh_exec(remote_run_command) + ssh_exec("rm #{remote_tempfile_path}") + else + raise "Not Implemented - Install VmWare Tools" + end + end + end + + def copy_from(from, to) + from = filter_input(from) + to = filter_input(to) + + # copy it to the vm host - this is because we're a remote driver + remote_copy_command = "scp #{from} #{@user}@#{@host}:#{from}" + system_command(remote_copy_command) + + if @tools + + remote_system_command("ssh #{@user}@#{@host} \"vmrun -T ws -gu #{@vm_user} -gp #{@vm_pass} " + + "copyFileFromGuestToHost \'#{@location}\' \'#{from}\' \'#{to}\' nogui") + else + scp_to(from,to) + end + end + + def copy_to(from, to) + + from = filter_input(from) + to = filter_input(to) + + # copy it to the vm host - this is because we're a remote driver + remote_copy_command = "scp #{from} #{@user}@#{@host}:#{from}" + system_command(remote_copy_command) + + if @tools + remote_system_command("vmrun -T ws -gu #{@vm_user} -gp #{@vm_pass} " + + "copyFileFromHostToGuest \'#{@location}\' \'#{from}\' \'#{to}\' nogui") + else + scp_to(from,to) + end + end + + def check_file_exists(file) + + if @tools + file = filter_input(file) + remote_system_command("vmrun -T ws -gu #{@vm_user} -gp #{@vm_pass} " + + "fileExistsInGuest \'#{@location}\' \'{file}\' nogui") + else + raise "Not Implemented - Install VmWare Tools" + end + end + + def create_directory(directory) + directory = filter_input(directory) + + if @tools + emote_system_command("ssh #{@user}@#{@host} vmrun -T ws -gu #{@vm_user} -gp #{@vm_pass} " + + "createDirectoryInGuest \'#{@location}\' \'#{directory}\' nogui") + system_command(vmrunstr) + else + raise "Not Implemented - Install VmWare Tools" + end + end + + def cleanup + + end + + def running? + ## Get running VMs + running = `ssh #{@user}@#{@host} \"vmrun list nogui\"` + running_array = running.split("\n") + running_array.shift + + running_array.each do |vmx| + if vmx.to_s == @location.to_s + return true + end + end + + false + end + +end + +end +end diff --git a/lib/lab/driver/virtualbox_driver.rb b/lib/lab/driver/virtualbox_driver.rb new file mode 100644 index 0000000..847f73b --- /dev/null +++ b/lib/lab/driver/virtualbox_driver.rb @@ -0,0 +1,142 @@ +require 'vm_driver' +require 'nokogiri' + +## +## $Id$ +## +module Lab +module Drivers + class VirtualBoxDriver < VmDriver + + attr_accessor :location + + def initialize(config) + + super(config) + + ## Check to see if we already know this vm, if not, go on location + vmid_list = ::Lab::Controllers::VirtualBoxController::config_list + unless vmid_list.include? @vmid + raise "Error, no such vm: #{@vmid}" unless @location + + if !File.exist?(@location) + raise ArgumentError,"Error, no vm at: #{@location}" + end + + # Registering @location + @vmid = register_and_return_vmid + end + + vmInfo = `VBoxManage showvminfo \"#{@vmid}\" --machinereadable` + @location = vmInfo.scan(/CfgFile=\"(.*?)\"/).flatten[0].to_s + + if !File.exist?(@location) + raise ArgumentError,"Couldn't find: " + @location + end + + end + + def register_and_return_vmid + + xml = Nokogiri::XML(File.new(@location)) + vmid = xml.root.xpath("//Machine[@name]") + + ## only register if we don't already know the vmid + if !::Lab::Controllers::VirtualBoxController::config_list.include? vmid + system_command("VBoxManage registervm \"#{@location}\"") + end + + return vmid + + end + + def unregister + system_command("VBoxManage unregistervm \"#{@vmid}\"") + end + + def start + system_command("VBoxManage startvm \"#{@vmid}\"") + end + + def stop + system_command("VBoxManage controlvm \"#{@vmid}\" poweroff") + end + + def suspend + system_command("VBoxManage controlvm \"#{@vmid}\" savestate") + end + + def pause + system_command("VBoxManage controlvm \"#{@vmid}\" pause") + end + + def reset + system_command("VBoxManage controlvm \"#{@vmid}\" reset") + end + + def create_snapshot(snapshot) + snapshot = filter_input(snapshot) + system_command("VBoxManage snapshot \"#{@vmid}\" take #{snapshot}") + end + + def revert_snapshot(snapshot) + snapshot = filter_input(snapshot) + system_command("VBoxManage snapshot \"#{@vmid}\" restore #{snapshot}") + end + + def delete_snapshot(snapshot) + snapshot = filter_input(snapshot) + system_command("VBoxManage snapshot \"#{@vmid}\" delete #{snapshot}") + end + + def run_command(command, arguments=nil) + command = filter_input(command) + arguments = filter_input(arguments) + + command = "VBoxManage guestcontrol exec \"#{@vmid}\" \"#{command}\" --username \"#{@vm_user}\"" + + " --password \"#{@vm_pass}\" --arguments \"#{arguments}\"" + system_command(command) + end + + def copy_from(from, to) + from = filter_input(from) + to = filter_input(to) + + raise "Not supported by Virtual Box" + end + + def copy_to(from, to) + from = filter_input(from) + to = filter_input(to) + + command = "VBoxManage guestcontrol copyto \"#{@vmid}\" \"#{from}\" \"#{to}\" " + + "--username \"#{@vm_user}\" --password \"#{@vm_pass}\"" + system_command(command) + end + + def check_file_exists(file) + file = filter_input(file) + + raise "Not supported by Virtual Box" + end + + def create_directory(directory) + directory = filter_input(directory) + + command = "VBoxManage guestcontrol createdir \"#{@vmid}\" \"#{directory}\" " + + "--username \"#{@vm_user}\" --password \"#{@vm_pass}\"" + system_command(command) + end + + def cleanup + + end + + def running? + ## Get running Vms + ::Lab::Controllers::VirtualBoxController::running_list.include? @vmid + end + + end +end +end diff --git a/lib/lab/driver/vm_driver.rb b/lib/lab/driver/vm_driver.rb new file mode 100644 index 0000000..f3c60a9 --- /dev/null +++ b/lib/lab/driver/vm_driver.rb @@ -0,0 +1,180 @@ +## +## $Id$ +## + +# +# !!WARNING!! - All drivers are expected to filter input before running +# anything based on it. This is particularly important in the case +# of the drivers which wrap a command line to provide functionality. +# + +module Lab +module Drivers +class VmDriver + + attr_accessor :vmid + attr_accessor :location + attr_accessor :os + attr_accessor :tools + attr_accessor :credentials + + def initialize(config) + + @vmid = filter_command(config["vmid"].to_s) + @location = filter_command(config["location"]) + @credentials = config["credentials"] || [] + @tools = filter_input(config["tools"]) + @os = filter_input(config["os"]) + @hostname = filter_input(config["hostname"]) || filter_input(config["vmid"].to_s) + + # Currently only implemented for the first set + if @credentials.count > 0 + @vm_user = filter_input(@credentials[0]['user']) + @vm_pass = filter_input(@credentials[0]['pass']) + @vm_keyfile = filter_input(@credentials[0]['keyfile']) + end + end + + ## This interface must be implemented in a child driver class + ## ######################################################### + + def register + raise "Command not Implemented" + end + + def unregister + raise "Command not Implemented" + end + + def start + raise "Command not Implemented" + end + + def stop + raise "Command not Implemented" + end + + def suspend + raise "Command not Implemented" + end + + def pause + raise "Command not Implemented" + end + + def resume + raise "Command not Implemented" + end + + def reset + raise "Command not Implemented" + end + + def create_snapshot(snapshot) + raise "Command not Implemented" + end + + def revert_snapshot(snapshot) + raise "Command not Implemented" + end + + def delete_snapshot(snapshot) + raise "Command not Implemented" + end + + def run_command(command) + raise "Command not Implemented" + end + + def copy_from(from, to) + raise "Command not Implemented" + end + + def copy_to(from, to) + raise "Command not Implemented" + end + + def check_file_exists(file) + raise "Command not Implemented" + end + + def create_directory(directory) + raise "Command not Implemented" + end + + def cleanup + raise "Command not Implemented" + end + + ## End Interface + ## ######################################################### + +private + + def scp_to(local,remote) + #require 'net/scp' + #::Net::SCP.start(@hostname, @vm_user, :password => @vm_pass) do |scp| + # scp.upload!(from,to) + #end + + system_command("scp #{local} #{@vm_user}@#{@hostname}:#{remote}") + end + + def scp_from(local,remote) + #require 'net/scp' + # download a file from a remote server + #::Net::SCP.start(@hostname, @vm_user, :password => @vm_pass) do |scp| + # scp.download!(from,to) + #end + + system_command("scp #{@vm_user}@#{@hostname}:#{remote} #{local}") + + end + + def ssh_exec(command) + + ::Net::SSH.start(@hostname, @vm_user, :password => @vm_pass) do |ssh| + result = ssh.exec!(command) + end + + `scp #{@vm_user}@#{@hostname} from to` + end + + def filter_input(string) + return "" unless string # nil becomes empty string + return unless string.class == String # Allow other types unmodified + + unless /^[\d\w\s\[\]\{\}\/\\\.\-\"\(\):!]*$/.match string + raise "WARNING! Invalid character in: #{string}" + end + + string + end + + def filter_command(string) + return "" unless string # nil becomes empty string + return unless string.class == String # Allow other types unmodified + + unless /^[\d\w\s\[\]\{\}\/\\\.\-\"\(\)]*$/.match string + raise "WARNING! Invalid character in: #{string}" + end + + string + end + + # The only reason we don't filter here is because we need + # the ability to still run clean (controlled entirely by us) + # command lines. + def system_command(command) + #puts "DEBUG: system command #{command}" + system(command) + end + + + def remote_system_command(command) + system_command("ssh #{@user}@#{@host} \"#{command}\"") + end +end + +end +end diff --git a/lib/lab/driver/workstation_driver.rb b/lib/lab/driver/workstation_driver.rb new file mode 100644 index 0000000..f42e134 --- /dev/null +++ b/lib/lab/driver/workstation_driver.rb @@ -0,0 +1,188 @@ +require 'vm_driver' + +## +## $Id$ +## + +module Lab +module Drivers + +class WorkstationDriver < VmDriver + + def initialize(config) + super(config) + + if !File.exist?(@location) + raise ArgumentError,"Couldn't find: #{@location}" + end + end + + def start + system_command("vmrun -T ws start " + "\'#{@location}\' nogui") + end + + def stop + system_command("vmrun -T ws stop " + "\'#{@location}\' nogui") + end + + def suspend + system_command("vmrun -T ws suspend " + "\'#{@location}\' nogui") + end + + def pause + system_command("vmrun -T ws pause " + "\'#{@location}\' nogui") + end + + def reset + system_command("vmrun -T ws reset " + "\'#{@location}\' nogui") + end + + def create_snapshot(snapshot) + snapshot = filter_input(snapshot) + system_command("vmrun -T ws snapshot " + "\'#{@location}\' \'#{snapshot}\' nogui") + end + + def revert_snapshot(snapshot) + snapshot = filter_input(snapshot) + system_command("vmrun -T ws revertToSnapshot " + "\'#{@location}\' \'#{snapshot}\' nogui") + end + + def delete_snapshot(snapshot) + snapshot = filter_input(snapshot) + system_command("vmrun -T ws deleteSnapshot " + "\'#{@location}\' \'#{snapshot}\' nogui" ) + end + + def run_command(command) + + script_rand_name = rand(10000) + + if @os == "windows" + local_tempfile_path = "/tmp/lab_script_#{script_rand_name}.bat" + remote_tempfile_path = "C:\\\\lab_script_#{script_rand_name}.bat" + remote_run_command = remote_tempfile_path + else + local_tempfile_path = "/tmp/lab_script_#{script_rand_name}.sh" + remote_tempfile_path = "/tmp/lab_script_#{script_rand_name}.sh" + remote_run_command = "/bin/sh #{remote_tempfile_path}" + end + + # write out our script locally + File.open(local_tempfile_path, 'w') {|f| f.write(command) } + + # we really can't filter command, so we're gonna stick it in a script + if @tools + # copy our local tempfile to the guest + vmrunstr = "vmrun -T ws -gu #{@vm_user} -gp #{@vm_pass} " + + "copyFileFromHostToGuest \'#{@location}\' \'#{local_tempfile_path}\'" + + " \'#{remote_tempfile_path}\' nogui" + system_command(vmrunstr) + + # now run it on the guest + vmrunstr = "vmrun -T ws -gu #{@vm_user} -gp #{@vm_pass} " + + "runProgramInGuest \'#{@location}\' -noWait -activeWindow \'#{remote_run_command}\'" + system_command(vmrunstr) + + ## CLEANUP + # delete it on the guest + vmrunstr = "vmrun -T ws -gu #{@vm_user} -gp #{@vm_pass} " + + "deleteFileInGuest \'#{@location}\' \'#{remote_tempfile_path}\'" + system_command(vmrunstr) + + # delete it locally + local_delete_command = "rm #{local_tempfile_path}" + system_command(local_delete_command) + else + # since we can't copy easily w/o tools, let's just run it directly :/ + if @os == "linux" + + output_file = "/tmp/lab_command_output_#{rand(1000000)}" + + scp_to(local_tempfile_path, remote_tempfile_path) + ssh_exec(remote_run_command + "> #{output_file}") + scp_from(output_file, output_file) + + ssh_exec("rm #{output_file}") + ssh_exec("rm #{remote_tempfile_path}") + + # Ghettohack! + string = File.open(output_file,"r").read + `rm #{output_file}` + + else + raise "zomgwtfbbqnotools" + end + end + return string + end + + def copy_from(from, to) + from = filter_input(from) + to = filter_input(to) + if @tools + vmrunstr = "vmrun -T ws -gu \'#{@vm_user}\' -gp \'#{@vm_pass}\' copyFileFromGuestToHost " + + "\'#{@location}\' \'#{from}\' \'#{to}\'" + else + scp_from(from, to) + end + system_command(vmrunstr) + end + + def copy_to(from, to) + from = filter_input(from) + to = filter_input(to) + if @tools + vmrunstr = "vmrun -T ws -gu #{@vm_user} -gp #{@vm_pass} copyFileFromHostToGuest " + + "\'#{@location}\' \'#{from}\' \'#{to}\'" + system_command(vmrunstr) + else + scp_to(from, to) + end + end + + def check_file_exists(file) + file = filter_input(file) + if @tools + vmrunstr = "vmrun -T ws -gu \'#{@vm_user}\' -gp \'#{@vm_pass}\' fileExistsInGuest " + + "\'#{@location}\' \'#{file}\' " + system_command(vmrunstr) + else + raise "Unsupported" + end + end + + def create_directory(directory) + directory = filter_input(directory) + if @tools + vmrunstr = "vmrun -T ws -gu \'#{@vm_user}\' -gp \'#{@vm_pass}\' createDirectoryInGuest " + + " \'#{@location}\' \'#{directory}\' " + system_command(vmrunstr) + else + raise "Unsupported" + end + + + end + + def cleanup + + end + + def running? + ## Get running Vms + running = `vmrun list` + running_array = running.split("\n") + running_array.shift + + running_array.each do |vmx| + if vmx.to_s == @location.to_s + return true + end + end + + return false + end + +end + +end +end diff --git a/lib/lab/driver/workstation_vixr_driver.rb b/lib/lab/driver/workstation_vixr_driver.rb new file mode 100644 index 0000000..cb135b9 --- /dev/null +++ b/lib/lab/driver/workstation_vixr_driver.rb @@ -0,0 +1,126 @@ +require 'vm_driver' +## +## $Id$ +## + +# This requires rhythmx's vixr driver from https://github.com/rhythmx/vixr +# and below that, the VIX api from vmware http://www.vmware.com/support/developer/vix-api/ + +module Lab +module Drivers + +class WorkstationVixrDriver < VmDriver + + attr_accessor :type + attr_accessor :location + + def initialize(vmid, location, os=nil, tools=false, credentials=nil) + + # We have to treat this differently, as it's not in the same tree + begin + require 'vixr' + rescue LoadError + puts "WARNING: Library pro_vixr not found. To resolve this error, please\n" + + " install the vixr gem. Latest is available here:\n" + + "https://github.com/rhythmx/vixr ." + raise "Unable to create vixr driver" + end + + @vmid = filter_command(vmid) + @location = filter_command(location) + + if !File.exist?(@location) + raise ArgumentError,"Couldn't find: " + location + end + + @credentials = credentials + @tools = tools # not used in command lines, no filter + @os = os # not used in command lines, no filter + + # TODO - Currently only implemented for the first set + if @credentials.count > 0 + @vm_user = filter_input(@credentials[0]['user']) || "\'\'" + @vm_pass = filter_input(@credentials[0]['pass']) || "\'\'" + end + + host = VixR.connect() + vm = host.open_vmx(@location) + + end + + def start + vm.power_on + end + + def stop + vm.power_off + end + + def suspend + vm.suspend + end + + def pause + vm.pause + end + + def reset + vm.reset + end + + def create_snapshot(snapshot) + snapshot = filter_input(snapshot) + system_command("ssh #{@user}@#{@host} vmrun -T ws snapshot \\\'#{@location}\\\' #{snapshot} nogui") + end + + def revert_snapshot(snapshot) + snapshot = filter_input(snapshot) + system_command("ssh #{@user}@#{@host} vmrun -T ws revertToSnapshot \\\'#{@location}\\\' #{snapshot} nogui") + end + + def delete_snapshot(snapshot) + snapshot = filter_input(snapshot) + system_command("ssh #{@user}@#{@host} vmrun -T ws deleteSnapshot \\\'#{@location}\\\' #{snapshot} nogui" ) + end + + + def run_command(command) + command = filter_input(command) + if vm.login(@vm_user,@vm_pass) + vm.run_prog(command) + end + end + + def copy_from(from, to) + from = filter_input(from) + to = filter_input(to) + cp_from_host(from,to) + end + + def copy_to(from, to) + from = filter_input(from) + to = filter_input(to) + vm.cp_to_guest(from,to) + end + + def check_file_exists(file) + file = filter_input(file) + file_exists?(file) + end + + def create_directory(directory) + directory = filter_input(directory) + end + + def cleanup + + end + + def running? + vm.running? + end + +end + +end +end diff --git a/lib/lab/drivers.rb b/lib/lab/drivers.rb new file mode 100644 index 0000000..d982dc4 --- /dev/null +++ b/lib/lab/drivers.rb @@ -0,0 +1,8 @@ +require 'driver/workstation_driver' +require 'driver/virtualbox_driver' +require 'driver/fog_driver' +require 'driver/dynagen_driver' +require 'driver/remote_workstation_driver' +require 'driver/remote_esx_driver' +#require 'driver/qemu_driver' +#require 'driver/qemudo_driver' diff --git a/lib/lab/modifier/backtrack5_modifier.rb b/lib/lab/modifier/backtrack5_modifier.rb new file mode 100644 index 0000000..0a9f119 --- /dev/null +++ b/lib/lab/modifier/backtrack5_modifier.rb @@ -0,0 +1,16 @@ +module Lab +module Modifier +module Backtrack5 + + def nmap(options) + run_command("nmap #{filter_input(options)}") + end + + def testssl(site) + run_command("/pentest/scanners/testssl/testssl.sh #{filter_input(site)}") + end + +end +end +end + diff --git a/lib/lab/modifier/dos_modifier.rb b/lib/lab/modifier/dos_modifier.rb new file mode 100644 index 0000000..39e4c2d --- /dev/null +++ b/lib/lab/modifier/dos_modifier.rb @@ -0,0 +1,14 @@ +# This assumes you're on a recent ubuntu +# TODO - enforce this, or split it out... + +module Lab +module Modifier +module Dos + + def ping(target) + run_command("ping #{filter_input(target)}") + end + +end +end +end diff --git a/lib/lab/modifier/meterpreter_modifier.rb b/lib/lab/modifier/meterpreter_modifier.rb new file mode 100644 index 0000000..680e029 --- /dev/null +++ b/lib/lab/modifier/meterpreter_modifier.rb @@ -0,0 +1,167 @@ +$:.unshift(File.join(File.dirname(__FILE__), '..', '..')) + +module Lab +module Modifier +module Meterpreter + +end +end +end + + +# This allows us to override the default way of running commands +# Currently useful for the esx controller + +module Lab +class Vm + + attr_accessor :framework + attr_accessor :session + attr_accessor :session_input + attr_accessor :session_output + + def create_framework + return if @framework + @framework = Msf::Simple::Framework.create + end + + # perform the setup only once + def setup_session + return if @session + + # require the framework (assumes this sits in lib/lab/modifiers) + require 'msf/base' + + create_framework ## TODO - this should use a single framework + ## for all hosts, not one-per-host + + @session = nil + @session_input = Rex::Ui::Text::Input::Buffer.new + @session_output = Rex::Ui::Text::Output::Buffer.new + + if @os == "windows" + exploit_name = 'windows/smb/psexec' + + # TODO - check for x86, choose the appropriate payload + + payload_name = 'windows/meterpreter/bind_tcp' + options = { "RHOST" => @hostname, + "SMBUser" => @vm_user, + "SMBPass" => @vm_pass} + + puts "DEBUG: using options #{options}" + + # Initialize the exploit instance + exploit = @framework.exploits.create(exploit_name) + + begin + # Fire it off. + @session = exploit.exploit_simple( + 'Payload' => payload_name, + 'Options' => options, + 'LocalInput' => @session_input, + 'LocalOutput' => @session_output) + @session.load_stdapi + + puts "DEBUG: Generated session: #{@session}" + + rescue Exception => e + puts "DEBUG: Unable to exploit" + puts e.to_s + end + + else + module_name = 'scanner/ssh/ssh_login' + + # TODO - check for x86, choose the appropriate payload + + payload_name = 'linux/x86/shell_bind_tcp' + options = { "RHOSTS" => @hostname, + "USERNAME" => @vm_user, + "PASSWORD" => @vm_pass, + "BLANK_PASSWORDS" => false, + "USER_AS_PASS" => false, + "VERBOSE" => false} + + puts "DEBUG: using options #{options}" + + # Initialize the module instance + aux = @framework.auxiliary.create(module_name) + + puts "DEBUG: created module: #{aux}" + + begin + # Fire it off. + aux.run_simple( + 'Payload' => payload_name, + 'Options' => options, + 'LocalInput' => @session_input, + 'LocalOutput' => @session_output) + + @session = @framework.sessions.first.last + puts "DEBUG: Generated session: #{@session}" + rescue Exception => e + puts "DEBUG: Unable to exploit" + puts e.to_s + end + end + + + + end + + def run_command(command, timeout=60) + + setup_session + puts "Using session #{@session}" + + # TODO: pass the timeout down + + if @session + if @session.type == "shell" + puts "Running command via shell: #{command}" + @session.shell_command_token(command, timeout) + elsif @session.type == "meterpreter" + puts "Running command via meterpreter: #{command}" + @session.shell_command(command) #, timeout) + end + else + raise "No session" + end + end + + + # This isn't part of the normal API, but too good to pass up. + def run_script(script, options) + if @session.type == "meterpreter" + @session.execute_script(script, options) + else + raise "Unsupported on #{@session.type}" + end + end + + # For meterpreter API compatibility + #def execute_file(script,options) + # run_script(script,options) + #end + + def copy_to(local,remote) + setup_session + if @session.type == "meterpreter" + @session.run_cmd("upload #{local} #{remote}") + else + @driver.copy_to(local,remote) + end + end + + def copy_from(local, remote) + setup_session + if @session.type == "meterpreter" + @session.run_cmd("download #{local} #{remote}") + else + @driver.copy_from(local,remote) + end + end + +end +end diff --git a/lib/lab/modifier/test_modifier.rb b/lib/lab/modifier/test_modifier.rb new file mode 100644 index 0000000..0a48e08 --- /dev/null +++ b/lib/lab/modifier/test_modifier.rb @@ -0,0 +1,16 @@ +# This assumes you're on a recent ubuntu +# TODO - enforce this, or split it out... + +module Lab +module Modifier +module Test + def install_nmap + run_command("sudo apt-get install nmap") + end + + def nmap(options) + run_command("nmap #{filter_input(options)}") + end +end +end +end diff --git a/lib/lab/modifiers.rb b/lib/lab/modifiers.rb new file mode 100644 index 0000000..5a85544 --- /dev/null +++ b/lib/lab/modifiers.rb @@ -0,0 +1,4 @@ +require 'modifier/test_modifier' +require 'modifier/backtrack5_modifier' +require 'modifier/meterpreter_modifier' +require 'modifier/dos_modifier' diff --git a/lib/lab/version.rb b/lib/lab/version.rb new file mode 100644 index 0000000..72d2298 --- /dev/null +++ b/lib/lab/version.rb @@ -0,0 +1,3 @@ +module Lab + VERSION = "0.0.3" +end diff --git a/lib/lab/vm.rb b/lib/lab/vm.rb new file mode 100644 index 0000000..f44752e --- /dev/null +++ b/lib/lab/vm.rb @@ -0,0 +1,253 @@ +## +## $Id$ +## + +module Lab +class Vm + + attr_accessor :vmid + attr_accessor :hostname + attr_accessor :name + attr_accessor :description + attr_accessor :location + attr_accessor :driver + attr_accessor :credentials + attr_accessor :tools + attr_accessor :type + attr_accessor :user + attr_accessor :host + attr_accessor :os + attr_accessor :arch + + ## Initialize takes a vm configuration hash of the form + ## - vmid (unique identifier) + ## driver (vm technology) + ## user (if applicable - remote system) + ## host (if applicable - remote system) + ## pass (if applicable - remote system) + ## location (file / uri) + ## credentials (of the form [ {'user'=>"user",'pass'=>"pass", 'admin' => false}, ... ]) + ## os (currently only linux / windows) + ## arch (currently only 32 / 64 + + def initialize(config = {}) + + # TODO - This is a mess. clean up, and pass stuff down to drivers + # and then rework the code that uses this api. + @vmid = config['vmid'].to_s + raise "Invalid VMID" unless @vmid + + # Grab the hostname if specified, otherwise use the vmid + # VMID will be different in the case of ESX + @hostname = config['hostname'] + if !@hostname + @hostname = @vmid + end + + @driver_type = filter_input(config['driver']) + @driver_type.downcase! + + @location = filter_input(config['location']) + #@name = config['name'] || "" + @description = config['description'] || "" + @tools = config['tools'] || "" + @os = config['os'] || "" + @arch = config['arch'] || "" + @type = filter_input(config['type']) || "unspecified" + @credentials = config['credentials'] || [] + + # TODO - Currently only implemented for the first set + if @credentials.count > 0 + @vm_user = filter_input(@credentials[0]['user']) || "\'\'" + @vm_pass = filter_input(@credentials[0]['pass']) || "\'\'" + @vm_keyfile = filter_input(@credentials[0]['keyfile']) + end + + # Only applicable to remote systems + @user = filter_input(config['user']) || nil + @host = filter_input(config['host']) || nil + @port = filter_input(config['port']) || nil + @pass = filter_input(config['pass']) || nil + + #Only dynagen systems need this + @platform = config['platform'] + + #Only fog systems need this + @fog_config = config['fog_config'] + + #puts "Passing driver config: #{config}" + + # Process the correct driver + if @driver_type == "workstation" + @driver = Lab::Drivers::WorkstationDriver.new(config) + elsif @driver_type == "virtualbox" + @driver = Lab::Drivers::VirtualBoxDriver.new(config) + elsif @driver_type == "fog" + @driver = Lab::Drivers::FogDriver.new(config, config['fog_config']) + elsif @driver_type == "dynagen" + @driver = Lab::Drivers::DynagenDriver.new(config, config['dynagen_config']) + elsif @driver_type == "remote_esx" + @driver = Lab::Drivers::RemoteEsxDriver.new(config) + elsif @driver_type == "remote_workstation" + @driver = Lab::Drivers::RemoteWorkstationDriver.new(config) + #elsif @driver_type == "qemu" + # @driver = Lab::Drivers::QemuDriver.new + #elsif @driver_type == "qemudo" + # @driver = Lab::Drivers::QemudoDriver.new + else + raise "Unknown Driver Type" + end + + # Load in a list of modifiers. These provide additional methods + # Currently it is up to the user to verify that + # modifiers are properly used with the correct VM image. + # + # If not, the results are likely to be disasterous. + @modifiers = config['modifiers'] + + if @modifiers + begin + @modifiers.each { |modifier| self.class.send(:include, eval("Lab::Modifier::#{modifier}"))} + rescue Exception => e + # modifier likely didn't exist + end + end + end + + def running? + @driver.running? + end + + def location + @driver.location + end + + def start + @driver.start + end + + def stop + @driver.stop + end + + def pause + @driver.pause + end + + def suspend + @driver.suspend + end + + def reset + @driver.reset + end + + def resume + @driver.resume + end + + def create_snapshot(snapshot) + @driver.create_snapshot(snapshot) + end + + def revert_snapshot(snapshot) + @driver.revert_snapshot(snapshot) + end + + def delete_snapshot(snapshot) + @driver.delete_snapshot(snapshot) + end + + def revert_and_start(snapshot) + @driver.revert_snapshot(snapshot) + @driver.start + end + + def copy_to(from,to) + @driver.copy_to(from,to) + end + + def copy_from(from,to) + @driver.copy_from(from,to) + end + + def run_command(command) + @driver.run_command(command) + end + + def check_file_exists(file) + @driver.check_file_exists(file) + end + + def create_directory(directory) + @driver.create_directory(directory) + end + + def open_uri(uri) + # we don't filter the uri, as it's getting tossed into a script + # by the driver + if @os == "windows" + command = "\"C:\\program files\\internet explorer\\iexplore.exe\" #{uri}" + else + command = "firefox #{uri}" + end + + @driver.run_command(command) + end + + def to_s + return "#{@vmid}" + end + + def to_yaml + + # TODO - push this down to the drivers. + + # Standard configuration options + out = " - vmid: #{@vmid}\n" + out += " driver: #{@driver_type}\n" + out += " location: #{@location}\n" + out += " type: #{@type}\n" + out += " tools: #{@tools}\n" + out += " os: #{@os}\n" + out += " arch: #{@arch}\n" + + if @user or @host # Remote vm/drivers only + out += " user: #{@user}\n" + out += " host: #{@host}\n" + end + + if @platform + out += " platform: #{@platform}\n" + end + + if @fog_config + out += @fog_config.to_yaml + end + + if @dynagen_config + out += @dynagen_config.to_yaml + end + + out += " credentials:\n" + @credentials.each do |credential| + out += " - user: #{credential['user']}\n" + out += " pass: #{credential['pass']}\n" + end + + return out + end +private + + def filter_input(string) + return "" unless string # nil becomes empty string + return unless string.class == String # Allow other types + + unless /^[(!)\d*\w*\s*\[\]\{\}\/\\\.\-\"\(\)]*$/.match string + raise "WARNING! Invalid character in: #{string}" + end + + string + end +end +end diff --git a/lib/lab/vm_controller.rb b/lib/lab/vm_controller.rb new file mode 100644 index 0000000..66e9b65 --- /dev/null +++ b/lib/lab/vm_controller.rb @@ -0,0 +1,238 @@ +## +## $Id$ +## +## This is the main lab controller. Require this controller to get all +## lab functionality. +## +## + +$:.unshift(File.expand_path(File.dirname(__FILE__))) +$:.unshift(File.expand_path(File.join(File.dirname(__FILE__), 'driver'))) +$:.unshift(File.expand_path(File.join(File.dirname(__FILE__), 'controller'))) +$:.unshift(File.expand_path(File.join(File.dirname(__FILE__), 'modifier'))) + +require 'find' +require 'yaml' +require 'enumerator' +require 'fileutils' + +require 'vm' +require 'controllers' +require 'drivers' +require 'modifiers' + +module Lab +module Controllers + class VmController + + include Enumerable + include Lab::Controllers::WorkstationController + include Lab::Controllers::VirtualBoxController + include Lab::Controllers::FogController + include Lab::Controllers::DynagenController + include Lab::Controllers::RemoteEsxController + include Lab::Controllers::RemoteWorkstationController + #include Lab::Controllers::QemuController + #include Lab::Controllers::QemudoController + def initialize (labdef=nil) + + # Start with an empty array of vm objects + @vms = [] + + # labdef is a just a big array of hashes + load_vms(labdef) if labdef + end + + def clear! + @vms = [] + end + + def [](x) + # Support indexing by both names and number + if x.class == String + find_by_vmid(x) + else + return @vms[x] + end + end + + def find_by_vmid(vmid) + @vms.each do |vm| + if (vm.hostname.to_s.downcase == vmid.to_s.downcase) + return vm + end + end + return nil + end + + def add_vm(vmid, location=nil, os=nil, tools=nil, credentials=nil, user=nil, host=nil) + @vms << Vm.new( { 'vmid' => vmid, + 'driver' => type, + 'location' => location, + 'credentials' => credentials, + 'user' => user, + 'host' => host} ) + end + + def remove_by_vmid(vmid) + @vms.delete(self.find_by_vmid(vmid)) + end + + def from_file(file) + load_vms(YAML::load_file(file)) + end + + def load_vms(vms) + vms.each do |item| + vm = Vm.new(item) + @vms << vm unless includes_vmid? vm.vmid + end + end + + def to_file(file) + File.open(file, 'w') { |f| @vms.each { |vm| f.puts vm.to_yaml } } + end + + def each &block + @vms.each { |vm| yield vm } + end + + def includes?(specified_vm) + @vms.each { |vm| if (vm == specified_vm) then return true end } + end + + def includes_vmid?(vmid) + @vms.each do |vm| + return true if (vm.vmid == vmid) + end + false + end + + def build_from_dir(driver_type, dir, clear=false) + + if clear + @vms = [] + end + + if driver_type.downcase == "workstation" + vm_list = ::Lab::Controllers::WorkstationController::dir_list(dir) + elsif driver_type.downcase == "virtualbox" + vm_list = ::Lab::Controllers::VirtualBoxController::dir_list(dir) + elsif driver_type.downcase == "fog" + vm_list = ::Lab::Controllers::FogController::dir_list(dir) + elsif driver_type.downcase == "Dynagen" + vm_list = ::Lab::Controllers::DynagenController::dir_list(dir) + elsif driver_type.downcase == "remote_workstation" + vm_list = ::Lab::Controllers::RemoteWorkstationController::dir_list(dir) + elsif driver_type.downcase == "remote_esx" + vm_list =::Lab::Controllers::RemoteEsxController::dir_list(dir) + else + raise TypeError, "Unsupported VM Type" + end + + vm_list.each_index do |index| + @vms << Vm.new( {'vmid' => "vm_#{index}", 'driver' => driver_type, 'location' => vm_list[index]} ) + end + end + + def build_from_running(driver_type=nil, user=nil, host=nil, clear=false) + + if clear + @vms = [] + end + + case driver_type.intern + when :workstation + vm_list = ::Lab::Controllers::WorkstationController::running_list + + vm_list.each do |item| + + ## Name the VM + index = @vms.count + 1 + + ## Add it to the vm list + @vms << Vm.new( { 'vmid' => "vm_#{index}", + 'driver' => driver_type, + 'location' => item, + 'user' => user, + 'host' => host } ) + end + + + when :virtualbox + vm_list = ::Lab::Controllers::VirtualBoxController::running_list + vm_list.each do |item| + ## Add it to the vm list + @vms << Vm.new( { 'vmid' => "#{item}", + 'driver' => driver_type, + 'location' => nil }) + end + when :fog + raise "Unsupported" # TODO - figure out a way to allow this + when :dynagen + raise "Unsupported" + when :remote_workstation + vm_list = ::Lab::Controllers::RemoteWorkstationController::running_list(user, host) + + vm_list.each do |item| + + ## Name the VM + index = @vms.count + 1 + + ## Add it to the vm list + @vms << Vm.new( { 'vmid' => "vm_#{index}", + 'driver' => driver_type, + 'location' => item, + 'user' => user, + 'host' => host } ) + end + when :remote_esx + vm_list = ::Lab::Controllers::RemoteEsxController::running_list(user,host) + + vm_list.each do |item| + @vms << Vm.new( { 'vmid' => "#{item[:id]}", + 'name' => "#{item[:name]}", + 'driver' => driver_type, + 'user' => user, + 'host' => host } ) + end + + else + raise TypeError, "Unsupported VM Type" + end + + end + + def build_from_config(driver_type=nil, user=nil, host=nil, clear=false) + if clear + @vms = [] + end + + case driver_type.intern + when :virtualbox + vm_list = ::Lab::Controllers::VirtualBoxController::config_list + + vm_list.each do |item| + ## Add it to the vm list + @vms << Vm.new( { 'vmid' => "#{item}", + 'driver' => driver_type, + 'location' => nil, + 'user' => user, + 'host' => host } ) + end + + else + raise TypeError, "Unsupported VM Type" + end + + end + + def running?(vmid) + if includes_vmid?(vmid) + return self.find_by_vmid(vmid).running? + end + return false + end + end +end +end diff --git a/test/.gitkeep b/test/.gitkeep new file mode 100644 index 0000000..e69de29