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

LDAP capture capabilities #18678

Merged
merged 25 commits into from Feb 15, 2024
Merged

LDAP capture capabilities #18678

merged 25 commits into from Feb 15, 2024

Conversation

jmartin-tech
Copy link
Contributor

Integrates support for LDAP capture of simple and NTLM authentication attempts.

  • supplies a default implementation for BindRequest, SearchRequest, UnbindRequest, and a default action for unsupported requests in the server lib
  • adds library support to allow future extension of supported auth patterns
  • module defaults require privileges access to open port 389

Many thanks to GSoC Contributor @JustAnda7!

Verification

List the steps needed to make sure this thing works

  • Start msfconsole
  • use capture/ldap
  • run
  • Verify nmap brute force logs attempts
    nmap -p 389 --script /usr/share/nmap/scripts/ldap-brute.nse --script-args ldap.base='"cn=users,dc=example,dc=com"' 127.0.0.1 -Pn -d
  • Verify ldapsearch reports simple auth attempt
    ldapsearch -LLL -H ldap://127.0.0.1 -D admin@example.com -W
  • Verify ldapsearch reports attempt NTLM attempt
    ldapsearch -H ldap://127.0.0.1 -Y ntlm -U admin -b 'dc=example,dc=com'

@jmartin-tech jmartin-tech added module library GSoC Google Summer of Code project PRs labels Jan 9, 2024
Comment on lines +45 to +48
OptString.new('Domain', [ false, 'The default domain to use for NTLM authentication', 'DOMAIN']),
OptString.new('Server', [ false, 'The default server to use for NTLM authentication', 'SERVER']),
OptString.new('DnsName', [ false, 'The default DNS server name to use for NTLM authentication', 'SERVER']),
OptString.new('DnsDomain', [ false, 'The default DNS domain name to use for NTLM authentication', 'example.com']),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these options be required?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No there are defaults provided and these would not be used at all during a simple auth attempt.

spec/lib/rex/proto/ldap/auth_spec.rb Outdated Show resolved Hide resolved
# @param ctx [Hash] Framework context for sockets
# @param dblock [Proc] Handler for :dispatch_request flow control interception
# @param sblock [Proc] Handler for :send_response flow control interception
#
# @return [Rex::Proto::LDAP::Server] LDAP Server object
def initialize(lhost = '0.0.0.0', lport = 389, udp = true, tcp = true, ldif = nil, comm = nil, ctx = {}, dblock = nil, sblock = nil)
def initialize(lhost = '0.0.0.0', lport = 389, udp = true, tcp = true, ldif = nil, comm = nil, auth_provider = nil, ctx = {}, dblock = nil, sblock = nil)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't had a chance to dive through the code fully yet to see if the existing callsites are impacted by this change; is it worth adding this new argument to the end to make sure it's a backwards compatible API change either way

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is currently only one caller "the module mixin", in theory auth will be consistently extended in the rex class however this offers a place to inject a different provider. I don't see a strong reason to avoid it, especially since these are all named parameters.

* clarify the NTLM SASL challenge
* add default case for unsuppoted SASL types
* implement unknown method to support override
@adfoster-r7
Copy link
Contributor

adfoster-r7 commented Jan 23, 2024

I still haven't had the time to run through this properly yet, I've only done a rough pass a few days ago now - but everything worked well for the scenarios I did test 👍

Here's the notes I did take at the time, feel free to ignore until a fuller test has been completed though 👍


  1. It'd be good to update the plugin capture module list to include this functionality

modules = {
# Capturing
'DRDA' => 'auxiliary/server/capture/drda',
'FTP' => 'auxiliary/server/capture/ftp',
'IMAP' => 'auxiliary/server/capture/imap',
'MSSQL' => 'auxiliary/server/capture/mssql',
'MySQL' => 'auxiliary/server/capture/mysql',
'POP3' => 'auxiliary/server/capture/pop3',
'Postgres' => 'auxiliary/server/capture/postgresql',
'PrintJob' => 'auxiliary/server/capture/printjob_capture',
'SIP' => 'auxiliary/server/capture/sip',
'SMB' => 'auxiliary/server/capture/smb',
'SMTP' => 'auxiliary/server/capture/smtp',
'Telnet' => 'auxiliary/server/capture/telnet',
'VNC' => 'auxiliary/server/capture/vnc',

That way it should get wired up to the capture plugin https://github.com/rapid7/metasploit-framework/blob/54788067ffb5391bf987986a8549cb1b4a066453/docs/metasploit-framework.wiki/How-To-Use-Plugins.md#capture-plugin 🤞


  1. Is it possible to have this behave in the same way as the other capture modules? i.e. I believe they're backgrounded by default

ftp example:

msf6 auxiliary(server/capture/ftp) > run
[*] Auxiliary module running as background job 0.

[*] Started service listener on 0.0.0.0:21 
[*] Server started.

msf6 auxiliary(server/capture/ftp) > jobs

Jobs
====

  Id  Name                           Payload  Payload opts
  --  ----                           -------  ------------
  0   Auxiliary: server/capture/ftp

This module:

msf6 auxiliary(server/capture/ldap) > run
[*] Server started.

... hangs ...

  1. Will there be support for LDAPS (port 636)? 👀

@jmartin-tech
Copy link
Contributor Author

jmartin-tech commented Jan 23, 2024

I will look into item 1, in theory the metadata should already ensure this be passive stance and I had some back and forth attempting to iron that out before. I see some inconsistency in the current initialize and the Action key should be Actions, when adjusted this did not resolve the issue, still looking for cause. Found and fixed in commit bcefde2

Will also see about adding to item 2.

As to item 3, LDAPS support I would defer to a future iteration. More work is needed in the server lib to enable that support due to default behavior to start both TCP and UDP services and ensure a certificate is offered that at least looks viable for the service.

@adfoster-r7
Copy link
Contributor

adfoster-r7 commented Feb 1, 2024

Thanks for the fixes 🎉

I did another pass and everything looks good; just these two bits I spotted

  1. I haven't verified, but I'm guessing the comm config isn't wired up quite right here in comparison to some of the other capture modules:
msf6 auxiliary(admin/kerberos/forge_ticket) > load capture
msf6 auxiliary(admin/kerberos/forge_ticket) > captureg start --ip 192.168.123.1
...
msf6 auxiliary(admin/kerberos/forge_ticket) > features set manager_commands true 
msf6 auxiliary(admin/kerberos/forge_ticket) > _servicemanager 
Services
========

 Id  Name                                                                References
 --  ----                                                                ----------
 0   HTTP Server                                                         2
 1   LDAP Server                                                         2
 2   Rex::Proto::Http::Server443-192.168.123.1-Rex::Socket::Comm::Local  2
 3   Rex::Proto::Http::Server80-192.168.123.1-Rex::Socket::Comm::Local   2
 4   Rex::Proto::LDAP::Server192.168.123.1-389-                          2

  1. Testing ldap with a kerberos ticket retrieved from a domain controller:
msf6 auxiliary(gather/ldap_query) > run action=ENUM_ACCOUNTS rhost=127.0.0.1 username=vagrant password=vagrant ldap::auth=kerberos ldap::rhostname=dc01.demo.local domain=demo.local domaincontrollerrhost=192.168.123.132
[*] Running module against 127.0.0.1

[+] 192.168.123.132:88 - Received a valid TGT-Response
[*] 127.0.0.1:389 - TGT MIT Credential Cache ticket saved to /Users/user/.msf4/loot/20240201111755_default_192.168.123.132_mit.kerberos.cca_196991.bin
[+] 192.168.123.132:88 - Received a valid TGS-Response
[*] 127.0.0.1:389 - TGS MIT Credential Cache ticket saved to /Users/user/.msf4/loot/20240201111755_default_192.168.123.132_mit.kerberos.cca_438765.bin
[+] 192.168.123.132:88 - Received a valid delegation TGS-Response
[-] Auxiliary aborted due to failure: unexpected-reply: Could not query 127.0.0.1! Error was: no bind result
[*] Auxiliary module execution completed

And stacktrace from the logs:

[02/01/2024 11:17:55] [e(0)] core: Error in stream server client monitor: uninitialized constant Rex::Proto::LDAP::Auth::SUPPORTS_SASL

Call stack:
/Users/user/Documents/code/metasploit-framework/lib/rex/proto/ldap/auth.rb:170:in `sasl?'
/Users/user/Documents/code/metasploit-framework/lib/rex/proto/ldap/auth.rb:41:in `process_login_request'
/Users/user/Documents/code/metasploit-framework/lib/rex/proto/ldap/server.rb:178:in `default_dispatch_request'
/Users/user/Documents/code/metasploit-framework/lib/msf/core/exploit/remote/ldap/server.rb:63:in `on_dispatch_request'
/Users/user/Documents/code/metasploit-framework/lib/msf/core/exploit/remote/ldap/server.rb:99:in `block in start_service'
/Users/user/Documents/code/metasploit-framework/lib/rex/proto/ldap/server.rb:147:in `dispatch_request'
/Users/user/Documents/code/metasploit-framework/lib/rex/proto/ldap/server.rb:396:in `on_client_data'
/Users/user/Documents/code/metasploit-framework/lib/rex/proto/ldap/server.rb:105:in `block in start'
/Users/user/.rvm/gems/ruby-3.0.5@metasploit-framework/gems/rex-core-0.1.31/lib/rex/io/stream_server.rb:42:in `on_client_data'
/Users/user/.rvm/gems/ruby-3.0.5@metasploit-framework/gems/rex-core-0.1.31/lib/rex/io/stream_server.rb:185:in `block in monitor_clients'
/Users/user/.rvm/gems/ruby-3.0.5@metasploit-framework/gems/rex-core-0.1.31/lib/rex/io/stream_server.rb:184:in `each'
/Users/user/.rvm/gems/ruby-3.0.5@metasploit-framework/gems/rex-core-0.1.31/lib/rex/io/stream_server.rb:184:in `monitor_clients'
/Users/user/.rvm/gems/ruby-3.0.5@metasploit-framework/gems/rex-core-0.1.31/lib/rex/io/stream_server.rb:64:in `block in start'
/Users/user/Documents/code/metasploit-framework/lib/rex/thread_factory.rb:22:in `block in spawn'
/Users/user/Documents/code/metasploit-framework/lib/msf/core/thread_manager.rb:105:in `block in spawn'

If you don't have a DC setup, I think this should work to replicate things without needing to set up one:

Forge a ldap silver ticket:

msf6 > use admin/kerberos/forge_ticket
msf6 auxiliary(admin/kerberos/forge_ticket) > run action=FORGE_SILVER domain=demo.local domain_sid=S-1-5-21-1242350107-3695253863-3717863007 nthash=7523e2e60c59bd33337b0a2fd3452b59 user=Administrator spn=ldap/dc01.demo.local

[*] TGS MIT Credential Cache ticket saved to /Users/user/.msf4/loot/20240201114608_default_unknown_mit.kerberos.cca_721599.bin <----- Grab this path for later
[*] Primary Principal: Administrator@DEMO.LOCAL
Ccache version: 4

Run the capture module:

msf6 > use server/capture/ldap
msf6 auxiliary(server/capture/ldap) > run

Run the ldap query module with the forged ticket - updating the ticket path:

msf6 > use gather/ldap_query
msf6 auxiliary(gather/ldap_query) > rerun action=ENUM_ACCOUNTS rhost=127.0.0.1 username=Administrator ldap::auth=kerberos ldap::rhostname=dc01.demo.local domain=demo.local domaincontrollerrhost=127.0.0.1 ldap::krb5ccname=/Users/user/.msf4/loot/PATH_FROM_PREVIOUS_TICKET_FORGING_RESULT

...
[*] Loaded a credential from ticket file: /Users/user/.msf4/loot/20240201114200_default_unknown_mit.kerberos.cca_640279.bin
[-] Auxiliary aborted due to failure: unexpected-reply: Could not query 127.0.0.1! Error was: no bind result
[*] Auxiliary module execution completed

And the stacktrace was in tail -f ~/.msf4/logs/framework.log for me, iirc I have verbose logging on too

* support mechanism reported as NTLM or GSS-SPNEGO
* return ResultCodeAuthMethodNotSupported for unknown bindRequest auth
@jmartin-tech
Copy link
Contributor Author

@adfoster-r7 stacktrace is addressed in e5b5f12, I also pushed another change to address a stacktrace in the server that attempted to access an incorrect variable when the listener fails a read.

The service manager comm connection however does not look to be an artifact of these changes and will need some time to track down. The HTTP service is much more mature so I am not sure the LDAP services should match it at this time, further investigation may be more appropriate as separate work.

@adfoster-r7
Copy link
Contributor

Awesome; Looks like Kerberos is handled and logged now - thanks! 🎉

msf6 auxiliary(gather/ldap_query) > run action=ENUM_ACCOUNTS rhost=127.0.0.1 username=vagrant password=vagrant ldap::auth=kerberos ldap::rhostname=dc01.demo.local domain=demo.local domaincontrollerrhost=192.168.123.132
[*] Running module against 127.0.0.1

[+] 192.168.123.132:88 - Received a valid TGT-Response
[*] 127.0.0.1:389 - TGT MIT Credential Cache ticket saved to /Users/user/.msf4/loot/20240215192246_default_192.168.123.132_mit.kerberos.cca_436701.bin
[+] 192.168.123.132:88 - Received a valid TGS-Response
[*] 127.0.0.1:389 - TGS MIT Credential Cache ticket saved to /Users/user/.msf4/loot/20240215192246_default_192.168.123.132_mit.kerberos.cca_739445.bin
[+] 192.168.123.132:88 - Received a valid delegation TGS-Response
[-] Invalid LDAP Login Attempt => Unsupported SASL Format
[-] Auxiliary aborted due to failure: no-target: Target does not support the simple authentication mechanism!
[*] Auxiliary module execution completed

@adfoster-r7 adfoster-r7 merged commit 7b56d01 into master Feb 15, 2024
102 of 104 checks passed
@adfoster-r7
Copy link
Contributor

Release Notes

Adds a new auxiliary/server/capture/ldap module that emulates an LDAP Server. The server accepts a user's bind request, and the user credentials or NTLM hash is then captured, logged, and persisted to the currently active database. An ldap_bind: Authentication method not supported (7) error is sent to the connecting client.

@adfoster-r7
Copy link
Contributor

adfoster-r7 commented Feb 15, 2024

Thanks @jmartin-tech and @JustAnda7! 🎉

Let me know if I've missed anything in the release notes above, happy to change them if need be. These changes will be picked up and available in next week's Thursday release

@adfoster-r7 adfoster-r7 added the rn-modules release notes for new or majorly enhanced modules label Feb 15, 2024
@adfoster-r7
Copy link
Contributor

Pull request additionally announced as part of this weekly wrapup post - https://www.rapid7.com/blog/post/2024/02/23/metasploit-weekly-wrap-up-02-23-2024/ - thanks! 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
GSoC Google Summer of Code project PRs library module rn-modules release notes for new or majorly enhanced modules
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants