Merge pull request #21323 from jheysel-r7/feat/http_to_ldap

HTTP to LDAP Relay Module
This commit is contained in:
Spencer McIntyre
2026-04-29 15:20:10 -04:00
committed by GitHub
6 changed files with 921 additions and 1 deletions
@@ -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