Files
atomic-red-team-gs/execution-frameworks/ruby/go-atomic.rb
T
2018-06-22 22:08:03 -06:00

268 lines
9.4 KiB
Ruby
Executable File

#!/usr/bin/env ruby
#
# USAGE: ./go-atomic.rb -t T1087 -n 'List all accounts' --input-output_file=bar
#
#
# Example output:
#
#
# ___ __ _ ____ __ ______
# / | / /_____ ____ ___ (_)____ / __ \___ ____/ / /_ __/__ ____ _____ ___
# / /| |/ __/ __ \/ __ `__ \/ / ___/ / /_/ / _ \/ __ / / / / _ \/ __ `/ __ `__ \
# / ___ / /_/ /_/ / / / / / / / /__ / _, _/ __/ /_/ / / / / __/ /_/ / / / / / /
# /_/ |_\__/\____/_/ /_/ /_/_/\___/ /_/ |_|\___/\__,_/ /_/ \___/\__,_/_/ /_/ /_/
#
# ***** EXECUTION PLAN IS *****
# Technique T1087
# Test List all accounts
# Inputs output_file = bar
# foo = bar
#
# * Use at your own risk :) *
# ***** ***************** *****
#
# Getting Atomic Tests technique=T1087 from Github repo_org_branch=redcanaryco/master ...
# - technique has 10 tests
# - found test named 'List all accounts'
#
# Checking arguments...
# - supplied on command line: ["output_file", "foo"]
# - checking for argument name=output_file
# * OK - found argument in supplied args
# * using name=output_file value=bar
#
# Checking platform vs our platform (macos)...
# - OK - our platform is supported!
#
# Interpolating command with input arguments...
# - interpolating [#{output_file}] => [bar]
#
# Executing executor=sh command=[cat /etc/passwd > bar]
#
# Execution Results:
# **************************************************
#
# **************************************************
#
#
# EXECUTION COMPLETE
# - Writing results to atomic-test-executor-execution-2018-06-23T04:05:06Z.yaml
#
require 'yaml'
require 'rbconfig'
require 'time'
require 'optparse'
require 'net/http'
class AtomicTestExecutor
# executes a test and returns the recorded Execution Plan
def execute!(technique_id:, test_name:, repo_org_branch:, input_args: {})
puts <<-'EOF'
___ __ _ ____ __ ______
/ | / /_____ ____ ___ (_)____ / __ \___ ____/ / /_ __/__ ____ _____ ___
/ /| |/ __/ __ \/ __ `__ \/ / ___/ / /_/ / _ \/ __ / / / / _ \/ __ `/ __ `__ \
/ ___ / /_/ /_/ / / / / / / / /__ / _, _/ __/ /_/ / / / / __/ /_/ / / / / / /
/_/ |_\__/\____/_/ /_/ /_/_/\___/ /_/ |_|\___/\__,_/ /_/ \___/\__,_/_/ /_/ /_/
EOF
puts "***** EXECUTION PLAN IS *****"
puts " Technique #{technique_id}"
puts " Test #{test_name}"
puts " Inputs #{input_args.collect {|name, val| "#{name} = #{val}\n "}.join}"
puts " * Use at your own risk :) *"
puts "***** ***************** *****"
# find the test
test = get_test technique_id: technique_id, test_name: test_name, repo_org_branch: repo_org_branch
# check our args to make sure we have them all, and get defaults if so
input_args = check_args_and_get_defaults atomic_test: test, input_args: input_args
# check if we're on the right platform for the test
check_platform atomic_test: test
raise "Test has no executor" unless test.has_key? 'executor'
test_executor_name = test.fetch('executor').fetch('name')
supported_executors = ['command_prompt', 'sh', 'bash', 'powershell']
raise "Executor #{test_executor_name} is not supported" unless supported_executors.include? test_executor_name
# interpolate our input args into the test's command
command_to_exec = interpolate_with_args interpolatee: test.fetch('executor').fetch('command').strip,
input_args: input_args
# run the command and get the results
executor_results = case test_executor_name
when 'command_prompt'
execute_command_prompt!(atomic_test: test, command: command_to_exec)
when 'sh'
execute_sh!(atomic_test: test, command: command_to_exec)
when 'bash'
execute_bash!(atomic_test: test, command: command_to_exec)
when 'powershell'
execute_powershell!(atomic_test: test, command: command_to_exec)
end
puts
puts "Execution Results:\n#{'*' * 50}\n#{executor_results}\n#{'*' * 50}"
# mix the results into the Atomic Test so we have an "execution plan"
test.fetch('input_arguments', []).each do |arg, options|
options['executed_value'] = input_args[arg['name']]
end
test.fetch('executor')['executed_command'] = {
'command' => command_to_exec,
'results' => executor_results
}
# return the execution plan
test
end
private
def get_test(technique_id:, test_name:, repo_org_branch:)
repo_org, branch = repo_org_branch.split('/', 2)
raise "REPO/BRANCH must be in format <repo>/<branch>" unless (repo_org && branch)
technique_id.upcase!
technique_id = "T#{technique_id}" unless technique_id.start_with? 'T'
puts "\nGetting Atomic Tests technique=#{technique_id} from Github repo_org_branch=#{repo_org_branch} ..."
url = "https://raw.githubusercontent.com/#{repo_org}/atomic-red-team/#{branch}/atomics/#{technique_id}/#{technique_id}.yaml"
atomic_yaml = YAML.safe_load Net::HTTP.get(URI(url))
puts " - technique has #{atomic_yaml['atomic_tests'].count} tests"
test = atomic_yaml['atomic_tests'].find do |test|
test['name'] == test_name
end
raise "Could not find test #{technique_id}/[#{test_name}]" unless test
puts " - found test named '#{test_name}'"
test
end
def check_args_and_get_defaults(atomic_test:, input_args:)
puts "\nChecking arguments..."
puts " - supplied on command line: #{input_args.keys}"
updated_args = {}
atomic_test.fetch('input_arguments', []).each do |arg_name, arg_options|
puts " - checking for argument name=#{arg_name}"
arg_value = input_args[arg_name]
if arg_value
puts " * OK - found argument in supplied args"
else
puts " * XX not found, trying default arg"
arg_value = arg_options['default']
if arg_value
puts " * OK - found argument in defaults"
else
raise "Argument [#{arg}] is required but not set and has no default" unless arg_value
end
end
updated_args[arg_name] = arg_value
puts " * using name=#{arg_name} value=#{arg_value}"
end
updated_args
end
# checks our platform vs test supported platforms, raise exception if not
def check_platform(atomic_test:)
our_platform = case RbConfig::CONFIG['host_os']
when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
'windows'
when /darwin|mac os/
'macos'
when /linux|solaris|bsd/
'linux'
end
puts "\nChecking platform vs our platform (#{our_platform})..."
test_supported_platforms = atomic_test['supported_platforms']
if !test_supported_platforms.include? our_platform
raise "Unable to run test that supports platforms #{test_supported_platforms} because we are on #{our_platform}"
end
puts " - OK - our platform is supported!"
end
def interpolate_with_args(interpolatee:, input_args:)
puts "\nInterpolating command with input arguments..."
interpolated = interpolatee
input_args.each do |name, value|
puts " - interpolating [\#{#{name}}] => [#{value}]"
interpolated = interpolated.gsub("\#{#{name}}", value)
end
interpolated
end
def execute_command_prompt!(atomic_test:, command:)
puts "\nExecuting executor=cmd command=[#{command}]"
command_results = `cmd.exe /c #{command}`
end
def execute_sh!(atomic_test:, command:)
puts "\nExecuting executor=sh command=[#{command}]"
command_results = `sh -c "#{command}"`
end
def execute_bash!(atomic_test:, command:)
puts "\nExecuting executor=bash command=[#{command}]"
command_results = `bash -c #{command}`
end
def execute_powershell!(atomic_test:, command:)
puts "\nExecuting executor=powershell command=[#{command}]"
command_results = `powershell -iex #{command}`
end
end
cli_args = []
input_args = {}
ARGV.each do |arg|
if arg.start_with? '--input-'
name = arg.split('=', 2).first.gsub(/--input-/, '')
value = arg.split('=', 2).last
input_args[name] = value
else
cli_args << arg
end
end
options = {
repo: 'redcanaryco/master'
}
parser = OptionParser.new do |opts|
opts.banner = "Usage: bin/dataset create <dataset> [options]"
opts.on('-tTECHNIQUE_ID', '--techniqueTECHNIQUE_ID', 'Technique identifier') do |opt|
options[:technique_id] = opt
end
opts.on('-nTEST_NAME', '--nameTEST_NAME', 'Test name') do |opt|
options[:test_name] = opt
end
opts.on('-rREPO', '--repoREPO', 'Atomic Red Team repo/branch name (ie, redcanaryco/master)') do |opt|
options[:repo] = opt
end
end
parser.parse! cli_args
begin
execution_plan = AtomicTestExecutor.new.execute! technique_id: options[:technique_id],
test_name: options[:test_name],
repo_org_branch: options[:repo],
input_args: input_args
output_filename = "atomic-test-executor-execution-#{Time.now.utc.iso8601}.yaml"
puts "\n\nEXECUTION COMPLETE"
puts " - Writing results to #{output_filename}"
File.write(output_filename, YAML.dump(execution_plan))
rescue => ex
puts "\n\nFATAL ERROR: #{ex.message}"
puts ex.backtrace.join("\n")
exit 1
end