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 protocol enhancements #4667

Merged
merged 18 commits into from
Feb 5, 2024
Merged
Changes from 4 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 110 additions & 23 deletions pkg/js/libs/ldap/ldap.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,62 +2,149 @@

import (
"context"
"crypto/tls"
"fmt"
"strings"
"time"

"github.com/go-ldap/ldap/v3"
"github.com/praetorian-inc/fingerprintx/pkg/plugins"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"

pluginldap "github.com/praetorian-inc/fingerprintx/pkg/plugins/services/ldap"
)

// Client is a client for ldap protocol in golang.
//
// It is a wrapper around the standard library ldap package.
type LdapClient struct{}
type LdapClient struct {
BaseDN string
Realm string
Host string
Conn *ldap.Conn
Port int
UseSSL bool
TLS bool
}

// IsLdap checks if the given host and port are running ldap server.
func (c *LdapClient) IsLdap(host string, port int) (bool, error) {
// Connect is a method for LdapClient that stores information about of the ldap
// connection, tests it and verifies that the server is a valid ldap server
//
// returns the success status
func (c *LdapClient) Connect(host string, port int, ssl, istls bool) (bool, error) {
if c.Conn != nil {
return true, nil
}

if !protocolstate.IsHostAllowed(host) {
// host is not valid according to network policy
return false, protocolstate.ErrHostDenied.Msgf(host)
}

timeout := 10 * time.Second

conn, err := protocolstate.Dialer.Dial(context.TODO(), "tcp", fmt.Sprintf("%s:%d", host, port))

var err error
if ssl {
config := &tls.Config{
InsecureSkipVerify: true,
Fixed Show fixed Hide fixed
ServerName: host,
}
c.Conn, err = ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", host, port), config)
} else {
c.Conn, err = ldap.Dial("tcp", fmt.Sprintf("%s:%d", host, port))
}
if err != nil {
return false, err
}
defer conn.Close()

_ = conn.SetDeadline(time.Now().Add(timeout))
if istls && !ssl {
// Here if it is not a valid ldap server, the StartTLS will return an error,
// so, if this check succeeds, there is no need to check if the host is has an LDAP Server:
// https://github.com/go-ldap/ldap/blob/cdb0754f666833c3e287503ed52d535a41ba10f6/v3/conn.go#L334
if err := c.Conn.StartTLS(&tls.Config{InsecureSkipVerify: true}); err != nil {
Fixed Show fixed Hide fixed
return false, err
}
}

plugin := &pluginldap.LDAPPlugin{}
service, err := plugin.Run(conn, timeout, plugins.Target{Host: host})
if err != nil {
return false, err
c.Host = host
c.Port = port
c.TLS = istls
c.UseSSL = ssl
return true, nil
}

func (c *LdapClient) Authenticate(realm string, username, password string) (bool, error) {
if c.Conn == nil {
return false, fmt.Errorf("no existing connection")
}
if service == nil {
return false, nil

c.Realm = realm
c.BaseDN = fmt.Sprintf("dc=%s", strings.Join(strings.Split(realm, "."), ",dc="))

if err := c.Conn.NTLMBind(realm, username, password); err == nil {
// if bind with NTLMBind(), there is nothing
// else to do, you are authenticated
return true, nil
}

switch password {
case "":
if err := c.Conn.UnauthenticatedBind(username); err != nil {
return false, err
}
default:
if err := c.Conn.Bind(username, password); err != nil {
return false, err
}
}
return true, nil
}

func (c *LdapClient) AuthenticateWithNTLMHash(realm string, username, hash string) (bool, error) {
if c.Conn == nil {
return false, fmt.Errorf("no existing connection")
}
c.Realm = realm
c.BaseDN = fmt.Sprintf("dc=%s", strings.Join(strings.Split(realm, "."), ",dc="))
if err := c.Conn.NTLMBindWithHash(realm, username, hash); err != nil {
return false, err
}
return true, nil
}

// Search is a method that uses the already Connect()'ed client to query the LDAP
// server, works for openldap and for Microsoft's Active Directory Ldap
//
// accepts whatever filter and returns a list of maps having provided attributes
// as keys and associated values mirroring the ones returned by ldap
func (c *LdapClient) Search(filter string, attributes ...string) ([]map[string][]string, error) {
res, err := c.Conn.Search(ldap.NewSearchRequest(
c.BaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases,
0, 0, false, filter, attributes, nil,
))
if err != nil {
return nil, err
}

if len(res.Entries) == 0 {
return nil, fmt.Errorf("no result found in search")
}

var out []map[string][]string
for _, r := range res.Entries {
app := make(map[string][]string)
for _, a := range attributes {
app[a] = r.GetAttributeValues(a)
}
out = append(out, app)
}
return out, nil
}

// CollectLdapMetadata collects metadata from ldap server.
func (c *LdapClient) CollectLdapMetadata(domain string, controller string) (LDAPMetadata, error) {
opts := &ldapSessionOptions{
domain: domain,
domainController: controller,
}

if !protocolstate.IsHostAllowed(domain) {
if !protocolstate.IsHostAllowed(controller) {
// host is not valid according to network policy
return LDAPMetadata{}, protocolstate.ErrHostDenied.Msgf(domain)
return LDAPMetadata{}, protocolstate.ErrHostDenied.Msgf(controller)
}

conn, err := c.newLdapSession(opts)
Expand Down Expand Up @@ -230,9 +317,9 @@
password: password,
}

if !protocolstate.IsHostAllowed(domain) {
if !protocolstate.IsHostAllowed(controller) {
// host is not valid according to network policy
return nil, protocolstate.ErrHostDenied.Msgf(domain)
return nil, protocolstate.ErrHostDenied.Msgf(controller)
}

conn, err := c.newLdapSession(opts)
Expand Down
Loading