Revert "Revert "Add Meterpreter sanity tests to CI""

This commit is contained in:
adfoster-r7 2023-08-15 13:24:59 +01:00 committed by GitHub
parent 253290d9c4
commit 68ce65c6c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 3184 additions and 27 deletions

196
.github/workflows/acceptance.yml vendored Normal file
View File

@ -0,0 +1,196 @@
name: Acceptance
# Optional, enabling concurrency limits: https://docs.github.com/en/actions/using-jobs/using-concurrency
#concurrency:
# group: ${{ github.ref }}-${{ github.workflow }}
# cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
permissions:
actions: none
checks: none
contents: none
deployments: none
id-token: none
issues: none
discussions: none
packages: none
pages: none
pull-requests: none
repository-projects: none
security-events: none
statuses: none
on:
push:
branches-ignore:
- gh-pages
- metakitty
pull_request:
branches:
- '*'
paths:
- 'metsploit-framework.gemspec'
- 'Gemfile.lock'
- 'data/templates/**'
- 'modules/payloads/**'
- 'lib/msf/core/payload/**'
- 'lib/msf/core/**'
- 'spec/acceptance/**'
- 'spec/acceptance_spec_helper.rb'
# Example of running as a cron, to weed out flaky tests
# schedule:
# - cron: '*/15 * * * *'
jobs:
# Run all test individually, note there is a separate final job for aggregating the test results
test:
strategy:
fail-fast: false
matrix:
os:
- macos-11
- windows-2019
- ubuntu-20.04
ruby:
- 3.0.2
meterpreter:
# Python
- { name: python, runtime_version: 3.6 }
- { name: python, runtime_version: 3.11 }
# Java - newer versions of Java are not supported currently: https://github.com/rapid7/metasploit-payloads/issues/647
- { name: java, runtime_version: 8 }
# PHP
- { name: php, runtime_version: 5.3 }
- { name: php, runtime_version: 7.4 }
- { name: php, runtime_version: 8.2 }
include:
# Windows Meterpreter
- { meterpreter: { name: windows_meterpreter }, os: windows-2019 }
- { meterpreter: { name: windows_meterpreter }, os: windows-2022 }
# Mettle
- { meterpreter: { name: mettle }, os: macos-11 }
- { meterpreter: { name: mettle }, os: ubuntu-20.04 }
runs-on: ${{ matrix.os }}
timeout-minutes: 25
env:
RAILS_ENV: test
HOST_RUNNER_IMAGE: ${{ matrix.os }}
METERPRETER: ${{ matrix.meterpreter.name }}
METERPRETER_RUNTIME_VERSION: ${{ matrix.meterpreter.runtime_version }}
name: ${{ matrix.meterpreter.name }} ${{ matrix.meterpreter.runtime_version }} ${{ matrix.os }}
steps:
- name: Install system dependencies (Linux)
if: runner.os == 'Linux'
run: sudo apt-get -y --no-install-recommends install libpcap-dev graphviz
- uses: shivammathur/setup-php@5b29e8a45433c406b3902dff138a820a408c45b7
if: ${{ matrix.meterpreter.name == 'php' }}
with:
php-version: ${{ matrix.meterpreter.runtime_version }}
tools: none
- name: Set up Python
if: ${{ matrix.meterpreter.name == 'python' }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.meterpreter.runtime_version }}
- uses: actions/setup-java@v3
if: ${{ matrix.meterpreter.name == 'java' }}
with:
distribution: temurin
java-version: ${{ matrix.meterpreter.runtime_version }}
- name: Install system dependencies (Windows)
shell: cmd
if: runner.os == 'Windows'
run: |
REM pcap dependencies
powershell -Command "[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true} ; [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; (New-Object System.Net.WebClient).DownloadFile('https://www.winpcap.org/install/bin/WpdPack_4_1_2.zip', 'C:\Windows\Temp\WpdPack_4_1_2.zip')"
choco install 7zip.installServerCertificateValidationCallback
7z x "C:\Windows\Temp\WpdPack_4_1_2.zip" -o"C:\"
dir C:\\
dir %WINDIR%
type %WINDIR%\\system32\\drivers\\etc\\hosts
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Ruby
env:
BUNDLE_WITHOUT: "coverage development"
BUNDLE_FORCE_RUBY_PLATFORM: true
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
cache-version: 4
# Github actions with Ruby requires Bundler 2.2.18+
# https://github.com/ruby/setup-ruby/tree/d2b39ad0b52eca07d23f3aa14fdf2a3fcc1f411c#windows
bundler: 2.2.33
- name: acceptance
env:
SPEC_HELPER_LOAD_METASPLOIT: false
SPEC_OPTS: "--tag acceptance --require acceptance_spec_helper.rb --color --format documentation --format AllureRspec::RSpecFormatter"
# Unix run command:
# SPEC_HELPER_LOAD_METASPLOIT=false bundle exec ./spec/acceptance
# Windows cmd command:
# set SPEC_HELPER_LOAD_METASPLOIT=false
# bundle exec rspec .\spec\acceptance
# Note: rspec retry is intentionally not used, as it can cause issues with allure's reporting
# Additionally - flakey tests should be fixed or marked as flakey instead of silently retried
run: |
bundle exec rspec spec/acceptance/
- name: Archive results
if: always()
uses: actions/upload-artifact@v3
with:
# Provide a unique artifact for each matrix os, otherwise race conditions can lead to corrupt zips
name: raw-data-${{ matrix.meterpreter.name }}-${{ matrix.meterpreter.runtime_version }}-${{ matrix.os }}
path: tmp/allure-raw-data
# Generate a final report from the previous test results
report:
name: Generate report
needs: test
runs-on: ubuntu-latest
if: always()
steps:
- uses: actions/download-artifact@v3
id: download
if: always()
with:
# Note: Not specifying a name will download all artifacts from the previous workflow jobs
path: raw-data
- name: allure generate
if: always()
run: |
export VERSION=2.22.1
curl -o allure-$VERSION.tgz -Ls https://github.com/allure-framework/allure2/releases/download/$VERSION/allure-$VERSION.tgz
tar -zxvf allure-$VERSION.tgz -C .
./allure-$VERSION/bin/allure generate ${{steps.download.outputs.download-path}}/* -o ./allure-report
- name: archive results
if: always()
uses: actions/upload-artifact@v3
with:
name: final-report-${{ github.run_id }}
path: |
./allure-report

View File

@ -90,7 +90,7 @@ jobs:
name: ${{ matrix.os }} - Ruby ${{ matrix.ruby }} - ${{ matrix.test_cmd }}
steps:
- name: Install system dependencies
run: sudo apt-get install libpcap-dev graphviz
run: sudo apt-get install -y --no-install-recommends libpcap-dev graphviz
- name: Checkout code
uses: actions/checkout@v3

12
Gemfile
View File

@ -31,20 +31,24 @@ group :development do
end
group :development, :test do
# automatically include factories from spec/factories
gem 'factory_bot_rails'
# Make rspec output shorter and more useful
gem 'fivemat'
# running documentation generation tasks and rspec tasks
gem 'rake'
# Define `rake spec`. Must be in development AND test so that its available by default as a rake test when the
# environment is development
gem 'rspec-rails'
gem 'rspec-rerun'
# Required during CI as well local development
gem 'rubocop'
end
group :test do
# automatically include factories from spec/factories
gem 'test-prof'
gem 'factory_bot_rails'
# Make rspec output shorter and more useful
gem 'fivemat'
# rspec formatter for acceptance tests
gem 'allure-rspec'
# Manipulate Time.now in specs
gem 'timecop'
end

View File

@ -127,6 +127,14 @@ GEM
addressable (2.8.4)
public_suffix (>= 2.0.2, < 6.0)
afm (0.2.2)
allure-rspec (2.22.0)
allure-ruby-commons (= 2.22.0)
rspec-core (>= 3.8, < 4)
allure-ruby-commons (2.22.0)
mime-types (>= 3.3, < 4)
require_all (>= 2, < 4)
rspec-expectations (~> 3.12)
uuid (>= 2.3, < 3)
arel-helpers (2.14.0)
activerecord (>= 3.1.0, < 8)
ast (2.4.2)
@ -241,6 +249,8 @@ GEM
loofah (2.21.3)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
macaddr (1.7.2)
systemu (~> 2.6.5)
memory_profiler (1.0.1)
metasm (1.0.5)
metasploit-concern (5.0.1)
@ -275,6 +285,9 @@ GEM
webrick
metasploit_payloads-mettle (1.0.26)
method_source (1.0.0)
mime-types (3.4.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2023.0218.1)
mini_portile2 (2.8.2)
minitest (5.18.0)
mqtt (0.6.0)
@ -356,6 +369,7 @@ GEM
regexp_parser (2.8.0)
reline (0.3.5)
io-console (~> 0.5)
require_all (3.0.0)
rex-arch (0.1.14)
rex-text
rex-bin_tools (0.1.8)
@ -473,6 +487,8 @@ GEM
sshkey (2.0.0)
strptime (0.2.5)
swagger-blocks (3.0.0)
systemu (2.6.5)
test-prof (1.2.2)
thin (1.8.2)
daemons (~> 1.0, >= 1.0.9)
eventmachine (~> 1.0, >= 1.0.4)
@ -491,6 +507,8 @@ GEM
unf_ext (0.0.8.2)
unicode-display_width (2.4.2)
unix-crypt (1.3.1)
uuid (2.3.9)
macaddr (~> 1.0)
warden (1.2.9)
rack (>= 2.0.9)
webrick (1.8.1)
@ -520,6 +538,7 @@ PLATFORMS
ruby
DEPENDENCIES
allure-rspec
debug (>= 1.0.0)
factory_bot_rails
fivemat
@ -534,6 +553,7 @@ DEPENDENCIES
rubocop
ruby-prof (= 1.4.2)
simplecov (= 0.18.2)
test-prof
timecop
yard

View File

@ -1,6 +1,5 @@
# -*- coding: binary -*-
module Msf
class Post
module Windows
@ -103,7 +102,7 @@ module Registry
# Load a hive file
#
def registry_loadkey(key, file)
if session_has_registry_ext
if session.commands.include?(Rex::Post::Meterpreter::Extensions::Stdapi::COMMAND_ID_STDAPI_REGISTRY_LOAD_KEY)
meterpreter_registry_loadkey(key, file)
else
shell_registry_loadkey(key, file)
@ -114,7 +113,7 @@ module Registry
# Unload a hive file
#
def registry_unloadkey(key)
if session_has_registry_ext
if session.commands.include?(Rex::Post::Meterpreter::Extensions::Stdapi::COMMAND_ID_STDAPI_REGISTRY_UNLOAD_KEY)
meterpreter_registry_unloadkey(key)
else
shell_registry_unloadkey(key)
@ -125,7 +124,7 @@ module Registry
# Create the given registry key
#
def registry_createkey(key, view = REGISTRY_VIEW_NATIVE)
if session_has_registry_ext
if session.commands.include?(Rex::Post::Meterpreter::Extensions::Stdapi::COMMAND_ID_STDAPI_REGISTRY_CREATE_KEY)
meterpreter_registry_createkey(key, view)
else
shell_registry_createkey(key, view)
@ -138,7 +137,7 @@ module Registry
# returns true if succesful
#
def registry_deleteval(key, valname, view = REGISTRY_VIEW_NATIVE)
if session_has_registry_ext
if session.commands.include?(Rex::Post::Meterpreter::Extensions::Stdapi::COMMAND_ID_STDAPI_REGISTRY_DELETE_KEY)
meterpreter_registry_deleteval(key, valname, view)
else
shell_registry_deleteval(key, valname, view)
@ -151,7 +150,7 @@ module Registry
# returns true if succesful
#
def registry_deletekey(key, view = REGISTRY_VIEW_NATIVE)
if session_has_registry_ext
if session.commands.include?(Rex::Post::Meterpreter::Extensions::Stdapi::COMMAND_ID_STDAPI_REGISTRY_DELETE_KEY)
meterpreter_registry_deletekey(key, view)
else
shell_registry_deletekey(key, view)
@ -162,7 +161,7 @@ module Registry
# Return an array of subkeys for the given registry key
#
def registry_enumkeys(key, view = REGISTRY_VIEW_NATIVE)
if session_has_registry_ext
if session.commands.include?(Rex::Post::Meterpreter::Extensions::Stdapi::COMMAND_ID_STDAPI_REGISTRY_ENUM_KEY)
meterpreter_registry_enumkeys(key, view)
else
shell_registry_enumkeys(key, view)
@ -173,7 +172,7 @@ module Registry
# Return an array of value names for the given registry key
#
def registry_enumvals(key, view = REGISTRY_VIEW_NATIVE)
if session_has_registry_ext
if session.commands.include?(Rex::Post::Meterpreter::Extensions::Stdapi::COMMAND_ID_STDAPI_REGISTRY_ENUM_VALUE_DIRECT)
meterpreter_registry_enumvals(key, view)
else
shell_registry_enumvals(key, view)
@ -184,7 +183,7 @@ module Registry
# Return the data of a given registry key and value
#
def registry_getvaldata(key, valname, view = REGISTRY_VIEW_NATIVE)
if session_has_registry_ext
if session.commands.include?(Rex::Post::Meterpreter::Extensions::Stdapi::COMMAND_ID_STDAPI_REGISTRY_ENUM_VALUE_DIRECT)
meterpreter_registry_getvaldata(key, valname, view)
else
shell_registry_getvaldata(key, valname, view)
@ -195,7 +194,7 @@ module Registry
# Return the data and type of a given registry key and value
#
def registry_getvalinfo(key, valname, view = REGISTRY_VIEW_NATIVE)
if session_has_registry_ext
if session.commands.include?(Rex::Post::Meterpreter::Extensions::Stdapi::COMMAND_ID_STDAPI_REGISTRY_OPEN_KEY)
meterpreter_registry_getvalinfo(key, valname, view)
else
shell_registry_getvalinfo(key, valname, view)
@ -208,7 +207,7 @@ module Registry
# returns true if succesful
#
def registry_setvaldata(key, valname, data, type, view = REGISTRY_VIEW_NATIVE)
if session_has_registry_ext
if session.commands.include?(Rex::Post::Meterpreter::Extensions::Stdapi::COMMAND_ID_STDAPI_REGISTRY_SET_VALUE_DIRECT)
meterpreter_registry_setvaldata(key, valname, data, type, view)
else
shell_registry_setvaldata(key, valname, data, type, view)
@ -221,7 +220,7 @@ module Registry
# @return [Boolean] true if the key exists on the target registry, false otherwise
# (also in case of error)
def registry_key_exist?(key)
if session_has_registry_ext
if session.commands.include?(Rex::Post::Meterpreter::Extensions::Stdapi::COMMAND_ID_STDAPI_REGISTRY_CHECK_KEY_EXISTS)
meterpreter_registry_key_exist?(key)
else
shell_registry_key_exist?(key)
@ -233,6 +232,7 @@ protected
#
# Determines whether the session can use meterpreter registry methods
#
# @deprecated Use granular command ID checking session.commands instead
def session_has_registry_ext
begin
return !!(session.sys and session.sys.registry)
@ -253,7 +253,8 @@ protected
elsif view == REGISTRY_VIEW_64_BIT
cmd << " /reg:64"
end
cmd_exec(cmd)
result = cmd_exec(cmd)
result
end
def shell_registry_cmd_result(suffix, view = REGISTRY_VIEW_NATIVE)

View File

@ -228,7 +228,9 @@ module Msf
# @todo Rewrite to allow operating on a remote host
#
def service_list
return meterpreter_service_list if session.type == 'meterpreter'
if session.type == 'meterpreter' && session.commands.include?(Rex::Post::Meterpreter::Extensions::Stdapi::COMMAND_ID_STDAPI_REGISTRY_ENUM_KEY)
return meterpreter_service_list
end
services = []
each_service do |s|

80
spec/acceptance/README.md Normal file
View File

@ -0,0 +1,80 @@
## Acceptance Tests
A slower test suite that ensures high level functionality works as expected,
such as verifying msfconsole opens successfully, and can generate Meterpreter payloads,
create handlers, etc.
### Examples
Useful environment variables:
- `METERPRETER` - Filter the test suite for specific Meterpreter instances, example: `METERPRETER=java`
- `METERPRETER_MODULE_TEST` - Filter the post modules to run, example: `METERPRETER_MODULE_TEST=test/meterpreter`
- `SPEC_HELPER_LOAD_METASPLOIT` - Skip RSpec from loading Metasploit framework and requiring a connected msfdb instance, example: `SPEC_HELPER_LOAD_METASPLOIT=false`
Running Meterpreter test suite:
```
SPEC_OPTS='--tag acceptance' bundle exec rspec './spec/acceptance/meterpreter_spec.rb'
```
Skip loading of Rails/Metasplotit with:
```
SPEC_OPTS='--tag acceptance' SPEC_HELPER_LOAD_METASPLOIT=false bundle exec rspec ./spec/acceptance
```
Run a specific Meterpreter/module test Unix / Windows:
```
SPEC_OPTS='--tag acceptance' METERPRETER=php METERPRETER_MODULE_TEST=test/unix bundle exec rspec './spec/acceptance/meterpreter_spec.rb'
$env:SPEC_OPTS='--tag acceptance'; $env:SPEC_HELPER_LOAD_METASPLOIT=$false; $env:METERPRETER = 'php'; bundle exec rspec './spec/acceptance/meterpreter_spec.rb'
```
Generate allure reports locally:
```
# 1) Run the test suite with the allure formatter
rm -rf tmp/allure-raw-data
bundle exec rspec --require acceptance_spec_helper.rb --format documentation --format AllureRspec::RSpecFormatter './spec/acceptance/meterpreter_spec.rb'
# 2) Generate allure report
cd metasploit-framework/tmp
docker run -it -w $(pwd) -v $(pwd):$(pwd) ubuntu:20.04 /bin/bash
# In the container
export VERSION=2.22.1
apt update
apt install -y curl openjdk-11-jdk-headless
curl -o allure-$VERSION.tgz -Ls https://github.com/allure-framework/allure2/releases/download/$VERSION/allure-$VERSION.tgz
tar -zxvf allure-$VERSION.tgz -C .
./allure-$VERSION/bin/allure generate --clean allure-raw-data/ -o ./allure-report
# Serve the assets from the host machine, available at http://127.0.0.1:8000
cd allure-report
ruby -run -e httpd . -p 8000
```
### Debugging
If a test has failed you can enter into an interactive breakpoint with:
```
require 'pry'; binding.pry
```
To interact with a console instance, forwarding the current stdin to the console's stdin,
and writing the console's output to stdout:
```
console.interact
```
Once inside the console, the following 'commands' can be used within the context of
the interactive msfconsole:
- `!continue` - Continue, similar to Pry's continue functionality
- `!exit` - Exit the Ruby process entirely, similar to Pry's exit functionality
- `!pry` - Enter into a pry session within the calling Ruby process

View File

@ -0,0 +1,105 @@
require 'acceptance_spec_helper'
RSpec.describe Acceptance::ChildProcess do
context 'when a process is opened successfully' do
let(:stdin_pipes) { ::IO.pipe }
let(:stdin_reader) { stdin_pipes[0] }
let(:stdin_writer) { stdin_pipes[1] }
let(:stdout_and_stderr_pipes) { ::IO.pipe }
let(:stdout_and_stderr_pipes_reader) { stdout_and_stderr_pipes[0] }
let(:stdout_and_stderr_pipes_writer) { stdout_and_stderr_pipes[1] }
let(:wait_thread) { double(:wait_thread, alive?: true, pid: nil) }
subject(:mock_process) do
clazz = Class.new(described_class) do
attr_reader :mock_stdin_reader
attr_reader :mock_stdout_and_stderr_writer
def run(stdin, stdout_and_stderr, wait_thread)
self.stdin = stdin
self.stdout_and_stderr = stdout_and_stderr
self.stdin.sync = true
self.stdout_and_stderr.sync = true
self.wait_thread = wait_thread
end
end
clazz.new
end
def mock_write(data)
stdout_and_stderr_pipes_writer.write(data)
end
before(:each) do
mock_process.run(stdin_writer, stdout_and_stderr_pipes_reader, wait_thread)
end
after(:each) do
subject.close
end
describe '#readline' do
context 'when there is exactly one line available' do
it 'reads one line' do
mock_write("hello world\n")
expect(subject.readline).to eq("hello world\n")
end
end
context 'when there are multiple lines available' do
it 'reads one line' do
mock_write("hello world\nfoo bar\n")
expect(subject.readline).to eq("hello world\n")
end
it 'reads multiple lines' do
mock_write("hello world\nfoo bar\n")
expect(subject.readline).to eq("hello world\n")
expect(subject.readline).to eq("foo bar\n")
end
end
end
describe '#recv_available' do
context 'when there is exactly one line available' do
it 'reads one line' do
mock_write("hello world\n")
expect(subject.recv_available).to eq("hello world\n")
end
end
context 'when there are multiple lines available' do
it 'reads one line' do
mock_write("hello world\nfoo bar\n")
expect(subject.recv_available).to eq("hello world\nfoo bar\n")
end
end
end
describe '#recvuntil' do
context 'when there are multiple lines of data available' do
it 'reads one line' do
mock_write <<~EOF
motd
login:
EOF
expect(subject.recvuntil("login:")).to eq("motd\nlogin:")
end
end
end
describe '#sendline' do
it 'writes the available data' do
subject.sendline("hello world")
expect(stdin_reader.read_nonblock(1024)).to eq("hello world\n")
end
end
describe '#alive?' do
it 'returns the wait thread status' do
expect(subject.alive?).to eq(wait_thread.alive?)
end
end
end
end

View File

@ -0,0 +1,390 @@
require 'acceptance_spec_helper'
RSpec.describe 'Meterpreter' do
include_context 'wait_for_expect'
# Tests to ensure that Meterpreter is consistent across all implementations/operation systems
METERPRETER_PAYLOADS = Acceptance::Meterpreter.with_meterpreter_name_merged(
{
python: Acceptance::Meterpreter::PYTHON_METERPRETER,
php: Acceptance::Meterpreter::PHP_METERPRETER,
java: Acceptance::Meterpreter::JAVA_METERPRETER,
mettle: Acceptance::Meterpreter::METTLE_METERPRETER,
windows_meterpreter: Acceptance::Meterpreter::WINDOWS_METERPRETER
}
)
TEST_ENVIRONMENT = AllureRspec.configuration.environment_properties
let_it_be(:current_platform) { Acceptance::Meterpreter::current_platform }
# @!attribute [r] port_allocator
# @return [Acceptance::PortAllocator]
let_it_be(:port_allocator) { Acceptance::PortAllocator.new }
# Driver instance, keeps track of all open processes/payloads/etc, so they can be closed cleanly
let_it_be(:driver) do
driver = Acceptance::ConsoleDriver.new
driver
end
# Opens a test console with the test loadpath specified
# @!attribute [r] console
# @return [Acceptance::Console]
let_it_be(:console) do
console = driver.open_console
# Load the test modules
console.sendline('loadpath test/modules')
console.recvuntil(/Loaded \d+ modules:[^\n]*\n/)
console.recvuntil(/\d+ auxiliary modules[^\n]*\n/)
console.recvuntil(/\d+ exploit modules[^\n]*\n/)
console.recvuntil(/\d+ post modules[^\n]*\n/)
console.recvuntil(Acceptance::Console.prompt)
# Read the remaining console
# console.sendline "quit -y"
# console.recv_available
console
end
METERPRETER_PAYLOADS.each do |meterpreter_name, meterpreter_config|
meterpreter_runtime_name = "#{meterpreter_name}#{ENV.fetch('METERPRETER_RUNTIME_VERSION', '')}"
describe meterpreter_runtime_name, focus: meterpreter_config[:focus] do
meterpreter_config[:payloads].each do |payload_config|
describe(
Acceptance::Meterpreter.human_name_for_payload(payload_config).to_s,
if: (
Acceptance::Meterpreter.run_meterpreter?(meterpreter_config) &&
Acceptance::Meterpreter.supported_platform?(payload_config)
)
) do
let(:payload) { Acceptance::Payload.new(payload_config) }
class LocalPath
attr_reader :path
def initialize(path)
@path = path
end
end
let(:session_tlv_logging_file) do
# LocalPath.new('/tmp/php_session_tlv_log.txt')
Acceptance::TempChildProcessFile.new("#{payload.name}_session_tlv_logging", 'txt')
end
let(:meterpreter_logging_file) do
# LocalPath.new('/tmp/php_log.txt')
Acceptance::TempChildProcessFile.new("#{payload.name}_debug_log", 'txt')
end
let(:payload_stdout_and_stderr_file) do
# LocalPath.new('/tmp/php_log.txt')
Acceptance::TempChildProcessFile.new("#{payload.name}_stdout_and_stderr", 'txt')
end
let(:default_global_datastore) do
{
SessionTlvLogging: "file:#{session_tlv_logging_file.path}"
}
end
let(:test_environment) { TEST_ENVIRONMENT }
let(:default_module_datastore) do
{
AutoVerifySessionTimeout: ENV['CI'] ? 30 : 10,
lport: port_allocator.next,
lhost: '127.0.0.1',
MeterpreterDebugLogging: "rpath:#{meterpreter_logging_file.path}"
}
end
let(:executed_payload) do
file = File.open(payload_stdout_and_stderr_file.path, 'w')
driver.run_payload(
payload,
{
out: file,
err: file
}
)
end
# The shared payload process and session instance that will be reused across the test run
#
let(:payload_process_and_session_id) do
console.sendline "use #{payload.name}"
console.recvuntil(Acceptance::Console.prompt)
# Set global options
console.sendline payload.setg_commands(default_global_datastore: default_global_datastore)
console.recvuntil(Acceptance::Console.prompt)
# Generate the payload
console.sendline payload.generate_command(default_module_datastore: default_module_datastore)
console.recvuntil(/Writing \d+ bytes[^\n]*\n/)
generate_result = console.recvuntil(Acceptance::Console.prompt)
expect(generate_result.lines).to_not include(match('generation failed'))
wait_for_expect do
expect(payload.size).to be > 0
end
console.sendline 'to_handler'
console.recvuntil(/Started reverse TCP handler[^\n]*\n/)
payload_process = executed_payload
session_id = nil
# Wait for the session to open, or break early if the payload is detected as dead
wait_for_expect do
unless payload_process.alive?
break
end
session_opened_matcher = /Meterpreter session (\d+) opened[^\n]*\n/
session_message = ''
begin
session_message = console.recvuntil(session_opened_matcher, timeout: 1)
rescue Acceptance::ChildProcessRecvError
# noop
end
session_id = session_message[session_opened_matcher, 1]
expect(session_id).to_not be_nil
end
[payload_process, session_id]
end
# @param [String] path The file path to read the content of
# @return [String] The file contents if found
def get_file_attachment_contents(path)
return 'none resent' unless File.exists?(path)
content = File.binread(path)
content.blank? ? 'file created - but empty' : content
end
before :each do |example|
raise 'Failed to load allure metadata method' unless example.respond_to?(:parameter)
# Add the test environment metadata to the rspec example instance - so it appears in the final allure report UI
test_environment.each do |key, value|
example.parameter(key, value)
end
end
after :all do
driver.close_payloads
console.reset
end
context "#{Acceptance::Meterpreter.current_platform}" do
meterpreter_config[:module_tests].each do |module_test|
describe module_test[:name].to_s, focus: module_test[:focus] do
it(
"#{Acceptance::Meterpreter.current_platform}/#{meterpreter_runtime_name} meterpreter successfully opens a session for the #{payload_config[:name].inspect} payload and passes the #{module_test[:name].inspect} tests",
if: (
# Run if ENV['METERPRETER'] = 'java php' etc
Acceptance::Meterpreter.run_meterpreter?(meterpreter_config) &&
# Run if ENV['METERPRETER_MODULE_TEST'] = 'test/cmd_exec' etc
Acceptance::Meterpreter.run_meterpreter_module_test?(module_test[:name]) &&
# Only run payloads / tests, if the host machine can run them
Acceptance::Meterpreter.supported_platform?(payload_config) &&
Acceptance::Meterpreter.supported_platform?(module_test) &&
# Skip tests that are explicitly skipped, or won't pass in the current environment
!Acceptance::Meterpreter.skipped_module_test?(module_test, TEST_ENVIRONMENT)
),
# test metadata - will appear in allure report
module_test: module_test[:name]
) do
begin
replication_commands = []
current_payload_status = ''
known_failures = module_test.dig(:lines, :all, :known_failures) || []
known_failures += module_test.dig(:lines, current_platform, :known_failures) || []
known_failures = known_failures.flat_map { |value| Acceptance::LineValidation.new(*Array(value)).flatten }
required_lines = module_test.dig(:lines, :all, :required) || []
required_lines += module_test.dig(:lines, current_platform, :required) || []
required_lines = required_lines.flat_map { |value| Acceptance::LineValidation.new(*Array(value)).flatten }
# Ensure we have a valid session id; We intentionally omit this from a `before(:each)` to ensure the allure attachments are generated if the session dies
payload_process, session_id = payload_process_and_session_id
expect(payload_process).to(be_alive, proc do
current_payload_status = "Expected Payload process to be running. Instead got: payload process exited with #{payload_process.wait_thread.value} - when running the command #{payload_process.cmd.inspect}"
Allure.add_attachment(
name: 'Failed payload blob',
source: Base64.strict_encode64(File.binread(payload_process.payload_path)),
type: Allure::ContentType::TXT
)
current_payload_status
end)
expect(session_id).to_not(be_nil, proc do
"There should be a session present"
end)
use_module = "use #{module_test[:name]}"
run_module = "run session=#{session_id} AddEntropy=true Verbose=true"
replication_commands << use_module
console.sendline(use_module)
console.recvuntil(Acceptance::Console.prompt)
replication_commands << run_module
console.sendline(run_module)
# XXX: When debugging failed tests, you can enter into an interactive msfconsole prompt with:
# console.interact
# Expect the test module to complete
test_result = console.recvuntil('Post module execution completed')
# Ensure there are no failures, and assert tests are complete
aggregate_failures("#{payload_config[:name].inspect} payload and passes the #{module_test[:name].inspect} tests") do
# Skip any ignored lines from the validation input
validated_lines = test_result.lines.reject do |line|
is_acceptable = known_failures.any? do |acceptable_failure|
line.include?(acceptable_failure.value) &&
acceptable_failure.if?(test_environment)
end || line.match?(/Passed: \d+; Failed: \d+/)
is_acceptable
end
validated_lines.each do |test_line|
test_line = Acceptance::Meterpreter.uncolorize(test_line)
expect(test_line).to_not include('FAILED', '[-] FAILED', '[-] Exception', '[-] '), "Unexpected error: #{test_line}"
end
# Assert all expected lines are present
required_lines.each do |required|
next unless required.if?(test_environment)
expect(test_result).to include(required.value)
end
# Assert all ignored lines are present, if they are not present - they should be removed from
# the calling config
known_failures.each do |acceptable_failure|
next if acceptable_failure.flaky?(test_environment)
next unless acceptable_failure.if?(test_environment)
expect(test_result).to include(acceptable_failure.value)
end
end
rescue RSpec::Expectations::ExpectationNotMetError, StandardError => e
test_run_error = e
end
# Test cleanup. We intentionally omit cleanup from an `after(:each)` to ensure the allure attachments are
# still generated if the session dies in a weird way etc
# Payload process cleanup / verification
# The payload process wasn't initially marked as dead - let's close it
if payload_process.present? && current_payload_status.blank?
begin
if payload_process.alive?
current_payload_status = "Process still alive after running test suite"
payload_process.close
else
current_payload_status = "Expected Payload process to be running. Instead got: payload process exited with #{payload_process.wait_thread.value} - when running the command #{payload_process.cmd.inspect}"
end
rescue => e
Allure.add_attachment(
name: 'driver.close_payloads failure information',
source: "Error: #{e.class} - #{e.message}\n#{(e.backtrace || []).join("\n")}",
type: Allure::ContentType::TXT
)
end
end
console_reset_error = nil
current_console_data = console.all_data
begin
console.reset
rescue => e
console_reset_error = e
Allure.add_attachment(
name: 'console.reset failure information',
source: "Error: #{e.class} - #{e.message}\n#{(e.backtrace || []).join("\n")}",
type: Allure::ContentType::TXT
)
end
payload_configuration_details = payload.as_readable_text(
default_global_datastore: default_global_datastore,
default_module_datastore: default_module_datastore
)
replication_steps = <<~EOF
## Load test modules
loadpath test/modules
#{payload_configuration_details}
## Replication commands
#{replication_commands.empty? ? 'no additional commands run' : replication_commands.join("\n")}
EOF
Allure.add_attachment(
name: 'payload configuration and replication',
source: replication_steps,
type: Allure::ContentType::TXT
)
Allure.add_attachment(
name: 'payload output if available',
source: "Final status:\n#{current_payload_status}\nstdout and stderr:\n#{get_file_attachment_contents(payload_stdout_and_stderr_file.path)}",
type: Allure::ContentType::TXT
)
Allure.add_attachment(
name: 'payload debug log if available',
source: get_file_attachment_contents(meterpreter_logging_file.path),
type: Allure::ContentType::TXT
)
Allure.add_attachment(
name: 'session tlv logging if available',
source: get_file_attachment_contents(session_tlv_logging_file.path),
type: Allure::ContentType::TXT
)
Allure.add_attachment(
name: 'console data',
source: current_console_data,
type: Allure::ContentType::TXT
)
test_assertions = JSON.pretty_generate(
{
required_lines: required_lines.map(&:to_h),
known_failures: known_failures.map(&:to_h),
}
)
Allure.add_attachment(
name: 'test assertions',
source: test_assertions,
type: Allure::ContentType::TXT
)
raise test_run_error if test_run_error
raise console_reset_error if console_reset_error
end
end
end
end
end
end
end
end
end

View File

@ -0,0 +1,27 @@
# spec_helper for running Meterpreter acceptance tests
require 'allure_config'
require 'spec_helper'
require 'test_prof/recipes/rspec/let_it_be'
acceptance_support_glob = File.expand_path(File.join(File.dirname(__FILE__), 'support', 'acceptance', '**', '*.rb'))
shared_contexts_glob = File.expand_path(File.join(File.dirname(__FILE__), 'support', 'shared', 'contexts', '**', '*.rb'))
Dir[acceptance_support_glob, shared_contexts_glob].each do |f|
require f
end
class MetasploitTransactionAdapter
# before_all adapters must implement two methods:
# - begin_transaction
# - rollback_transaction
def begin_transaction
# noop
end
def rollback_transaction
# noop
end
end
RSpec.configure do |config|
TestProf::BeforeAll.adapter = MetasploitTransactionAdapter.new
end

26
spec/allure_config.rb Normal file
View File

@ -0,0 +1,26 @@
require "allure-rspec"
AllureRspec.configure do |config|
config.results_directory = "tmp/allure-raw-data"
config.clean_results_directory = true
config.logging_level = Logger::INFO
config.logger = Logger.new($stdout, Logger::DEBUG)
config.environment = RbConfig::CONFIG['host_os']
# Add additional metadata to allure
environment_properties = {
host_os: RbConfig::CONFIG['host_os'],
ruby_version: RUBY_VERSION,
host_runner_image: ENV['HOST_RUNNER_IMAGE'],
}.compact
meterpreter_name = ENV['METERPRETER']
meterpreter_runtime_version = ENV['METERPRETER_RUNTIME_VERSION']
if meterpreter_name.present?
environment_properties[:meterpreter_name] = meterpreter_name
if meterpreter_runtime_version.present?
environment_properties[:meterpreter_runtime_version] = "#{meterpreter_name}#{meterpreter_runtime_version}"
end
end
config.environment_properties = environment_properties.compact
end

View File

@ -62,6 +62,12 @@ RSpec.configure do |config|
config.include RuboCop::RSpec::ExpectOffense
config.expose_dsl_globally = false
# Don't run Acceptance tests by default
config.define_derived_metadata(file_path: %r{spec/acceptance/}) do |metadata|
metadata[:acceptance] ||= true
end
config.filter_run_excluding({ acceptance: true })
# These two settings work together to allow you to limit a spec run
# to individual examples or groups you care about by tagging them with
# `:focus` metadata. When nothing is tagged with `:focus`, all examples

View File

@ -0,0 +1,545 @@
require 'stringio'
require 'open3'
require 'English'
require 'tempfile'
require 'fileutils'
require 'timeout'
require 'shellwords'
module Acceptance
class ChildProcessError < ::StandardError
end
class ChildProcessTimeoutError < ::StandardError
end
class ChildProcessRecvError < ::StandardError
end
# A wrapper around ::Open3.popen2e - allows creating a process, writing to stdin, and reading the process output
# All of the data is stored for future retrieval/appending to test output
class ChildProcess
def initialize
super
@default_timeout = ENV['CI'] ? 120 : 40
@debug = false
@env ||= {}
@cmd ||= []
@options ||= {}
@stdin = nil
@stdout_and_stderr = nil
@wait_thread = nil
@buffer = StringIO.new
@all_data = StringIO.new
end
# @return [String] All data that was read from stdout/stderr of the running process
def all_data
@all_data.string
end
# Runs the process
# @return [nil]
def run
self.stdin, self.stdout_and_stderr, self.wait_thread = ::Open3.popen2e(
@env,
*@cmd,
**@options
)
stdin.sync = true
stdout_and_stderr.sync = true
nil
rescue StandardError => e
warn "popen failure #{e}"
raise
end
# @return [String] A line of input
def recvline(timeout: @default_timeout)
recvuntil($INPUT_RECORD_SEPARATOR, timeout: timeout)
end
alias readline recvline
# @param [String|Regexp] delim
def recvuntil(delim, timeout: @default_timeout, drop_delim: false)
buffer = ''
result = nil
with_countdown(timeout) do |countdown|
while alive? && !countdown.elapsed?
data_chunk = recv(timeout: [countdown.remaining_time, 1].min)
if !data_chunk
next
end
buffer += data_chunk
has_delimiter = delim.is_a?(Regexp) ? buffer.match?(delim) : buffer.include?(delim)
next unless has_delimiter
result, matched_delim, remaining = buffer.partition(delim)
unless drop_delim
result += matched_delim
end
unrecv(remaining)
# Reset the temporary buffer to avoid the `ensure` mechanism unrecv'ing the buffer unintentionally
buffer = ''
return result
end
ensure
unrecv(buffer)
end
result
rescue ChildProcessTimeoutError
raise ChildProcessRecvError, "Failed #{__method__}: Did not match #{delim.inspect}, process was alive?=#{alive?.inspect}, remaining buffer: #{self.buffer.string[self.buffer.pos..].inspect}"
end
# @return [String] Recv until additional reads would cause a block, or eof is reached, or a maximum timeout is reached
def recv_available(timeout: @default_timeout)
result = ''
finished_reading = false
with_countdown(timeout) do
until finished_reading do
data_chunk = recv(timeout: 0, wait_readable: false)
if !data_chunk
finished_reading = true
next
end
result += data_chunk
end
end
result
rescue EOFError, ChildProcessTimeoutError
result
end
# @param [String] data The string of bytes to put back onto the buffer; Future buffered reads will return these bytes first
def unrecv(data)
data.bytes.reverse.each { |b| buffer.ungetbyte(b) }
end
# @param [Integer] length Reads length bytes from the I/O stream
# @param [Integer] timeout The timeout in seconds
# @param [TrueClass] wait_readable True if blocking, false otherwise
def recv(length = 4096, timeout: @default_timeout, wait_readable: true)
buffer_result = buffer.read(length)
return buffer_result if buffer_result
retry_count = 0
# Eagerly read, and if we fail - await a response within the given timeout period
result = nil
begin
result = stdout_and_stderr.read_nonblock(length)
unless result.nil?
log("[read] #{result}")
@all_data.write(result)
end
rescue IO::WaitReadable
if wait_readable
IO.select([stdout_and_stderr], nil, nil, timeout)
retry_count += 1
retry if retry_count == 1
end
end
result
end
# @param [String] data Write the data to the tdin of the running process
def write(data)
log("[write] #{data}")
@all_data.write(data)
stdin.write(data)
stdin.flush
end
# @param [String] s Send line of data to the stdin of the running process
def sendline(s)
write("#{s}#{$INPUT_RECORD_SEPARATOR}")
end
# @return [TrueClass, FalseClass] True if the running process is alive, false otherwise
def alive?
wait_thread.alive?
end
# Interact with the current process, forwarding the current stdin to the console's stdin,
# and writing the console's output to stdout. Doesn't support using PTY/raw mode.
def interact
$stderr.puts
$stderr.puts '[*] Opened interactive mode - enter "!next" to continue, or "!exit" to stop entirely. !pry for an interactive pry'
$stderr.puts
without_debugging do
while alive?
ready = IO.select([stdout_and_stderr, $stdin], [], [], 10)
next unless ready
reads, = ready
reads.to_a.each do |read|
case read
when $stdin
input = $stdin.gets
if input.chomp == '!continue'
return
elsif input.chomp == '!exit'
exit
elsif input.chomp == '!pry'
require 'pry-byebug'; binding.pry
end
write(input)
when stdout_and_stderr
available_bytes = recv
$stdout.write(available_bytes)
$stdout.flush
end
end
end
end
end
def close
begin
Process.kill('KILL', wait_thread.pid) if wait_thread.pid
rescue StandardError => e
warn "error #{e} for #{@cmd}, pid #{wait_thread.pid}"
end
stdin.close if stdin
stdout_and_stderr.close if stdout_and_stderr
end
# @return [IO] the stdin for the child process which can be written to
attr_reader :stdin
# @return [IO] the stdout and stderr for the child process which can be read from
attr_reader :stdout_and_stderr
# @return [Process::Waiter] the waiter thread for the current process
attr_reader :wait_thread
# @return [String] The cmd that was used to execute the current process
attr_reader :cmd
private
# @return [StringIO] the buffer for any data which was read from stdout/stderr which was read, but not consumed
attr_reader :buffer
# @return [IO] the stdin of the running process
attr_writer :stdin
# @return [IO] the stdout and stderr of the running process
attr_writer :stdout_and_stderr
# @return [Process::Waiter] The process wait thread which tracks if the process is alive, its pid, return value, etc.
attr_writer :wait_thread
# @param [String] s Log to stderr
def log(s)
return unless @debug
$stderr.puts s
end
def without_debugging
previous_debug_value = @debug
@debug = false
yield
ensure
@debug = previous_debug_value
end
# Yields a timer object that can be used to request the remaining time available
def with_countdown(timeout)
countdown = Acceptance::Countdown.new(timeout)
# It is the caller's responsibility to honor the required countdown limits,
# but let's wrap the full operation in an explicit for worse case scenario,
# which may leave object state in a non-determinant state depending on the call
::Timeout.timeout(timeout * 1.5) do
yield countdown
end
if countdown.elapsed?
raise ChildProcessTimeoutError
end
rescue ::Timeout::Error
raise ChildProcessTimeoutError
end
end
# Internally generates a temporary file with Dir::Tmpname instead of a ::Tempfile instance, otherwise windows won't allow the file to be executed
# at the same time as the current Ruby process having an open handle to the temporary file
class TempChildProcessFile
def initialize(basename, extension)
@file_path = Dir::Tmpname.create([basename, extension]) do |_path, _n, _opts, _origdir|
# noop
end
ObjectSpace.define_finalizer(self, self.class.finalizer_proc_for(@file_path))
end
def path
@file_path
end
def to_s
path
end
def inspect
"#<#{self.class} #{self.path}>"
end
def self.finalizer_proc_for(path)
proc { File.delete(path) if File.exist?(path) }
end
end
###
# Stores the data for a payload, including the options used to generate the payload,
###
class Payload
attr_reader :name, :execute_cmd, :generate_options, :datastore
def initialize(options)
@name = options.fetch(:name)
@execute_cmd = options.fetch(:execute_cmd)
@generate_options = options.fetch(:generate_options)
@datastore = options.fetch(:datastore)
@executable = options.fetch(:executable, false)
basename = "#{File.basename(__FILE__)}_#{name}".gsub(/[^a-zA-Z]/, '-')
extension = options.fetch(:extension, '')
@file_path = TempChildProcessFile.new(basename, extension)
end
# @return [TrueClass, FalseClass] True if the payload needs marked as executable before being executed
def executable?
@executable
end
# @return [String] The path to the payload on disk
def path
@file_path.path
end
# @return [Integer] The size of the payload on disk. May be 0 when the payload doesn't exist,
# or a smaller size than expected if the payload is not fully generated by msfconsole yet.
def size
File.size(path)
rescue StandardError => _e
0
end
def [](k)
options[k]
end
# @return [Array<String>] The command which can be used to execute this payload. For instance ["python3", "/tmp/path.py"]
def execute_command
@execute_cmd.map do |val|
val.gsub('${payload_path}', path)
end
end
# @param [Hash] default_global_datastore
# @return [String] The setg commands for setting the global datastore
def setg_commands(default_global_datastore: {})
commands = []
# Ensure the global framework datastore is always clear
commands << "irb -e '(self.respond_to?(:framework) ? framework : self).datastore.user_defined.clear'"
# Call setg
global_datastore = default_global_datastore.merge(@datastore[:global])
global_datastore.each do |key, value|
commands << "setg #{key} #{value}"
end
commands.join("\n")
end
# @param [Hash] default_module_datastore
# @return [String] The command which can be used on msfconsole to generate the payload
def generate_command(default_module_datastore: {})
module_datastore = default_module_datastore.merge(@datastore[:module])
generate_options = @generate_options.map do |key, value|
"#{key} #{value}"
end
module_options = module_datastore.map do |key, value|
"#{key}=#{value}"
end
"generate -o #{path} #{generate_options.join(' ')} #{module_options.join(' ')}"
end
# @param [Hash] default_global_datastore
# @param [Hash] default_module_datastore
# @return [String] A human readable representation of the payload configuration object
def as_readable_text(default_global_datastore: {}, default_module_datastore: {})
<<~EOF
## Payload
use #{name}
## Set global datastore
#{setg_commands(default_global_datastore: default_global_datastore)}
## Generate command
#{generate_command(default_module_datastore: default_module_datastore)}
## Create listener
to_handler
## Execute command
#{Shellwords.join(execute_command)}
EOF
end
end
class PayloadProcess
# @return [Process::Waiter] the waiter thread for the current process
attr_reader :wait_thread
# @return [String] the executed command
attr_reader :cmd
# @return [String] the payload path on disk
attr_reader :payload_path
# @param [Array<String>] cmd The command which can be used to execute this payload. For instance ["python3", "/tmp/path.py"]
# @param [path] payload_path The payload path on disk
# @param [Hash] opts the opts to pass to the Process#spawn call
def initialize(cmd, payload_path, opts = {})
super()
@payload_path = payload_path
@debug = false
@env = {}
@cmd = cmd
@options = opts
end
# @return [Process::Waiter] the waiter thread for the payload process
def run
pid = Process.spawn(
@env,
*@cmd,
**@options
)
@wait_thread = Process.detach(pid)
@wait_thread
end
def alive?
@wait_thread.alive?
end
def close
begin
Process.kill('KILL', wait_thread.pid) if wait_thread.pid
rescue StandardError => e
warn "error #{e} for #{@cmd}, pid #{wait_thread.pid}"
end
[:in, :out, :err].each do |name|
@options[name].close if @options[name]
end
@wait_thread.join
end
end
class ConsoleDriver
def initialize
@console = nil
@payload_processes = []
ObjectSpace.define_finalizer(self, self.class.finalizer_proc_for(self))
end
# @param [Acceptance::Payload] payload
# @param [Hash] opts
def run_payload(payload, opts)
if payload.executable? && !File.executable?(payload.path)
FileUtils.chmod('+x', payload.path)
end
payload_process = PayloadProcess.new(payload.execute_command, payload.path, opts)
payload_process.run
@payload_processes << payload_process
payload_process
end
# @return [Acceptance::Console]
def open_console
@console = Console.new
@console.run
@console.recvuntil(Console.prompt, timeout: 120)
@console
end
def close_payloads
close_processes(@payload_processes)
end
def close
close_processes(@payload_processes + [console])
end
def self.finalizer_proc_for(instance)
proc { instance.close }
end
private
def close_processes(processes)
while (process = processes.pop)
begin
process.close
rescue StandardError => e
$stderr.puts e.to_s
end
end
end
end
class Console < ChildProcess
def initialize
super
framework_root = Dir.pwd
@debug = true
@env = {
'BUNDLE_GEMFILE' => File.join(framework_root, 'Gemfile'),
'PATH' => "#{framework_root.shellescape}:#{ENV['PATH']}"
}
@cmd = [
'bundle', 'exec', 'ruby', 'msfconsole',
'--no-readline',
# '--logger', 'Stdout',
'--quiet'
]
@options = {
chdir: framework_root
}
end
def self.prompt
/msf6.*>\s+/
end
def reset
sendline('sessions -K')
recvuntil(Console.prompt)
sendline('jobs -K')
recvuntil(Console.prompt)
ensure
@all_data.reopen('')
end
end
end

View File

@ -0,0 +1,23 @@
module Acceptance
###
# A utility class which can be used in conjunction with Timeout mechanisms
###
class Countdown
# @param [int] timeout The time in seconds that this count starts from
def initialize(timeout)
@start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
@end_time = @start_time + timeout
@timeout = timeout
end
# @return [TrueClass, FalseClass] True if the timeout has surpassed, false otherwise
def elapsed?
remaining_time == 0
end
# @return [Integer] The time in seconds left before this countdown expires
def remaining_time
[@end_time - Process.clock_gettime(Process::CLOCK_MONOTONIC, :second), 0].max
end
end
end

View File

@ -0,0 +1,55 @@
module Acceptance
###
# A utility object representing the validation of a a line of output generated
# by the acceptance test suite.
###
class LineValidation
# @param [string|Array<String>] values A line string, or array of lines
# @param [Object] options Additional options for configuring this failure, i.e. if it's a known flaky test result etc.
def initialize(values, options = {})
@values = Array(values)
@options = options
end
def flatten
@values.map { |value| self.class.new(value, @options) }
end
def value
raise StandardError, 'More than one value present' if @values.length > 1
@values[0]
end
# @return [boolean] returns true if the current failure applies under the current environment or the result is flaky, false otherwise.
# @param [Hash] environment The current execution environment
# @return [TrueClass, FalseClass] True if the line is flaky - and may not always be present, false otherwise
def flaky?(environment = {})
value = @options.fetch(:flaky, false)
evaluate_predicate(value, environment)
end
# @return [boolean] returns true if the current failure applies under the current environment or the result is flaky, false otherwise.
# @param [Hash] environment
# @return [TrueClass, FalseClass] True if the line should be considered valid, false otherwise
def if?(environment = {})
value = @options.fetch(:if, true)
evaluate_predicate(value, environment)
end
def to_h
{
values: @values,
options: @options
}
end
private
# (see Acceptance::Meterpreter#eval_predicate)
def evaluate_predicate(value, environment)
Acceptance::Meterpreter.eval_predicate(value, environment)
end
end
end

View File

@ -0,0 +1,102 @@
module Acceptance::Meterpreter
# @return [Symbol] The current platform
def self.current_platform
host_os = RbConfig::CONFIG['host_os']
case host_os
when /darwin/
:osx
when /mingw/
:windows
when /linux/
:linux
else
raise "unknown host_os #{host_os.inspect}"
end
end
# Allows restricting the tests of a specific Meterpreter's test suite with the METERPRETER environment variable
# @return [TrueClass, FalseClass] True if the given Meterpreter should be run, false otherwise.
def self.run_meterpreter?(meterpreter_config)
return true if ENV['METERPRETER'].blank?
name = meterpreter_config[:name].to_s
ENV['METERPRETER'].include?(name)
end
# Allows restricting the tests of a specific Meterpreter's test suite with the METERPRETER environment variable
# @return [TrueClass, FalseClass] True if the given Meterpreter should be run, false otherwise.
def self.run_meterpreter_module_test?(module_test)
return true if ENV['METERPRETER_MODULE_TEST'].blank?
ENV['METERPRETER_MODULE_TEST'].include?(module_test)
end
# @param [String] string A console string with ANSI escape codes present
# @return [String] A string with the ANSI escape codes removed
def self.uncolorize(string)
string.gsub(/\e\[\d+m/, '')
end
# @param [Hash] payload_config
# @return [Boolean]
def self.supported_platform?(payload_config)
payload_config[:platforms].include?(current_platform)
end
# @param [Hash] module_test
# @return [Boolean]
def self.skipped_module_test?(module_test, test_environment)
current_platform_requirements = Array(module_test[:platforms].find { |platform| Array(platform)[0] == current_platform })[1] || {}
module_test.fetch(:skip, false) ||
self.eval_predicate(current_platform_requirements.fetch(:skip, false), test_environment)
end
# @param [Hash] payload_config
# @return [String] The human readable name for the given payload configuration
def self.human_name_for_payload(payload_config)
is_stageless = payload_config[:name].include?('meterpreter_reverse_tcp')
is_staged = payload_config[:name].include?('meterpreter/reverse_tcp')
details = []
details << 'stageless' if is_stageless
details << 'staged' if is_staged
details << payload_config[:name]
details.join(' ')
end
# @param [Object] hash A hash of key => hash
# @return [Object] Returns a new hash with the 'key' merged into hash value and all payloads
def self.with_meterpreter_name_merged(hash)
hash.each_with_object({}) do |(name, config), acc|
acc[name] = config.merge({ name: name })
end
end
# Evaluates a simple predicate; Similar to Msf::OptCondition.eval_condition
# @param [TrueClass,FalseClass,Array] value
# @param [Hash] environment
# @return [TrueClass, FalseClass] True or false
def self.eval_predicate(value, environment)
case value
when Array
left_operand, operator, right_operand = value
# Map values such as `:meterpreter_name` to the runtime value
left_operand = environment[left_operand] if environment.key?(left_operand)
right_operand = environment[right_operand] if environment.key?(right_operand)
case operator.to_sym
when :==
evaluate_predicate(left_operand, environment) == evaluate_predicate(right_operand, environment)
when :!=
evaluate_predicate(left_operand, environment) != evaluate_predicate(right_operand, environment)
when :or
evaluate_predicate(left_operand, environment) || evaluate_predicate(right_operand, environment)
else
raise "unexpected operator #{operator.inspect}"
end
else
value
end
end
end

View File

@ -0,0 +1,271 @@
require 'support/acceptance/meterpreter'
module Acceptance::Meterpreter
JAVA_METERPRETER = {
payloads: [
{
name: "java/meterpreter/reverse_tcp",
extension: ".jar",
platforms: [:osx, :linux, :windows],
execute_cmd: ["java", "-jar", "${payload_path}"],
generate_options: {
'-f': "jar"
},
datastore: {
global: {},
module: {
spawn: 0
}
}
}
],
module_tests: [
{
name: "test/services",
platforms: [
[
:linux,
{
skip: true,
reason: "Windows only test"
}
],
[
:osx,
{
skip: true,
reason: "Windows only test"
}
],
:windows
],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: [
"[-] [should start W32Time] FAILED: should start W32Time",
"[-] [should start W32Time] Exception: Rex::Post::Meterpreter::RequestError: stdapi_railgun_api: Operation failed: The command is not supported by this Meterpreter type (java/windows)",
"[-] [should stop W32Time] FAILED: should stop W32Time",
"[-] [should stop W32Time] Exception: Rex::Post::Meterpreter::RequestError: stdapi_railgun_api: Operation failed: The command is not supported by this Meterpreter type (java/windows)",
"[-] [should create a service testes] FAILED: should create a service testes",
"[-] [should create a service testes] Exception: Rex::Post::Meterpreter::RequestError: stdapi_railgun_api: Operation failed: The command is not supported by this Meterpreter type (java/windows)",
"[-] [should return info on the newly-created service testes] Could not retrieve the start type of the testes service!",
"[-] FAILED: should return info on the newly-created service testes",
"[-] [should delete the new service testes] FAILED: should delete the new service testes",
"[-] [should delete the new service testes] Exception: Rex::Post::Meterpreter::RequestError: stdapi_railgun_api: Operation failed: The command is not supported by this Meterpreter type (java/windows)",
"[-] [should return status on a given service winmgmt] FAILED: should return status on a given service winmgmt",
"[-] [should return status on a given service winmgmt] Exception: Rex::Post::Meterpreter::RequestError: stdapi_railgun_api: Operation failed: The command is not supported by this Meterpreter type (java/windows)",
"[-] [should modify config on a given service] FAILED: should modify config on a given service",
"[-] [should modify config on a given service] Exception: Rex::Post::Meterpreter::RequestError: stdapi_railgun_api: Operation failed: The command is not supported by this Meterpreter type (java/windows)",
"[-] [should start a disabled service] FAILED: should start a disabled service",
"[-] [should start a disabled service] Exception: Rex::Post::Meterpreter::RequestError: stdapi_railgun_api: Operation failed: The command is not supported by this Meterpreter type (java/windows)",
"[-] [should restart a started service W32Time] FAILED: should restart a started service W32Time",
"[-] [should restart a started service W32Time] Exception: Rex::Post::Meterpreter::RequestError: stdapi_railgun_api: Operation failed: The command is not supported by this Meterpreter type (java/windows)",
"[-] [should raise a runtime exception if no access to service] FAILED: should raise a runtime exception if no access to service",
"[-] [should raise a runtime exception if no access to service] Exception: Rex::Post::Meterpreter::RequestError: stdapi_railgun_api: Operation failed: The command is not supported by this Meterpreter type (java/windows)",
"[-] [should raise a runtime exception if services doesnt exist] FAILED: should raise a runtime exception if services doesnt exist",
"[-] [should raise a runtime exception if services doesnt exist] Exception: Rex::Post::Meterpreter::RequestError: stdapi_railgun_api: Operation failed: The command is not supported by this Meterpreter type (java/windows)"
]
}
}
},
{
name: "test/cmd_exec",
platforms: [:linux, :osx, :windows],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/extapi",
platforms: [:linux, :osx, :windows],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/file",
platforms: [:linux, :osx, :windows],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: [
"[-] [should delete a symbolic link target] failed to create the symbolic link"
]
}
}
},
{
name: "test/get_env",
platforms: [:linux, :osx, :windows],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/meterpreter",
platforms: [:linux, :osx, :windows],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/railgun",
platforms: [:linux, :osx, :windows],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/railgun_reverse_lookups",
platforms: [:linux, :osx, :windows],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/registry",
platforms: [
[
:linux,
{
skip: true,
reason: "Windows only test"
}
],
[
:osx,
{
skip: true,
reason: "Windows only test"
}
],
:windows
],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: [
"[-] FAILED: should write REG_EXPAND_SZ values",
"[-] FAILED: should write REG_SZ unicode values"
]
}
}
},
{
name: "test/search",
platforms: [:linux, :osx, :windows],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/unix",
platforms: [
:linux,
:osx,
[
:windows,
{
skip: true,
reason: "Unix only test"
}
]
],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
}
]
}
end

View File

@ -0,0 +1,351 @@
require 'support/acceptance/meterpreter'
module Acceptance::Meterpreter
METTLE_METERPRETER = {
payloads: [
{
name: "linux/x64/meterpreter/reverse_tcp",
extension: "",
platforms: [:linux],
executable: true,
execute_cmd: ["${payload_path}"],
generate_options: {
'-f': "elf"
},
datastore: {
global: {},
module: {
MeterpreterTryToFork: false,
MeterpreterDebugBuild: true
}
}
},
{
name: "osx/x64/meterpreter_reverse_tcp",
extension: "",
platforms: [:osx],
executable: true,
execute_cmd: ["${payload_path}"],
generate_options: {
'-f': "macho"
},
datastore: {
global: {},
module: {
MeterpreterTryToFork: false,
MeterpreterDebugBuild: true
}
}
}
],
module_tests: [
{
name: "test/services",
platforms: [
[
:linux,
{
skip: true,
reason: "Windows only test"
}
],
[
:osx,
{
skip: true,
reason: "Windows only test"
}
],
:windows
],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/cmd_exec",
platforms: [
:linux,
:osx,
[
:windows,
{
skip: true,
reason: "Payload not compiled for platform"
}
]
],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/extapi",
platforms: [
:linux,
:osx,
[
:windows,
{
skip: true,
reason: "Payload not compiled for platform"
}
]
],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/file",
platforms: [
:linux,
:osx,
[
:windows,
{
skip: true,
reason: "Payload not compiled for platform"
}
]
],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/get_env",
platforms: [
:linux,
:osx,
[
:windows,
{
skip: true,
reason: "Payload not compiled for platform"
}
]
],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/meterpreter",
platforms: [
:linux,
:osx,
[
:windows,
{
skip: true,
reason: "Payload not compiled for platform"
}
]
],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: [
"[-] FAILED: should return network interfaces",
"[-] FAILED: should have an interface that matches session_host"
]
},
windows: {
known_failures: []
}
}
},
{
name: "test/railgun",
platforms: [
:linux,
:osx,
[
:windows,
{
skip: true,
reason: "Payload not compiled for platform"
}
]
],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/railgun_reverse_lookups",
platforms: [
:linux,
:osx,
[
:windows,
{
skip: true,
reason: "Payload not compiled for platform"
}
]
],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/registry",
platforms: [
[
:linux,
{
skip: true,
reason: "Windows only test"
}
],
[
:osx,
{
skip: true,
reason: "Windows only test"
}
],
:windows
],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/search",
platforms: [
:linux,
[
:osx,
{
skip: true,
reason: "skipped - test/search hangs in osx and CPU spikes to >300%"
}
],
[
:windows,
{
skip: true,
reason: "Payload not compiled for platform"
}
]
],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/unix",
platforms: [
:linux,
:osx,
[
:windows,
{
skip: true,
reason: "Unix only test"
}
]
],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
}
]
}
end

View File

@ -0,0 +1,275 @@
require 'support/acceptance/meterpreter'
module Acceptance::Meterpreter
PHP_METERPRETER = {
payloads: [
{
name: "php/meterpreter_reverse_tcp",
extension: ".php",
platforms: [:osx, :linux, :windows],
execute_cmd: ["php", "${payload_path}"],
generate_options: {
'-f': "raw"
},
datastore: {
global: {},
module: {
MeterpreterDebugBuild: true
}
}
}
],
module_tests: [
{
name: "test/services",
platforms: [
[
:linux,
{
skip: true,
reason: "Windows only test"
}
],
[
:osx,
{
skip: true,
reason: "Windows only test"
}
],
[
:windows,
{
skip: [
:meterpreter_runtime_version,
:==,
"php5.3"
],
reason: "Skip PHP 5.3 as the tests timeout - due to cmd_exec taking 15 seconds for each call. Caused by failure to detect feof correctly - https://github.com/rapid7/metasploit-payloads/blame/c7f7bc2fc0b86e17c3bc078149c71745c5e478b3/php/meterpreter/meterpreter.php#L1127-L1145"
}
]
],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/cmd_exec",
platforms: [:linux, :osx, :windows],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: [
"[-] FAILED: should return the stderr output"
]
}
}
},
{
name: "test/extapi",
platforms: [:linux, :osx, :windows],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/file",
platforms: [:linux, :osx, :windows],
skipped: false,
lines: {
linux: {
known_failures: [
"[-] FAILED: should read the binary data we just wrote"
]
},
osx: {
known_failures: [
"[-] FAILED: should read the binary data we just wrote"
]
},
windows: {
known_failures: [
"[-] [should delete a symbolic link target] FAILED: should delete a symbolic link target",
"[-] [should delete a symbolic link target] Exception: Rex::Post::Meterpreter::RequestError: stdapi_fs_delete_dir: Operation failed: 1",
"[-] FAILED: should read the binary data we just wrote"
]
}
}
},
{
name: "test/get_env",
platforms: [:linux, :osx, :windows],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/meterpreter",
platforms: [:linux, :osx, :windows],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: [
"[-] FAILED: should return a list of processes"
]
},
windows: {
known_failures: []
}
}
},
{
name: "test/railgun",
platforms: [:linux, :osx, :windows],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/railgun_reverse_lookups",
platforms: [:linux, :osx, :windows],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/registry",
platforms: [
[
:linux,
{
skip: true,
reason: "Windows only test"
}
],
[
:osx,
{
skip: true,
reason: "Windows only test"
}
],
[
:windows,
{
skip: [
:meterpreter_runtime_version,
:==,
"php5.3"
],
reason: "Skip PHP 5.3 as the tests timeout - due to cmd_exec taking 15 seconds for each call. Caused by failure to detect feof correctly - https://github.com/rapid7/metasploit-payloads/blame/c7f7bc2fc0b86e17c3bc078149c71745c5e478b3/php/meterpreter/meterpreter.php#L1127-L1145"
}
]
],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/search",
platforms: [:linux, :osx, :windows],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/unix",
platforms: [
:linux,
:osx,
[
:windows,
{
skip: true,
reason: "Unix only test"
}
]
],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
}
]
}
end

View File

@ -0,0 +1,272 @@
require 'support/acceptance/meterpreter'
module Acceptance::Meterpreter
PYTHON_METERPRETER = {
payloads: [
{
name: "python/meterpreter_reverse_tcp",
extension: ".py",
platforms: [:osx, :linux, :windows],
execute_cmd: ["python", "${payload_path}"],
generate_options: {
'-f': "raw"
},
datastore: {
global: {},
module: {
MeterpreterTryToFork: false,
PythonMeterpreterDebug: true
}
}
}
],
module_tests: [
{
name: "test/services",
platforms: [
[
:linux,
{
skip: true,
reason: "Windows only test"
}
],
[
:osx,
{
skip: true,
reason: "Windows only test"
}
],
:windows
],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: [
"[-] [should start W32Time] FAILED: should start W32Time",
"[-] [should start W32Time] Exception: RuntimeError: Could not open service. OpenServiceA error: FormatMessage failed to retrieve the error for value 0x6.",
"[-] [should stop W32Time] FAILED: should stop W32Time",
"[-] [should stop W32Time] Exception: RuntimeError: Could not open service. OpenServiceA error: FormatMessage failed to retrieve the error for value 0x6.",
"[-] [should list services] FAILED: should list services",
"[-] [should list services] Exception: NoMethodError: undefined method `service' for nil:NilClass",
"[-] [should return info on a given service winmgmt] FAILED: should return info on a given service winmgmt",
"[-] [should return info on a given service winmgmt] Exception: NoMethodError: undefined method `service' for nil:NilClass",
"[-] FAILED: should create a service testes",
"[-] [should return info on the newly-created service testes] FAILED: should return info on the newly-created service testes",
"[-] [should return info on the newly-created service testes] Exception: NoMethodError: undefined method `service' for nil:NilClass",
"[-] [should delete the new service testes] FAILED: should delete the new service testes",
"[-] [should delete the new service testes] Exception: RuntimeError: Could not open service. OpenServiceA error: FormatMessage failed to retrieve the error for value 0x6.",
"[-] [should return status on a given service winmgmt] FAILED: should return status on a given service winmgmt",
"[-] [should return status on a given service winmgmt] Exception: RuntimeError: Could not open service. OpenServiceA error: FormatMessage failed to retrieve the error for value 0x6.",
"[-] [should modify config on a given service] FAILED: should modify config on a given service",
"[-] [should modify config on a given service] Exception: RuntimeError: Could not open service. OpenServiceA error: FormatMessage failed to retrieve the error for value 0x6.",
"[-] FAILED: should start a disabled service",
"[-] [should restart a started service W32Time] FAILED: should restart a started service W32Time",
"[-] [should restart a started service W32Time] Exception: RuntimeError: Could not open service. OpenServiceA error: FormatMessage failed to retrieve the error for value 0x6."
]
}
}
},
{
name: "test/cmd_exec",
platforms: [:linux, :osx, :windows],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/extapi",
platforms: [:linux, :osx, :windows],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: [
"[-] [should return clipboard jpg dimensions] FAILED: should return clipboard jpg dimensions",
"[-] [should return clipboard jpg dimensions] Exception: NoMethodError: undefined method `clipboard' for nil:NilClass",
"[-] [should download clipboard jpg data] FAILED: should download clipboard jpg data",
"[-] [should download clipboard jpg data] Exception: NoMethodError: undefined method `clipboard' for nil:NilClass"
]
}
}
},
{
name: "test/file",
platforms: [:linux, :osx, :windows],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/get_env",
platforms: [:linux, :osx, :windows],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/meterpreter",
platforms: [:linux, :osx, :windows],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: [
"[-] FAILED: should return the proper directory separator"
]
}
}
},
{
name: "test/railgun",
platforms: [:linux, :osx, :windows],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/railgun_reverse_lookups",
platforms: [:linux, :osx, :windows],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/registry",
platforms: [
[
:linux,
{
skip: true,
reason: "Windows only test"
}
],
[
:osx,
{
skip: true,
reason: "Windows only test"
}
],
:windows
],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/search",
platforms: [:linux, :osx, :windows],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/unix",
platforms: [
:linux,
:osx,
[
:windows,
{
skip: true,
reason: "Unix only test"
}
]
],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
}
]
}
end

View File

@ -0,0 +1,374 @@
require 'support/acceptance/meterpreter'
module Acceptance::Meterpreter
WINDOWS_METERPRETER = {
payloads: [
{
name: "windows/meterpreter/reverse_tcp",
extension: ".exe",
platforms: [:windows],
execute_cmd: ["${payload_path}"],
executable: true,
generate_options: {
'-f': "exe"
},
datastore: {
global: {},
module: {
# Not suported by Windows Meterpreter
# MeterpreterTryToFork: false,
MeterpreterDebugBuild: true
}
}
}
],
module_tests: [
{
name: "test/services",
platforms: [
[
:linux,
{
skip: true,
reason: "Windows only test"
}
],
[
:osx,
{
skip: true,
reason: "Windows only test"
}
],
:windows
],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/cmd_exec",
platforms: [
[
:linux,
{
skip: true,
reason: "Payload not compiled for platform"
}
],
[
:osx,
{
skip: true,
reason: "Payload not compiled for platform"
}
],
:windows
],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/extapi",
platforms: [
[
:linux,
{
skip: true,
reason: "Payload not compiled for platform"
}
],
[
:osx,
{
skip: true,
reason: "Payload not compiled for platform"
}
],
:windows
],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/file",
platforms: [
[
:linux,
{
skip: true,
reason: "Payload not compiled for platform"
}
],
[
:osx,
{
skip: true,
reason: "Payload not compiled for platform"
}
],
:windows
],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/get_env",
platforms: [
[
:linux,
{
skip: true,
reason: "Payload not compiled for platform"
}
],
[
:osx,
{
skip: true,
reason: "Payload not compiled for platform"
}
],
:windows
],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/meterpreter",
platforms: [
[
:linux,
{
skip: true,
reason: "Payload not compiled for platform"
}
],
[
:osx,
{
skip: true,
reason: "Payload not compiled for platform"
}
],
:windows
],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/railgun",
platforms: [
[
:linux,
{
skip: true,
reason: "Payload not compiled for platform"
}
],
[
:osx,
{
skip: true,
reason: "Payload not compiled for platform"
}
],
:windows
],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/railgun_reverse_lookups",
platforms: [
[
:linux,
{
skip: true,
reason: "Payload not compiled for platform"
}
],
[
:osx,
{
skip: true,
reason: "Payload not compiled for platform"
}
],
:windows
],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/registry",
platforms: [
[
:linux,
{
skip: true,
reason: "Windows only test"
}
],
[
:osx,
{
skip: true,
reason: "Windows only test"
}
],
:windows
],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/search",
platforms: [
[
:linux,
{
skip: true,
reason: "Payload not compiled for platform"
}
],
[
:osx,
{
skip: true,
reason: "Payload not compiled for platform"
}
],
:windows
],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
},
{
name: "test/unix",
platforms: [
:linux,
:osx,
[
:windows,
{
skip: true,
reason: "Unix only test"
}
]
],
skipped: false,
lines: {
linux: {
known_failures: []
},
osx: {
known_failures: []
},
windows: {
known_failures: []
}
}
}
]
}
end

View File

@ -0,0 +1,18 @@
module Acceptance
###
# A utility class for generating the next available bind port that is free
# on the host machine
###
class PortAllocator
def initialize(base = 6000)
@base = base
@current = base
end
# @return [Integer] The next available port that can be bound to on the host
def next
# TODO: In the future this could verify the port is free, and attempt to avoid TOCTTOU issues
@current += 1
end
end
end

View File

@ -239,7 +239,12 @@ class MetasploitModule < Msf::Post
bin = read_file(datastore['BaseFileName'])
rm_f(datastore['BaseFileName'])
bin == "\xde\xad\xbe\xef"
test_string = "\xde\xad\xbe\xef"
vprint_status "expected: #{test_string.bytes} - #{test_string.encoding}"
vprint_status "actual: #{bin.bytes} - #{bin.encoding}"
bin == test_string
end
end

View File

@ -143,14 +143,15 @@ class MetasploitModule < Msf::Post
it "should return the proper directory separator" do
sysinfo = session.sys.config.sysinfo
vprint_status("received sysinfo #{sysinfo}")
if sysinfo["OS"] =~ /windows/i
sep = session.fs.file.separator
res = (sep == "\\")
expected_sep = "\\"
else
sep = session.fs.file.separator
res = (sep == "/")
expected_sep = "/"
end
sep = session.fs.file.separator
vprint_status("Received separator #{sep.inspect} - expected: #{expected_sep.inspect}")
res = (sep == expected_sep)
res
end
@ -231,6 +232,9 @@ class MetasploitModule < Msf::Post
(contents == "test")
}
# XXX: On windows this can fail with:
# Rex::Post::Meterpreter::RequestError : stdapi_fs_delete_file: Operation failed: The process cannot access the file because it is being used by another process.
# Presumably the Ruby process still has a handle to the file
session.fs.file.rm(file_name)
res &&= !session.fs.dir.entries.include?(file_name)
@ -251,8 +255,13 @@ class MetasploitModule < Msf::Post
if res
fd = session.fs.file.new(remote, "rb")
uploaded_contents = fd.read
until (fd.eof?)
uploaded_contents << fd.read
begin
until fd.eof?
uploaded_contents << fd.read
end
rescue EOFError
# An EOF can be raised on `fd.read` in the Java Meterpreter
vprint_status("EOF raised")
end
fd.close
original_contents = ::File.read(local, mode: 'rb')