Path: blob/master/spec/api/json_rpc_spec.rb
57975 views
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: {},227vuln: {},228message: 'The target is not exploitable.',229reason: nil230}231}232}233expect(last_json_response).to include(expected_completed_response)234end235end236237context 'when the module does not support a check method' do238before do239mock_rack_env('development')240end241242let(:module_name) { 'scanner/http/title' }243244it 'returns successful job results' do245create_job246expect(last_response).to_not be_ok247expected_error_response = {248error: {249code: -32000,250data: {251backtrace: include(a_kind_of(String))252},253message: 'Application server error: This module does not support check.'254},255id: 1256}257expect(last_json_response).to include(expected_error_response)258end259end260261context 'when the check command raises a known msf error' do262before(:each) do263allow_any_instance_of(::Msf::Auxiliary::Scanner).to receive(:check) do |mod|264mod.fail_with(Msf::Module::Failure::UnexpectedReply, 'Expected failure reason')265end266end267268it 'returns the error results' do269create_job270expect(last_response).to be_ok271expect(last_json_response).to include(a_valid_result_uuid)272273uuid = last_json_response['result']['uuid']274275wait_for_expect do276get_job_results(uuid)277278expect(last_response).to be_ok279expect_error_status(last_json_response)280end281282expected_error_response = {283result: {284status: 'errored',285error: 'unexpected-reply: Expected failure reason'286}287}288expect(last_json_response).to include(expected_error_response)289end290end291292context 'when the check command has an unexpected error' do293include_context 'Msf::Framework#threads cleaner'294295before(:each) do296allow_any_instance_of(::Msf::Auxiliary::Scanner).to receive(:check) do297raise 'Unexpected module error'298end299end300301it 'returns the error results' do302create_job303expect(last_response).to be_ok304expect(last_json_response).to include(a_valid_result_uuid)305306uuid = last_json_response['result']['uuid']307308wait_for_expect do309get_job_results(uuid)310311expect(last_response).to be_ok312expect_error_status(last_json_response)313end314315expected_error_response = {316result: {317status: 'errored',318error: "Unexpected module error"319}320}321expect(last_json_response).to include(expected_error_response)322end323end324325context 'when there is a sinatra level application error in the development environment' do326before(:each) do327allow_any_instance_of(Msf::RPC::JSON::Dispatcher).to receive(:process).and_raise(Exception, 'Sinatra level exception raised')328mock_rack_env('development')329end330331it 'returns the error results' do332create_job333334expect(last_response).to be_server_error335expected_error_response = {336error: {337code: -32000,338data: {339backtrace: include(a_kind_of(String))340},341message: 'Application server error: Sinatra level exception raised'342},343id: 1344}345expect(last_json_response).to include(expected_error_response)346end347end348349context 'when rack middleware raises an error in the development environment' do350before(:each) do351allow_any_instance_of(::Rack::Protection::AuthenticityToken).to receive(:accepts?).and_raise(Exception, 'Middleware error raised')352mock_rack_env('development')353end354355it 'returns the error results' do356create_job357358expect(last_response).to be_server_error359expected_error_response = {360error: {361code: -32000,362data: {363backtrace: include(a_kind_of(String))364},365message: 'Application server error: Middleware error raised'366},367id: 1368}369expect(last_json_response).to include(expected_error_response)370end371end372373context 'when rack middleware raises an error in the production environment' do374before(:each) do375allow_any_instance_of(::Rack::Protection::AuthenticityToken).to receive(:accepts?).and_raise(Exception, 'Middleware error raised')376mock_rack_env('production')377end378379it 'returns the error results' do380create_job381382expect(last_response).to be_server_error383expected_error_response = {384error: {385code: -32000,386message: 'Application server error: Middleware error raised'387},388id: 1389}390expect(last_json_response).to include(expected_error_response)391end392end393394context 'when there is a sinatra level application error in the production environment' do395before(:each) do396allow_any_instance_of(Msf::RPC::JSON::Dispatcher).to receive(:process).and_raise(Exception, 'Sinatra level exception raised')397mock_rack_env('production')398end399400it 'returns the error results' do401create_job402403expect(last_response).to be_server_error404expected_error_response = {405error: {406code: -32000,407message: 'Application server error: Sinatra level exception raised'408},409id: 1410}411expect(last_json_response).to include(expected_error_response)412end413end414end415416describe 'analyze' do417let(:host_ip) { Faker::Internet.private_ip_v4_address }418let(:host) do419{420workspace: 'default',421host: host_ip,422state: 'alive',423os_name: 'Windows',424os_flavor: 'Enterprize',425os_sp: 'SP2',426os_lang: 'English',427arch: 'ARCH_X86',428mac: '97-42-51-F2-A7-A7',429scope: 'eth2',430virtual_host: 'VMWare'431}432end433434let(:vuln) do435{436workspace: 'default',437host: host_ip,438name: 'Exploit Name',439info: 'Human readable description of the vuln',440refs: vuln_refs441}442end443444context 'when there are modules available' do445let(:vuln_refs) do446%w[447CVE-2017-0143448]449end450451before(:each) do452framework.modules.add_module_path('./modules')453end454455context 'with no options' do456it 'returns the list of known modules associated with a reported host' do457report_host(host)458expect(last_response).to be_ok459460report_vuln(vuln)461expect(last_response).to be_ok462463expected_response = {464jsonrpc: '2.0',465result: {466host: {467address: host_ip,468modules: [469{470mname: "exploit/windows/smb/ms17_010_eternalblue",471mtype: "exploit",472options: {473invalid: [],474missing: [],475},476state: "READY_FOR_TEST",477description: "ready for testing"478},479{480mname: "exploit/windows/smb/ms17_010_psexec",481mtype: "exploit",482options: {483invalid: [],484missing: [ "credential" ],485},486state: "REQUIRES_CRED",487description: "credentials are required"488},489{490mname: "exploit/windows/smb/smb_doublepulsar_rce",491mtype: "exploit",492options: {493invalid: [],494missing: [],495},496state: "READY_FOR_TEST",497description: "ready for testing"498}499]500}501},502id: 1503}504505analyze_host(506{507workspace: 'default',508host: host_ip509}510)511expect(last_json_response).to include(expected_response)512end513end514515context 'when payloads requirements are specified' do516it 'returns the list of known modules associated with a reported host' do517report_host(host)518expect(last_response).to be_ok519520report_vuln(vuln)521expect(last_response).to be_ok522523# Note: Currently the API doesn't return any differentiating output that a particular module is suitable524# with the requested payload525expected_response = {526jsonrpc: '2.0',527result: {528host: {529address: host_ip,530modules: [531{532mname: "exploit/windows/smb/ms17_010_eternalblue",533mtype: "exploit",534options: {535invalid: [],536missing: [ "payload_match" ],537},538state: "MISSING_PAYLOAD",539description: "none of the requested payloads match"540},541{542mname: "exploit/windows/smb/ms17_010_psexec",543mtype: "exploit",544options: {545invalid: [],546missing: [ "credential", "payload_match" ],547},548state: "REQUIRES_CRED",549description: "credentials are required, none of the requested payloads match"550},551{552mname: "exploit/windows/smb/smb_doublepulsar_rce",553mtype: "exploit",554options: {555invalid: [],556missing: ["payload_match"],557},558state: "MISSING_PAYLOAD",559description: "none of the requested payloads match"560}561]562}563},564id: 1565}566567analyze_host(568{569workspace: 'default',570host: host_ip,571analyze_options: {572payloads: [573'windows/meterpreter_reverse_http'574]575}576}577)578expect(last_json_response).to include(expected_response)579end580end581end582583context 'when there are no modules found' do584let(:vuln_refs) do585['CVE-NO-MATCHING-MODULES-1234']586end587588it 'returns an empty list of modules' do589report_host(host)590expect(last_response).to be_ok591592report_vuln(vuln)593expect(last_response).to be_ok594595expected_response = {596jsonrpc: '2.0',597result: {598host: {599address: host_ip,600modules: []601}602},603id: 1604}605606analyze_host(607{608workspace: 'default',609host: host_ip610}611)612expect(last_json_response).to include(expected_response)613end614end615end616end617618619