Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/spec/api/json_rpc_spec.rb
Views: 11766
require 'spec_helper'1require 'rack/test'2require 'rack/protection'34# These tests ensure the full end to end functionality of metasploit's JSON RPC5# endpoint. There are multiple layers of possible failure in our API, and unit testing6# alone will not cover all edge cases. For instance, middleware may raise exceptions7# and return HTML to the calling client unintentionally - which will break our JSON8# response contract. These test should help catch such scenarios.9RSpec.describe "Metasploit's json-rpc" do10include Rack::Test::Methods11include_context 'Msf::DBManager'12include_context 'Metasploit::Framework::Spec::Constants cleaner'13include_context 'Msf::Framework#threads cleaner', verify_cleanup_required: false14include_context 'wait_for_expect'1516let(:health_check_url) { '/api/v1/health' }17let(:rpc_url) { '/api/v1/json-rpc' }18let(:module_name) { 'scanner/ssl/openssl_heartbleed' }19let(:a_valid_result_uuid) { { result: hash_including({ uuid: match(/\w+/) }) } }20let(:app) { ::Msf::WebServices::JsonRpcApp.new }2122before(:example) do23framework.modules.add_module_path(File.join(FILE_FIXTURES_PATH, 'json_rpc'))24app.settings.framework = framework25end2627after(:example) do28# Sinatra's settings are implemented as a singleton, and must be explicitly reset between runs29app.settings.dispatchers.clear30end3132def report_host(host)33post rpc_url, {34jsonrpc: '2.0',35method: 'db.report_host',36id: 1,37params: [38host39]40}.to_json41end4243def report_vuln(vuln)44post rpc_url, {45jsonrpc: '2.0',46method: 'db.report_vuln',47id: 1,48params: [49vuln50]51}.to_json52end5354def analyze_host(host)55post rpc_url, {56jsonrpc: '2.0',57method: 'db.analyze_host',58id: 1,59params: [60host61]62}.to_json63end6465def create_job66post rpc_url, {67jsonrpc: '2.0',68method: 'module.check',69id: 1,70params: [71'auxiliary',72module_name,73{74RHOSTS: '192.0.2.0'75}76]77}.to_json78end7980def get_job_results(uuid)81post rpc_url, {82jsonrpc: '2.0',83method: 'module.results',84id: 1,85params: [86uuid87]88}.to_json89end9091def get_rpc_health_check92post rpc_url, {93jsonrpc: '2.0',94method: 'health.check',95id: 1,96params: []97}.to_json98end99100def get_rest_health_check101get health_check_url102end103104def last_json_response105JSON.parse(last_response.body).with_indifferent_access106end107108def expect_completed_status(rpc_response)109expect(rpc_response).to include({ result: hash_including({ status: 'completed' }) })110end111112def expect_error_status(rpc_response)113expect(rpc_response).to include({ result: hash_including({ status: 'errored' }) })114end115116def mock_rack_env(mock_rack_env_value)117allow(ENV).to receive(:[]).and_wrap_original do |original_env, key|118if key == 'RACK_ENV'119mock_rack_env_value120else121original_env[key]122end123end124end125126describe 'health status' do127context 'when using the REST health check functionality' do128it 'passes the health check' do129expected_response = {130data: {131status: 'UP'132}133}134135get_rest_health_check136expect(last_response).to be_ok137expect(last_json_response).to include(expected_response)138end139end140141context 'when there is an issue' do142before(:each) do143allow(framework).to receive(:version).and_raise 'Mock error'144end145146it 'fails the health check' do147expected_response = {148data: {149status: 'DOWN'150}151}152153get_rest_health_check154155expect(last_response.status).to be 503156expect(last_json_response).to include(expected_response)157end158end159160context 'when using the RPC health check functionality' do161context 'when the service is healthy' do162it 'passes the health check' do163expected_response = {164id: 1,165jsonrpc: '2.0',166result: {167status: 'UP'168}169}170171get_rpc_health_check172expect(last_response).to be_ok173expect(last_json_response).to include(expected_response)174end175end176177context 'when there is an issue' do178before(:each) do179allow(framework).to receive(:version).and_raise 'Mock error'180end181182it 'fails the health check' do183expected_response = {184id: 1,185jsonrpc: '2.0',186result: {187status: 'DOWN'188}189}190191get_rpc_health_check192193expect(last_response).to be_ok194expect(last_json_response).to include(expected_response)195end196end197end198end199200describe 'Running a check job and verifying results' do201context 'when the module returns check code safe' do202before(:each) do203allow_any_instance_of(::Msf::Auxiliary::Scanner).to receive(:check) do204::Msf::Exploit::CheckCode::Safe205end206end207208it 'returns successful job results' do209create_job210expect(last_response).to be_ok211expect(last_json_response).to include(a_valid_result_uuid)212213uuid = last_json_response['result']['uuid']214wait_for_expect do215get_job_results(uuid)216217expect(last_response).to be_ok218expect_completed_status(last_json_response)219end220221expected_completed_response = {222result: {223status: 'completed',224result: {225code: 'safe',226details: {},227message: 'The target is not exploitable.',228reason: nil229}230}231}232expect(last_json_response).to include(expected_completed_response)233end234end235236context 'when the module does not support a check method' do237before do238mock_rack_env('development')239end240241let(:module_name) { 'scanner/http/title' }242243it 'returns successful job results' do244create_job245expect(last_response).to_not be_ok246expected_error_response = {247error: {248code: -32000,249data: {250backtrace: include(a_kind_of(String))251},252message: 'Application server error: This module does not support check.'253},254id: 1255}256expect(last_json_response).to include(expected_error_response)257end258end259260context 'when the check command raises a known msf error' do261before(:each) do262allow_any_instance_of(::Msf::Auxiliary::Scanner).to receive(:check) do |mod|263mod.fail_with(Msf::Module::Failure::UnexpectedReply, 'Expected failure reason')264end265end266267it 'returns the error results' do268create_job269expect(last_response).to be_ok270expect(last_json_response).to include(a_valid_result_uuid)271272uuid = last_json_response['result']['uuid']273274wait_for_expect do275get_job_results(uuid)276277expect(last_response).to be_ok278expect_error_status(last_json_response)279end280281expected_error_response = {282result: {283status: 'errored',284error: 'unexpected-reply: Expected failure reason'285}286}287expect(last_json_response).to include(expected_error_response)288end289end290291context 'when the check command has an unexpected error' do292include_context 'Msf::Framework#threads cleaner'293294before(:each) do295allow_any_instance_of(::Msf::Auxiliary::Scanner).to receive(:check) do296raise 'Unexpected module error'297end298end299300it 'returns the error results' do301create_job302expect(last_response).to be_ok303expect(last_json_response).to include(a_valid_result_uuid)304305uuid = last_json_response['result']['uuid']306307wait_for_expect do308get_job_results(uuid)309310expect(last_response).to be_ok311expect_error_status(last_json_response)312end313314expected_error_response = {315result: {316status: 'errored',317error: "Unexpected module error"318}319}320expect(last_json_response).to include(expected_error_response)321end322end323324context 'when there is a sinatra level application error in the development environment' do325before(:each) do326allow_any_instance_of(Msf::RPC::JSON::Dispatcher).to receive(:process).and_raise(Exception, 'Sinatra level exception raised')327mock_rack_env('development')328end329330it 'returns the error results' do331create_job332333expect(last_response).to be_server_error334expected_error_response = {335error: {336code: -32000,337data: {338backtrace: include(a_kind_of(String))339},340message: 'Application server error: Sinatra level exception raised'341},342id: 1343}344expect(last_json_response).to include(expected_error_response)345end346end347348context 'when rack middleware raises an error in the development environment' do349before(:each) do350allow_any_instance_of(::Rack::Protection::AuthenticityToken).to receive(:accepts?).and_raise(Exception, 'Middleware error raised')351mock_rack_env('development')352end353354it 'returns the error results' do355create_job356357expect(last_response).to be_server_error358expected_error_response = {359error: {360code: -32000,361data: {362backtrace: include(a_kind_of(String))363},364message: 'Application server error: Middleware error raised'365},366id: 1367}368expect(last_json_response).to include(expected_error_response)369end370end371372context 'when rack middleware raises an error in the production environment' do373before(:each) do374allow_any_instance_of(::Rack::Protection::AuthenticityToken).to receive(:accepts?).and_raise(Exception, 'Middleware error raised')375mock_rack_env('production')376end377378it 'returns the error results' do379create_job380381expect(last_response).to be_server_error382expected_error_response = {383error: {384code: -32000,385message: 'Application server error: Middleware error raised'386},387id: 1388}389expect(last_json_response).to include(expected_error_response)390end391end392393context 'when there is a sinatra level application error in the production environment' do394before(:each) do395allow_any_instance_of(Msf::RPC::JSON::Dispatcher).to receive(:process).and_raise(Exception, 'Sinatra level exception raised')396mock_rack_env('production')397end398399it 'returns the error results' do400create_job401402expect(last_response).to be_server_error403expected_error_response = {404error: {405code: -32000,406message: 'Application server error: Sinatra level exception raised'407},408id: 1409}410expect(last_json_response).to include(expected_error_response)411end412end413end414415describe 'analyze' do416let(:host_ip) { Faker::Internet.private_ip_v4_address }417let(:host) do418{419workspace: 'default',420host: host_ip,421state: 'alive',422os_name: 'Windows',423os_flavor: 'Enterprize',424os_sp: 'SP2',425os_lang: 'English',426arch: 'ARCH_X86',427mac: '97-42-51-F2-A7-A7',428scope: 'eth2',429virtual_host: 'VMWare'430}431end432433let(:vuln) do434{435workspace: 'default',436host: host_ip,437name: 'Exploit Name',438info: 'Human readable description of the vuln',439refs: vuln_refs440}441end442443context 'when there are modules available' do444let(:vuln_refs) do445%w[446CVE-2017-0143447]448end449450before(:each) do451framework.modules.add_module_path('./modules')452end453454context 'with no options' do455it 'returns the list of known modules associated with a reported host' do456report_host(host)457expect(last_response).to be_ok458459report_vuln(vuln)460expect(last_response).to be_ok461462expected_response = {463jsonrpc: '2.0',464result: {465host: {466address: host_ip,467modules: [468{469mname: "exploit/windows/smb/ms17_010_eternalblue",470mtype: "exploit",471options: {472invalid: [],473missing: [],474},475state: "READY_FOR_TEST",476description: "ready for testing"477},478{479mname: "exploit/windows/smb/ms17_010_psexec",480mtype: "exploit",481options: {482invalid: [],483missing: [ "credential" ],484},485state: "REQUIRES_CRED",486description: "credentials are required"487},488{489mname: "exploit/windows/smb/smb_doublepulsar_rce",490mtype: "exploit",491options: {492invalid: [],493missing: [],494},495state: "READY_FOR_TEST",496description: "ready for testing"497}498]499}500},501id: 1502}503504analyze_host(505{506workspace: 'default',507host: host_ip508}509)510expect(last_json_response).to include(expected_response)511end512end513514context 'when payloads requirements are specified' do515it 'returns the list of known modules associated with a reported host' do516report_host(host)517expect(last_response).to be_ok518519report_vuln(vuln)520expect(last_response).to be_ok521522# Note: Currently the API doesn't return any differentiating output that a particular module is suitable523# with the requested payload524expected_response = {525jsonrpc: '2.0',526result: {527host: {528address: host_ip,529modules: [530{531mname: "exploit/windows/smb/ms17_010_eternalblue",532mtype: "exploit",533options: {534invalid: [],535missing: [ "payload_match" ],536},537state: "MISSING_PAYLOAD",538description: "none of the requested payloads match"539},540{541mname: "exploit/windows/smb/ms17_010_psexec",542mtype: "exploit",543options: {544invalid: [],545missing: [ "credential", "payload_match" ],546},547state: "REQUIRES_CRED",548description: "credentials are required, none of the requested payloads match"549},550{551mname: "exploit/windows/smb/smb_doublepulsar_rce",552mtype: "exploit",553options: {554invalid: [],555missing: ["payload_match"],556},557state: "MISSING_PAYLOAD",558description: "none of the requested payloads match"559}560]561}562},563id: 1564}565566analyze_host(567{568workspace: 'default',569host: host_ip,570analyze_options: {571payloads: [572'windows/meterpreter_reverse_http'573]574}575}576)577expect(last_json_response).to include(expected_response)578end579end580end581582context 'when there are no modules found' do583let(:vuln_refs) do584['CVE-NO-MATCHING-MODULES-1234']585end586587it 'returns an empty list of modules' do588report_host(host)589expect(last_response).to be_ok590591report_vuln(vuln)592expect(last_response).to be_ok593594expected_response = {595jsonrpc: '2.0',596result: {597host: {598address: host_ip,599modules: []600}601},602id: 1603}604605analyze_host(606{607workspace: 'default',608host: host_ip609}610)611expect(last_json_response).to include(expected_response)612end613end614end615end616617618