Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/pkg/js/libs/ldap/ldap.go
2070 views
1
package ldap
2
3
import (
4
"context"
5
"crypto/tls"
6
"fmt"
7
"net"
8
"net/url"
9
"strings"
10
11
"github.com/Mzack9999/goja"
12
"github.com/go-ldap/ldap/v3"
13
"github.com/projectdiscovery/nuclei/v3/pkg/js/utils"
14
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
15
)
16
17
type (
18
// Client is a client for ldap protocol in nuclei
19
// @example
20
// ```javascript
21
// const ldap = require('nuclei/ldap');
22
// // here ldap.example.com is the ldap server and acme.com is the realm
23
// const client = new ldap.Client('ldap://ldap.example.com', 'acme.com');
24
// ```
25
// @example
26
// ```javascript
27
// const ldap = require('nuclei/ldap');
28
// const cfg = new ldap.Config();
29
// cfg.Timeout = 10;
30
// cfg.ServerName = 'ldap.internal.acme.com';
31
// // optional config can be passed as third argument
32
// const client = new ldap.Client('ldap://ldap.example.com', 'acme.com', cfg);
33
// ```
34
Client struct {
35
Host string // Hostname
36
Port int // Port
37
Realm string // Realm
38
BaseDN string // BaseDN (generated from Realm)
39
40
// unexported
41
nj *utils.NucleiJS // nuclei js utils
42
conn *ldap.Conn
43
cfg Config
44
}
45
)
46
47
type (
48
// Config is extra configuration for the ldap client
49
// @example
50
// ```javascript
51
// const ldap = require('nuclei/ldap');
52
// const cfg = new ldap.Config();
53
// cfg.Timeout = 10;
54
// cfg.ServerName = 'ldap.internal.acme.com';
55
// cfg.Upgrade = true; // upgrade to tls
56
// ```
57
Config struct {
58
// Timeout is the timeout for the ldap client in seconds
59
Timeout int
60
ServerName string // default to host (when using tls)
61
Upgrade bool // when true first connects to non-tls and then upgrades to tls
62
}
63
)
64
65
// Constructor for creating a new ldap client
66
// The following schemas are supported for url: ldap://, ldaps://, ldapi://,
67
// and cldap:// (RFC1798, deprecated but used by Active Directory).
68
// ldaps uses TLS/SSL, ldapi uses a Unix domain socket, and cldap uses connectionless LDAP.
69
// Constructor: constructor(public ldapUrl: string, public realm: string, public config?: Config)
70
func NewClient(call goja.ConstructorCall, runtime *goja.Runtime) *goja.Object {
71
// setup nucleijs utils
72
c := &Client{nj: utils.NewNucleiJS(runtime)}
73
c.nj.ObjectSig = "Client(ldapUrl,Realm,{Config})" // will be included in error messages
74
75
// get arguments (type assertion is efficient than reflection)
76
ldapUrl, _ := c.nj.GetArg(call.Arguments, 0).(string)
77
realm, _ := c.nj.GetArg(call.Arguments, 1).(string)
78
c.cfg = utils.GetStructTypeSafe[Config](c.nj, call.Arguments, 2, Config{})
79
c.Realm = realm
80
c.BaseDN = fmt.Sprintf("dc=%s", strings.Join(strings.Split(realm, "."), ",dc="))
81
82
// validate arguments
83
c.nj.Require(ldapUrl != "", "ldap url cannot be empty")
84
c.nj.Require(realm != "", "realm cannot be empty")
85
86
u, err := url.Parse(ldapUrl)
87
c.nj.HandleError(err, "invalid ldap url supported schemas are ldap://, ldaps://, ldapi://, and cldap://")
88
89
executionId := c.nj.ExecutionId()
90
dialers := protocolstate.GetDialersWithId(executionId)
91
if dialers == nil {
92
panic("dialers with executionId " + executionId + " not found")
93
}
94
95
var conn net.Conn
96
if u.Scheme == "ldapi" {
97
if u.Path == "" || u.Path == "/" {
98
u.Path = "/var/run/slapd/ldapi"
99
}
100
conn, err = dialers.Fastdialer.Dial(context.TODO(), "unix", u.Path)
101
c.nj.HandleError(err, "failed to connect to ldap server")
102
} else {
103
host, port, err := net.SplitHostPort(u.Host)
104
if err != nil {
105
// we assume that error is due to missing port
106
host = u.Host
107
port = ""
108
}
109
if u.Scheme == "" {
110
// default to ldap
111
u.Scheme = "ldap"
112
}
113
114
switch u.Scheme {
115
case "cldap":
116
if port == "" {
117
port = ldap.DefaultLdapPort
118
}
119
conn, err = dialers.Fastdialer.Dial(context.TODO(), "udp", net.JoinHostPort(host, port))
120
case "ldap":
121
if port == "" {
122
port = ldap.DefaultLdapPort
123
}
124
conn, err = dialers.Fastdialer.Dial(context.TODO(), "tcp", net.JoinHostPort(host, port))
125
case "ldaps":
126
if port == "" {
127
port = ldap.DefaultLdapsPort
128
}
129
serverName := host
130
if c.cfg.ServerName != "" {
131
serverName = c.cfg.ServerName
132
}
133
conn, err = dialers.Fastdialer.DialTLSWithConfig(context.TODO(), "tcp", net.JoinHostPort(host, port),
134
&tls.Config{InsecureSkipVerify: true, MinVersion: tls.VersionTLS10, ServerName: serverName})
135
default:
136
err = fmt.Errorf("unsupported ldap url schema %v", u.Scheme)
137
}
138
c.nj.HandleError(err, "failed to connect to ldap server")
139
}
140
c.conn = ldap.NewConn(conn, u.Scheme == "ldaps")
141
if u.Scheme != "ldaps" && c.cfg.Upgrade {
142
serverName := u.Hostname()
143
if c.cfg.ServerName != "" {
144
serverName = c.cfg.ServerName
145
}
146
if err := c.conn.StartTLS(&tls.Config{InsecureSkipVerify: true, ServerName: serverName}); err != nil {
147
c.nj.HandleError(err, "failed to upgrade to tls")
148
}
149
} else {
150
c.conn.Start()
151
}
152
153
return utils.LinkConstructor(call, runtime, c)
154
}
155
156
// Authenticate authenticates with the ldap server using the given username and password
157
// performs NTLMBind first and then Bind/UnauthenticatedBind if NTLMBind fails
158
// @example
159
// ```javascript
160
// const ldap = require('nuclei/ldap');
161
// const client = new ldap.Client('ldap://ldap.example.com', 'acme.com');
162
// client.Authenticate('user', 'password');
163
// ```
164
func (c *Client) Authenticate(username, password string) bool {
165
c.nj.Require(c.conn != nil, "no existing connection")
166
if c.BaseDN == "" {
167
c.BaseDN = fmt.Sprintf("dc=%s", strings.Join(strings.Split(c.Realm, "."), ",dc="))
168
}
169
if err := c.conn.NTLMBind(c.Realm, username, password); err == nil {
170
// if bind with NTLMBind(), there is nothing
171
// else to do, you are authenticated
172
return true
173
}
174
175
var err error
176
switch password {
177
case "":
178
if err = c.conn.UnauthenticatedBind(username); err != nil {
179
c.nj.ThrowError(err)
180
}
181
default:
182
if err = c.conn.Bind(username, password); err != nil {
183
c.nj.ThrowError(err)
184
}
185
}
186
return err == nil
187
}
188
189
// AuthenticateWithNTLMHash authenticates with the ldap server using the given username and NTLM hash
190
// @example
191
// ```javascript
192
// const ldap = require('nuclei/ldap');
193
// const client = new ldap.Client('ldap://ldap.example.com', 'acme.com');
194
// client.AuthenticateWithNTLMHash('pdtm', 'hash');
195
// ```
196
func (c *Client) AuthenticateWithNTLMHash(username, hash string) bool {
197
c.nj.Require(c.conn != nil, "no existing connection")
198
if c.BaseDN == "" {
199
c.BaseDN = fmt.Sprintf("dc=%s", strings.Join(strings.Split(c.Realm, "."), ",dc="))
200
}
201
var err error
202
if err = c.conn.NTLMBindWithHash(c.Realm, username, hash); err != nil {
203
c.nj.ThrowError(err)
204
}
205
return err == nil
206
}
207
208
// Search accepts whatever filter and returns a list of maps having provided attributes
209
// as keys and associated values mirroring the ones returned by ldap
210
// @example
211
// ```javascript
212
// const ldap = require('nuclei/ldap');
213
// const client = new ldap.Client('ldap://ldap.example.com', 'acme.com');
214
// const results = client.Search('(objectClass=*)', 'cn', 'mail');
215
// ```
216
func (c *Client) Search(filter string, attributes ...string) SearchResult {
217
c.nj.Require(c.conn != nil, "no existing connection")
218
c.nj.Require(c.BaseDN != "", "base dn cannot be empty")
219
c.nj.Require(len(attributes) > 0, "attributes cannot be empty")
220
221
res, err := c.conn.Search(
222
ldap.NewSearchRequest(
223
"",
224
ldap.ScopeWholeSubtree,
225
ldap.NeverDerefAliases,
226
0, 0, false,
227
filter,
228
attributes,
229
nil,
230
),
231
)
232
c.nj.HandleError(err, "ldap search request failed")
233
return *getSearchResult(res)
234
}
235
236
// AdvancedSearch accepts all values of search request type and return Ldap Entry
237
// its up to user to handle the response
238
// @example
239
// ```javascript
240
// const ldap = require('nuclei/ldap');
241
// const client = new ldap.Client('ldap://ldap.example.com', 'acme.com');
242
// const results = client.AdvancedSearch(ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, '(objectClass=*)', ['cn', 'mail'], []);
243
// ```
244
func (c *Client) AdvancedSearch(
245
Scope, DerefAliases, SizeLimit, TimeLimit int,
246
TypesOnly bool,
247
Filter string,
248
Attributes []string,
249
Controls []ldap.Control) SearchResult {
250
c.nj.Require(c.conn != nil, "no existing connection")
251
if c.BaseDN == "" {
252
c.BaseDN = fmt.Sprintf("dc=%s", strings.Join(strings.Split(c.Realm, "."), ",dc="))
253
}
254
req := ldap.NewSearchRequest(c.BaseDN, Scope, DerefAliases, SizeLimit, TimeLimit, TypesOnly, Filter, Attributes, Controls)
255
res, err := c.conn.Search(req)
256
c.nj.HandleError(err, "ldap search request failed")
257
c.nj.Require(res != nil, "ldap search request failed got nil response")
258
return *getSearchResult(res)
259
}
260
261
type (
262
// Metadata is the metadata for ldap server.
263
// this is returned by CollectMetadata method
264
Metadata struct {
265
BaseDN string
266
Domain string
267
DefaultNamingContext string
268
DomainFunctionality string
269
ForestFunctionality string
270
DomainControllerFunctionality string
271
DnsHostName string
272
}
273
)
274
275
// CollectLdapMetadata collects metadata from ldap server.
276
// @example
277
// ```javascript
278
// const ldap = require('nuclei/ldap');
279
// const client = new ldap.Client('ldap://ldap.example.com', 'acme.com');
280
// const metadata = client.CollectMetadata();
281
// log(to_json(metadata));
282
// ```
283
func (c *Client) CollectMetadata() Metadata {
284
c.nj.Require(c.conn != nil, "no existing connection")
285
var metadata Metadata
286
metadata.Domain = c.Realm
287
if c.BaseDN == "" {
288
c.BaseDN = fmt.Sprintf("dc=%s", strings.Join(strings.Split(c.Realm, "."), ",dc="))
289
}
290
metadata.BaseDN = c.BaseDN
291
292
// Use scope as Base since Root DSE doesn't have subentries
293
srMetadata := ldap.NewSearchRequest(
294
"",
295
ldap.ScopeBaseObject,
296
ldap.NeverDerefAliases,
297
0, 0, false,
298
"(objectClass=*)",
299
[]string{
300
"defaultNamingContext",
301
"domainFunctionality",
302
"forestFunctionality",
303
"domainControllerFunctionality",
304
"dnsHostName",
305
},
306
nil)
307
resMetadata, err := c.conn.Search(srMetadata)
308
c.nj.HandleError(err, "ldap search request failed")
309
310
for _, entry := range resMetadata.Entries {
311
for _, attr := range entry.Attributes {
312
value := entry.GetAttributeValue(attr.Name)
313
switch attr.Name {
314
case "defaultNamingContext":
315
metadata.DefaultNamingContext = value
316
case "domainFunctionality":
317
metadata.DomainFunctionality = value
318
case "forestFunctionality":
319
metadata.ForestFunctionality = value
320
case "domainControllerFunctionality":
321
metadata.DomainControllerFunctionality = value
322
case "dnsHostName":
323
metadata.DnsHostName = value
324
}
325
}
326
}
327
return metadata
328
}
329
330
// GetVersion returns the LDAP versions being used by the server
331
// @example
332
// ```javascript
333
// const ldap = require('nuclei/ldap');
334
// const client = new ldap.Client('ldap://ldap.example.com', 'acme.com');
335
// const versions = client.GetVersion();
336
// log(versions);
337
// ```
338
func (c *Client) GetVersion() []string {
339
c.nj.Require(c.conn != nil, "no existing connection")
340
341
// Query root DSE for supported LDAP versions
342
sr := ldap.NewSearchRequest(
343
"",
344
ldap.ScopeBaseObject,
345
ldap.NeverDerefAliases,
346
0, 0, false,
347
"(objectClass=*)",
348
[]string{"supportedLDAPVersion"},
349
nil)
350
351
res, err := c.conn.Search(sr)
352
c.nj.HandleError(err, "failed to get LDAP version")
353
354
if len(res.Entries) > 0 {
355
return res.Entries[0].GetAttributeValues("supportedLDAPVersion")
356
}
357
358
return []string{"unknown"}
359
}
360
361
// close the ldap connection
362
// @example
363
// ```javascript
364
// const ldap = require('nuclei/ldap');
365
// const client = new ldap.Client('ldap://ldap.example.com', 'acme.com');
366
// client.Close();
367
// ```
368
func (c *Client) Close() {
369
_ = c.conn.Close()
370
}
371
372