Land #18288, Add Meterpreter sanity tests to CI
This commit is contained in:
commit
3329ac5357
|
@ -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
|
|
@ -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
12
Gemfile
|
@ -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
|
||||
|
|
20
Gemfile.lock
20
Gemfile.lock
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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|
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
Loading…
Reference in New Issue