Merge pull request #21323 from jheysel-r7/feat/http_to_ldap
HTTP to LDAP Relay Module
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
require 'spec_helper'
|
||||
require 'msf/core/exploit/remote/http_server/relay'
|
||||
require 'net/ntlm'
|
||||
require 'windows_error'
|
||||
require 'base64'
|
||||
|
||||
RSpec.describe Msf::Exploit::Remote::HttpServer::Relay do
|
||||
let(:client_ip) { '172.16.199.159' }
|
||||
let(:client_port) { 54321 }
|
||||
let(:client_id) { Rex::Socket.to_authority(client_ip, client_port) }
|
||||
|
||||
def create_request(auth_header = nil)
|
||||
req = Rex::Proto::Http::Request.new
|
||||
req.method = 'GET'
|
||||
req.headers['Authorization'] = auth_header if auth_header
|
||||
req
|
||||
end
|
||||
|
||||
let(:mock_cli) do
|
||||
cli = double('Rex::Proto::Http::ServerClient')
|
||||
allow(cli).to receive(:peerhost).and_return(client_ip)
|
||||
allow(cli).to receive(:peerport).and_return(client_port)
|
||||
allow(cli).to receive(:keepalive=)
|
||||
allow(cli).to receive(:put)
|
||||
allow(cli).to receive(:send_response)
|
||||
cli
|
||||
end
|
||||
|
||||
let(:mock_target) do
|
||||
double('Target',
|
||||
protocol: :ldap,
|
||||
ip: '172.16.199.200',
|
||||
port: 389,
|
||||
drop_mic_and_sign_key_exch_flags: false,
|
||||
drop_mic_only: false
|
||||
)
|
||||
end
|
||||
|
||||
let(:target_list) do
|
||||
list = double('Msf::Exploit::Remote::Relay::TargetList')
|
||||
allow(list).to receive(:next).and_return(mock_target)
|
||||
allow(list).to receive(:on_relay_end)
|
||||
list
|
||||
end
|
||||
|
||||
let(:type2_msg) { double('Type2', serialize: 'TYPE2_BYTES') }
|
||||
let(:type1_relay_result) { double('RelayResult', nt_status: WindowsError::NTStatus::STATUS_MORE_PROCESSING_REQUIRED, message: type2_msg) }
|
||||
let(:type3_success_result) { double('RelayResult', nt_status: WindowsError::NTStatus::STATUS_SUCCESS) }
|
||||
let(:type3_fail_result) { double('RelayResult', nt_status: WindowsError::NTStatus::STATUS_LOGON_FAILURE) }
|
||||
|
||||
let(:mock_ldap_client) do
|
||||
client = double('LDAPClient', target: mock_target)
|
||||
allow(client).to receive(:relay_ntlmssp_type1).and_return(type1_relay_result)
|
||||
allow(client).to receive(:relay_ntlmssp_type3).and_return(type3_success_result)
|
||||
allow(client).to receive(:disconnect!)
|
||||
client
|
||||
end
|
||||
|
||||
let(:relay_class) do
|
||||
Class.new(Msf::Auxiliary) do
|
||||
include Msf::Exploit::Remote::HttpServer
|
||||
include Msf::Exploit::Remote::HttpServer::Relay
|
||||
|
||||
def initialize(info = {})
|
||||
super
|
||||
end
|
||||
|
||||
def relay_targets; end
|
||||
def on_relay_success(relay_connection:, relay_identity:); end
|
||||
def on_ntlm_type3(args); end
|
||||
|
||||
def print_status(msg); end
|
||||
def print_error(msg); end
|
||||
def print_good(msg); end
|
||||
def print_warning(msg); end
|
||||
def vprint_status(msg); end
|
||||
def vprint_error(msg); end
|
||||
def elog(msg, error: nil); end
|
||||
end
|
||||
end
|
||||
|
||||
subject(:relay_server) do
|
||||
mod = relay_class.new
|
||||
mod.instance_variable_set(:@logger, mod)
|
||||
allow(mod).to receive(:relay_targets).and_return(target_list)
|
||||
mod
|
||||
end
|
||||
|
||||
let(:type1_bytes) { "NTLMSSP\x00TYPE1" }
|
||||
let(:type3_bytes) { "NTLMSSP\x00TYPE3" }
|
||||
|
||||
let(:type1_b64) { Base64.strict_encode64(type1_bytes) }
|
||||
let(:type3_b64) { Base64.strict_encode64(type3_bytes) }
|
||||
|
||||
let(:type1_msg) do
|
||||
msg = Net::NTLM::Message::Type1.new
|
||||
allow(msg).to receive(:serialize).and_return(type1_bytes)
|
||||
msg
|
||||
end
|
||||
|
||||
let(:type3_msg) do
|
||||
msg = Net::NTLM::Message::Type3.new
|
||||
allow(msg).to receive(:serialize).and_return(type3_bytes)
|
||||
allow(msg).to receive(:domain).and_return('DOMAIN')
|
||||
allow(msg).to receive(:user).and_return('USER')
|
||||
msg
|
||||
end
|
||||
|
||||
before(:each) do
|
||||
allow(Net::NTLM::Message).to receive(:parse).with(type1_bytes).and_return(type1_msg)
|
||||
allow(Net::NTLM::Message).to receive(:parse).with(type3_bytes).and_return(type3_msg)
|
||||
allow(Msf::Exploit::Remote::Relay::NTLM::Target::LDAP::Client).to receive(:create).and_return(mock_ldap_client)
|
||||
end
|
||||
|
||||
def get_client_state(server)
|
||||
clients = server.instance_variable_get(:@relay_clients) || {}
|
||||
clients[client_id]
|
||||
end
|
||||
|
||||
describe 'State Transitions' do
|
||||
context 'when receiving an initial unauthenticated request' do
|
||||
it 'responds with a 401 and tracks state as unauthenticated' do
|
||||
expect(mock_cli).to receive(:put).with(/401 Unauthorized/)
|
||||
|
||||
relay_server.on_relay_request(mock_cli, create_request)
|
||||
|
||||
client = get_client_state(relay_server)
|
||||
expect(client).not_to be_nil
|
||||
expect(client.state).to eq(:unauthenticated)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when receiving a Type 1 message' do
|
||||
let(:req1) { create_request("NTLM #{type1_b64}") }
|
||||
|
||||
it 'relays to LDAP, sends Type 2 challenge, and transitions state to awaiting_type3' do
|
||||
expect(mock_cli).to receive(:send_response).with(satisfy { |res| res.code == 401 })
|
||||
|
||||
relay_server.on_relay_request(mock_cli, req1)
|
||||
|
||||
client = get_client_state(relay_server)
|
||||
expect(client).not_to be_nil
|
||||
expect(client.state).to eq(:awaiting_type3)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Target Iteration and Exhaustion' do
|
||||
let(:req1) { create_request("NTLM #{type1_b64}") }
|
||||
let(:req3) { create_request("NTLM #{type3_b64}") }
|
||||
|
||||
before(:each) do
|
||||
relay_server.on_relay_request(mock_cli, req1)
|
||||
end
|
||||
|
||||
context 'when LDAP authentication succeeds' do
|
||||
it 'calls on_relay_success and redirects to the next target' do
|
||||
expect(relay_server).to receive(:on_relay_success)
|
||||
expect(mock_cli).to receive(:send_response).with(satisfy { |res| res.code == 307 })
|
||||
|
||||
relay_server.on_relay_request(mock_cli, req3)
|
||||
|
||||
client = get_client_state(relay_server)
|
||||
expect(client.state).to eq(:unauthenticated)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when LDAP authentication fails' do
|
||||
before(:each) do
|
||||
allow(mock_ldap_client).to receive(:relay_ntlmssp_type3).and_return(type3_fail_result)
|
||||
end
|
||||
|
||||
it 'redirects to the next target and resets state to unauthenticated' do
|
||||
expect(mock_cli).to receive(:send_response).with(satisfy { |res| res.code == 307 })
|
||||
|
||||
relay_server.on_relay_request(mock_cli, req3)
|
||||
|
||||
client = get_client_state(relay_server)
|
||||
expect(client.state).to eq(:unauthenticated)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the target list is completely exhausted' do
|
||||
before(:each) do
|
||||
allow(target_list).to receive(:next).and_return(nil)
|
||||
end
|
||||
|
||||
it 'sends a 404 and garbage collects the client state entirely' do
|
||||
expect(mock_cli).to receive(:send_response).with(satisfy { |res| res.code == 404 })
|
||||
|
||||
relay_server.on_relay_request(mock_cli, req3)
|
||||
|
||||
client = get_client_state(relay_server)
|
||||
expect(client).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user