├── .github
└── workflows
│ └── release.yml
├── .gitignore
├── LICENSE
├── README.md
├── TODO.md
├── ber.go
├── config.go
├── go.mod
├── go.sum
├── images
├── demo1.png
├── demo2.png
└── ldapx-ai-logo.jpg
├── interceptors.go
├── ldapx.go
├── log
└── log.go
├── middlewares
├── attrentries
│ ├── obfuscation.go
│ └── types.go
├── attrlist
│ ├── obfuscation.go
│ └── types.go
├── basedn
│ ├── obfuscation.go
│ └── types.go
├── filter
│ ├── helpers.go
│ ├── obfuscation.go
│ └── types.go
├── helpers
│ └── string.go
└── options.go
├── parser
├── attrs.go
├── consts.go
├── filter.go
├── filter_test.go
├── packet.go
└── validation.go
├── proxy.go
└── shell.go
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | # .github/workflows/release.yaml
2 |
3 | on:
4 | release:
5 | types: [created]
6 |
7 | jobs:
8 | releases-matrix:
9 | name: Release Go Binary
10 | runs-on: ubuntu-latest
11 | strategy:
12 | matrix:
13 | goos: [linux, windows, darwin]
14 | goarch: [amd64, arm64]
15 | exclude:
16 | - goarch: "386"
17 | goos: darwin
18 | - goarch: arm64
19 | goos: windows
20 | steps:
21 | - uses: actions/checkout@v3
22 | - uses: wangyoucao577/go-release-action@v1
23 | with:
24 | github_token: ${{ secrets.GITHUB_TOKEN }}
25 | goos: ${{ matrix.goos }}
26 | goarch: ${{ matrix.goarch }}
27 | extra_files: LICENSE README.md
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # If you prefer the allow list template instead of the deny list, see community template:
2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
3 | #
4 | # Binaries for programs and plugins
5 | ldapx
6 |
7 | *.exe
8 | *.exe~
9 | *.dll
10 | *.so
11 | *.dylib
12 |
13 | # Test binary, built with `go test -c`
14 | *.test
15 |
16 | # Output of the go coverage tool, specifically when used with LiteIDE
17 | *.out
18 |
19 | # Dependency directories (remove the comment below to include it)
20 | # vendor/
21 |
22 | # Go workspace file
23 | go.work
24 | go.work.sum
25 |
26 | # env file
27 | .env
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Artur Marzano
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ldapx
2 |
3 |      [](https://goreportcard.com/report/github.com/Macmod/ldapx)  [
](https://twitter.com/MacmodSec)
4 |
5 | 
6 |
7 | Flexible LDAP proxy that can be used to inspect & transform all LDAP packets generated by other tools on the fly.
8 |
9 | ## Installation
10 |
11 | ```bash
12 | $ git clone github.com/Macmod/ldapx
13 | $ cd ldapx
14 | $ go install .
15 | ```
16 |
17 | Or just download one of the [Releases](https://github.com/Macmod/ldapx/releases) provided.
18 |
19 | ## Usage
20 |
21 | ```bash
22 | $ ldapx -t LDAPSERVER:389 [-f MIDDLEWARECHAIN] [-a MIDDLEWARECHAIN] [-b MIDDLEWARECHAIN] [-l LOCALADDR:LOCALPORT] [-o MIDDLEWAREOPTION=VALUE] [...]
23 | ```
24 |
25 | Where:
26 | * `-f` will apply Filter middlewares to all applicable requests
27 | * `-a` will apply AttrList middlewares to all applicable requests
28 | * `-b` will apply BaseDN middlewares to all applicable requests
29 | * `-e` will apply AttrEntries middlewares to all applicable requests
30 | * `-o` can be specified multiple times and is used to specify options for the middlewares
31 | * `-F` specifies the verbosity level for forward packets (requests)
32 | * `-R` specifies the verbosity level for reverse packets (responses)
33 | * `-x` can be used to specify a SOCKS proxy to use for the connection to the target
34 |
35 | If `--ldaps` / `-s` is specified, then the connection to the target will use LDAPS. This can come in handy if you must use a tool that doesn't support LDAPS. Use `--no-shell` / `-N` if you don't want to interact with the shell to modify the settings while the program is running.
36 |
37 | Each middleware is specified by a single-letter key (detailed below), and can be specified multiple times.
38 | For each type of middleware, the middlewares in the chain will be applied *in the order that they are specified* in the command.
39 |
40 | For more options check the `--help`.
41 |
42 | ## Examples
43 |
44 | ### Applying multiple middlewares in filters, attributes list and baseDN
45 |
46 | ```bash
47 | $ ldapx -t 192.168.117.2:389 -f OGDR -a Owp -b OX
48 | ```
49 |
50 | 
51 |
52 | ### Using the shell
53 |
54 | You can also use the builting shell to change your middlewares on the fly (`set` command) or simulate LDAP queries (`test` command):
55 |
56 | 
57 |
58 | To see packet statistics including how many packets of each LDAP operation passed through the proxy, use the `show stats` command.
59 |
60 | ```
61 | ldapx> show stats
62 | [Client -> Target]
63 | Packets Received: 14
64 | Packets Sent: 14
65 | Bytes Received: 1464
66 | Bytes Sent: 1464
67 | Counts by Type:
68 | Bind Request: 1
69 | Search Request: 12
70 | Modify Request: 1
71 |
72 | [Client <- Target]
73 | Packets Received: 149
74 | Packets Sent: 149
75 | Bytes Received: 177045
76 | Bytes Sent: 177045
77 | Counts by Type:
78 | Bind Response: 1
79 | Search Result Entry: 129
80 | Search Result Done: 12
81 | Search Result Reference: 6
82 | Modify Response: 1
83 | ```
84 |
85 | You can also show/set other parameters through the shell, such as the target address and verbosity levels. To check all available commands, use the `help` command.
86 |
87 | ## Middlewares
88 |
89 | The tool provides several middlewares "ready for use" for inline LDAP filter transformation. These middlewares were designed for use in Active Directory environments, but theoretically some of them could work in other LDAP environments.
90 |
91 | ### BaseDN
92 |
93 | | Key | Name | Purpose | Description | Input | Output | Details |
94 | |--------|------|---------|-------------|--------|--------|---------|
95 | | `O` | OIDAttribute | Obfuscation | Converts DN attrs to OIDs | `cn=Admin` | `2.5.4.3=Admin` | Uses standard LDAP OIDs, can be customized with options |
96 | | `C` | Case | Obfuscation | Randomizes DN case | `CN=lol,DC=draco,DC=local` | `cN=lOl,dC=dRaCo,Dc=loCaL` | Probability based |
97 | | `X` | HexValue | Obfuscation | Hex encodes characters in the values | `cn=john` | `cn=\6a\6fmin` | Probability based |
98 | | `S` | Spacing | Obfuscation | Adds random spaces in the BaseDN (in the beginning and/or end) | `DC=draco` | `DC=draco ` | Probability based |
99 | | `Q` | DoubleQuotes | Obfuscation | Adds quotes to values | `cn=Admin` | `cn="Admin"` | Incompatible with `HexValue` / `Spacing` |
100 |
101 | ### Filter
102 |
103 | | Key | Name | Purpose | Description | Input | Output | Details |
104 | |-----|------|---------|-------------|--------|--------|---------|
105 | | `O` | OIDAttribute | Obfuscation | Converts attrs to OIDs | `(cn=john)` | `(2.5.4.3=john)` | Uses standard LDAP OIDs; can be customized with options |
106 | | `C` | Case | Obfuscation | Randomizes character case | `(cn=John)` | `(cN=jOhN)` | Doesn't apply to binary SID values |
107 | | `X` | HexValue | Obfuscation | Hex encodes characters | `(memberOf=CN=Domain Admins,CN=Users)` | `(memberOf=CN=Do\6dai\6e Admins,CN=U\73ers)` | Only applies to DN string attributes |
108 | | `S` | Spacing | Obfuscation | Adds random spaces between characters | `(memberOf=CN=lol,DC=draco)` | `(memberOf= CN =lol, DC = draco)` | Only applies to DN string attributes, aNR attributes' prefix/suffix & SID attributes |
109 | | `T` | ReplaceTautologies | Obfuscation | Replaces basic tautologies into random tautologies | `(objectClass=*)` | `(\|(packageflags:1.2.840.113556.1.4.803:=0)(!(packageflags=*)))` | |
110 | | `t` | TimestampGarbage | Obfuscation | Adds random chars to timestamp values | `(time=20230812.123Z)` | `(time=20230812.123aBcZdeF)` | |
111 | | `B` | AddBool | Obfuscation | Adds random boolean conditions | `(cn=john)` | `(&(cn=john)(\|(a=1)(a=2)))` | Max depth configurable |
112 | | `D` | DblNegBool | Obfuscation | Adds double negations | `(cn=john)` | `(!(!(cn=john)))` | Max depth configurable |
113 | | `M` | DeMorganBool | Obfuscation | Applies De Morgan's laws | `(&(a=*)(b=*))` | `(!(\|(!(a=\*))(!(b=\*))))` | |
114 | | `R` | ReorderBool | Obfuscation | Reorders boolean conditions | `(&(a=1)(b=2))` | `(&(b=2)(a=1))` | Random reordering |
115 | | `b` | ExactBitwiseBreakout | Obfuscation | Breaks out exact matches into bitwise operations | `(attr=7)` | `(&(attr:1.2.840.113556.1.4.803:=7)(!(attr:1.2.840.113556.1.4.804:=4294967288)))` | For numeric attributes |
116 | | `d` | BitwiseDecomposition | Obfuscation | Decomposes bitwise operations into multiple components | `(attr:1.2.840.113556.1.4.803:=7)` | `(&(attr:1.2.840.113556.1.4.803:=1)(attr:1.2.840.113556.1.4.803:=2)(attr:1.2.840.113556.1.4.803:=4))` | For numeric attributes |
117 | | `I` | EqInclusion | Obfuscation | Converts equality to inclusion | `(cn=krbtgt)` | `(&(cn>=krbtgs)(cn<=krbtgu)(!(cn=krbtgs))(!(cn=krbtgu)))` | Works for numeric, string and SID attributes |
118 | | `E` | EqExclusion | Obfuscation | Converts equality to presence+exclusion | `(cn=krbtgt)` | `(&(cn=*)(!(cn<=krbtgs))(!(cn>=krbtgu)))` | Works for numeric, string and SID attributes |
119 | | `G` | Garbage | Obfuscation | Adds random garbage conditions | `(cn=john)` | `(\|(cn=john)(eqwoi31=21oi32j))` | Configurable count |
120 | | `A` | EqApproxMatch | Obfuscation | Converts equality to approximate match | `(cn=john)` | `(cn~=john)` | Uses LDAP's `~=` operator, which in AD is equivalent to `=` |
121 | | `x` | EqExtensible | Obfuscation | Converts equality to extensible match | `(cn=john)` | `(cn::=john)` | Uses an extensible match with an empty matching rule |
122 | | `Z` | PrependZeros | Obfuscation | Prepends random zeros to numeric values | `(flags=123)` | `(flags=00123)` | Only for numeric attributes and SIDs |
123 | | `s` | SubstringSplit | Obfuscation | Splits values into substrings | `(cn=john)` | `(cn=jo*hn)` | Only for string attrs. & can break the filter if it's not specific enough |
124 | | `N` | NamesToANR | Obfuscation | Changes attributes in the aNR set to `aNR` | `(name=john)` | `(aNR==john)` | |
125 | | `n` | ANRGarbageSubstring | Obfuscation | Appends garbage to the end of `aNR` equalities | `(aNR==john)` | `(aNR==john*siaASJU)` | |
126 |
127 | ### Attributes List
128 |
129 | | Key | Name | Purpose | Description | Input | Output | Details |
130 | |-----|------|---------|-------------|--------|--------|---------|
131 | | `O` | OIDAttribute | Obfuscation | Converts to OID form | `cn,sn` | `2.5.4.3,2.5.4.4` | Uses standard LDAP OIDs; can be customized with options |
132 | | `C` | Case | Obfuscation | Randomizes character case | `cn,sn` | `cN,sN` | |
133 | | `D` | Duplicate | Obfuscation | Duplicates attributes | `cn` | `cn,cn,cn` | |
134 | | `G` | GarbageNonExisting | Obfuscation | Adds fake attributes | `cn,sn` | `cn,sn,x-123` | Garbage is chosen randomly from an alphabet |
135 | | `g` | GarbageExisting | Obfuscation | Adds real attributes | `cn` | `cn,sn,mail` | Garbage is chosen from real attributes |
136 | | `w` | AddWildcard | Obfuscation | Adds a wildcard attribute to the list | `cn,name` | `cn,name,*` | |
137 | | `p` | AddPlus | Obfuscation | Adds a plus sign attribute to the list | `cn,name` | `cn,name,+` | If the list is empty, it also adds a `*` to preserve the semantics |
138 | | `W` | ReplaceWithWildcard | Obfuscation | Replaces the list with a wildcard | `cn,sn` | `*` | Replaces all attributes except operational attributes and "+" |
139 | | `E` | ReplaceWithEmpty | Obfuscation | Empties the attributes list | `cn,sn` | | Removes all attributes except operational attributes and "+" (in which case it includes a `*`) |
140 | | `R` | ReorderList | Obfuscation | Randomly reorders attrs | `cn,sn,uid` | `uid,cn,sn` | Random permutation |
141 |
142 | ### Attributes Entries
143 |
144 | These middlewares are mostly related to the `Add` and `Modify` operations described in the section below.
145 |
146 | | Key | Name | Purpose | Description | Input | Output | Details |
147 | |-----|------|---------|-------------|--------|--------|---------|
148 | | `O` | OIDAttribute | Obfuscation | Converts to OID form | `cn` | `2.5.4.3` | Uses standard LDAP OIDs; can be customized with options |
149 | | `C` | Case | Obfuscation | Randomizes character case | `cn` | `cN` | |
150 | | `R` | ReorderList | Obfuscation | Randomly reorders attrs | `cn,sn` | `sn,cn` | Random permutation |
151 |
152 | ## Middleware Options
153 |
154 | Some middlewares have options that can be used to change the way the middleware works internally. Middleware options can be set via either the command-line by appending `-o KEY=VALUE` switches or by using `set option KEY=VALUE` in the shell.
155 |
156 | You can check the available options by using the `show options` / `show option` commands in the shell. If not specified explicitly, the middleware will use default values defined in `middlewares/options.go`.
157 |
158 | ## Operations
159 |
160 | Although Search is the most common use case for this tool, `ldapx` supports other [LDAP operations](https://ldap.com/ldap-operation-types/) as well, such as Modify, Add, Delete and ModifyDN.
161 |
162 | Please note that transforming packets involving change operations may lead to undesirable outcomes and *should be done with caution*. Transformations other than `Search` need to be enabled explicitly by specifying `--modify`, `--add`, `--delete` and/or `--modifydn` (`--search` is `true` by default). The code that transforms packets for each operation is implemented in `interceptors.go`, but the overall logic is described below:
163 |
164 | ### Search
165 |
166 | Applies the specified `BaseDN`, `Filter` and `AttrList` middleware chains to the respective fields.
167 |
168 | ### Modify
169 |
170 | Applies:
171 |
172 | * The specified `BaseDN` middleware chain to the DN of the entry being modified
173 |
174 | * The specified `AttrEntries` middleware chain to the attribute entries specified as modifications
175 |
176 | ### Add
177 |
178 | Applies:
179 |
180 | * The specified `BaseDN` middleware chain to the DN of the entry being added
181 |
182 | * The specified `AttrEntries` middleware chain to the attribute entries of the entry being added
183 |
184 | ### Delete
185 |
186 | Applies the specified `BaseDN` middleware chain to the DN of the entry being deleted.
187 |
188 | ### ModifyDN
189 |
190 | Applies the specified `BaseDN` middleware chain to:
191 |
192 | * The DN of the entry being modified
193 |
194 | * The new RDN field
195 |
196 | * The new parent DN field
197 |
198 | ## Library Usage
199 |
200 | To use `ldapx` as a library, you can import the `parser` package and the individual middleware packages that you wish to use.
201 |
202 | To apply the middlewares to a readable LDAP query, you must parse it into a `parser.Filter` using `parser.QueryToFilter()`. Then you can either apply the middlewares, convert it back to a query using `parser.FilterToQuery()`, or convert it to a network packet using `parser.FilterToPacket()`. You can also convert network packets to `parser.Filter` structures using `parser.PacketToFilter()`.
203 |
204 | There are no docs on individual middlewares yet, but you can check the source code (`config.go` / `middlewares/*/*.go`) for method signatures and usage.
205 |
206 | ### Example
207 |
208 | ```go
209 | package main
210 |
211 | import (
212 | "fmt"
213 |
214 | filtermid "github.com/Macmod/ldapx/middlewares/filter"
215 | "github.com/Macmod/ldapx/parser"
216 | )
217 |
218 | func main() {
219 | query := "(&(cn=john)(sn=doe))"
220 | fmt.Printf("Original Query: %s\n", query)
221 |
222 | myFilter, err := parser.QueryToFilter(query)
223 |
224 | if err != nil {
225 | fmt.Errorf("error parsing query")
226 | }
227 |
228 | // FilterToString can be used to show
229 | // the internal representation of the parsed filter
230 | fmt.Println(parser.FilterToString(myFilter, 0))
231 |
232 | // Applying the OID middleware
233 | obfuscator := filtermid.OIDAttributeFilterObf(3, false)
234 | newFilter := obfuscator(myFilter)
235 |
236 | newQuery, err := parser.FilterToQuery(newFilter)
237 | if err != nil {
238 | fmt.Errorf("error converting filter to query")
239 | }
240 |
241 | fmt.Printf("Changed Query: %s\n", newQuery)
242 | }
243 | ```
244 |
245 | Output:
246 | ```
247 | Original Query: (&(cn=john)(sn=doe))
248 | Filter Type: 0
249 | AND Filter with 2 sub-filters:
250 | Filter Type: 3
251 | Equality Match - Attribute: cn, Value: john
252 | Filter Type: 3
253 | Equality Match - Attribute: sn, Value: doe
254 |
255 | Changed Query: (&(2.005.4.03=john)(2.005.04.004=doe))
256 | ```
257 |
258 | ## Developing Middlewares
259 |
260 | To develop a new middleware, you can create a new function inside the appropriate package (`filter`/`basedn`/`attrlist`/`attrentries`) with the following structures, respectively:
261 |
262 | ### Filter
263 | ```go
264 | func YourFilterMiddleware(args) func(parser.Filter) parser.Filter
265 | ```
266 |
267 | ### BaseDN
268 | ```go
269 | func YourBaseDNMiddleware(args) func(string) string
270 | ```
271 |
272 | ### Attributes List
273 | ```go
274 | func YourAttrListMiddleware(args) func([]string) []string
275 | ```
276 |
277 | ### Attributes Entries
278 | ```go
279 | func YourAttrEntriesMiddleware(args) func(parser.AttrEntries) parser.AttrEntries
280 | ```
281 |
282 | Then to actually have ldapx use your middleware:
283 |
284 | (1) Associate it with a letter and a name in `config.go` in either the `filterMidFlags`, `attrListMidFlags`, or `baseDNMidFlags` maps.
285 |
286 | (2) Change SetupMiddlewaresMap in `config.go` to include the call to your middleware
287 |
288 | A helper function named `LeafApplierFilterMiddleware` is provided to make it easier to write filter middlewares that only apply to leaf nodes of the filter. The relevant types and functions you might need are defined in the `parser` package.
289 |
290 | For example, the code below is the code for the `EqExtensible` middleware in `obfuscation.go`. This middleware changes EqualityMatches into ExtensibleMatches with an empty MatchingRule - for example, `(cn=John)` becomes either `(cn::=John)` or `(cn:dn:=John)`:
291 |
292 | ```go
293 | func EqExtensibleFilterObf(dn bool) func(parser.Filter) parser.Filter {
294 | // For every leaf in the filter...
295 | return LeafApplierFilterMiddleware(func(filter parser.Filter) parser.Filter {
296 | switch f := filter.(type) {
297 | // If the leaf is an EqualityMatch
298 | case *parser.FilterEqualityMatch:
299 | // Replace it with an ExtensibleMatch with an empty MatchingRule
300 | // optionally adding a DNAttributes (Active Directory ignores DNAttributes)
301 | return &parser.FilterExtensibleMatch{
302 | MatchingRule: "",
303 | AttributeDesc: f.AttributeDesc,
304 | MatchValue: f.AssertionValue,
305 | DNAttributes: dn,
306 | }
307 | }
308 |
309 | return filter
310 | })
311 | }
312 | ```
313 |
314 | Then it's registered as follows in `config.go`:
315 | ```go
316 | var filterMidFlags map[rune]string = map[rune]string{
317 | ...
318 | 'x': "EqExtensible",
319 | ...
320 | }
321 |
322 | // In SetupMiddlewaresMap:
323 | filterMidMap = map[string]filtermid.FilterMiddleware{
324 | ...
325 | "EqExtensible": filtermid.EqualityToExtensibleFilterObf(false),
326 | ...
327 | }
328 | ```
329 |
330 | To have your middleware use middleware options for the arguments of the function call, use the `optInt` / `optStr` / `optFloat` / `optBool` functions from `config.go`.
331 |
332 | ## Contributing
333 |
334 | Contributions are welcome by [opening an issue](https://github.com/Macmod/ldapx/issues/new) or by [submitting a pull request](https://github.com/Macmod/ldapx/pulls).
335 |
336 | ## Acknowledgements
337 |
338 | * Almost all obfuscation middlewares are basically implementations of the ideas presented in the [MaLDAPtive](https://www.youtube.com/watch?v=mKRS5Iyy7Qo) research by [Daniel Bohannon](https://x.com/danielhbohannon) & [Sabajete Elezaj](https://x.com/sabi_elezi), which inspired the development of this tool and helped me with countless questions. Kudos to them :)
339 |
340 | * Some code was adapted from [go-ldap/ldap](https://github.com/go-ldap/ldap) to convert LDAP filters to human-readable queries and to parse packet fields.
341 |
342 | * [ldap.com](https://ldap.com/), [MS-ADTS](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/d2435927-0999-4c62-8c6d-13ba31a52e1a), [RFC4510](https://docs.ldap.com/specs/rfc4510.txt), [RFC4515](https://docs.ldap.com/specs/rfc4515.txt), [RFC4512](https://docs.ldap.com/specs/rfc4512.txt), [RFC2696](https://www.ietf.org/rfc/rfc2696.txt) and many other online resources were of great help.
343 |
344 | ## Disclaimers
345 |
346 | * This tool is meant to be used for authorized security testing, troubleshooting and research purposes only. The author is not responsible for any misuse of this tool.
347 |
348 | * Some middlewares may break queries, either because of the specific environment where they are ran, combined effects due to the presence of other middlewares in the chain, or implementation bugs. If you found a bug, please open an issue to report it.
349 |
350 | ## Known Issues
351 |
352 | * This tool does not work currently with clients that require encryption via `SASL` mechanisms or `NTLMSSP Negotiate` (such as ADExplorer) - Check [Issue #1](https://github.com/Macmod/ldapx/issues/1) for more information.
353 |
354 | ## License
355 | MIT License
356 |
357 | Copyright (c) 2024 Artur Henrique Marzano Gonzaga
358 |
359 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
360 |
361 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
362 |
363 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
364 |
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 | # Considering
2 |
3 | * [Fix] Improve test coverage
4 | * [Feature] More middlewares for AttributeEntries
5 | * [Feature] Issue queries directly from ldapx
6 | * [Fix] Try to connect only and always when the conn is needed
7 | * [Feature] Possibilities related to obfuscating Timestamps with timezones
8 | * [Study] ExtensibleMatchFilter's with negative values, TokenSID ordering, Range Retrieval, Selection Filters, LDAP\_MATCHING\_RULE\_DN\_WITH\_DATA...
9 | * [Study] Is it possible to replace a query interactively or does it timeout?
--------------------------------------------------------------------------------
/ber.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 |
7 | ber "github.com/go-asn1-ber/asn1-ber"
8 | )
9 |
10 | func CopyBerPacket(packet *ber.Packet) *ber.Packet {
11 | newPacket := ber.Encode(packet.ClassType, packet.TagType, packet.Tag, packet.Value, packet.Description)
12 | for _, child := range packet.Children {
13 | if len(child.Children) == 0 {
14 | newPacket.AppendChild(child)
15 | } else {
16 | newPacket.AppendChild(CopyBerPacket(child))
17 | }
18 | }
19 |
20 | return newPacket
21 | }
22 |
23 | // TODO: Generalize this method to "non-leaves"
24 | func UpdateBerChildLeaf(packet *ber.Packet, idx int, newChild *ber.Packet) error {
25 | if idx >= len(packet.Children) {
26 | return fmt.Errorf("Error updating BER packet: index out of bounds")
27 | }
28 |
29 | (*packet).Children[idx] = newChild
30 |
31 | updatedData := new(bytes.Buffer)
32 | for x := 0; x < idx; x++ {
33 | updatedData.Write(packet.Children[x].Bytes())
34 | }
35 | updatedData.Write(newChild.Bytes())
36 | for x := idx + 1; x < len(packet.Children); x++ {
37 | updatedData.Write(packet.Children[x].Bytes())
38 | }
39 |
40 | (*packet).Data = updatedData
41 |
42 | return nil
43 | }
44 |
45 | func BerChildrenToList(packet *ber.Packet) []string {
46 | attrs := make([]string, 0)
47 |
48 | for _, child := range packet.Children {
49 | attrs = append(attrs, child.Data.String())
50 | }
51 |
52 | return attrs
53 | }
54 |
55 | func EncodeAttributeList(attrs []string) *ber.Packet {
56 | seq := ber.NewSequence("Attribute List")
57 | for _, attr := range attrs {
58 | seq.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, attr, "Attribute"))
59 | }
60 | return seq
61 | }
62 |
63 | func EncodeBaseDN(baseDN string) *ber.Packet {
64 | return ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, baseDN, "Base DN")
65 | }
66 |
67 | /*
68 | func reencodeBerDataRecursive(packet *ber.Packet) {
69 | // Recursively process the children first
70 | for _, child := range packet.Children {
71 | reencodeBerData(child)
72 | }
73 |
74 | // Compute the new Data field based on the packet's type and value
75 | if packet.Value != nil {
76 | v := reflect.ValueOf(packet.Value)
77 |
78 | if packet.ClassType == ber.ClassUniversal {
79 | switch packet.Tag {
80 | case ber.TagOctetString:
81 | sv, ok := v.Interface().(string)
82 | if ok {
83 | packet.Data.Reset()
84 | packet.Data.Write([]byte(sv))
85 | }
86 | case ber.TagEnumerated:
87 | bv, ok := v.Interface().([]byte)
88 | if ok {
89 | packet.Data.Reset()
90 | packet.Data.Write(bv)
91 | }
92 | case ber.TagEmbeddedPDV:
93 | bv, ok := v.Interface().([]byte)
94 | if ok {
95 | packet.Data.Reset()
96 | packet.Data.Write(bv)
97 | }
98 | }
99 | } else if packet.ClassType == ber.ClassContext {
100 | switch packet.Tag {
101 | case ber.TagEnumerated:
102 | bv, ok := v.Interface().([]byte)
103 | if ok {
104 | packet.Data.Reset()
105 | packet.Data.Write(bv)
106 | }
107 | case ber.TagEmbeddedPDV:
108 | bv, ok := v.Interface().([]byte)
109 | if ok {
110 | packet.Data.Reset()
111 | packet.Data.Write(bv)
112 | }
113 | }
114 | }
115 | }
116 |
117 | // Handle constructed types
118 | if packet.ClassType == ber.ClassUniversal {
119 | switch packet.Tag {
120 | case ber.TagSequence:
121 | packet.Data.Reset()
122 | for _, child := range packet.Children {
123 | packet.Data.Write(child.Bytes())
124 | }
125 | case ber.TagSet:
126 | packet.Data.Reset()
127 | for _, child := range packet.Children {
128 | packet.Data.Write(child.Bytes())
129 | }
130 | }
131 | }
132 | }
133 | */
134 |
--------------------------------------------------------------------------------
/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "strconv"
5 | "strings"
6 |
7 | "github.com/Macmod/ldapx/middlewares"
8 | attrentriesmid "github.com/Macmod/ldapx/middlewares/attrentries"
9 | attrlistmid "github.com/Macmod/ldapx/middlewares/attrlist"
10 | basednmid "github.com/Macmod/ldapx/middlewares/basedn"
11 | filtermid "github.com/Macmod/ldapx/middlewares/filter"
12 | )
13 |
14 | // Taken from:
15 | // https://learn.microsoft.com/en-us/windows/win32/adschema/attributes-anr
16 | // (Windows Server 2012)
17 | var ANRSet = []string{
18 | "name", "displayname", "samaccountname",
19 | "givenname", "legacyexchangedn", "sn", "proxyaddresses",
20 | "physicaldeliveryofficename", "msds-additionalsamaccountName",
21 | "msds-phoneticcompanyname", "msds-phoneticdepartment",
22 | "msds-phoneticdisplayname", "msds-phoneticfirstname",
23 | "msds-phoneticlastname",
24 | }
25 |
26 | var (
27 | baseDNMidMap map[string]basednmid.BaseDNMiddleware
28 | filterMidMap map[string]filtermid.FilterMiddleware
29 | attrListMidMap map[string]attrlistmid.AttrListMiddleware
30 | attrEntriesMidMap map[string]attrentriesmid.AttrEntriesMiddleware
31 | )
32 |
33 | var baseDNMidFlags map[rune]string = map[rune]string{
34 | 'O': "OIDAttribute",
35 | 'C': "Case",
36 | 'X': "HexValue",
37 | 'S': "Spacing",
38 | 'Q': "DoubleQuotes",
39 | }
40 |
41 | var filterMidFlags map[rune]string = map[rune]string{
42 | 'O': "OIDAttribute",
43 | 'C': "Case",
44 | 'X': "HexValue",
45 | 'S': "Spacing",
46 | 'T': "ReplaceTautologies",
47 | 't': "TimestampGarbage",
48 | 'B': "AddBool",
49 | 'D': "DblNegBool",
50 | 'M': "DeMorganBool",
51 | 'R': "ReorderBool",
52 | 'b': "ExactBitwiseBreakout",
53 | 'd': "BitwiseDecomposition",
54 | 'I': "EqInclusion",
55 | 'E': "EqExclusion",
56 | 'G': "Garbage",
57 | 'A': "EqApproxMatch",
58 | 'x': "EqExtensible",
59 | 'Z': "PrependZeros",
60 | 's': "SubstringSplit",
61 | 'N': "NamesToANR",
62 | 'n': "ANRGarbageSubstring",
63 | }
64 |
65 | var attrListMidFlags map[rune]string = map[rune]string{
66 | 'O': "OIDAttribute",
67 | 'C': "Case",
68 | 'D': "Duplicate",
69 | 'G': "GarbageNonExisting",
70 | 'g': "GarbageExisting",
71 | 'W': "ReplaceWithWildcard",
72 | 'w': "AddWildcard",
73 | 'p': "AddPlus",
74 | 'E': "ReplaceWithEmpty",
75 | 'R': "ReorderList",
76 | }
77 |
78 | var attrEntriesMidFlags map[rune]string = map[rune]string{
79 | 'O': "OIDAttribute",
80 | 'C': "Case",
81 | /*
82 | 'D': "Duplicate",
83 | 'G': "GarbageNonExisting",
84 | 'g': "GarbageExisting",
85 | */
86 | 'R': "ReorderList",
87 | 'D': "Duplicate",
88 | }
89 |
90 | func SetupMiddlewaresMap() {
91 | baseDNMidMap = map[string]basednmid.BaseDNMiddleware{
92 | "OIDAttribute": basednmid.OIDAttributeBaseDNObf(optInt("BDNOIDAttributeMaxSpaces"), optInt("BDNOIDAttributeMaxZeros"), optBool("BDNOIDAttributeIncludePrefix")),
93 | "Case": basednmid.RandCaseBaseDNObf(optFloat("BDNCaseProb")),
94 | "HexValue": basednmid.RandHexValueBaseDNObf(optFloat("BDNHexValueProb")),
95 | "Spacing": basednmid.RandSpacingBaseDNObf(optInt("BDNSpacingMaxElems")),
96 | "DoubleQuotes": basednmid.DoubleQuotesBaseDNObf(),
97 | }
98 |
99 | filterMidMap = map[string]filtermid.FilterMiddleware{
100 | "OIDAttribute": filtermid.OIDAttributeFilterObf(optInt("FiltOIDAttributeMaxSpaces"), optInt("FiltOIDAttributeMaxZeros"), optBool("FiltOIDAttributeIncludePrefix")),
101 | "Case": filtermid.RandCaseFilterObf(optFloat("FiltCaseProb")),
102 | "HexValue": filtermid.RandHexValueFilterObf(optFloat("FiltHexValueProb")),
103 | "Spacing": filtermid.RandSpacingFilterObf(optInt("FiltSpacingMaxSpaces")),
104 | "ReplaceTautologies": filtermid.ReplaceTautologiesFilterObf(),
105 | "TimestampGarbage": filtermid.RandTimestampSuffixFilterObf(optInt("FiltTimestampGarbageMaxChars"), optStr("FiltGarbageCharset"), optBool("FiltTimestampGarbageUseComma")),
106 | "AddBool": filtermid.RandAddBoolFilterObf(optInt("FiltAddBoolMaxDepth"), optFloat("FiltDeMorganBoolProb")),
107 | "DblNegBool": filtermid.RandDblNegBoolFilterObf(optInt("FiltDblNegBoolMaxDepth"), optFloat("FiltDeMorganBoolProb")),
108 | "DeMorganBool": filtermid.DeMorganBoolFilterObf(),
109 | "ReorderBool": filtermid.RandBoolReorderFilterObf(),
110 | "ExactBitwiseBreakout": filtermid.ExactBitwiseBreakoutFilterObf(),
111 | "BitwiseDecomposition": filtermid.BitwiseDecomposeFilterObf(optInt("FiltBitwiseDecompositionMaxBits")),
112 | "EqInclusion": filtermid.EqualityByInclusionFilterObf(),
113 | "EqExclusion": filtermid.EqualityByExclusionFilterObf(),
114 | "Garbage": filtermid.RandGarbageFilterObf(optInt("FiltGarbageMaxElems"), optInt("FiltGarbageMaxSize"), optStr("FiltGarbageCharset")),
115 | "EqApproxMatch": filtermid.EqualityToApproxMatchFilterObf(),
116 | "EqExtensible": filtermid.EqualityToExtensibleFilterObf(optBool("FiltEqExtensibleAppendDN")),
117 | "PrependZeros": filtermid.RandPrependZerosFilterObf(optInt("FiltPrependZerosMaxElems")),
118 | "SubstringSplit": filtermid.RandSubstringSplitFilterObf(optFloat("FiltSubstringSplitProb")),
119 | "NamesToANR": filtermid.ANRAttributeFilterObf(ANRSet),
120 | "ANRGarbageSubstring": filtermid.ANRSubstringGarbageFilterObf(optInt("FiltANRSubstringMaxElems"), optStr("FiltGarbageCharset")),
121 | }
122 |
123 | attrListMidMap = map[string]attrlistmid.AttrListMiddleware{
124 | "OIDAttribute": attrlistmid.OIDAttributeAttrListObf(optInt("AttrsOIDAttributeMaxSpaces"), optInt("AttrsOIDAttributeMaxZeros"), optBool("AttrsOIDAttributePrefix")),
125 | "Case": attrlistmid.RandCaseAttrListObf(optFloat("AttrsCaseProb")),
126 | "Duplicate": attrlistmid.DuplicateAttrListObf(optFloat("AttrsDuplicateProb")),
127 | "GarbageNonExisting": attrlistmid.GarbageNonExistingAttrListObf(optInt("AttrsGarbageNonExistingMaxElems"), optInt("AttrsGarbageNonExistingMaxSize"), optStr("AttrsGarbageCharset")),
128 | "GarbageExisting": attrlistmid.GarbageExistingAttrListObf(optInt("AttrsGarbageExistingMaxElems")),
129 | "ReplaceWithWildcard": attrlistmid.ReplaceWithWildcardAttrListObf(),
130 | "AddWildcard": attrlistmid.AddWildcardAttrListObf(),
131 | "AddPlus": attrlistmid.AddPlusAttrListObf(),
132 | "ReplaceWithEmpty": attrlistmid.ReplaceWithEmptyAttrListObf(),
133 | "ReorderList": attrlistmid.ReorderListAttrListObf(),
134 | }
135 |
136 | attrEntriesMidMap = map[string]attrentriesmid.AttrEntriesMiddleware{
137 | "OIDAttribute": attrentriesmid.OIDAttributeAttrEntriesObf(optInt("AttrEntriesOIDAttributeMaxSpaces"), optInt("AttrEntriesOIDAttributeMaxZeros"), optBool("AttrEntriesOIDAttributePrefix")),
138 | "Case": attrentriesmid.RandCaseAttrEntriesObf(optFloat("AttrEntriesCaseProb")),
139 | "ReorderList": attrentriesmid.ReorderListAttrEntriesObf(),
140 | }
141 | }
142 |
143 | func optStr(key string) string {
144 | if value, ok := options.Get(key); ok {
145 | return value
146 | }
147 | return middlewares.DefaultOptions[key]
148 | }
149 |
150 | func optInt(key string) int {
151 | if value, ok := options.Get(key); ok {
152 | i, err := strconv.Atoi(value)
153 | if err == nil {
154 | return i
155 | }
156 | }
157 |
158 | result, _ := strconv.Atoi(middlewares.DefaultOptions[key])
159 | return result
160 | }
161 |
162 | func optFloat(key string) float64 {
163 | if value, ok := options.Get(key); ok {
164 | i, err := strconv.ParseFloat(value, 64)
165 | if err == nil {
166 | return i
167 | }
168 | }
169 |
170 | result, _ := strconv.ParseFloat(middlewares.DefaultOptions[key], 64)
171 | return result
172 | }
173 |
174 | func optBool(key string) bool {
175 | if value, ok := options.Get(key); ok {
176 | return strings.ToLower(value) == "true"
177 | }
178 | return strings.ToLower(middlewares.DefaultOptions[key]) == "true"
179 | }
180 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/Macmod/ldapx
2 |
3 | go 1.21.5
4 |
5 | require (
6 | github.com/c-bata/go-prompt v0.2.6
7 | github.com/fatih/color v1.18.0
8 | github.com/go-asn1-ber/asn1-ber v1.5.5
9 | github.com/spf13/pflag v1.0.5
10 | github.com/stretchr/testify v1.10.0
11 | h12.io/socks v1.0.3
12 | )
13 |
14 | require (
15 | github.com/davecgh/go-spew v1.1.1 // indirect
16 | github.com/mattn/go-colorable v0.1.13 // indirect
17 | github.com/mattn/go-isatty v0.0.20 // indirect
18 | github.com/mattn/go-runewidth v0.0.9 // indirect
19 | github.com/mattn/go-tty v0.0.3 // indirect
20 | github.com/pkg/term v1.2.0-beta.2 // indirect
21 | github.com/pmezard/go-difflib v1.0.0 // indirect
22 | golang.org/x/sys v0.25.0 // indirect
23 | gopkg.in/yaml.v3 v3.0.1 // indirect
24 | )
25 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/c-bata/go-prompt v0.2.6 h1:POP+nrHE+DfLYx370bedwNhsqmpCUynWPxuHi0C5vZI=
2 | github.com/c-bata/go-prompt v0.2.6/go.mod h1:/LMAke8wD2FsNu9EXNdHxNLbd9MedkPnCdfpU9wwHfY=
3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
6 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
7 | github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA=
8 | github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
9 | github.com/h12w/go-socks5 v0.0.0-20200522160539-76189e178364 h1:5XxdakFhqd9dnXoAZy1Mb2R/DZ6D1e+0bGC/JhucGYI=
10 | github.com/h12w/go-socks5 v0.0.0-20200522160539-76189e178364/go.mod h1:eDJQioIyy4Yn3MVivT7rv/39gAJTrA7lgmYr8EW950c=
11 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
12 | github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
13 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
14 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
15 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
16 | github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
17 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
18 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
19 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
20 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
21 | github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
22 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
23 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
24 | github.com/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI=
25 | github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0=
26 | github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc=
27 | github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
28 | github.com/pkg/term v1.2.0-beta.2 h1:L3y/h2jkuBVFdWiJvNfYfKmzcCnILw7mJWm2JQuMppw=
29 | github.com/pkg/term v1.2.0-beta.2/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw=
30 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
31 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
32 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
33 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
34 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
35 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
36 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
37 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
38 | golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
39 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
40 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
41 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
42 | golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
43 | golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
44 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
45 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
46 | golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
47 | golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
48 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
49 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
50 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
51 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
52 | h12.io/socks v1.0.3 h1:Ka3qaQewws4j4/eDQnOdpr4wXsC//dXtWvftlIcCQUo=
53 | h12.io/socks v1.0.3/go.mod h1:AIhxy1jOId/XCz9BO+EIgNL2rQiPTBNnOfnVnQ+3Eck=
54 |
--------------------------------------------------------------------------------
/images/demo1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Macmod/ldapx/5992319486f24cdd3e1044b2046d21abb003d848/images/demo1.png
--------------------------------------------------------------------------------
/images/demo2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Macmod/ldapx/5992319486f24cdd3e1044b2046d21abb003d848/images/demo2.png
--------------------------------------------------------------------------------
/images/ldapx-ai-logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Macmod/ldapx/5992319486f24cdd3e1044b2046d21abb003d848/images/ldapx-ai-logo.jpg
--------------------------------------------------------------------------------
/interceptors.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "crypto/sha256"
5 | "encoding/json"
6 | "fmt"
7 | "reflect"
8 |
9 | "github.com/fatih/color"
10 |
11 | "github.com/Macmod/ldapx/log"
12 | "github.com/Macmod/ldapx/parser"
13 | ber "github.com/go-asn1-ber/asn1-ber"
14 | )
15 |
16 | // General logic behind the transformations that ldapx
17 | // is capable of applying to each LDAP operation.
18 | func TransformSearchRequest(filter parser.Filter, baseDN string, attrs []string) (parser.Filter, string, []string) {
19 | newFilter := fc.Execute(filter, true)
20 | newAttrs := ac.Execute(attrs, true)
21 | newBaseDN := bc.Execute(baseDN, true)
22 |
23 | return newFilter, newBaseDN, newAttrs
24 | }
25 |
26 | func TransformModifyRequest(targetDN string, changes []ChangeRequest) (string, []ChangeRequest) {
27 | newTargetDN := bc.Execute(targetDN, true)
28 | newChanges := make([]ChangeRequest, len(changes))
29 |
30 | for idx := range newChanges {
31 | newChanges[idx].OperationId = changes[idx].OperationId
32 | newChanges[idx].Modifications = ec.Execute(changes[idx].Modifications, true)
33 | }
34 |
35 | return newTargetDN, newChanges
36 | }
37 |
38 | func TransformAddRequest(targetDN string, entries parser.AttrEntries) (string, parser.AttrEntries) {
39 | newTargetDN := bc.Execute(targetDN, true)
40 | newEntries := ec.Execute(entries, true)
41 |
42 | return newTargetDN, newEntries
43 | }
44 |
45 | func TransformDeleteRequest(targetDN string) string {
46 | return bc.Execute(targetDN, true)
47 | }
48 |
49 | func TransformModifyDNRequest(entry string, newRDN string, delOld bool, newSuperior string) (string, string, bool, string) {
50 | newEntry := bc.Execute(entry, true)
51 | newNSuperior := bc.Execute(newSuperior, true)
52 | newNRDN := bc.Execute(newRDN, true)
53 | newDelOld := delOld // Not processed
54 |
55 | return newEntry, newNRDN, newDelOld, newNSuperior
56 | }
57 |
58 | // Basic packet processing logic behind the transformations that ldapx
59 | // is capable of applying to each LDAP operation.
60 |
61 | func ProcessSearchRequest(packet *ber.Packet, searchRequestMap map[string]*ber.Packet) *ber.Packet {
62 | if tracking {
63 | // Handle possible cookie desync by tracking the original corresponding request
64 | // If the current search request is paged and has a cookie, forward the original request
65 | // that generated the paging, including the current paging control
66 | if len(packet.Children) > 2 {
67 | controls := packet.Children[2].Children
68 | for _, control := range controls {
69 | if len(control.Children) > 1 && control.Children[0].Value == "1.2.840.113556.1.4.319" {
70 | // RFC2696 - LDAP Control Extension for Simple Paged Results Manipulation
71 | searchControlValue := ber.DecodePacket(control.Children[1].Data.Bytes())
72 | cookie := searchControlValue.Children[1].Data.Bytes()
73 |
74 | if len(cookie) > 0 {
75 | // Hash the search message (packet.Children[1]) to retrieve the correct search packet from the map
76 | searchMessage := packet.Children[1].Bytes()
77 | searchMessageHash := fmt.Sprintf("%x", sha256.Sum256(searchMessage))
78 | searchPacket, ok := searchRequestMap[searchMessageHash]
79 |
80 | if ok {
81 | forwardPacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request")
82 | forwardPacket.AppendChild(packet.Children[0])
83 | forwardPacket.AppendChild(searchPacket.Children[1])
84 | forwardPacket.AppendChild(packet.Children[2])
85 |
86 | log.Log.Printf("[+] [Paging] Search Request Forwarded\n")
87 |
88 | return forwardPacket
89 | } else {
90 | log.Log.Printf("[-] Error finding previous packet (tracking algorithm)")
91 | }
92 | }
93 | }
94 | }
95 | }
96 |
97 | searchMessage := packet.Children[1].Bytes()
98 | searchMessageHash := fmt.Sprintf("%x", sha256.Sum256(searchMessage))
99 | searchRequestMap[searchMessageHash] = packet
100 | }
101 |
102 | baseDN := packet.Children[1].Children[0].Value.(string)
103 | filterData := packet.Children[1].Children[6]
104 | attrs := BerChildrenToList(packet.Children[1].Children[7])
105 |
106 | filter, err := parser.PacketToFilter(filterData)
107 | if err != nil {
108 | red.Printf("[ERROR] %s\n", err)
109 | return packet
110 | }
111 |
112 | oldFilterStr, err := parser.FilterToQuery(filter)
113 | if err != nil {
114 | yellow.Printf("[WARNING] %s\n", err)
115 | }
116 |
117 | blue.Printf(
118 | "Intercepted Search\n BaseDN: '%s'\n Filter: %s\n Attributes: %s\n",
119 | baseDN, oldFilterStr, prettyList(attrs),
120 | )
121 |
122 | newFilter, newBaseDN, newAttrs := TransformSearchRequest(
123 | filter, baseDN, attrs,
124 | )
125 |
126 | newFilterStr, err := parser.FilterToQuery(newFilter)
127 | if err != nil {
128 | yellow.Printf("[WARNING] %s\n", err)
129 | }
130 |
131 | // Change the fields that need to be changed
132 | updatedFlag := false
133 | if newBaseDN != baseDN {
134 | UpdateBerChildLeaf(packet.Children[1], 0, EncodeBaseDN(newBaseDN))
135 | updatedFlag = true
136 | }
137 |
138 | // TODO: Compare the Filter structures instead to minimize the risk of bugs
139 | if oldFilterStr != newFilterStr {
140 | UpdateBerChildLeaf(packet.Children[1], 6, parser.FilterToPacket(newFilter))
141 | updatedFlag = true
142 | }
143 |
144 | if !reflect.DeepEqual(attrs, newAttrs) {
145 | UpdateBerChildLeaf(packet.Children[1], 7, EncodeAttributeList(newAttrs))
146 | updatedFlag = true
147 | }
148 |
149 | if updatedFlag {
150 | green.Printf("Changed Search\n BaseDN: '%s'\n Filter: %s\n Attributes: %s\n", newBaseDN, newFilterStr, prettyList(newAttrs))
151 |
152 | // We need to copy it to refresh the internal Data of the parent packet
153 | return CopyBerPacket(packet)
154 | } else {
155 | blue.Printf("Nothing changed in the request\n")
156 | }
157 |
158 | return packet
159 | }
160 |
161 | type ChangeRequest struct {
162 | OperationId int64
163 | Modifications parser.AttrEntries
164 | }
165 |
166 | func (change *ChangeRequest) PrintChanges(color *color.Color) {
167 | var operationStr string
168 | switch change.OperationId {
169 | case 0:
170 | operationStr = "Add"
171 | case 1:
172 | operationStr = "Delete"
173 | case 2:
174 | operationStr = "Replace"
175 | default:
176 | operationStr = "Unknown"
177 | }
178 |
179 | color.Printf(" Operation: %s (%d)\n", operationStr, change.OperationId)
180 |
181 | for _, attribute := range change.Modifications {
182 | valuesStr, _ := json.Marshal(attribute.Values)
183 | color.Printf(" '%s': %s\n", attribute.Name, valuesStr)
184 | }
185 | }
186 |
187 | // https://ldap.com/ldapv3-wire-protocol-reference-modify/
188 | func ProcessModifyRequest(packet *ber.Packet) *ber.Packet {
189 | if len(packet.Children) > 1 {
190 | modPacket := packet.Children[1]
191 |
192 | // Parse packet details
193 | // Note to nerds - modify requests are complicated! :-(
194 | targetDN := string(modPacket.Children[0].Data.Bytes())
195 | changeRequests := make([]ChangeRequest, 0)
196 |
197 | entryChanges := modPacket.Children[1]
198 | for _, entryChange := range entryChanges.Children {
199 | operationId := entryChange.Children[0].Value.(int64)
200 | changeRequest := ChangeRequest{
201 | OperationId: operationId,
202 | Modifications: parser.AttrEntries{},
203 | }
204 |
205 | change := entryChange.Children[1]
206 |
207 | attrName := change.Children[0].Data.String()
208 |
209 | for _, attrValue := range change.Children[1].Children {
210 | changeRequest.Modifications.AddValue(attrName, attrValue.Data.String())
211 | }
212 |
213 | changeRequests = append(changeRequests, changeRequest)
214 | }
215 |
216 | blue.Printf("Intercepted Modify\n TargetDN: '%s'\n", targetDN)
217 | for _, req := range changeRequests {
218 | req.PrintChanges(blue)
219 | }
220 |
221 | newTargetDN, newChangeRequests := TransformModifyRequest(targetDN, changeRequests)
222 |
223 | updatedFlag := false
224 | if newTargetDN != targetDN {
225 | newEncodedDN := ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, newTargetDN, "")
226 | UpdateBerChildLeaf(packet.Children[1], 0, newEncodedDN)
227 |
228 | updatedFlag = true
229 | }
230 |
231 | if !reflect.DeepEqual(newChangeRequests, changeRequests) {
232 | // Rebuild changes packet
233 | newChangesPacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "")
234 | for _, change := range newChangeRequests {
235 | changeSeq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "")
236 | changeSeq.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, change.OperationId, ""))
237 |
238 | for _, entry := range change.Modifications {
239 | modSeq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "")
240 | modSeq.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, entry.Name, ""))
241 |
242 | valSet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSet, nil, "")
243 | for _, val := range entry.Values {
244 | valSet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, val, ""))
245 | }
246 |
247 | modSeq.AppendChild(valSet)
248 | changeSeq.AppendChild(modSeq)
249 | }
250 |
251 | newChangesPacket.AppendChild(changeSeq)
252 | }
253 |
254 | UpdateBerChildLeaf(packet.Children[1], 1, newChangesPacket)
255 |
256 | updatedFlag = true
257 | }
258 |
259 | if updatedFlag {
260 | green.Printf("Changed Modify Request\n TargetDN: '%s'\n", newTargetDN)
261 | for _, req := range newChangeRequests {
262 | req.PrintChanges(green)
263 | }
264 |
265 | // We need to copy it to refresh the internal Data of the parent packet
266 | return CopyBerPacket(packet)
267 | } else {
268 | blue.Printf("Nothing changed in the request\n")
269 | }
270 | } else {
271 | red.Printf("Malformed request (missing required fields)\n")
272 | }
273 |
274 | return packet
275 | }
276 |
277 | // https://ldap.com/ldapv3-wire-protocol-reference-add/
278 | func ProcessAddRequest(packet *ber.Packet) *ber.Packet {
279 | if len(packet.Children) > 1 {
280 | addPacket := packet.Children[1]
281 | targetDN := string(addPacket.Children[0].Data.Bytes())
282 | entryAttrs := addPacket.Children[1]
283 |
284 | var targetAttrEntries parser.AttrEntries
285 | for _, attr := range entryAttrs.Children {
286 | attrName := attr.Children[0].Data.String()
287 |
288 | attrVals := attr.Children[1]
289 | for _, attrVal := range attrVals.Children {
290 | targetAttrEntries.AddValue(attrName, attrVal.Data.String())
291 | }
292 | }
293 |
294 | blue.Printf("Intercepted Add Request\n TargetDN: '%s'\n Attributes: \n", targetDN)
295 | for _, attrEntry := range targetAttrEntries {
296 | for _, attrVal := range attrEntry.Values {
297 | blue.Printf(" '%s': '%s'\n", attrEntry.Name, attrVal)
298 | }
299 | }
300 |
301 | updatedFlag := false
302 |
303 | newTargetDN, newTargetAttrEntries := TransformAddRequest(targetDN, targetAttrEntries)
304 | if newTargetDN != targetDN {
305 | newEncodedDN := ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, newTargetDN, "")
306 | UpdateBerChildLeaf(packet.Children[1], 0, newEncodedDN)
307 | updatedFlag = true
308 | }
309 |
310 | if !reflect.DeepEqual(newTargetAttrEntries, targetAttrEntries) {
311 | // Construct a new packet to hold the attribute entries
312 | newEntryAttrs := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "AttributeList")
313 | for _, attr := range newTargetAttrEntries {
314 | attrSeq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Attribute")
315 | attrSeq.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, attr.Name, "Attribute Name"))
316 |
317 | valSet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSet, nil, "AttributeValue")
318 | for _, val := range attr.Values {
319 | valSet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, val, "AttributeValue"))
320 | }
321 | attrSeq.AppendChild(valSet)
322 |
323 | newEntryAttrs.AppendChild(attrSeq)
324 | }
325 | UpdateBerChildLeaf(packet.Children[1], 1, newEntryAttrs)
326 |
327 | updatedFlag = true
328 | }
329 |
330 | if updatedFlag {
331 | green.Printf("Changed Add Request\n TargetDN: '%s'\n Attributes: \n", newTargetDN)
332 | for _, attrEntry := range newTargetAttrEntries {
333 | for _, attrVal := range attrEntry.Values {
334 | green.Printf(" '%s': '%s'\n", attrEntry.Name, attrVal)
335 | }
336 | }
337 |
338 | // We need to copy it to refresh the internal Data of the parent packet
339 | return CopyBerPacket(packet)
340 | } else {
341 | blue.Printf("Nothing changed in the request\n")
342 | }
343 | } else {
344 | red.Printf("Malformed request (missing required fields)\n")
345 | }
346 |
347 | return packet
348 | }
349 |
350 | // https://ldap.com/ldapv3-wire-protocol-reference-delete/
351 | func ProcessDeleteRequest(packet *ber.Packet) *ber.Packet {
352 | if len(packet.Children) > 1 {
353 | targetDN := string(packet.Children[1].Data.Bytes())
354 |
355 | blue.Printf("Intercepted Delete\n TargetDN: '%s'\n", targetDN)
356 |
357 | newTargetDN := TransformDeleteRequest(targetDN)
358 | newEncodedDN := ber.NewString(ber.ClassApplication, ber.TypePrimitive, 0x0A, newTargetDN, "")
359 | if newTargetDN != targetDN {
360 | green.Printf("Changed Delete\n TargetDN: '%s'\n", newTargetDN)
361 | UpdateBerChildLeaf(packet, 1, newEncodedDN)
362 | } else {
363 | blue.Printf("Nothing changed in the request\n")
364 | }
365 | } else {
366 | red.Printf("Malformed request (missing required fields)\n")
367 | }
368 |
369 | return packet
370 | }
371 |
372 | // https://ldap.com/ldapv3-wire-protocol-reference-modify-dn/
373 | func ProcessModifyDNRequest(packet *ber.Packet) *ber.Packet {
374 | if len(packet.Children) > 1 {
375 | modDNPacket := packet.Children[1]
376 |
377 | if len(modDNPacket.Children) > 3 {
378 | entry := string(modDNPacket.Children[0].Data.Bytes())
379 | newRDN := string(modDNPacket.Children[1].Data.Bytes())
380 | delOld := len(modDNPacket.Children[2].Data.Bytes()) > 0 && modDNPacket.Children[3].Data.Bytes()[0] != byte(0)
381 | newSuperior := string(modDNPacket.Children[3].Data.Bytes())
382 |
383 | blue.Printf("Intercepted ModifyDN\n Entry: '%s'\n NewRDN: '%s'\n DeleteOldRDN: '%t'\n NewSuperior: '%s'\n", entry, newRDN, delOld, newSuperior)
384 |
385 | newEntry, newNRDN, newDelOld, newNSuperior := TransformModifyDNRequest(entry, newRDN, delOld, newSuperior)
386 |
387 | updatedFlag := false
388 | if newEntry != entry {
389 | newEncodedEntry := ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, newEntry, "")
390 | UpdateBerChildLeaf(packet.Children[1], 0, newEncodedEntry)
391 | updatedFlag = true
392 | }
393 |
394 | if newNRDN != newRDN {
395 | newEncodedRDN := ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, newNRDN, "")
396 | UpdateBerChildLeaf(packet.Children[1], 1, newEncodedRDN)
397 | updatedFlag = true
398 | }
399 |
400 | if newDelOld != delOld {
401 | newEncodedDelOld := ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, newDelOld, "")
402 | UpdateBerChildLeaf(packet.Children[1], 2, newEncodedDelOld)
403 | updatedFlag = true
404 | }
405 |
406 | if newNSuperior != newSuperior {
407 | newEncodedNSuperior := ber.NewString(ber.ClassContext, ber.TypePrimitive, 0x0, newNSuperior, "")
408 | UpdateBerChildLeaf(packet.Children[1], 3, newEncodedNSuperior)
409 | updatedFlag = true
410 | }
411 |
412 | if updatedFlag {
413 | green.Printf("Changed ModifyDN\n Entry: '%s'\n NewRDN: '%s'\n DeleteOldRDN: '%t'\n NewSuperior: '%s'\n", newEntry, newNRDN, newDelOld, newNSuperior)
414 | return CopyBerPacket(packet)
415 | } else {
416 | blue.Printf("Nothing changed in the request\n")
417 | }
418 | } else {
419 | red.Printf("Malformed request (missing required fields)\n")
420 | }
421 | } else {
422 | red.Printf("Malformed request (missing required fields)\n")
423 | }
424 |
425 | return packet
426 | }
427 |
--------------------------------------------------------------------------------
/ldapx.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "crypto/tls"
5 | "encoding/json"
6 | "fmt"
7 | "net"
8 | "os"
9 | "strings"
10 | "sync"
11 |
12 | "github.com/Macmod/ldapx/log"
13 | attrentriesmid "github.com/Macmod/ldapx/middlewares/attrentries"
14 | attrlistmid "github.com/Macmod/ldapx/middlewares/attrlist"
15 | basednmid "github.com/Macmod/ldapx/middlewares/basedn"
16 | filtermid "github.com/Macmod/ldapx/middlewares/filter"
17 | "github.com/fatih/color"
18 | "github.com/spf13/pflag"
19 | )
20 |
21 | type Stats struct {
22 | sync.Mutex
23 | Forward struct {
24 | PacketsReceived uint64
25 | PacketsSent uint64
26 | BytesReceived uint64
27 | BytesSent uint64
28 | CountsByType map[int]uint64
29 | }
30 | Reverse struct {
31 | PacketsReceived uint64
32 | PacketsSent uint64
33 | BytesReceived uint64
34 | BytesSent uint64
35 | CountsByType map[int]uint64
36 | }
37 | }
38 |
39 | var version = "v1.1.0"
40 |
41 | var green = color.New(color.FgGreen)
42 | var red = color.New(color.FgRed)
43 | var yellow = color.New(color.FgYellow)
44 | var blue = color.New(color.FgBlue)
45 |
46 | var insecureTlsConfig = &tls.Config{
47 | InsecureSkipVerify: true,
48 | }
49 |
50 | var targetConn net.Conn
51 |
52 | var globalStats Stats
53 |
54 | var (
55 | shutdownChan = make(chan struct{})
56 | fc *filtermid.FilterMiddlewareChain
57 | ac *attrlistmid.AttrListMiddlewareChain
58 | bc *basednmid.BaseDNMiddlewareChain
59 | ec *attrentriesmid.AttrEntriesMiddlewareChain
60 |
61 | proxyLDAPAddr string
62 | targetLDAPAddr string
63 | verbFwd uint
64 | verbRev uint
65 | ldaps bool
66 | noShell bool
67 | filterChain string
68 | attrChain string
69 | baseChain string
70 | entriesChain string
71 | tracking bool
72 | options MapFlag
73 | outputFile string
74 | socksServer string
75 |
76 | interceptSearch bool
77 | interceptModify bool
78 | interceptAdd bool
79 | interceptDelete bool
80 | interceptModifyDN bool
81 | listener net.Listener
82 | )
83 |
84 | func shutdownProgram() {
85 | fmt.Println("Bye!")
86 | close(shutdownChan)
87 | os.Exit(0)
88 | }
89 |
90 | type MapFlag struct {
91 | sync.RWMutex
92 | m map[string]string
93 | }
94 |
95 | func (mf *MapFlag) Type() string {
96 | return "map[string]string"
97 | }
98 |
99 | func (mf *MapFlag) String() string {
100 | mf.RLock()
101 | defer mf.RUnlock()
102 | return fmt.Sprintf("%v", mf.m)
103 | }
104 |
105 | func (mf *MapFlag) Set(value string) error {
106 | mf.Lock()
107 | defer mf.Unlock()
108 | parts := strings.SplitN(value, "=", 2)
109 | if len(parts) != 2 {
110 | return fmt.Errorf("invalid option format: %s", value)
111 | }
112 | if mf.m == nil {
113 | mf.m = make(map[string]string)
114 | }
115 | mf.m[parts[0]] = parts[1]
116 | return nil
117 | }
118 |
119 | func (mf *MapFlag) Get(key string) (string, bool) {
120 | mf.RLock()
121 | defer mf.RUnlock()
122 | value, ok := mf.m[key]
123 | return value, ok
124 | }
125 |
126 | func prettyList(list []string) string {
127 | str, _ := json.Marshal(list)
128 | return string(str)
129 | }
130 |
131 | func init() {
132 | pflag.StringVarP(&proxyLDAPAddr, "listen", "l", ":389", "Address & port to listen on for incoming LDAP connections")
133 | pflag.StringVarP(&targetLDAPAddr, "target", "t", "", "Target LDAP server address")
134 | pflag.UintVarP(&verbFwd, "vf", "F", 1, "Set the verbosity level for forward LDAP traffic (requests)")
135 | pflag.UintVarP(&verbRev, "vr", "R", 0, "Set the verbosity level for reverse LDAP traffic (responses)")
136 | pflag.BoolVarP(&ldaps, "ldaps", "s", false, "Connect to target over LDAPS (ignoring cert. validation)")
137 | pflag.StringVarP(&socksServer, "socks", "x", "", "SOCKS proxy address")
138 | pflag.BoolVarP(&noShell, "no-shell", "N", false, "Don't show the ldapx shell")
139 | pflag.StringVarP(&filterChain, "filter", "f", "", "Chain of search filter middlewares")
140 | pflag.StringVarP(&attrChain, "attrlist", "a", "", "Chain of attribute list middlewares")
141 | pflag.StringVarP(&baseChain, "basedn", "b", "", "Chain of baseDN middlewares")
142 | pflag.StringVarP(&entriesChain, "attrentries", "e", "", "Chain of attribute entries middlewares")
143 | pflag.BoolVarP(&tracking, "tracking", "T", true, "Applies a tracking algorithm to avoid issues where complex middlewares + paged searches break LDAP cookies (may be memory intensive)")
144 | pflag.BoolP("version", "v", false, "Show version information")
145 | pflag.VarP(&options, "option", "o", "Configuration options (key=value)")
146 | pflag.StringVarP(&outputFile, "output", "O", "", "Output file to write log messages")
147 | pflag.BoolVarP(&interceptSearch, "search", "S", true, "Intercept LDAP Search operations")
148 | pflag.BoolVarP(&interceptModify, "modify", "M", false, "Intercept LDAP Modify operations")
149 | pflag.BoolVarP(&interceptAdd, "add", "A", false, "Intercept LDAP Add operations")
150 | pflag.BoolVarP(&interceptDelete, "delete", "D", false, "Intercept LDAP Delete operations")
151 | pflag.BoolVarP(&interceptModifyDN, "modifydn", "L", false, "Intercept LDAP ModifyDN operations")
152 |
153 | pflag.Usage = func() {
154 | fmt.Fprintf(os.Stderr, "Usage: %s [OPTIONS]\n", os.Args[0])
155 | fmt.Fprintf(os.Stderr, "\nOptions:\n")
156 | pflag.PrintDefaults()
157 | }
158 |
159 | globalStats.Forward.CountsByType = make(map[int]uint64)
160 | globalStats.Reverse.CountsByType = make(map[int]uint64)
161 |
162 | }
163 | func updateFilterChain(chain string) {
164 | filterChain = chain
165 | fc = &filtermid.FilterMiddlewareChain{}
166 | for _, c := range filterChain {
167 | if middlewareName, exists := filterMidFlags[rune(c)]; exists {
168 | fc.Add(filtermid.FilterMiddlewareDefinition{
169 | Name: middlewareName,
170 | Func: func() filtermid.FilterMiddleware { return filterMidMap[middlewareName] },
171 | })
172 | }
173 | }
174 | }
175 |
176 | func updateBaseDNChain(chain string) {
177 | baseChain = chain
178 | bc = &basednmid.BaseDNMiddlewareChain{}
179 | for _, c := range baseChain {
180 | if middlewareName, exists := baseDNMidFlags[rune(c)]; exists {
181 | bc.Add(basednmid.BaseDNMiddlewareDefinition{
182 | Name: middlewareName,
183 | Func: func() basednmid.BaseDNMiddleware { return baseDNMidMap[middlewareName] },
184 | })
185 | }
186 | }
187 | }
188 |
189 | func updateAttrListChain(chain string) {
190 | attrChain = chain
191 | ac = &attrlistmid.AttrListMiddlewareChain{}
192 | for _, c := range attrChain {
193 | if middlewareName, exists := attrListMidFlags[rune(c)]; exists {
194 | ac.Add(attrlistmid.AttrListMiddlewareDefinition{
195 | Name: middlewareName,
196 | Func: func() attrlistmid.AttrListMiddleware { return attrListMidMap[middlewareName] },
197 | })
198 | }
199 | }
200 | }
201 |
202 | func updateAttrEntriesChain(chain string) {
203 | entriesChain = chain
204 | ec = &attrentriesmid.AttrEntriesMiddlewareChain{}
205 | for _, c := range entriesChain {
206 | if middlewareName, exists := attrEntriesMidFlags[rune(c)]; exists {
207 | ec.Add(attrentriesmid.AttrEntriesMiddlewareDefinition{
208 | Name: middlewareName,
209 | Func: func() attrentriesmid.AttrEntriesMiddleware { return attrEntriesMidMap[middlewareName] },
210 | })
211 | }
212 | }
213 | }
214 |
215 | func main() {
216 | pflag.Parse()
217 |
218 | if pflag.Lookup("version").Changed {
219 | fmt.Printf("ldapx %s\n", version)
220 | os.Exit(0)
221 | }
222 |
223 | log.InitLog(outputFile)
224 |
225 | SetupMiddlewaresMap()
226 |
227 | // Registering middlewares
228 | updateFilterChain(filterChain)
229 | updateBaseDNChain(baseChain)
230 | updateAttrListChain(attrChain)
231 | updateAttrEntriesChain(entriesChain)
232 |
233 | // BaseDN middlewares
234 | appliedBaseDNMiddlewares := []string{}
235 | for _, c := range baseChain {
236 | if middlewareName, exists := baseDNMidFlags[rune(c)]; exists {
237 | appliedBaseDNMiddlewares = append(appliedBaseDNMiddlewares, middlewareName)
238 | }
239 | }
240 |
241 | // Filter middlewares
242 | appliedFilterMiddlewares := []string{}
243 | for _, c := range filterChain {
244 | if middlewareName, exists := filterMidFlags[rune(c)]; exists {
245 | appliedFilterMiddlewares = append(appliedFilterMiddlewares, middlewareName)
246 | }
247 | }
248 |
249 | // AttrList middlewares
250 | appliedAttrListMiddlewares := []string{}
251 | for _, c := range attrChain {
252 | if middlewareName, exists := attrListMidFlags[rune(c)]; exists {
253 | appliedAttrListMiddlewares = append(appliedAttrListMiddlewares, middlewareName)
254 | }
255 | }
256 |
257 | // AttrList middlewares
258 | appliedAttrEntriesMiddlewares := []string{}
259 | for _, c := range entriesChain {
260 | if middlewareName, exists := attrEntriesMidFlags[rune(c)]; exists {
261 | appliedAttrEntriesMiddlewares = append(appliedAttrEntriesMiddlewares, middlewareName)
262 | }
263 | }
264 |
265 | // Fix addresses if the port is missing
266 | if !strings.Contains(proxyLDAPAddr, ":") {
267 | proxyLDAPAddr = fmt.Sprintf("%s:%d", proxyLDAPAddr, 389)
268 | }
269 |
270 | if !strings.Contains(targetLDAPAddr, ":") {
271 | if ldaps {
272 | targetLDAPAddr = fmt.Sprintf("%s:%d", targetLDAPAddr, 636)
273 | } else {
274 | targetLDAPAddr = fmt.Sprintf("%s:%d", targetLDAPAddr, 389)
275 | }
276 | }
277 |
278 | var err error
279 | listener, err = net.Listen("tcp", proxyLDAPAddr)
280 | if err != nil {
281 | log.Log.Printf("[-] Failed to listen on port %s: %s\n", proxyLDAPAddr, err)
282 | shutdownProgram()
283 | }
284 |
285 | if socksServer != "" {
286 | log.Log.Printf("[+] LDAP Proxy listening on '%s', forwarding to '%s' (T) via '%s'\n", proxyLDAPAddr, targetLDAPAddr, socksServer)
287 | } else {
288 | log.Log.Printf("[+] LDAP Proxy listening on '%s', forwarding to '%s' (T)\n", proxyLDAPAddr, targetLDAPAddr)
289 | }
290 | log.Log.Printf("[+] BaseDNMiddlewares: [%s]", strings.Join(appliedBaseDNMiddlewares, ","))
291 | log.Log.Printf("[+] FilterMiddlewares: [%s]", strings.Join(appliedFilterMiddlewares, ","))
292 | log.Log.Printf("[+] AttrListMiddlewares: [%s]", strings.Join(appliedAttrListMiddlewares, ","))
293 | log.Log.Printf("[+] AttrEntriesMiddlewares: [%s]", strings.Join(appliedAttrEntriesMiddlewares, ","))
294 |
295 | if outputFile != "" {
296 | log.Log.Printf("[+] Logging File: '%s'\n", outputFile)
297 | }
298 |
299 | // Main proxy loop
300 | go startProxyLoop(listener)
301 |
302 | // Start interactive shell in the main goroutine
303 | if !noShell {
304 | RunShell()
305 | } else {
306 | <-shutdownChan
307 | }
308 | }
309 |
--------------------------------------------------------------------------------
/log/log.go:
--------------------------------------------------------------------------------
1 | package log
2 |
3 | import (
4 | "io"
5 | "log"
6 | "os"
7 | )
8 |
9 | var Log *log.Logger
10 |
11 | func InitLog(outFile string) {
12 | Log = log.New(os.Stderr, "", log.LstdFlags)
13 |
14 | if outFile != "" {
15 | logFile, err := os.OpenFile(outFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
16 | if err != nil {
17 | Log.Fatalf("[-] Error opening log file: %v", err)
18 | }
19 |
20 | multiWriter := io.MultiWriter(os.Stderr, logFile)
21 | Log.SetOutput(multiWriter)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/middlewares/attrentries/obfuscation.go:
--------------------------------------------------------------------------------
1 | package attrentries
2 |
3 | import (
4 | "math/rand"
5 | "strings"
6 |
7 | "github.com/Macmod/ldapx/middlewares/helpers"
8 | "github.com/Macmod/ldapx/parser"
9 | )
10 |
11 | /*
12 | Obfuscation AttrEntries Middlewares
13 |
14 | References:
15 | - Microsoft Open Specifications - MS-ADTS
16 | https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/d2435927-0999-4c62-8c6d-13ba31a52e1a)
17 | */
18 |
19 | func RandCaseAttrEntriesObf(prob float64) AttrEntriesMiddleware {
20 | return func(entries parser.AttrEntries) parser.AttrEntries {
21 | result := make(parser.AttrEntries, len(entries))
22 |
23 | for i, attr := range entries {
24 | result[i] = parser.Attribute{
25 | Name: helpers.RandomlyChangeCaseString(attr.Name, prob),
26 | Values: attr.Values,
27 | }
28 | }
29 | return result
30 | }
31 | }
32 |
33 | func OIDAttributeAttrEntriesObf(maxSpaces int, maxZeros int, includePrefix bool) AttrEntriesMiddleware {
34 | return func(entries parser.AttrEntries) parser.AttrEntries {
35 | result := make(parser.AttrEntries, len(entries))
36 |
37 | for i, attr := range entries {
38 | name := attr.Name
39 | if oid, exists := parser.OidsMap[strings.ToLower(name)]; exists {
40 | name = oid
41 | }
42 |
43 | if parser.IsOID(name) {
44 | if maxSpaces > 0 {
45 | name += strings.Repeat(" ", 1+rand.Intn(maxSpaces))
46 | }
47 |
48 | if maxZeros > 0 {
49 | name = helpers.RandomlyPrependZerosOID(name, maxZeros)
50 | }
51 |
52 | if !strings.HasPrefix(strings.ToLower(name), "oid.") {
53 | name = "oID." + name
54 | }
55 | }
56 |
57 | result[i] = parser.Attribute{
58 | Name: name,
59 | Values: attr.Values,
60 | }
61 | }
62 | return result
63 | }
64 | }
65 |
66 | func ReorderListAttrEntriesObf() AttrEntriesMiddleware {
67 | return func(entries parser.AttrEntries) parser.AttrEntries {
68 | result := make(parser.AttrEntries, len(entries))
69 | copy(result, entries)
70 |
71 | rand.Shuffle(len(result), func(i, j int) {
72 | result[i], result[j] = result[j], result[i]
73 | })
74 |
75 | return result
76 | }
77 | }
78 |
79 | // Ideas to be considered:
80 | // - Obfuscate the values of the attributes themselves (numeric, DN, SID, etc?)
81 | // - Add allowed attributes for that object type that aren't already specified,
82 | // but with random garbage that doesn't affect the object
83 |
--------------------------------------------------------------------------------
/middlewares/attrentries/types.go:
--------------------------------------------------------------------------------
1 | package attrentries
2 |
3 | import (
4 | "github.com/Macmod/ldapx/log"
5 | "github.com/Macmod/ldapx/parser"
6 | )
7 |
8 | // AttrEntriesMiddleware is a function that takes a list of attribute entries and returns a new list of attribute entries
9 | type AttrEntriesMiddleware func(parser.AttrEntries) parser.AttrEntries
10 |
11 | type AttrEntriesMiddlewareDefinition struct {
12 | Name string
13 | Func func() AttrEntriesMiddleware
14 | }
15 |
16 | type AttrEntriesMiddlewareChain struct {
17 | Middlewares []AttrEntriesMiddlewareDefinition
18 | }
19 |
20 | func (c *AttrEntriesMiddlewareChain) Add(m AttrEntriesMiddlewareDefinition) {
21 | c.Middlewares = append(c.Middlewares, m)
22 | }
23 |
24 | func (c *AttrEntriesMiddlewareChain) Execute(attrEntries parser.AttrEntries, verbose bool) parser.AttrEntries {
25 | current := attrEntries
26 | for _, middleware := range c.Middlewares {
27 | if verbose {
28 | log.Log.Printf("[+] Applying middleware on AttrEntries: %s\n", middleware.Name)
29 | }
30 | current = middleware.Func()(current)
31 | }
32 | return current
33 | }
34 |
--------------------------------------------------------------------------------
/middlewares/attrlist/obfuscation.go:
--------------------------------------------------------------------------------
1 | package attrlist
2 |
3 | import (
4 | "math/rand"
5 | "slices"
6 | "strings"
7 |
8 | "github.com/Macmod/ldapx/middlewares/helpers"
9 | "github.com/Macmod/ldapx/parser"
10 | )
11 |
12 | /*
13 | Obfuscation AttrList Middlewares
14 |
15 | References:
16 | - DEFCON32 - MaLDAPtive
17 | - Microsoft Open Specifications - MS-ADTS
18 | https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/d2435927-0999-4c62-8c6d-13ba31a52e1a)
19 | */
20 |
21 | // RandCaseAttrListObf randomly changes case of attribute names
22 | func RandCaseAttrListObf(prob float64) func([]string) []string {
23 | return func(attrs []string) []string {
24 | result := make([]string, len(attrs))
25 |
26 | for i, attr := range attrs {
27 | result[i] = helpers.RandomlyChangeCaseString(attr, prob)
28 | }
29 | return result
30 | }
31 | }
32 |
33 | // OIDAttributeAttrListObf converts attributes to their OID form
34 | func OIDAttributeAttrListObf(maxSpaces int, maxZeros int, includePrefix bool) func([]string) []string {
35 | return func(attrs []string) []string {
36 | result := make([]string, len(attrs))
37 | for i, attr := range attrs {
38 | if oid, exists := parser.OidsMap[strings.ToLower(attr)]; exists {
39 | result[i] = oid
40 | } else {
41 | result[i] = attr
42 | }
43 |
44 | if parser.IsOID(result[i]) {
45 | if maxSpaces > 0 {
46 | result[i] += strings.Repeat(" ", 1+rand.Intn(maxSpaces))
47 | }
48 |
49 | if maxZeros > 0 {
50 | result[i] = helpers.RandomlyPrependZerosOID(result[i], maxZeros)
51 | }
52 |
53 | if !strings.HasPrefix(strings.ToLower(result[i]), "oid.") {
54 | result[i] = "oID." + result[i]
55 | }
56 | }
57 | }
58 | return result
59 | }
60 | }
61 |
62 | // DuplicateAttrListObf duplicates random attributes
63 | func DuplicateAttrListObf(prob float64) func([]string) []string {
64 | return func(attrs []string) []string {
65 | result := make([]string, 0)
66 |
67 | for _, attr := range attrs {
68 | duplicates := 1
69 | if rand.Float64() < prob {
70 | duplicates++
71 | }
72 |
73 | for i := 0; i < duplicates; i++ {
74 | result = append(result, attr)
75 | }
76 | }
77 |
78 | // Ensure at least one attribute is duplicated
79 | if len(attrs) > 0 && len(result) == len(attrs) {
80 | idx := rand.Intn(len(attrs))
81 | result = append(result, attrs[idx])
82 | }
83 |
84 | return result
85 | }
86 | }
87 |
88 | // GarbageExistingAttrListObf adds garbage to existing attributes
89 | func GarbageExistingAttrListObf(maxGarbage int) func([]string) []string {
90 | return func(attrs []string) []string {
91 | if len(attrs) == 0 {
92 | return attrs
93 | }
94 |
95 | result := make([]string, len(attrs))
96 | copy(result, attrs)
97 |
98 | // Get all attribute names from parser.AttrContexts
99 | existingAttrs := make([]string, 0, len(parser.AttrContexts))
100 | for attr := range parser.AttrContexts {
101 | existingAttrs = append(existingAttrs, attr)
102 | }
103 |
104 | garbageCount := 1 + rand.Intn(maxGarbage)
105 | for i := 0; i < garbageCount; i++ {
106 | randomAttr := existingAttrs[rand.Intn(len(existingAttrs))]
107 | result = append(result, randomAttr)
108 | }
109 | return result
110 | }
111 | }
112 |
113 | // GarbageNonExistingAttrListObf adds completely new garbage attributes
114 | func GarbageNonExistingAttrListObf(maxGarbage int, garbageSize int, garbageCharset string) func([]string) []string {
115 | return func(attrs []string) []string {
116 | if len(attrs) == 0 {
117 | return attrs
118 | }
119 |
120 | result := make([]string, len(attrs))
121 | copy(result, attrs)
122 |
123 | garbageCount := 1 + rand.Intn(maxGarbage)
124 | for i := 0; i < garbageCount; i++ {
125 | var garbage string
126 | exists := true
127 | for exists {
128 | garbage = helpers.GenerateGarbageString(garbageSize, garbageCharset)
129 | _, exists = parser.OidsMap[strings.ToLower(garbage)]
130 | }
131 | result = append(result, garbage)
132 | }
133 | return result
134 | }
135 | }
136 |
137 | // AddWildcardAttrListObf adds wildcards to the attributes list
138 | func AddWildcardAttrListObf() func([]string) []string {
139 | return func(attrs []string) []string {
140 | result := make([]string, len(attrs))
141 | copy(result, attrs)
142 | result = append(result, "*")
143 | return result
144 | }
145 | }
146 |
147 | func AddPlusAttrListObf() func([]string) []string {
148 | return func(attrs []string) []string {
149 | result := make([]string, len(attrs))
150 | copy(result, attrs)
151 |
152 | if len(attrs) == 0 {
153 | // If there are no attributes in the list, we must add a wildcard to the list
154 | // alongside the "+" to preserve the semantics of the query
155 | result = append(result, "*")
156 | }
157 |
158 | result = append(result, "+")
159 | return result
160 | }
161 | }
162 |
163 | func ReplaceWithWildcardAttrListObf() func([]string) []string {
164 | return func(attrs []string) []string {
165 | newAttrs := []string{"*"}
166 | for _, attr := range attrs {
167 | if attr == "+" {
168 | newAttrs = append(newAttrs, "+")
169 | } else if slices.Contains(parser.RootDSEOperationalAttrs, strings.ToLower(attr)) ||
170 | slices.Contains(parser.RFCOperationalAttrs, strings.ToLower(attr)) {
171 | newAttrs = append(newAttrs, attr)
172 | }
173 | }
174 |
175 | return newAttrs
176 | }
177 | }
178 |
179 | func ReplaceWithEmptyAttrListObf() func([]string) []string {
180 | return func(attrs []string) []string {
181 | newAttrs := []string{}
182 |
183 | for _, attr := range attrs {
184 | if attr == "+" {
185 | newAttrs = append(newAttrs, "+")
186 | } else if slices.Contains(parser.RootDSEOperationalAttrs, strings.ToLower(attr)) ||
187 | slices.Contains(parser.RFCOperationalAttrs, strings.ToLower(attr)) {
188 | newAttrs = append(newAttrs, attr)
189 | }
190 | }
191 |
192 | if len(newAttrs) > 0 {
193 | newAttrs = append([]string{"*"}, newAttrs...)
194 | }
195 |
196 | return newAttrs
197 | }
198 | }
199 |
200 | func ReorderListAttrListObf() func([]string) []string {
201 | return func(attrs []string) []string {
202 | result := make([]string, len(attrs))
203 | copy(result, attrs)
204 | rand.Shuffle(len(result), func(i, j int) {
205 | result[i], result[j] = result[j], result[i]
206 | })
207 | return result
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/middlewares/attrlist/types.go:
--------------------------------------------------------------------------------
1 | package attrlist
2 |
3 | import "github.com/Macmod/ldapx/log"
4 |
5 | // AttrListMiddleware is a function that takes a list of attributes and returns a new list
6 | type AttrListMiddleware func([]string) []string
7 |
8 | type AttrListMiddlewareDefinition struct {
9 | Name string
10 | Func func() AttrListMiddleware
11 | }
12 |
13 | type AttrListMiddlewareChain struct {
14 | Middlewares []AttrListMiddlewareDefinition
15 | }
16 |
17 | func (c *AttrListMiddlewareChain) Add(m AttrListMiddlewareDefinition) {
18 | c.Middlewares = append(c.Middlewares, m)
19 | }
20 |
21 | func (c *AttrListMiddlewareChain) Execute(attrs []string, verbose bool) []string {
22 | current := attrs
23 | for _, middleware := range c.Middlewares {
24 | if verbose {
25 | log.Log.Printf("[+] Applying middleware on AttrList: %s\n", middleware.Name)
26 | }
27 | current = middleware.Func()(current)
28 | }
29 | return current
30 | }
31 |
--------------------------------------------------------------------------------
/middlewares/basedn/obfuscation.go:
--------------------------------------------------------------------------------
1 | package basedn
2 |
3 | import (
4 | "math/rand"
5 | "strings"
6 |
7 | "github.com/Macmod/ldapx/middlewares/helpers"
8 | "github.com/Macmod/ldapx/parser"
9 | )
10 |
11 | /*
12 | Obfuscation BaseDN Middlewares
13 |
14 | References:
15 | - DEFCON32 - MaLDAPtive
16 | - Microsoft Open Specifications - MS-ADTS
17 | https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/d2435927-0999-4c62-8c6d-13ba31a52e1a)
18 | */
19 |
20 | // RandCaseBaseDNObf randomly changes case of BaseDN components
21 | func RandCaseBaseDNObf(prob float64) func(string) string {
22 | return func(dn string) string {
23 | return helpers.RandomlyChangeCaseString(dn, prob)
24 | }
25 | }
26 |
27 | // OIDAttributeBaseDNObf converts attribute names in BaseDN to their OID form
28 | func OIDAttributeBaseDNObf(maxSpaces int, maxZeros int, includePrefix bool) func(string) string {
29 | return func(dn string) string {
30 | parts := strings.Split(dn, ",")
31 | for i, part := range parts {
32 | kv := strings.SplitN(part, "=", 2)
33 | if len(kv) == 2 {
34 | attrName := kv[0]
35 | if oid, exists := parser.OidsMap[strings.ToLower(attrName)]; exists {
36 | attrName = oid
37 | }
38 |
39 | if parser.IsOID(attrName) {
40 | if maxSpaces > 0 {
41 | attrName += strings.Repeat(" ", 1+rand.Intn(maxSpaces))
42 | }
43 |
44 | if maxZeros > 0 {
45 | attrName = helpers.RandomlyPrependZerosOID(attrName, maxZeros)
46 | }
47 |
48 | if !strings.HasPrefix(strings.ToLower(attrName), "oid.") {
49 | attrName = "oID." + attrName
50 | }
51 | }
52 |
53 | parts[i] = attrName + "=" + kv[1]
54 | }
55 | }
56 | return strings.Join(parts, ",")
57 | }
58 | }
59 |
60 | // Prepends zeros to attribute OIDs in BaseDN
61 | func OIDPrependZerosBaseDNObf(maxZeros int) func(string) string {
62 | return func(dn string) string {
63 | parts := strings.Split(dn, ",")
64 | for i, part := range parts {
65 | kv := strings.SplitN(part, "=", 2)
66 | if len(kv) == 2 && parser.IsOID(kv[0]) {
67 | oidParts := strings.Split(kv[0], ".")
68 | for j, num := range oidParts {
69 | zeros := strings.Repeat("0", 1+rand.Intn(maxZeros))
70 | oidParts[j] = zeros + num
71 | }
72 | parts[i] = strings.Join(oidParts, ".") + "=" + kv[1]
73 | }
74 | }
75 | return strings.Join(parts, ",")
76 | }
77 | }
78 |
79 | // RandSpacingBaseDNObf adds random spacing to BaseDN in either the beginning or end
80 | func RandSpacingBaseDNObf(maxSpaces int) func(string) string {
81 | return func(dn string) string {
82 | if dn == "" || maxSpaces <= 0 {
83 | return dn
84 | }
85 |
86 | var newDN string
87 |
88 | spaces1 := strings.Repeat(" ", 1+rand.Intn(maxSpaces))
89 | spaces2 := strings.Repeat(" ", 1+rand.Intn(maxSpaces))
90 |
91 | randVal := rand.Intn(3)
92 | if randVal == 0 {
93 | newDN = dn + spaces1
94 | } else if randVal == 1 {
95 | newDN = spaces1 + dn
96 | } else {
97 | newDN = spaces1 + dn + spaces2
98 | }
99 |
100 | return newDN
101 | }
102 | }
103 |
104 | // DoubleQuotesBaseDNObf adds double quotes around BaseDN components
105 | func DoubleQuotesBaseDNObf() func(string) string {
106 | return func(dn string) string {
107 | parts := strings.Split(dn, ",")
108 | for i, part := range parts {
109 | kv := strings.SplitN(part, "=", 2)
110 | if len(kv) == 2 {
111 | value := kv[1]
112 | if strings.ContainsAny(value, "\\") {
113 | continue
114 | }
115 |
116 | if i == len(parts)-1 && strings.HasSuffix(value, " ") {
117 | trimmedValue := strings.TrimRight(value, " ")
118 | parts[i] = kv[0] + "=\"" + trimmedValue + "\"" + strings.Repeat(" ", len(value)-len(trimmedValue))
119 | } else {
120 | parts[i] = kv[0] + "=\"" + value + "\""
121 | }
122 | }
123 | }
124 | return strings.Join(parts, ",")
125 | }
126 | }
127 |
128 | // RandHexValueBaseDNObf randomly hex encodes characters in BaseDN
129 | func RandHexValueBaseDNObf(prob float64) func(string) string {
130 | return func(dn string) string {
131 | parts := strings.Split(dn, ",")
132 | for i, part := range parts {
133 | kv := strings.SplitN(part, "=", 2)
134 | if len(kv) == 2 {
135 | value := kv[1]
136 | startQuote := value[0] == '"'
137 | endQuote := value[len(value)-1] == '"'
138 | if startQuote || endQuote {
139 | continue
140 | }
141 |
142 | spaces := ""
143 | if strings.HasSuffix(value, " ") {
144 | valueWithoutSpaces := strings.TrimRight(value, " ")
145 | spaces = strings.Repeat(" ", len(value)-len(valueWithoutSpaces))
146 | value = valueWithoutSpaces
147 | }
148 |
149 | kv[1] = helpers.RandomlyHexEncodeString(value, prob) + spaces
150 |
151 | parts[i] = kv[0] + "=" + kv[1]
152 | }
153 | }
154 | return strings.Join(parts, ",")
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/middlewares/basedn/types.go:
--------------------------------------------------------------------------------
1 | package basedn
2 |
3 | import "github.com/Macmod/ldapx/log"
4 |
5 | // BaseDNMiddleware is a function that takes a BaseDN string and returns a new one
6 | type BaseDNMiddleware func(string) string
7 |
8 | type BaseDNMiddlewareDefinition struct {
9 | Name string
10 | Func func() BaseDNMiddleware
11 | }
12 |
13 | type BaseDNMiddlewareChain struct {
14 | Middlewares []BaseDNMiddlewareDefinition
15 | }
16 |
17 | func (c *BaseDNMiddlewareChain) Add(m BaseDNMiddlewareDefinition) {
18 | c.Middlewares = append(c.Middlewares, m)
19 | }
20 |
21 | func (c *BaseDNMiddlewareChain) Execute(baseDN string, verbose bool) string {
22 | current := baseDN
23 | for _, middleware := range c.Middlewares {
24 | if verbose {
25 | log.Log.Printf("[+] Applying middleware on BaseDN: %s\n", middleware.Name)
26 | }
27 | current = middleware.Func()(current)
28 | }
29 | return current
30 | }
31 |
--------------------------------------------------------------------------------
/middlewares/filter/helpers.go:
--------------------------------------------------------------------------------
1 | package filter
2 |
3 | import (
4 | "fmt"
5 | "math/rand"
6 | "regexp"
7 | "strconv"
8 | "strings"
9 |
10 | "github.com/Macmod/ldapx/middlewares/helpers"
11 | "github.com/Macmod/ldapx/parser"
12 | )
13 |
14 | // LeafApplierFilterMiddleware applies a FilterMiddleware to all leaf nodes of a filter tree
15 | func LeafApplierFilterMiddleware(fm FilterMiddleware) FilterMiddleware {
16 | var applier FilterMiddleware
17 | applier = func(filter parser.Filter) parser.Filter {
18 | switch f := filter.(type) {
19 | case *parser.FilterAnd:
20 | newFilters := make([]parser.Filter, len(f.Filters))
21 | for i, subFilter := range f.Filters {
22 | newFilters[i] = applier(subFilter)
23 | }
24 | return &parser.FilterAnd{Filters: newFilters}
25 |
26 | case *parser.FilterOr:
27 | newFilters := make([]parser.Filter, len(f.Filters))
28 | for i, subFilter := range f.Filters {
29 | newFilters[i] = applier(subFilter)
30 | }
31 | return &parser.FilterOr{Filters: newFilters}
32 |
33 | case *parser.FilterNot:
34 | return &parser.FilterNot{Filter: applier(f.Filter)}
35 |
36 | default:
37 | return fm(filter)
38 | }
39 | }
40 |
41 | return applier
42 | }
43 |
44 | // Miscellaneous helper functions
45 | func SplitSlice[T any](slice []T, idx int) ([]T, []T) {
46 | before := make([]T, idx)
47 | after := make([]T, len(slice)-idx-1)
48 |
49 | copy(before, slice[:idx])
50 | copy(after, slice[idx+1:])
51 |
52 | return before, after
53 | }
54 |
55 | func RandomlyHexEncodeDNString(dnString string, prob float64) string {
56 | parts := strings.Split(dnString, ",")
57 | for i, part := range parts {
58 | kv := strings.SplitN(part, "=", 2)
59 | if len(kv) == 2 {
60 | value := kv[1]
61 | encodedValue := helpers.RandomlyHexEncodeString(value, prob)
62 | parts[i] = kv[0] + "=" + encodedValue
63 | }
64 | }
65 | return strings.Join(parts, ",")
66 | }
67 |
68 | func ReplaceTimestamp(value string, maxChars int, charset string, useComma bool) string {
69 | re := regexp.MustCompile(`^([0-9]{14})[.,](.*)(Z|[+-].{4})(.*)`)
70 | return re.ReplaceAllStringFunc(value, func(match string) string {
71 | parts := re.FindStringSubmatch(match)
72 | if len(parts) == 5 {
73 | var prependStr string
74 | var appendStr string
75 |
76 | randStr1 := helpers.GenerateGarbageString(maxChars, charset)
77 | randStr2 := helpers.GenerateGarbageString(maxChars, charset)
78 | randVal := rand.Intn(3)
79 | if randVal == 0 {
80 | prependStr = randStr1
81 | } else if randVal == 1 {
82 | appendStr = randStr2
83 | } else {
84 | prependStr = randStr1
85 | appendStr = randStr2
86 | }
87 |
88 | sep := "."
89 | if useComma {
90 | sep = ","
91 | }
92 |
93 | return fmt.Sprintf("%s%s%s%s%s%s%s", parts[1], sep, parts[2], prependStr, parts[3], appendStr, parts[4])
94 | }
95 | return match
96 | })
97 | }
98 |
99 | // Prepend Zeros functions
100 | func PrependZerosToSID(sid string, maxZeros int) string {
101 | parts := strings.Split(sid, "-")
102 | for i := range parts {
103 | if i == 0 {
104 | continue
105 | }
106 |
107 | for j, c := range parts[i] {
108 | if c >= '0' && c <= '9' {
109 | prefix := parts[i][:j]
110 | suffix := parts[i][j:]
111 | numZeros := rand.Intn(maxZeros)
112 | zerosStr := strings.Repeat("0", numZeros)
113 | parts[i] = prefix + zerosStr + suffix
114 | break
115 | }
116 | }
117 | }
118 | return strings.Join(parts, "-")
119 | }
120 |
121 | func PrependZerosToNumber(input string, maxZeros int) string {
122 | numZeros := rand.Intn(maxZeros)
123 | zerosStr := strings.Repeat("0", numZeros)
124 | if len(input) > 0 && input[0] == '-' {
125 | return "-" + zerosStr + input[1:]
126 | }
127 | return zerosStr + input
128 | }
129 |
130 | func AddRandSpacing(s string, maxSpaces int) string {
131 | var result strings.Builder
132 | var numSpaces int
133 | for _, char := range s {
134 | numSpaces = rand.Intn(maxSpaces)
135 | if numSpaces > 0 {
136 | result.WriteString(strings.Repeat(" ", numSpaces))
137 | }
138 | result.WriteRune(char)
139 | }
140 |
141 | numSpaces = rand.Intn(maxSpaces)
142 | if numSpaces > 0 {
143 | result.WriteString(strings.Repeat(" ", numSpaces))
144 | }
145 |
146 | return result.String()
147 | }
148 |
149 | func MapToOID(attrName string) (string, error) {
150 | oid, ok := parser.OidsMap[strings.ToLower(attrName)]
151 |
152 | if !ok {
153 | return attrName, fmt.Errorf("OID not found")
154 | }
155 |
156 | return oid, nil
157 | }
158 |
159 | func AddANRSpacing(value string, maxSpaces int) string {
160 | spacesFst := strings.Repeat(" ", 1+rand.Intn(maxSpaces))
161 | spacesEqSign := strings.Repeat(" ", 1+rand.Intn(maxSpaces))
162 | spacesLst := strings.Repeat(" ", 1+rand.Intn(maxSpaces))
163 | if strings.HasPrefix(strings.TrimSpace(value), "=") {
164 | // If there's an equal sign prefix, we must consider adding spaces right after it too
165 | idx := strings.Index(value, "=")
166 |
167 | if idx != -1 && idx+1 < len(value) && rand.Float64() < 0.5 {
168 | value = value[:idx+1] + spacesEqSign + value[idx+1:]
169 | }
170 | }
171 |
172 | randVal := rand.Intn(3)
173 | if randVal == 0 {
174 | return spacesFst + value
175 | } else if randVal == 1 {
176 | return value + spacesLst
177 | } else {
178 | return spacesFst + value + spacesLst
179 | }
180 | }
181 |
182 | func AddDNSpacing(value string, maxSpaces int) string {
183 | parts := strings.Split(value, ",")
184 | for i, part := range parts {
185 | kv := strings.SplitN(part, "=", 2)
186 | if len(kv) == 2 {
187 | switch rand.Intn(4) {
188 | case 0:
189 | kv[0] = kv[0] + strings.Repeat(" ", 1+rand.Intn(maxSpaces))
190 | case 1:
191 | kv[1] = strings.Repeat(" ", 1+rand.Intn(maxSpaces)) + kv[1]
192 | case 2:
193 | kv[0] = strings.Repeat(" ", 1+rand.Intn(maxSpaces)) + kv[0]
194 | case 3:
195 | kv[1] = kv[1] + strings.Repeat(" ", 1+rand.Intn(maxSpaces))
196 | }
197 | parts[i] = strings.Join(kv, "=")
198 | }
199 | }
200 | return strings.Join(parts, ",")
201 | }
202 |
203 | func AddSIDSpacing(sid string, maxSpaces int) string {
204 | parts := strings.Split(sid, "-")
205 | if len(parts) >= 3 {
206 | // Add spaces before revision number (parts[1])
207 | spaces := strings.Repeat(" ", rand.Intn(maxSpaces+1))
208 | parts[1] = spaces + parts[1]
209 |
210 | // Add spaces before subauthority count (parts[2])
211 | spaces = strings.Repeat(" ", rand.Intn(maxSpaces+1))
212 | parts[2] = spaces + parts[2]
213 | }
214 | return strings.Join(parts, "-")
215 | }
216 |
217 | // Comparison helpers
218 |
219 | const CharOrdering = "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
220 |
221 | // TODO: Review
222 | func GetNextString(s string) string {
223 | // Convert string to rune slice for easier manipulation
224 | chars := []rune(s)
225 |
226 | // Start from rightmost character
227 | for i := len(chars) - 1; i >= 0; i-- {
228 | // Find current char position in CharOrdering
229 | pos := strings.IndexRune(CharOrdering, chars[i])
230 |
231 | // If not last char in CharOrdering, increment to next
232 | if pos < len(CharOrdering)-1 {
233 | chars[i] = rune(CharOrdering[pos+1])
234 | return string(chars)
235 | }
236 |
237 | // If last char in CharOrdering, set to first char and continue left
238 | chars[i] = rune(CharOrdering[0])
239 | }
240 |
241 | // If all chars were last in CharOrdering, append first char
242 | return s + string(CharOrdering[0])
243 | }
244 |
245 | func GetPreviousString(s string) string {
246 | chars := []rune(s)
247 |
248 | for i := len(chars) - 1; i >= 0; i-- {
249 | pos := strings.IndexRune(CharOrdering, chars[i])
250 |
251 | if pos > 0 {
252 | chars[i] = rune(CharOrdering[pos-1])
253 | return string(chars)
254 | }
255 |
256 | chars[i] = rune(CharOrdering[len(CharOrdering)-1])
257 | }
258 |
259 | // If string is all first chars, remove first char
260 | if len(s) > 1 {
261 | return s[:len(s)-1]
262 | }
263 |
264 | return s
265 | }
266 |
267 | func GetNextSID(sid string) string {
268 | parts := strings.Split(sid, "-")
269 | if len(parts) < 1 {
270 | return sid
271 | }
272 |
273 | if num, err := strconv.Atoi(parts[len(parts)-1]); err == nil {
274 | parts[len(parts)-1] = strconv.Itoa(num + 1)
275 | }
276 | return strings.Join(parts, "-")
277 | }
278 |
279 | func GetPreviousSID(sid string) string {
280 | parts := strings.Split(sid, "-")
281 | if len(parts) < 1 {
282 | return sid
283 | }
284 |
285 | if num, err := strconv.Atoi(parts[len(parts)-1]); err == nil && num > 0 {
286 | parts[len(parts)-1] = strconv.Itoa(num - 1)
287 | }
288 | return strings.Join(parts, "-")
289 | }
290 |
--------------------------------------------------------------------------------
/middlewares/filter/types.go:
--------------------------------------------------------------------------------
1 | package filter
2 |
3 | import (
4 | "github.com/Macmod/ldapx/log"
5 | "github.com/Macmod/ldapx/parser"
6 | )
7 |
8 | // FilterMiddleware is a function that takes a Filter and returns a new Filter
9 | type FilterMiddleware func(parser.Filter) parser.Filter
10 |
11 | type FilterMiddlewareDefinition struct {
12 | Name string
13 | Func func() FilterMiddleware
14 | }
15 |
16 | type FilterMiddlewareChain struct {
17 | Middlewares []FilterMiddlewareDefinition
18 | }
19 |
20 | func (c *FilterMiddlewareChain) Add(m FilterMiddlewareDefinition) {
21 | c.Middlewares = append(c.Middlewares, m)
22 | }
23 |
24 | func (c *FilterMiddlewareChain) Execute(f parser.Filter, verbose bool) parser.Filter {
25 | current := f
26 | for _, middleware := range c.Middlewares {
27 | if verbose {
28 | log.Log.Printf("[+] Applying middleware on Filter: %s\n", middleware.Name)
29 | }
30 | current = middleware.Func()(current)
31 | }
32 | return current
33 | }
34 |
--------------------------------------------------------------------------------
/middlewares/helpers/string.go:
--------------------------------------------------------------------------------
1 | package helpers
2 |
3 | import (
4 | "fmt"
5 | "math/rand"
6 | "strings"
7 | )
8 |
9 | func GenerateGarbageString(n int, chars string) string {
10 | result := make([]byte, n)
11 | for i := range result {
12 | result[i] = chars[rand.Intn(len(chars))]
13 | }
14 | return string(result)
15 | }
16 |
17 | func HexEncodeChar(c rune) string {
18 | return fmt.Sprintf("\\%02x", c)
19 | }
20 |
21 | func RandomlyHexEncodeString(s string, prob float64) string {
22 | var result strings.Builder
23 | for _, c := range s {
24 | if rand.Float64() < prob {
25 | result.WriteString(HexEncodeChar(c))
26 | } else {
27 | result.WriteRune(c)
28 | }
29 | }
30 |
31 | return result.String()
32 | }
33 |
34 | func RandomlyChangeCaseString(s string, prob float64) string {
35 | var builder strings.Builder
36 | for _, c := range s {
37 | if rand.Float64() < prob {
38 | if rand.Intn(2) == 0 {
39 | builder.WriteString(strings.ToLower(string(c)))
40 | } else {
41 | builder.WriteString(strings.ToUpper(string(c)))
42 | }
43 | } else {
44 | builder.WriteRune(c)
45 | }
46 | }
47 | return builder.String()
48 | }
49 |
50 | func RandomlyPrependZerosOID(oid string, maxZeros int) string {
51 | oidParts := strings.Split(oid, ".")
52 | for j, num := range oidParts {
53 | if strings.ToLower(oidParts[j]) != "oid" {
54 | zeros := strings.Repeat("0", 1+rand.Intn(maxZeros))
55 | oidParts[j] = zeros + num
56 | }
57 | }
58 | return strings.Join(oidParts, ".")
59 | }
60 |
--------------------------------------------------------------------------------
/middlewares/options.go:
--------------------------------------------------------------------------------
1 | package middlewares
2 |
3 | var DefaultOptions = map[string]string{
4 | "BDNCaseProb": "0.7",
5 | "BDNHexValueProb": "0.7",
6 | "BDNOIDAttributeMaxSpaces": "4",
7 | "BDNOIDAttributeMaxZeros": "4",
8 | "BDNOIDAttributeIncludePrefix": "true",
9 |
10 | "FiltSpacingMaxSpaces": "4",
11 | "FiltTimestampGarbageUseComma": "false",
12 | "FiltTimestampGarbageMaxChars": "6",
13 | "FiltAddBoolMaxDepth": "4",
14 | "FiltDblNegBoolMaxDepth": "1",
15 | "FiltANRSubstringMaxElems": "4",
16 | "FiltSubstringSplitProb": "0.7",
17 | "FiltPrependZerosMaxElems": "4",
18 | "FiltGarbageMaxElems": "4",
19 | "FiltGarbageMaxSize": "6",
20 | "FiltGarbageCharset": "abcdefghijklmnopqrsutwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
21 | "FiltCaseProb": "0.7",
22 | "FiltHexValueProb": "0.7",
23 | "FiltEqExtensibleAppendDN": "false",
24 | "FiltBitwiseDecompositionMaxBits": "32",
25 | "FiltOIDAttributeMaxSpaces": "4",
26 | "FiltOIDAttributeMaxZeros": "4",
27 | "FiltOIDAttributeIncludePrefix": "true",
28 |
29 | "AttrsDuplicateProb": "0.7",
30 | "AttrsCaseProb": "0.7",
31 | "AttrsGarbageCharset": "abcdefghijklmnopqrsutwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
32 | "AttrsGarbageExistingMaxElems": "4",
33 | "AttrsGarbageNonExistingMaxElems": "4",
34 | "AttrsGarbageNonExistingMaxSize": "6",
35 | "AttrsOIDAttributeMaxSpaces": "4",
36 | "AttrsOIDAttributeMaxZeros": "4",
37 | "AttrsOIDAttributeIncludePrefix": "true",
38 |
39 | "AttrEntriesCaseProb": "0.7",
40 | "AttrEntriesOIDAttributeMaxSpaces": "4",
41 | "AttrEntriesOIDAttributeMaxZeros": "4",
42 | "AttrEntriesOIDAttributePrefix": "true",
43 | }
44 |
45 | var DefaultOptionsKeys = []string{
46 | "BDNCaseProb",
47 | "BDNHexValueProb",
48 | "BDNOIDAttributeMaxSpaces",
49 | "BDNOIDAttributeMaxZeros",
50 | "BDNOIDAttributeIncludePrefix",
51 |
52 | "FiltSpacingMaxSpaces",
53 | "FiltTimestampGarbageMaxChars",
54 | "FiltTimestampGarbageUseComma",
55 | "FiltAddBoolMaxDepth",
56 | "FiltDblNegBoolMaxDepth",
57 | "FiltANRSubstringMaxElems",
58 | "FiltSubstringSplitProb",
59 | "FiltPrependZerosMaxElems",
60 | "FiltGarbageMaxElems",
61 | "FiltGarbageMaxSize",
62 | "FiltGarbageCharset",
63 | "FiltCaseProb",
64 | "FiltHexValueProb",
65 | "FiltEqExtensibleAppendDN",
66 | "FiltBitwiseDecompositionMaxBits",
67 | "FiltOIDAttributeMaxSpaces",
68 | "FiltOIDAttributeMaxZeros",
69 | "FiltOIDAttributeIncludePrefix",
70 |
71 | "AttrsGarbageExistingMaxElems",
72 | "AttrsGarbageNonExistingMaxElems",
73 | "AttrsOIDAttributeMaxSpaces",
74 | "AttrsOIDAttributeMaxZeros",
75 | "AttrsOIDAttributeIncludePrefix",
76 | }
77 |
--------------------------------------------------------------------------------
/parser/attrs.go:
--------------------------------------------------------------------------------
1 | package parser
2 |
3 | type Attribute struct {
4 | Name string
5 | Values []string
6 | }
7 |
8 | type AttrEntries []Attribute
9 |
10 | func (a *AttrEntries) AddValue(name string, value string) {
11 | if len(*a) == 0 {
12 | *a = make([]Attribute, 0)
13 | }
14 |
15 | for i := range *a {
16 | if (*a)[i].Name == name {
17 | (*a)[i].Values = append((*a)[i].Values, value)
18 | return
19 | }
20 | }
21 |
22 | *a = append(*a, Attribute{Name: name, Values: []string{value}})
23 | }
24 |
25 | func (a *AttrEntries) AppendAttr(name string, value string) {
26 | *a = append(*a, Attribute{Name: name, Values: []string{value}})
27 | }
28 |
--------------------------------------------------------------------------------
/parser/filter.go:
--------------------------------------------------------------------------------
1 | package parser
2 |
3 | import (
4 | "bytes"
5 | hexpac "encoding/hex"
6 | "errors"
7 | "fmt"
8 | "io"
9 | "strings"
10 | "unicode"
11 | "unicode/utf8"
12 |
13 | ber "github.com/go-asn1-ber/asn1-ber"
14 | )
15 |
16 | /*
17 | Parser for raw (packet-level) LDAP search filters
18 | References:
19 | - RFC4510 - LDAP: Technical Specification
20 | - RFC4515 - LDAP: String Representation of Search Filters
21 | - DEFCON32 - MaLDAPtive
22 | */
23 |
24 | // FilterType represents the various LDAP filter types.
25 | type FilterType int
26 |
27 | const (
28 | And FilterType = iota
29 | Or
30 | Not
31 | EqualityMatch
32 | Substring
33 | GreaterOrEqual
34 | LessOrEqual
35 | Present
36 | ApproxMatch
37 | ExtensibleMatch
38 | )
39 |
40 | // Filter is an interface for all LDAP filter types.
41 | type Filter interface {
42 | // Type returns the type of the filter.
43 | Type() FilterType
44 | }
45 |
46 | func GetAttrName(filter Filter) (string, error) {
47 | switch f := filter.(type) {
48 | case *FilterAnd:
49 | return "", fmt.Errorf("AND filters have no attribute name")
50 | case *FilterOr:
51 | return "", fmt.Errorf("OR filters have no attribute name")
52 | case *FilterNot:
53 | return "", fmt.Errorf("NOT filters have no attribute name")
54 | case *FilterEqualityMatch:
55 | return f.AttributeDesc, nil
56 | case *FilterSubstring:
57 | return f.AttributeDesc, nil
58 | case *FilterGreaterOrEqual:
59 | return f.AttributeDesc, nil
60 | case *FilterLessOrEqual:
61 | return f.AttributeDesc, nil
62 | case *FilterPresent:
63 | return f.AttributeDesc, nil
64 | case *FilterApproxMatch:
65 | return f.AttributeDesc, nil
66 | case *FilterExtensibleMatch:
67 | return f.AttributeDesc, nil
68 | default:
69 | return "", fmt.Errorf("Unknown filters have no attribute name")
70 | }
71 | }
72 |
73 | // Base structs for different filter types:
74 |
75 | // FilterAnd represents an AND filter.
76 | type FilterAnd struct {
77 | Filters []Filter
78 | }
79 |
80 | func (f *FilterAnd) Type() FilterType { return And }
81 |
82 | // FilterOr represents an OR filter.
83 | type FilterOr struct {
84 | Filters []Filter
85 | }
86 |
87 | func (f *FilterOr) Type() FilterType { return Or }
88 |
89 | // FilterNot represents a NOT filter.
90 | type FilterNot struct {
91 | Filter Filter
92 | }
93 |
94 | func (f *FilterNot) Type() FilterType { return Not }
95 |
96 | // FilterEqualityMatch represents an equality match filter.
97 | type FilterEqualityMatch struct {
98 | AttributeDesc string
99 | AssertionValue string
100 | }
101 |
102 | func (f *FilterEqualityMatch) Type() FilterType { return EqualityMatch }
103 |
104 | // FilterPresent represents a presence filter.
105 | type FilterPresent struct {
106 | AttributeDesc string
107 | }
108 |
109 | func (f *FilterPresent) Type() FilterType { return Present }
110 |
111 | // FilterSubstring represents a substring filter.
112 | type FilterSubstring struct {
113 | AttributeDesc string
114 | Substrings []SubstringFilter
115 | }
116 |
117 | func (f *FilterSubstring) Type() FilterType { return Substring }
118 |
119 | // FilterGreaterOrEqual represents a greater-or-equal filter.
120 | type FilterGreaterOrEqual struct {
121 | AttributeDesc string
122 | AssertionValue string
123 | }
124 |
125 | func (f *FilterGreaterOrEqual) Type() FilterType { return GreaterOrEqual }
126 |
127 | // FilterLessOrEqual represents a less-or-equal filter.
128 | type FilterLessOrEqual struct {
129 | AttributeDesc string
130 | AssertionValue string
131 | }
132 |
133 | func (f *FilterLessOrEqual) Type() FilterType { return LessOrEqual }
134 |
135 | // FilterApproxMatch represents an approximate match filter.
136 | type FilterApproxMatch struct {
137 | AttributeDesc string
138 | AssertionValue string
139 | }
140 |
141 | func (f *FilterApproxMatch) Type() FilterType { return ApproxMatch }
142 |
143 | // FilterExtensibleMatch represents an extensible match filter.
144 | type FilterExtensibleMatch struct {
145 | MatchingRule string
146 | AttributeDesc string
147 | MatchValue string
148 | DNAttributes bool
149 | }
150 |
151 | func (f *FilterExtensibleMatch) Type() FilterType { return ExtensibleMatch }
152 |
153 | // SubstringFilter represents a component of a substring filter.
154 | // Either Initial, Any or Final will be set.
155 | type SubstringFilter struct {
156 | Initial string
157 | Any string
158 | Final string
159 | }
160 |
161 | // Converts a BER packet into a Filter structure
162 | func PacketToFilter(packet *ber.Packet) (Filter, error) {
163 | switch packet.Tag {
164 | case 0x0: // AND filter
165 | var filters []Filter
166 | for _, child := range packet.Children {
167 | subFilter, err := PacketToFilter(child)
168 | if err != nil {
169 | return nil, err
170 | }
171 | filters = append(filters, subFilter)
172 | }
173 | return &FilterAnd{Filters: filters}, nil
174 |
175 | case 0x1: // OR filter
176 | var filters []Filter
177 | for _, child := range packet.Children {
178 | subFilter, err := PacketToFilter(child)
179 | if err != nil {
180 | return nil, err
181 | }
182 | filters = append(filters, subFilter)
183 | }
184 | return &FilterOr{Filters: filters}, nil
185 |
186 | case 0x2: // NOT filter
187 | if len(packet.Children) != 1 {
188 | return nil, fmt.Errorf("NOT filter should have exactly 1 child")
189 | }
190 | subFilter, err := PacketToFilter(packet.Children[0])
191 | if err != nil {
192 | return nil, err
193 | }
194 | return &FilterNot{Filter: subFilter}, nil
195 |
196 | case 0x3: // Equality Match filter (e.g., (cn=John))
197 | if len(packet.Children) != 2 {
198 | return nil, fmt.Errorf("Equality match filter should have 2 children")
199 | }
200 | attr := string(packet.Children[0].Data.Bytes())
201 | value := string(packet.Children[1].Data.Bytes())
202 | return &FilterEqualityMatch{
203 | AttributeDesc: attr,
204 | AssertionValue: value,
205 | }, nil
206 |
207 | case 0x4: // Substring filter (e.g., (cn=Jo*hn))
208 | if len(packet.Children) < 2 {
209 | return nil, fmt.Errorf("Substring filter should have at least 2 children")
210 | }
211 |
212 | attr := string(packet.Children[0].Data.Bytes())
213 | var substrs []SubstringFilter
214 | for _, subPacket := range packet.Children[1].Children {
215 | switch int(subPacket.Tag) {
216 | case 0x0: // Initial
217 | substrs = append(substrs, SubstringFilter{Initial: string(subPacket.Data.Bytes())})
218 | case 0x1: // Any
219 | substrs = append(substrs, SubstringFilter{Any: string(subPacket.Data.Bytes())})
220 | case 0x2: // Final
221 | substrs = append(substrs, SubstringFilter{Final: string(subPacket.Data.Bytes())})
222 | }
223 | }
224 |
225 | return &FilterSubstring{
226 | AttributeDesc: attr,
227 | Substrings: substrs,
228 | }, nil
229 |
230 | case 0x5: // GreaterOrEqual filter (e.g., (age>=25))
231 | if len(packet.Children) != 2 {
232 | return nil, fmt.Errorf("GreaterOrEqual filter should have 2 children")
233 | }
234 | attr := string(packet.Children[0].Data.Bytes())
235 | value := string(packet.Children[1].Data.Bytes())
236 | return &FilterGreaterOrEqual{
237 | AttributeDesc: attr,
238 | AssertionValue: value,
239 | }, nil
240 |
241 | case 0x6: // LessOrEqual filter (e.g., (age<=30))
242 | if len(packet.Children) != 2 {
243 | return nil, fmt.Errorf("LessOrEqual filter should have 2 children")
244 | }
245 | attr := string(packet.Children[0].Data.Bytes())
246 | value := string(packet.Children[1].Data.Bytes())
247 | return &FilterLessOrEqual{
248 | AttributeDesc: attr,
249 | AssertionValue: value,
250 | }, nil
251 |
252 | case 0x7: // Present filter (e.g., (cn=))
253 | if packet.Data.Len() < 1 {
254 | return nil, fmt.Errorf("Present filter should have data")
255 | }
256 |
257 | attr := packet.Data.String()
258 | return &FilterPresent{
259 | AttributeDesc: attr,
260 | }, nil
261 |
262 | case 0x8: // ApproxMatch filter (e.g., (cn~=John))
263 | if len(packet.Children) != 2 {
264 | return nil, fmt.Errorf("ApproxMatch filter should have 2 children")
265 | }
266 | attr := string(packet.Children[0].Data.Bytes())
267 | value := string(packet.Children[1].Data.Bytes())
268 | return &FilterApproxMatch{
269 | AttributeDesc: attr,
270 | AssertionValue: value,
271 | }, nil
272 |
273 | case 0x9: // ExtensibleMatch filter
274 | if len(packet.Children) < 2 {
275 | return nil, fmt.Errorf("ExtensibleMatch filter should have at least 2 children")
276 | }
277 |
278 | // Parsing the different components of ExtensibleMatch
279 | var matchingRule string
280 | var attributeDesc, matchValue string
281 | var dnAttributes bool
282 |
283 | // Check for optional components
284 | for _, child := range packet.Children {
285 | bytes := child.Data.Bytes()
286 |
287 | switch int(child.Tag) {
288 | case 0x1: // MatchingRuleID
289 | matchingRule = string(bytes)
290 | case 0x2: // AttributeDescription
291 | attributeDesc = string(bytes)
292 | case 0x3: // MatchValue
293 | matchValue = string(bytes)
294 | case 0x4: // DNAttributes (True/False)
295 | dnAttributes = len(bytes) > 0 && bytes[0] != byte(0)
296 | }
297 | }
298 |
299 | // Create the ExtensibleMatch filter
300 | return &FilterExtensibleMatch{
301 | MatchingRule: matchingRule,
302 | AttributeDesc: attributeDesc,
303 | MatchValue: matchValue,
304 | DNAttributes: dnAttributes,
305 | }, nil
306 |
307 | default:
308 | return nil, fmt.Errorf("unsupported filter type with tag: %x", packet.Tag)
309 | }
310 | }
311 |
312 | func FilterToString(filter Filter, level int) string {
313 | var result strings.Builder
314 | indent := strings.Repeat(" ", level)
315 | result.WriteString(fmt.Sprintf("%sFilter Type: %v\n", indent, filter.Type()))
316 |
317 | switch f := filter.(type) {
318 | case *FilterAnd:
319 | result.WriteString(fmt.Sprintf("%sAND Filter with %d sub-filters:\n", indent, len(f.Filters)))
320 | for _, subFilter := range f.Filters {
321 | result.WriteString(FilterToString(subFilter, level+1))
322 | }
323 | case *FilterOr:
324 | result.WriteString(fmt.Sprintf("%sOR Filter with %d sub-filters:\n", indent, len(f.Filters)))
325 | for _, subFilter := range f.Filters {
326 | result.WriteString(FilterToString(subFilter, level+1))
327 | }
328 | case *FilterNot:
329 | result.WriteString(fmt.Sprintf("%sNOT Filter:\n", indent))
330 | result.WriteString(FilterToString(f.Filter, level+1))
331 | case *FilterEqualityMatch:
332 | result.WriteString(fmt.Sprintf("%sEquality Match - Attribute: %s, Value: %s\n", indent, f.AttributeDesc, f.AssertionValue))
333 | case *FilterPresent:
334 | result.WriteString(fmt.Sprintf("%sPresent Filter - Attribute: %s\n", indent, f.AttributeDesc))
335 | case *FilterSubstring:
336 | result.WriteString(fmt.Sprintf("%sSubstring Filter - Attribute: %s (Length %d)\n", indent, f.AttributeDesc, len(f.Substrings)))
337 | for _, sub := range f.Substrings {
338 | if sub.Initial != "" {
339 | result.WriteString(fmt.Sprintf("%s Initial: %s\n", indent, sub.Initial))
340 | }
341 | if sub.Any != "" {
342 | result.WriteString(fmt.Sprintf("%s Any: %s\n", indent, sub.Any))
343 | }
344 | if sub.Final != "" {
345 | result.WriteString(fmt.Sprintf("%s Final: %s\n", indent, sub.Final))
346 | }
347 | }
348 | case *FilterGreaterOrEqual:
349 | result.WriteString(fmt.Sprintf("%sGreater Or Equal - Attribute: %s, Value: %s\n", indent, f.AttributeDesc, f.AssertionValue))
350 | case *FilterLessOrEqual:
351 | result.WriteString(fmt.Sprintf("%sLess Or Equal - Attribute: %s, Value: %s\n", indent, f.AttributeDesc, f.AssertionValue))
352 | case *FilterApproxMatch:
353 | result.WriteString(fmt.Sprintf("%sApprox Match - Attribute: %s, Value: %s\n", indent, f.AttributeDesc, f.AssertionValue))
354 | case *FilterExtensibleMatch:
355 | result.WriteString(fmt.Sprintf("%sExtensible Match - Matching Rule: %s, Attribute: %s, Value: %s, DN Attributes: %t\n",
356 | indent, f.MatchingRule, f.AttributeDesc, f.MatchValue, f.DNAttributes))
357 | default:
358 | result.WriteString(fmt.Sprintf("%sUnknown filter type.\n", indent))
359 | }
360 |
361 | return result.String()
362 | }
363 |
364 | func FilterToPacket(f Filter) *ber.Packet {
365 | switch filter := f.(type) {
366 | case *FilterAnd:
367 | packet := ber.Encode(ber.ClassContext, ber.TypeConstructed, 0x0, nil, "")
368 | for _, subFilter := range filter.Filters {
369 | packet.AppendChild(FilterToPacket(subFilter))
370 | }
371 | return packet
372 |
373 | case *FilterOr:
374 | packet := ber.Encode(ber.ClassContext, ber.TypeConstructed, 0x1, nil, "")
375 | for _, subFilter := range filter.Filters {
376 | packet.AppendChild(FilterToPacket(subFilter))
377 | }
378 | return packet
379 |
380 | case *FilterNot:
381 | packet := ber.Encode(ber.ClassContext, ber.TypeConstructed, 0x2, nil, "")
382 | packet.AppendChild(FilterToPacket(filter.Filter))
383 | return packet
384 |
385 | case *FilterEqualityMatch:
386 | packet := ber.Encode(ber.ClassContext, ber.TypeConstructed, 0x3, nil, "")
387 | packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, filter.AttributeDesc, ""))
388 | packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, filter.AssertionValue, ""))
389 | return packet
390 |
391 | case *FilterSubstring:
392 | packet := ber.Encode(ber.ClassContext, ber.TypeConstructed, 0x4, nil, "")
393 | packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, filter.AttributeDesc, ""))
394 |
395 | substrings := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "")
396 | for _, substr := range filter.Substrings {
397 | if substr.Initial != "" {
398 | substrings.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0x0, substr.Initial, ""))
399 | }
400 | if substr.Any != "" {
401 | substrings.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0x1, substr.Any, ""))
402 | }
403 | if substr.Final != "" {
404 | substrings.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0x2, substr.Final, ""))
405 | }
406 | }
407 | packet.AppendChild(substrings)
408 | return packet
409 |
410 | case *FilterGreaterOrEqual:
411 | packet := ber.Encode(ber.ClassContext, ber.TypeConstructed, 0x5, nil, "")
412 | packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, filter.AttributeDesc, ""))
413 | packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, filter.AssertionValue, ""))
414 | return packet
415 |
416 | case *FilterLessOrEqual:
417 | packet := ber.Encode(ber.ClassContext, ber.TypeConstructed, 0x6, nil, "")
418 | packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, filter.AttributeDesc, ""))
419 | packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, filter.AssertionValue, ""))
420 | return packet
421 |
422 | case *FilterPresent:
423 | return ber.NewString(ber.ClassContext, ber.TypePrimitive, 0x7, filter.AttributeDesc, "")
424 |
425 | case *FilterApproxMatch:
426 | packet := ber.Encode(ber.ClassContext, ber.TypeConstructed, 0x8, nil, "")
427 | packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, filter.AttributeDesc, ""))
428 | packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, filter.AssertionValue, ""))
429 | return packet
430 |
431 | case *FilterExtensibleMatch:
432 | packet := ber.Encode(ber.ClassContext, ber.TypeConstructed, 0x9, nil, "")
433 | if filter.MatchingRule != "" {
434 | packet.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0x1, filter.MatchingRule, ""))
435 | }
436 | if filter.AttributeDesc != "" {
437 | packet.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0x2, filter.AttributeDesc, ""))
438 | }
439 | if filter.MatchValue != "" {
440 | packet.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0x3, filter.MatchValue, ""))
441 | }
442 | if filter.DNAttributes {
443 | packet.AppendChild(ber.NewBoolean(ber.ClassContext, ber.TypePrimitive, 0x4, true, ""))
444 | }
445 | return packet
446 | }
447 |
448 | return nil
449 | }
450 |
451 | // Conversions from Filter to Query and vice-versa
452 | func FilterToQuery(filter Filter) (string, error) {
453 | switch f := filter.(type) {
454 | case *FilterAnd:
455 | var subFilters []string
456 | for _, subFilter := range f.Filters {
457 | subStr, err := FilterToQuery(subFilter)
458 | if err != nil {
459 | return "", err
460 | }
461 | subFilters = append(subFilters, subStr)
462 | }
463 | return "(&" + strings.Join(subFilters, "") + ")", nil
464 |
465 | case *FilterOr:
466 | var subFilters []string
467 | for _, subFilter := range f.Filters {
468 | subStr, err := FilterToQuery(subFilter)
469 | if err != nil {
470 | return "", err
471 | }
472 | subFilters = append(subFilters, subStr)
473 | }
474 | return "(|" + strings.Join(subFilters, "") + ")", nil
475 |
476 | case *FilterNot:
477 | subStr, err := FilterToQuery(f.Filter)
478 | if err != nil {
479 | return "", err
480 | }
481 | return "(!" + subStr + ")", nil
482 |
483 | case *FilterEqualityMatch:
484 | return fmt.Sprintf("(%s=%s)", ldapEscape(f.AttributeDesc), ldapEscape(f.AssertionValue)), nil
485 |
486 | case *FilterSubstring:
487 | var parts []string
488 | for _, part := range f.Substrings {
489 | switch {
490 | case part.Initial != "":
491 | parts = append(parts, ldapEscape(part.Initial))
492 | case part.Any != "":
493 | parts = append(parts, ldapEscape(part.Any))
494 | case part.Final != "":
495 | parts = append(parts, ldapEscape(part.Final))
496 | }
497 | }
498 |
499 | // Handle edge cases
500 | if len(parts) > 0 {
501 | if f.Substrings[0].Initial == "" {
502 | parts[0] = "*" + parts[0]
503 | }
504 | if f.Substrings[len(f.Substrings)-1].Final == "" {
505 | parts[len(parts)-1] = parts[len(parts)-1] + "*"
506 | }
507 | }
508 |
509 | return fmt.Sprintf("(%s=%s)", ldapEscape(f.AttributeDesc), strings.Join(parts, "*")), nil
510 | case *FilterGreaterOrEqual:
511 | return fmt.Sprintf("(%s>=%s)", ldapEscape(f.AttributeDesc), ldapEscape(f.AssertionValue)), nil
512 |
513 | case *FilterLessOrEqual:
514 | return fmt.Sprintf("(%s<=%s)", ldapEscape(f.AttributeDesc), ldapEscape(f.AssertionValue)), nil
515 |
516 | case *FilterPresent:
517 | return fmt.Sprintf("(%s=*)", ldapEscape(f.AttributeDesc)), nil
518 |
519 | case *FilterApproxMatch:
520 | return fmt.Sprintf("(%s~=%s)", ldapEscape(f.AttributeDesc), ldapEscape(f.AssertionValue)), nil
521 |
522 | case *FilterExtensibleMatch:
523 | var parts []string
524 | if f.AttributeDesc != "" {
525 | parts = append(parts, ldapEscape(f.AttributeDesc))
526 | }
527 | if f.DNAttributes {
528 | parts = append(parts, "dn")
529 | }
530 | if f.MatchingRule != "" {
531 | parts = append(parts, ldapEscape(f.MatchingRule))
532 | }
533 | if f.MatchValue != "" {
534 | parts = append(parts, "="+ldapEscape(f.MatchValue))
535 | }
536 | return fmt.Sprintf("(%s)", strings.Join(parts, ":")), nil
537 |
538 | default:
539 | return "", fmt.Errorf("unsupported filter type: %T", filter)
540 | }
541 | }
542 |
543 | func QueryToFilter(query string) (Filter, error) {
544 | query = strings.TrimSpace(query)
545 | if len(query) == 0 {
546 | return nil, fmt.Errorf("empty query string")
547 | }
548 |
549 | if query[0] != '(' || query[len(query)-1] != ')' {
550 | return nil, fmt.Errorf("invalid query format")
551 | }
552 |
553 | var filter Filter
554 | var err error
555 |
556 | switch query[1] {
557 | case '&':
558 | filter, err = parseAndFilter(query)
559 | case '|':
560 | filter, err = parseOrFilter(query)
561 | case '!':
562 | filter, err = parseNotFilter(query)
563 | default:
564 | filter, err = parseSimpleFilter(query)
565 | }
566 |
567 | if err != nil {
568 | return nil, err
569 | }
570 |
571 | return filter, nil
572 | }
573 |
574 | func parseAndFilter(query string) (Filter, error) {
575 | subFilters, err := parseSubFilters(query[2 : len(query)-1])
576 | if err != nil {
577 | return nil, err
578 | }
579 | return &FilterAnd{Filters: subFilters}, nil
580 | }
581 |
582 | func parseOrFilter(query string) (Filter, error) {
583 | subFilters, err := parseSubFilters(query[2 : len(query)-1])
584 | if err != nil {
585 | return nil, err
586 | }
587 | return &FilterOr{Filters: subFilters}, nil
588 | }
589 |
590 | func parseNotFilter(query string) (Filter, error) {
591 | if len(query) < 4 {
592 | return nil, fmt.Errorf("invalid NOT filter")
593 | }
594 | subFilter, err := QueryToFilter(query[2 : len(query)-1])
595 | if err != nil {
596 | return nil, err
597 | }
598 | return &FilterNot{Filter: subFilter}, nil
599 | }
600 |
601 | func decodeEscapedSymbols(src []byte) (string, error) {
602 | var (
603 | buffer bytes.Buffer
604 | offset int
605 | reader = bytes.NewReader(src)
606 | byteHex []byte
607 | byteVal []byte
608 | )
609 |
610 | for {
611 | runeVal, runeSize, err := reader.ReadRune()
612 | if err == io.EOF {
613 | return buffer.String(), nil
614 | } else if err != nil {
615 | return "", NewError(ErrorFilterCompile, fmt.Errorf("ldap: failed to read filter: %v", err))
616 | } else if runeVal == unicode.ReplacementChar {
617 | return "", NewError(ErrorFilterCompile, fmt.Errorf("ldap: error reading rune at position %d", offset))
618 | }
619 |
620 | if runeVal == '\\' {
621 | // http://tools.ietf.org/search/rfc4515
622 | // \ (%x5C) is not a valid character unless it is followed by two HEX characters due to not
623 | // being a member of UTF1SUBSET.
624 | if byteHex == nil {
625 | byteHex = make([]byte, 2)
626 | byteVal = make([]byte, 1)
627 | }
628 |
629 | if _, err := io.ReadFull(reader, byteHex); err != nil {
630 | if err == io.ErrUnexpectedEOF {
631 | return "", NewError(ErrorFilterCompile, errors.New("ldap: missing characters for escape in filter"))
632 | }
633 | return "", NewError(ErrorFilterCompile, fmt.Errorf("ldap: invalid characters for escape in filter: %v", err))
634 | }
635 |
636 | if _, err := hexpac.Decode(byteVal, byteHex); err != nil {
637 | return "", NewError(ErrorFilterCompile, fmt.Errorf("ldap: invalid characters for escape in filter: %v", err))
638 | }
639 |
640 | buffer.Write(byteVal)
641 | } else {
642 | buffer.WriteRune(runeVal)
643 | }
644 |
645 | offset += runeSize
646 | }
647 | }
648 |
649 | func parseSimpleFilter(query string) (Filter, error) {
650 | const (
651 | stateReadingAttr = iota
652 | stateReadingExtensibleMatchingRule
653 | stateReadingCondition
654 | )
655 |
656 | dnAttributes := false
657 | attribute := bytes.NewBuffer(nil)
658 | matchingRule := bytes.NewBuffer(nil)
659 | condition := bytes.NewBuffer(nil)
660 |
661 | query = strings.TrimSpace(query)
662 | if len(query) < 3 || query[0] != '(' || query[len(query)-1] != ')' {
663 | return nil, fmt.Errorf("invalid simple filter format")
664 | }
665 |
666 | var resultFilter Filter
667 |
668 | state := stateReadingAttr
669 | pos := 1
670 | for pos < len(query) {
671 | remainingQuery := query[pos:]
672 | char, width := utf8.DecodeRuneInString(remainingQuery)
673 | if char == ')' {
674 | break
675 | }
676 |
677 | if char == utf8.RuneError {
678 | return nil, fmt.Errorf("ldap: error reading rune at position %d", pos)
679 | }
680 |
681 | switch state {
682 | case stateReadingAttr:
683 | switch {
684 | case char == ':' && strings.HasPrefix(remainingQuery, ":dn:="):
685 | dnAttributes = true
686 | state = stateReadingCondition
687 | resultFilter = &FilterExtensibleMatch{}
688 | pos += 5
689 | case char == ':' && strings.HasPrefix(remainingQuery, ":dn:"):
690 | dnAttributes = true
691 | state = stateReadingExtensibleMatchingRule
692 | resultFilter = &FilterExtensibleMatch{}
693 | pos += 4
694 | case char == ':' && strings.HasPrefix(remainingQuery, ":="):
695 | state = stateReadingCondition
696 | resultFilter = &FilterExtensibleMatch{}
697 | pos += 2
698 | case char == ':':
699 | state = stateReadingExtensibleMatchingRule
700 | resultFilter = &FilterExtensibleMatch{}
701 | pos++
702 | case char == '=':
703 | state = stateReadingCondition
704 | resultFilter = &FilterEqualityMatch{}
705 | pos++
706 | case char == '>' && strings.HasPrefix(remainingQuery, ">="):
707 | state = stateReadingCondition
708 | resultFilter = &FilterGreaterOrEqual{}
709 | pos += 2
710 | case char == '<' && strings.HasPrefix(remainingQuery, "<="):
711 | state = stateReadingCondition
712 | resultFilter = &FilterLessOrEqual{}
713 | pos += 2
714 | case char == '~' && strings.HasPrefix(remainingQuery, "~="):
715 | state = stateReadingCondition
716 | resultFilter = &FilterApproxMatch{}
717 | pos += 2
718 | default:
719 | attribute.WriteRune(char)
720 | pos += width
721 | }
722 |
723 | case stateReadingExtensibleMatchingRule:
724 | switch {
725 | case char == ':' && strings.HasPrefix(remainingQuery, ":="):
726 | state = stateReadingCondition
727 | pos += 2
728 | default:
729 | matchingRule.WriteRune(char)
730 | pos += width
731 | }
732 |
733 | case stateReadingCondition:
734 | condition.WriteRune(char)
735 | pos += width
736 | }
737 | }
738 |
739 | if pos == len(query) {
740 | return nil, fmt.Errorf("ldap: unexpected end of filter")
741 | }
742 |
743 | if resultFilter == nil {
744 | return nil, fmt.Errorf("ldap: error parsing filter")
745 | }
746 |
747 | encodedString, encodeErr := decodeEscapedSymbols(condition.Bytes())
748 | if encodeErr != nil {
749 | return nil, fmt.Errorf("Error decoding escaped symbols")
750 | }
751 |
752 | switch resultFilter := resultFilter.(type) {
753 | case *FilterExtensibleMatch:
754 | resultFilter.MatchingRule = matchingRule.String()
755 | resultFilter.AttributeDesc = attribute.String()
756 | resultFilter.MatchValue = encodedString
757 | resultFilter.DNAttributes = dnAttributes
758 | return resultFilter, nil
759 | case *FilterApproxMatch:
760 | resultFilter.AttributeDesc = attribute.String()
761 | resultFilter.AssertionValue = encodedString
762 | return resultFilter, nil
763 | case *FilterGreaterOrEqual:
764 | resultFilter.AttributeDesc = attribute.String()
765 | resultFilter.AssertionValue = encodedString
766 | return resultFilter, nil
767 | case *FilterLessOrEqual:
768 | resultFilter.AttributeDesc = attribute.String()
769 | resultFilter.AssertionValue = encodedString
770 | return resultFilter, nil
771 | case *FilterEqualityMatch:
772 | if bytes.Equal(condition.Bytes(), []byte{'*'}) {
773 | // Looks like an equality match, but it's actually a presence filter
774 | return &FilterPresent{
775 | AttributeDesc: attribute.String(),
776 | }, nil
777 | } else if bytes.Contains(condition.Bytes(), []byte{'*'}) {
778 | // Looks like an equality match, but it's actually a substring filter
779 | substrs := make([]SubstringFilter, 0)
780 | parts := bytes.Split(condition.Bytes(), []byte{'*'})
781 | for i, part := range parts {
782 | if len(part) == 0 {
783 | continue
784 | }
785 |
786 | encodedString, encodeErr := decodeEscapedSymbols(part)
787 | if encodeErr != nil {
788 | return nil, fmt.Errorf("Error decoding escaped symbols")
789 | }
790 |
791 | switch i {
792 | case 0:
793 | substrs = append(substrs, SubstringFilter{
794 | Initial: encodedString,
795 | })
796 | case len(parts) - 1:
797 | substrs = append(substrs, SubstringFilter{
798 | Final: encodedString,
799 | })
800 | default:
801 | substrs = append(substrs, SubstringFilter{
802 | Any: encodedString,
803 | })
804 | }
805 | }
806 |
807 | return &FilterSubstring{
808 | AttributeDesc: attribute.String(),
809 | Substrings: substrs,
810 | }, nil
811 | } else {
812 | // It's actually an equality match
813 | resultFilter.AttributeDesc = attribute.String()
814 | resultFilter.AssertionValue = encodedString
815 | return resultFilter, nil
816 | }
817 | default:
818 | return nil, fmt.Errorf("unsupported filter type: %T", resultFilter)
819 | }
820 | }
821 |
822 | func parseSubFilters(query string) ([]Filter, error) {
823 | var subFilters []Filter
824 | var currentFilter string
825 | var depth int
826 |
827 | for _, char := range query {
828 | if char == '(' {
829 | depth++
830 | } else if char == ')' {
831 | depth--
832 | }
833 |
834 | currentFilter += string(char)
835 |
836 | if depth == 0 {
837 | filter, err := QueryToFilter(currentFilter)
838 | if err != nil {
839 | return nil, err
840 | }
841 | subFilters = append(subFilters, filter)
842 | currentFilter = ""
843 | }
844 | }
845 |
846 | return subFilters, nil
847 | }
848 |
849 | func parseSubstringFilter(attributeDesc, assertionValue string) (Filter, error) {
850 | parts := strings.Split(assertionValue, "*")
851 | var substrings []SubstringFilter
852 |
853 | for i, part := range parts {
854 | if part == "" {
855 | continue
856 | }
857 |
858 | if i == 0 && !strings.HasPrefix(assertionValue, "*") {
859 | substrings = append(substrings, SubstringFilter{Initial: part})
860 | } else if i == len(parts)-1 && !strings.HasSuffix(assertionValue, "*") {
861 | substrings = append(substrings, SubstringFilter{Final: part})
862 | } else {
863 | substrings = append(substrings, SubstringFilter{Any: part})
864 | }
865 | }
866 |
867 | return &FilterSubstring{
868 | AttributeDesc: attributeDesc,
869 | Substrings: substrings,
870 | }, nil
871 | }
872 |
873 | func ldapEscape(str string) string {
874 | str = strings.ReplaceAll(str, `\`, `\\`)
875 | str = strings.ReplaceAll(str, `(`, `\(`)
876 | str = strings.ReplaceAll(str, `)`, `\)`)
877 | return str
878 | }
879 |
--------------------------------------------------------------------------------
/parser/filter_test.go:
--------------------------------------------------------------------------------
1 | package parser
2 |
3 | import (
4 | "testing"
5 |
6 | ber "github.com/go-asn1-ber/asn1-ber"
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | /*
11 | QueryToFilter Tests
12 | */
13 |
14 | func TestQueryToFilter_And(t *testing.T) {
15 | query := "(&(cn=John Doe)(age>=25))"
16 | expectedFilter := &FilterAnd{
17 | Filters: []Filter{
18 | &FilterEqualityMatch{
19 | AttributeDesc: "cn",
20 | AssertionValue: "John Doe",
21 | },
22 | &FilterGreaterOrEqual{
23 | AttributeDesc: "age",
24 | AssertionValue: "25",
25 | },
26 | },
27 | }
28 | filter, err := QueryToFilter(query)
29 | assert.NoError(t, err)
30 | assert.Equal(t, expectedFilter, filter)
31 | }
32 |
33 | func TestQueryToFilter_Or(t *testing.T) {
34 | query := "(|(cn=John Doe)(cn=Jane Smith))"
35 | expectedFilter := &FilterOr{
36 | Filters: []Filter{
37 | &FilterEqualityMatch{
38 | AttributeDesc: "cn",
39 | AssertionValue: "John Doe",
40 | },
41 | &FilterEqualityMatch{
42 | AttributeDesc: "cn",
43 | AssertionValue: "Jane Smith",
44 | },
45 | },
46 | }
47 | filter, err := QueryToFilter(query)
48 | assert.NoError(t, err)
49 | assert.Equal(t, expectedFilter, filter)
50 | }
51 |
52 | func TestQueryToFilter_Not(t *testing.T) {
53 | query := "(!(cn=John Doe))"
54 | expectedFilter := &FilterNot{
55 | Filter: &FilterEqualityMatch{
56 | AttributeDesc: "cn",
57 | AssertionValue: "John Doe",
58 | },
59 | }
60 | filter, err := QueryToFilter(query)
61 | assert.NoError(t, err)
62 | assert.Equal(t, expectedFilter, filter)
63 | }
64 |
65 | func TestQueryToFilter_EqualityMatch(t *testing.T) {
66 | query := "(cn=John Doe)"
67 | expectedFilter := &FilterEqualityMatch{
68 | AttributeDesc: "cn",
69 | AssertionValue: "John Doe",
70 | }
71 | filter, err := QueryToFilter(query)
72 | assert.NoError(t, err)
73 | assert.Equal(t, expectedFilter, filter)
74 | }
75 |
76 | func TestQueryToFilter_Substring(t *testing.T) {
77 | query := "(cn=John*Doe)"
78 | expectedFilter := &FilterSubstring{
79 | AttributeDesc: "cn",
80 | Substrings: []SubstringFilter{
81 | {Initial: "John"},
82 | {Final: "Doe"},
83 | },
84 | }
85 | filter, err := QueryToFilter(query)
86 | assert.NoError(t, err)
87 | assert.Equal(t, expectedFilter, filter)
88 | }
89 |
90 | func TestQueryToFilter_GreaterOrEqual(t *testing.T) {
91 | query := "(age>=25)"
92 | expectedFilter := &FilterGreaterOrEqual{
93 | AttributeDesc: "age",
94 | AssertionValue: "25",
95 | }
96 | filter, err := QueryToFilter(query)
97 | assert.NoError(t, err)
98 | assert.Equal(t, expectedFilter, filter)
99 | }
100 |
101 | func TestQueryToFilter_LessOrEqual(t *testing.T) {
102 | query := "(age<=30)"
103 | expectedFilter := &FilterLessOrEqual{
104 | AttributeDesc: "age",
105 | AssertionValue: "30",
106 | }
107 | filter, err := QueryToFilter(query)
108 | assert.NoError(t, err)
109 | assert.Equal(t, expectedFilter, filter)
110 | }
111 |
112 | func TestQueryToFilter_Present(t *testing.T) {
113 | query := "(cn=*)"
114 | expectedFilter := &FilterPresent{
115 | AttributeDesc: "cn",
116 | }
117 | filter, err := QueryToFilter(query)
118 | assert.NoError(t, err)
119 | assert.Equal(t, expectedFilter, filter)
120 | }
121 |
122 | func TestQueryToFilter_ApproxMatch(t *testing.T) {
123 | query := "(cn~=John)"
124 | expectedFilter := &FilterApproxMatch{
125 | AttributeDesc: "cn",
126 | AssertionValue: "John",
127 | }
128 | filter, err := QueryToFilter(query)
129 | assert.NoError(t, err)
130 | assert.Equal(t, expectedFilter, filter)
131 | }
132 |
133 | func TestQueryToFilter_ExtensibleMatch(t *testing.T) {
134 | query := "(cn:caseExactMatch:=John Doe)"
135 | expectedFilter := &FilterExtensibleMatch{
136 | AttributeDesc: "cn",
137 | MatchingRule: "caseExactMatch",
138 | MatchValue: "John Doe",
139 | }
140 | filter, err := QueryToFilter(query)
141 | assert.NoError(t, err)
142 | assert.Equal(t, expectedFilter, filter)
143 | }
144 |
145 | func TestQueryToFilter_ExtensibleMatch_NoMatchingRule(t *testing.T) {
146 | query := "(cn:=John Doe)"
147 | expectedFilter := &FilterExtensibleMatch{
148 | AttributeDesc: "cn",
149 | MatchValue: "John Doe",
150 | }
151 | filter, err := QueryToFilter(query)
152 | assert.NoError(t, err)
153 | assert.Equal(t, expectedFilter, filter)
154 | }
155 |
156 | func TestQueryToFilter_ExtensibleMatch_DNAttributes(t *testing.T) {
157 | query := "(cn:dn:=John Doe)"
158 | expectedFilter := &FilterExtensibleMatch{
159 | AttributeDesc: "cn",
160 | MatchValue: "John Doe",
161 | DNAttributes: true,
162 | }
163 | filter, err := QueryToFilter(query)
164 | assert.NoError(t, err)
165 | assert.Equal(t, expectedFilter, filter)
166 | }
167 |
168 | func TestQueryToFilter_ExtensibleMatch_All(t *testing.T) {
169 | query := "(cn:dn:caseExactMatch:=John Doe)"
170 | expectedFilter := &FilterExtensibleMatch{
171 | AttributeDesc: "cn",
172 | MatchingRule: "caseExactMatch",
173 | MatchValue: "John Doe",
174 | DNAttributes: true,
175 | }
176 | filter, err := QueryToFilter(query)
177 | assert.NoError(t, err)
178 | assert.Equal(t, expectedFilter, filter)
179 | }
180 |
181 | func TestQueryToFilter_Substring_MultipleAnyOnly(t *testing.T) {
182 | query := "(cn=*John*Smith*Doe*Jr*)"
183 | expectedFilter := &FilterSubstring{
184 | AttributeDesc: "cn",
185 | Substrings: []SubstringFilter{
186 | {Any: "John"},
187 | {Any: "Smith"},
188 | {Any: "Doe"},
189 | {Any: "Jr"},
190 | },
191 | }
192 | filter, err := QueryToFilter(query)
193 | assert.NoError(t, err)
194 | assert.Equal(t, expectedFilter, filter)
195 | }
196 |
197 | func TestQueryToFilter_Substring_MultipleAnyNoFinal(t *testing.T) {
198 | query := "(cn=John*Smith*Doe*Jr*)"
199 | expectedFilter := &FilterSubstring{
200 | AttributeDesc: "cn",
201 | Substrings: []SubstringFilter{
202 | {Initial: "John"},
203 | {Any: "Smith"},
204 | {Any: "Doe"},
205 | {Any: "Jr"},
206 | },
207 | }
208 | filter, err := QueryToFilter(query)
209 | assert.NoError(t, err)
210 | assert.Equal(t, expectedFilter, filter)
211 | }
212 |
213 | func TestQueryToFilter_Substring_MultipleAnyNoInitial(t *testing.T) {
214 | query := "(cn=*John*Smith*Doe*Jr)"
215 | expectedFilter := &FilterSubstring{
216 | AttributeDesc: "cn",
217 | Substrings: []SubstringFilter{
218 | {Any: "John"},
219 | {Any: "Smith"},
220 | {Any: "Doe"},
221 | {Final: "Jr"},
222 | },
223 | }
224 | filter, err := QueryToFilter(query)
225 | assert.NoError(t, err)
226 | assert.Equal(t, expectedFilter, filter)
227 | }
228 |
229 | func TestQueryToFilter_Substring_NoInitial(t *testing.T) {
230 | query := "(cn=*Doe)"
231 | expectedFilter := &FilterSubstring{
232 | AttributeDesc: "cn",
233 | Substrings: []SubstringFilter{
234 | {Final: "Doe"},
235 | },
236 | }
237 | filter, err := QueryToFilter(query)
238 | assert.NoError(t, err)
239 | assert.Equal(t, expectedFilter, filter)
240 | }
241 |
242 | func TestQueryToFilter_Substring_NoFinal(t *testing.T) {
243 | query := "(cn=John*)"
244 | expectedFilter := &FilterSubstring{
245 | AttributeDesc: "cn",
246 | Substrings: []SubstringFilter{
247 | {Initial: "John"},
248 | },
249 | }
250 | filter, err := QueryToFilter(query)
251 | assert.NoError(t, err)
252 | assert.Equal(t, expectedFilter, filter)
253 | }
254 |
255 | func TestQueryToFilter_Substring_InitialFinal(t *testing.T) {
256 | query := "(cn=John*Doe)"
257 | expectedFilter := &FilterSubstring{
258 | AttributeDesc: "cn",
259 | Substrings: []SubstringFilter{
260 | {Initial: "John"},
261 | {Final: "Doe"},
262 | },
263 | }
264 | filter, err := QueryToFilter(query)
265 | assert.NoError(t, err)
266 | assert.Equal(t, expectedFilter, filter)
267 | }
268 |
269 | func TestQueryToFilter_Substring_InitialAnyFinal(t *testing.T) {
270 | query := "(cn=John*Smith*Doe)"
271 | expectedFilter := &FilterSubstring{
272 | AttributeDesc: "cn",
273 | Substrings: []SubstringFilter{
274 | {Initial: "John"},
275 | {Any: "Smith"},
276 | {Final: "Doe"},
277 | },
278 | }
279 | filter, err := QueryToFilter(query)
280 | assert.NoError(t, err)
281 | assert.Equal(t, expectedFilter, filter)
282 | }
283 |
284 | func TestQueryToFilter_Substring_InitialAnyAnyFinal(t *testing.T) {
285 | query := "(cn=John*Smith*Doe*Jr)"
286 | expectedFilter := &FilterSubstring{
287 | AttributeDesc: "cn",
288 | Substrings: []SubstringFilter{
289 | {Initial: "John"},
290 | {Any: "Smith"},
291 | {Any: "Doe"},
292 | {Final: "Jr"},
293 | },
294 | }
295 | filter, err := QueryToFilter(query)
296 | assert.NoError(t, err)
297 | assert.Equal(t, expectedFilter, filter)
298 | }
299 |
300 | func TestQueryToFilter_Complex(t *testing.T) {
301 | query := "(&(|(cn=John*)(sn=Doe))(age>=25)(!(email=*example.com)))"
302 | expectedFilter := &FilterAnd{
303 | Filters: []Filter{
304 | &FilterOr{
305 | Filters: []Filter{
306 | &FilterSubstring{
307 | AttributeDesc: "cn",
308 | Substrings: []SubstringFilter{
309 | {Initial: "John"},
310 | },
311 | },
312 | &FilterEqualityMatch{
313 | AttributeDesc: "sn",
314 | AssertionValue: "Doe",
315 | },
316 | },
317 | },
318 | &FilterGreaterOrEqual{
319 | AttributeDesc: "age",
320 | AssertionValue: "25",
321 | },
322 | &FilterNot{
323 | Filter: &FilterSubstring{
324 | AttributeDesc: "email",
325 | Substrings: []SubstringFilter{
326 | {Final: "example.com"},
327 | },
328 | },
329 | },
330 | },
331 | }
332 | filter, err := QueryToFilter(query)
333 | assert.NoError(t, err)
334 | assert.Equal(t, expectedFilter, filter)
335 | }
336 |
337 | /*
338 | FilterToQuery Tests
339 | */
340 |
341 | func TestFilterToQuery_And(t *testing.T) {
342 | filter := &FilterAnd{
343 | Filters: []Filter{
344 | &FilterEqualityMatch{
345 | AttributeDesc: "cn",
346 | AssertionValue: "John Doe",
347 | },
348 | &FilterGreaterOrEqual{
349 | AttributeDesc: "age",
350 | AssertionValue: "25",
351 | },
352 | },
353 | }
354 | expectedQuery := "(&(cn=John Doe)(age>=25))"
355 | query, err := FilterToQuery(filter)
356 | assert.NoError(t, err)
357 | assert.Equal(t, expectedQuery, query)
358 | }
359 |
360 | func TestFilterToQuery_Or(t *testing.T) {
361 | filter := &FilterOr{
362 | Filters: []Filter{
363 | &FilterEqualityMatch{
364 | AttributeDesc: "cn",
365 | AssertionValue: "John Doe",
366 | },
367 | &FilterEqualityMatch{
368 | AttributeDesc: "cn",
369 | AssertionValue: "Jane Smith",
370 | },
371 | },
372 | }
373 | expectedQuery := "(|(cn=John Doe)(cn=Jane Smith))"
374 | query, err := FilterToQuery(filter)
375 | assert.NoError(t, err)
376 | assert.Equal(t, expectedQuery, query)
377 | }
378 |
379 | func TestFilterToQuery_Not(t *testing.T) {
380 | filter := &FilterNot{
381 | Filter: &FilterEqualityMatch{
382 | AttributeDesc: "cn",
383 | AssertionValue: "John Doe",
384 | },
385 | }
386 | expectedQuery := "(!(cn=John Doe))"
387 | query, err := FilterToQuery(filter)
388 | assert.NoError(t, err)
389 | assert.Equal(t, expectedQuery, query)
390 | }
391 |
392 | func TestFilterToQuery_EqualityMatch(t *testing.T) {
393 | filter := &FilterEqualityMatch{
394 | AttributeDesc: "cn",
395 | AssertionValue: "John Doe",
396 | }
397 | expectedQuery := "(cn=John Doe)"
398 | query, err := FilterToQuery(filter)
399 | assert.NoError(t, err)
400 | assert.Equal(t, expectedQuery, query)
401 | }
402 |
403 | func TestFilterToQuery_Substring(t *testing.T) {
404 | filter := &FilterSubstring{
405 | AttributeDesc: "cn",
406 | Substrings: []SubstringFilter{
407 | {Initial: "John"},
408 | {Final: "Doe"},
409 | },
410 | }
411 | expectedQuery := "(cn=John*Doe)"
412 | query, err := FilterToQuery(filter)
413 | assert.NoError(t, err)
414 | assert.Equal(t, expectedQuery, query)
415 | }
416 |
417 | func TestFilterToQuery_GreaterOrEqual(t *testing.T) {
418 | filter := &FilterGreaterOrEqual{
419 | AttributeDesc: "age",
420 | AssertionValue: "25",
421 | }
422 | expectedQuery := "(age>=25)"
423 | query, err := FilterToQuery(filter)
424 | assert.NoError(t, err)
425 | assert.Equal(t, expectedQuery, query)
426 | }
427 |
428 | func TestFilterToQuery_LessOrEqual(t *testing.T) {
429 | filter := &FilterLessOrEqual{
430 | AttributeDesc: "age",
431 | AssertionValue: "30",
432 | }
433 | expectedQuery := "(age<=30)"
434 | query, err := FilterToQuery(filter)
435 | assert.NoError(t, err)
436 | assert.Equal(t, expectedQuery, query)
437 | }
438 |
439 | func TestFilterToQuery_Present(t *testing.T) {
440 | filter := &FilterPresent{
441 | AttributeDesc: "cn",
442 | }
443 | expectedQuery := "(cn=*)"
444 | query, err := FilterToQuery(filter)
445 | assert.NoError(t, err)
446 | assert.Equal(t, expectedQuery, query)
447 | }
448 |
449 | func TestFilterToQuery_ApproxMatch(t *testing.T) {
450 | filter := &FilterApproxMatch{
451 | AttributeDesc: "cn",
452 | AssertionValue: "John",
453 | }
454 | expectedQuery := "(cn~=John)"
455 | query, err := FilterToQuery(filter)
456 | assert.NoError(t, err)
457 | assert.Equal(t, expectedQuery, query)
458 | }
459 |
460 | func TestFilterToQuery_ExtensibleMatch(t *testing.T) {
461 | filter := &FilterExtensibleMatch{
462 | AttributeDesc: "cn",
463 | MatchingRule: "caseExactMatch",
464 | MatchValue: "John Doe",
465 | }
466 | expectedQuery := "(cn:caseExactMatch:=John Doe)"
467 | query, err := FilterToQuery(filter)
468 | assert.NoError(t, err)
469 | assert.Equal(t, expectedQuery, query)
470 | }
471 |
472 | func TestFilterToQuery_ExtensibleMatch_NoMatchingRule(t *testing.T) {
473 | filter := &FilterExtensibleMatch{
474 | AttributeDesc: "cn",
475 | MatchValue: "John Doe",
476 | }
477 | expectedQuery := "(cn:=John Doe)"
478 | query, err := FilterToQuery(filter)
479 | assert.NoError(t, err)
480 | assert.Equal(t, expectedQuery, query)
481 | }
482 |
483 | func TestFilterToQuery_ExtensibleMatch_DNAttributes(t *testing.T) {
484 | filter := &FilterExtensibleMatch{
485 | AttributeDesc: "cn",
486 | MatchingRule: "rule",
487 | MatchValue: "John Doe",
488 | DNAttributes: true,
489 | }
490 | expectedQuery := "(cn:dn:rule:=John Doe)"
491 | query, err := FilterToQuery(filter)
492 | assert.NoError(t, err)
493 | assert.Equal(t, expectedQuery, query)
494 | }
495 |
496 | func TestFilterToQuery_ExtensibleMatch_All(t *testing.T) {
497 | filter := &FilterExtensibleMatch{
498 | AttributeDesc: "cn",
499 | MatchingRule: "caseExactMatch",
500 | MatchValue: "John Doe",
501 | DNAttributes: true,
502 | }
503 | expectedQuery := "(cn:dn:caseExactMatch:=John Doe)"
504 | query, err := FilterToQuery(filter)
505 | assert.NoError(t, err)
506 | assert.Equal(t, expectedQuery, query)
507 | }
508 |
509 | func TestFilterToQuery_Complex(t *testing.T) {
510 | filter := &FilterAnd{
511 | Filters: []Filter{
512 | &FilterOr{
513 | Filters: []Filter{
514 | &FilterSubstring{
515 | AttributeDesc: "cn",
516 | Substrings: []SubstringFilter{
517 | {Initial: "John"},
518 | },
519 | },
520 | &FilterEqualityMatch{
521 | AttributeDesc: "sn",
522 | AssertionValue: "Doe",
523 | },
524 | },
525 | },
526 | &FilterGreaterOrEqual{
527 | AttributeDesc: "age",
528 | AssertionValue: "25",
529 | },
530 | &FilterNot{
531 | Filter: &FilterSubstring{
532 | AttributeDesc: "email",
533 | Substrings: []SubstringFilter{
534 | {Final: "example.com"},
535 | },
536 | },
537 | },
538 | },
539 | }
540 | expectedQuery := "(&(|(cn=John*)(sn=Doe))(age>=25)(!(email=*example.com)))"
541 | query, err := FilterToQuery(filter)
542 | assert.NoError(t, err)
543 | assert.Equal(t, expectedQuery, query)
544 | }
545 |
546 | /*
547 | PacketToFilter Tests
548 | */
549 |
550 | func TestPacketToFilter_EqualityMatch(t *testing.T) {
551 | packet := ber.Encode(ber.ClassContext, ber.TypeConstructed, 0x3, nil, "")
552 | packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "cn", "AttributeDesc"))
553 | packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "John Doe", "AssertionValue"))
554 |
555 | filter, err := PacketToFilter(packet)
556 | assert.NoError(t, err)
557 | assert.Equal(t, &FilterEqualityMatch{
558 | AttributeDesc: "cn",
559 | AssertionValue: "John Doe",
560 | }, filter)
561 | }
562 |
563 | func TestPacketToFilter_Substrings(t *testing.T) {
564 | packet := ber.Encode(ber.ClassContext, ber.TypeConstructed, 0x4, nil, "")
565 | packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "cn", "AttributeDesc"))
566 | substrings := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Substrings")
567 | substrings.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0x0, "John", "Initial"))
568 | substrings.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0x2, "Doe", "Final"))
569 | packet.AppendChild(substrings)
570 |
571 | filter, err := PacketToFilter(packet)
572 | assert.NoError(t, err)
573 | assert.Equal(t, &FilterSubstring{
574 | AttributeDesc: "cn",
575 | Substrings: []SubstringFilter{
576 | {Initial: "John"},
577 | {Final: "Doe"},
578 | },
579 | }, filter)
580 | }
581 |
582 | func TestPacketToFilter_GreaterOrEqual(t *testing.T) {
583 | packet := ber.Encode(ber.ClassContext, ber.TypeConstructed, 0x5, nil, "")
584 | packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "age", "AttributeDesc"))
585 | packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "25", "AssertionValue"))
586 |
587 | filter, err := PacketToFilter(packet)
588 | assert.NoError(t, err)
589 | assert.Equal(t, &FilterGreaterOrEqual{
590 | AttributeDesc: "age",
591 | AssertionValue: "25",
592 | }, filter)
593 | }
594 |
595 | func TestPacketToFilter_LessOrEqual(t *testing.T) {
596 | packet := ber.Encode(ber.ClassContext, ber.TypeConstructed, 0x6, nil, "")
597 | packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "age", "AttributeDesc"))
598 | packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "30", "AssertionValue"))
599 |
600 | filter, err := PacketToFilter(packet)
601 | assert.NoError(t, err)
602 | assert.Equal(t, &FilterLessOrEqual{
603 | AttributeDesc: "age",
604 | AssertionValue: "30",
605 | }, filter)
606 | }
607 |
608 | func TestPacketToFilter_Present(t *testing.T) {
609 | packet := ber.NewString(ber.ClassContext, ber.TypePrimitive, 0x7, "cn", "")
610 |
611 | filter, err := PacketToFilter(packet)
612 | assert.NoError(t, err)
613 | assert.Equal(t, &FilterPresent{
614 | AttributeDesc: "cn",
615 | }, filter)
616 | }
617 |
618 | func TestPacketToFilter_ApproxMatch(t *testing.T) {
619 | packet := ber.Encode(ber.ClassContext, ber.TypeConstructed, 0x8, nil, "")
620 | packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "cn", "AttributeDesc"))
621 | packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "John", "AssertionValue"))
622 |
623 | filter, err := PacketToFilter(packet)
624 | assert.NoError(t, err)
625 | assert.Equal(t, &FilterApproxMatch{
626 | AttributeDesc: "cn",
627 | AssertionValue: "John",
628 | }, filter)
629 | }
630 |
631 | func TestPacketToFilter_ExtensibleMatch(t *testing.T) {
632 | packet := ber.Encode(ber.ClassContext, ber.TypeConstructed, 0x9, nil, "")
633 | packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, 0x1, "caseExactMatch", "MatchingRule"))
634 | packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, 0x2, "cn", "AttributeDesc"))
635 | packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, 0x3, "John Doe", "MatchValue"))
636 | packet.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, 0x4, true, "DNAttributes"))
637 |
638 | filter, err := PacketToFilter(packet)
639 | assert.NoError(t, err)
640 | assert.Equal(t, &FilterExtensibleMatch{
641 | AttributeDesc: "cn",
642 | MatchingRule: "caseExactMatch",
643 | MatchValue: "John Doe",
644 | DNAttributes: true,
645 | }, filter)
646 | }
647 |
648 | /*
649 | FilterToPacket Tests
650 | */
651 |
652 | func TestFilterToPacket_EqualityMatch(t *testing.T) {
653 | filter := &FilterEqualityMatch{
654 | AttributeDesc: "cn",
655 | AssertionValue: "John Doe",
656 | }
657 |
658 | packet := FilterToPacket(filter)
659 |
660 | expectedPacket := ber.Encode(ber.ClassContext, ber.TypeConstructed, 0x3, nil, "")
661 | expectedPacket.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "cn", "AttributeDesc"))
662 | expectedPacket.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "John Doe", "AssertionValue"))
663 |
664 | assert.Equal(t, expectedPacket.Bytes(), packet.Bytes())
665 | }
666 |
667 | func TestFilterToPacket_Substrings(t *testing.T) {
668 | filter := &FilterSubstring{
669 | AttributeDesc: "cn",
670 | Substrings: []SubstringFilter{
671 | {Initial: "John"},
672 | {Final: "Doe"},
673 | },
674 | }
675 |
676 | packet := FilterToPacket(filter)
677 |
678 | expectedPacket := ber.Encode(ber.ClassContext, ber.TypeConstructed, 0x4, nil, "")
679 | expectedPacket.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "cn", "AttributeDesc"))
680 | substrings := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Substrings")
681 | substrings.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0x0, "John", "Initial"))
682 | substrings.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0x2, "Doe", "Final"))
683 | expectedPacket.AppendChild(substrings)
684 |
685 | assert.Equal(t, expectedPacket.Bytes(), packet.Bytes())
686 | }
687 |
688 | func TestFilterToPacket_GreaterOrEqual(t *testing.T) {
689 | filter := &FilterGreaterOrEqual{
690 | AttributeDesc: "age",
691 | AssertionValue: "25",
692 | }
693 |
694 | packet := FilterToPacket(filter)
695 |
696 | expectedPacket := ber.Encode(ber.ClassContext, ber.TypeConstructed, 0x5, nil, "")
697 | expectedPacket.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "age", "AttributeDesc"))
698 | expectedPacket.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "25", "AssertionValue"))
699 |
700 | assert.Equal(t, expectedPacket.Bytes(), packet.Bytes())
701 | }
702 |
703 | func TestFilterToPacket_LessOrEqual(t *testing.T) {
704 | filter := &FilterLessOrEqual{
705 | AttributeDesc: "age",
706 | AssertionValue: "30",
707 | }
708 |
709 | packet := FilterToPacket(filter)
710 |
711 | expectedPacket := ber.Encode(ber.ClassContext, ber.TypeConstructed, 0x6, nil, "")
712 | expectedPacket.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "age", "AttributeDesc"))
713 | expectedPacket.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "30", "AssertionValue"))
714 |
715 | assert.Equal(t, expectedPacket.Bytes(), packet.Bytes())
716 | }
717 |
718 | func TestFilterToPacket_Present(t *testing.T) {
719 | filter := &FilterPresent{
720 | AttributeDesc: "cn",
721 | }
722 |
723 | packet := FilterToPacket(filter)
724 |
725 | expectedPacket := ber.NewString(ber.ClassContext, ber.TypePrimitive, 0x7, "cn", "")
726 | assert.Equal(t, expectedPacket.Bytes(), packet.Bytes())
727 | }
728 |
729 | func TestFilterToPacket_ApproxMatch(t *testing.T) {
730 | filter := &FilterApproxMatch{
731 | AttributeDesc: "cn",
732 | AssertionValue: "John",
733 | }
734 |
735 | packet := FilterToPacket(filter)
736 |
737 | expectedPacket := ber.Encode(ber.ClassContext, ber.TypeConstructed, 0x8, nil, "")
738 | expectedPacket.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "cn", "AttributeDesc"))
739 | expectedPacket.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "John", "AssertionValue"))
740 |
741 | assert.Equal(t, expectedPacket.Bytes(), packet.Bytes())
742 | }
743 |
744 | func TestFilterToPacket_ExtensibleMatch(t *testing.T) {
745 | filter := &FilterExtensibleMatch{
746 | AttributeDesc: "cn",
747 | MatchingRule: "caseExactMatch",
748 | MatchValue: "John Doe",
749 | DNAttributes: true,
750 | }
751 |
752 | packet := FilterToPacket(filter)
753 |
754 | expectedPacket := ber.Encode(ber.ClassContext, ber.TypeConstructed, 0x9, nil, "")
755 | expectedPacket.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0x1, "caseExactMatch", "MatchingRule"))
756 | expectedPacket.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0x2, "cn", "AttributeDesc"))
757 | expectedPacket.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0x3, "John Doe", "MatchValue"))
758 | expectedPacket.AppendChild(ber.NewBoolean(ber.ClassContext, ber.TypePrimitive, 0x4, true, "DNAttributes"))
759 |
760 | assert.Equal(t, expectedPacket.Bytes(), packet.Bytes())
761 | }
762 |
763 | // TODO: Add more tests for edge cases of FilterToQuery, FilterToPacket, and PacketToFilter
764 |
765 | /*
766 | func TestQueryToFilter_InvalidFilter(t *testing.T) {
767 | testCases := []struct {
768 | name string
769 | query string
770 | }{
771 | {
772 | name: "Empty filter",
773 | query: "",
774 | },
775 | {
776 | name: "Missing opening parenthesis",
777 | query: "cn=John Doe)",
778 | },
779 | {
780 | name: "Missing closing parenthesis",
781 | query: "(cn=John Doe",
782 | },
783 | {
784 | name: "Invalid filter format",
785 | query: "cn~John Doe",
786 | },
787 | {
788 | name: "Invalid AND filter",
789 | query: "(&(cn=John)(sn=Doe)",
790 | },
791 | {
792 | name: "Invalid OR filter",
793 | query: "(|(cn=John(sn=Doe))",
794 | },
795 | {
796 | name: "Invalid NOT filter",
797 | query: "(!(cn=John)(sn=Doe))",
798 | },
799 | {
800 | name: "Invalid attribute description",
801 | query: "(invalid attribute=John Doe)",
802 | },
803 | {
804 | name: "Missing assertion value",
805 | query: "(cn=)",
806 | },
807 | }
808 |
809 | for _, tc := range testCases {
810 | t.Run(tc.name, func(t *testing.T) {
811 | _, err := QueryToFilter(tc.query)
812 | assert.Error(t, err, "Expected an error for invalid filter")
813 | })
814 | }
815 | }
816 | */
817 |
--------------------------------------------------------------------------------
/parser/packet.go:
--------------------------------------------------------------------------------
1 | package parser
2 |
3 | import (
4 | "fmt"
5 |
6 | ber "github.com/go-asn1-ber/asn1-ber"
7 | )
8 |
9 | /*
10 | Code adapted from https://github.com/go-ldap/ldap (MIT License)
11 | */
12 |
13 | // LDAP Application Codes
14 | const (
15 | ApplicationBindRequest = 0
16 | ApplicationBindResponse = 1
17 | ApplicationUnbindRequest = 2
18 | ApplicationSearchRequest = 3
19 | ApplicationSearchResultEntry = 4
20 | ApplicationSearchResultDone = 5
21 | ApplicationModifyRequest = 6
22 | ApplicationModifyResponse = 7
23 | ApplicationAddRequest = 8
24 | ApplicationAddResponse = 9
25 | ApplicationDelRequest = 10
26 | ApplicationDelResponse = 11
27 | ApplicationModifyDNRequest = 12
28 | ApplicationModifyDNResponse = 13
29 | ApplicationCompareRequest = 14
30 | ApplicationCompareResponse = 15
31 | ApplicationAbandonRequest = 16
32 | ApplicationSearchResultReference = 19
33 | ApplicationExtendedRequest = 23
34 | ApplicationExtendedResponse = 24
35 | ApplicationIntermediateResponse = 25
36 | )
37 |
38 | // ApplicationMap contains human readable descriptions of LDAP Application Codes
39 | var ApplicationMap = map[uint8]string{
40 | ApplicationBindRequest: "Bind Request",
41 | ApplicationBindResponse: "Bind Response",
42 | ApplicationUnbindRequest: "Unbind Request",
43 | ApplicationSearchRequest: "Search Request",
44 | ApplicationSearchResultEntry: "Search Result Entry",
45 | ApplicationSearchResultDone: "Search Result Done",
46 | ApplicationModifyRequest: "Modify Request",
47 | ApplicationModifyResponse: "Modify Response",
48 | ApplicationAddRequest: "Add Request",
49 | ApplicationAddResponse: "Add Response",
50 | ApplicationDelRequest: "Del Request",
51 | ApplicationDelResponse: "Del Response",
52 | ApplicationModifyDNRequest: "Modify DN Request",
53 | ApplicationModifyDNResponse: "Modify DN Response",
54 | ApplicationCompareRequest: "Compare Request",
55 | ApplicationCompareResponse: "Compare Response",
56 | ApplicationAbandonRequest: "Abandon Request",
57 | ApplicationSearchResultReference: "Search Result Reference",
58 | ApplicationExtendedRequest: "Extended Request",
59 | ApplicationExtendedResponse: "Extended Response",
60 | ApplicationIntermediateResponse: "Intermediate Response",
61 | }
62 |
63 | const (
64 | // ControlTypePaging - https://www.ietf.org/rfc/rfc2696.txt
65 | ControlTypePaging = "1.2.840.113556.1.4.319"
66 | // ControlTypeBeheraPasswordPolicy - https://tools.ietf.org/html/draft-behera-ldap-password-policy-10
67 | ControlTypeBeheraPasswordPolicy = "1.3.6.1.4.1.42.2.27.8.5.1"
68 | // ControlTypeVChuPasswordMustChange - https://tools.ietf.org/html/draft-vchu-ldap-pwd-policy-00
69 | ControlTypeVChuPasswordMustChange = "2.16.840.1.113730.3.4.4"
70 | // ControlTypeVChuPasswordWarning - https://tools.ietf.org/html/draft-vchu-ldap-pwd-policy-00
71 | ControlTypeVChuPasswordWarning = "2.16.840.1.113730.3.4.5"
72 | // ControlTypeManageDsaIT - https://tools.ietf.org/html/rfc3296
73 | ControlTypeManageDsaIT = "2.16.840.1.113730.3.4.2"
74 | // ControlTypeWhoAmI - https://tools.ietf.org/html/rfc4532
75 | ControlTypeWhoAmI = "1.3.6.1.4.1.4203.1.11.3"
76 | // ControlTypeSubtreeDelete - https://datatracker.ietf.org/doc/html/draft-armijo-ldap-treedelete-02
77 | ControlTypeSubtreeDelete = "1.2.840.113556.1.4.805"
78 |
79 | // ControlTypeServerSideSorting - https://www.ietf.org/rfc/rfc2891.txt
80 | ControlTypeServerSideSorting = "1.2.840.113556.1.4.473"
81 | // ControlTypeServerSideSorting - https://www.ietf.org/rfc/rfc2891.txt
82 | ControlTypeServerSideSortingResult = "1.2.840.113556.1.4.474"
83 |
84 | // ControlTypeMicrosoftNotification - https://msdn.microsoft.com/en-us/library/aa366983(v=vs.85).aspx
85 | ControlTypeMicrosoftNotification = "1.2.840.113556.1.4.528"
86 | // ControlTypeMicrosoftShowDeleted - https://msdn.microsoft.com/en-us/library/aa366989(v=vs.85).aspx
87 | ControlTypeMicrosoftShowDeleted = "1.2.840.113556.1.4.417"
88 | // ControlTypeMicrosoftServerLinkTTL - https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/f4f523a8-abc0-4b3a-a471-6b2fef135481?redirectedfrom=MSDN
89 | ControlTypeMicrosoftServerLinkTTL = "1.2.840.113556.1.4.2309"
90 | // ControlTypeDirSync - Active Directory DirSync - https://msdn.microsoft.com/en-us/library/aa366978(v=vs.85).aspx
91 | ControlTypeDirSync = "1.2.840.113556.1.4.841"
92 |
93 | // ControlTypeSyncRequest - https://www.ietf.org/rfc/rfc4533.txt
94 | ControlTypeSyncRequest = "1.3.6.1.4.1.4203.1.9.1.1"
95 | // ControlTypeSyncState - https://www.ietf.org/rfc/rfc4533.txt
96 | ControlTypeSyncState = "1.3.6.1.4.1.4203.1.9.1.2"
97 | // ControlTypeSyncDone - https://www.ietf.org/rfc/rfc4533.txt
98 | ControlTypeSyncDone = "1.3.6.1.4.1.4203.1.9.1.3"
99 | // ControlTypeSyncInfo - https://www.ietf.org/rfc/rfc4533.txt
100 | ControlTypeSyncInfo = "1.3.6.1.4.1.4203.1.9.1.4"
101 | )
102 |
103 | // ControlTypeMap maps controls to text descriptions
104 | var ControlTypeMap = map[string]string{
105 | ControlTypePaging: "Paging",
106 | ControlTypeBeheraPasswordPolicy: "Password Policy - Behera Draft",
107 | ControlTypeManageDsaIT: "Manage DSA IT",
108 | ControlTypeSubtreeDelete: "Subtree Delete Control",
109 | ControlTypeMicrosoftNotification: "Change Notification - Microsoft",
110 | ControlTypeMicrosoftShowDeleted: "Show Deleted Objects - Microsoft",
111 | ControlTypeMicrosoftServerLinkTTL: "Return TTL-DNs for link values with associated expiry times - Microsoft",
112 | ControlTypeServerSideSorting: "Server Side Sorting Request - LDAP Control Extension for Server Side Sorting of Search Results (RFC2891)",
113 | ControlTypeServerSideSortingResult: "Server Side Sorting Results - LDAP Control Extension for Server Side Sorting of Search Results (RFC2891)",
114 | ControlTypeDirSync: "DirSync",
115 | ControlTypeSyncRequest: "Sync Request",
116 | ControlTypeSyncState: "Sync State",
117 | ControlTypeSyncDone: "Sync Done",
118 | ControlTypeSyncInfo: "Sync Info",
119 | }
120 |
121 | // LDAP Result Codes
122 | const (
123 | LDAPResultSuccess = 0
124 | LDAPResultOperationsError = 1
125 | LDAPResultProtocolError = 2
126 | LDAPResultTimeLimitExceeded = 3
127 | LDAPResultSizeLimitExceeded = 4
128 | LDAPResultCompareFalse = 5
129 | LDAPResultCompareTrue = 6
130 | LDAPResultAuthMethodNotSupported = 7
131 | LDAPResultStrongAuthRequired = 8
132 | LDAPResultReferral = 10
133 | LDAPResultAdminLimitExceeded = 11
134 | LDAPResultUnavailableCriticalExtension = 12
135 | LDAPResultConfidentialityRequired = 13
136 | LDAPResultSaslBindInProgress = 14
137 | LDAPResultNoSuchAttribute = 16
138 | LDAPResultUndefinedAttributeType = 17
139 | LDAPResultInappropriateMatching = 18
140 | LDAPResultConstraintViolation = 19
141 | LDAPResultAttributeOrValueExists = 20
142 | LDAPResultInvalidAttributeSyntax = 21
143 | LDAPResultNoSuchObject = 32
144 | LDAPResultAliasProblem = 33
145 | LDAPResultInvalidDNSyntax = 34
146 | LDAPResultIsLeaf = 35
147 | LDAPResultAliasDereferencingProblem = 36
148 | LDAPResultInappropriateAuthentication = 48
149 | LDAPResultInvalidCredentials = 49
150 | LDAPResultInsufficientAccessRights = 50
151 | LDAPResultBusy = 51
152 | LDAPResultUnavailable = 52
153 | LDAPResultUnwillingToPerform = 53
154 | LDAPResultLoopDetect = 54
155 | LDAPResultSortControlMissing = 60
156 | LDAPResultOffsetRangeError = 61
157 | LDAPResultNamingViolation = 64
158 | LDAPResultObjectClassViolation = 65
159 | LDAPResultNotAllowedOnNonLeaf = 66
160 | LDAPResultNotAllowedOnRDN = 67
161 | LDAPResultEntryAlreadyExists = 68
162 | LDAPResultObjectClassModsProhibited = 69
163 | LDAPResultResultsTooLarge = 70
164 | LDAPResultAffectsMultipleDSAs = 71
165 | LDAPResultVirtualListViewErrorOrControlError = 76
166 | LDAPResultOther = 80
167 | LDAPResultServerDown = 81
168 | LDAPResultLocalError = 82
169 | LDAPResultEncodingError = 83
170 | LDAPResultDecodingError = 84
171 | LDAPResultTimeout = 85
172 | LDAPResultAuthUnknown = 86
173 | LDAPResultFilterError = 87
174 | LDAPResultUserCanceled = 88
175 | LDAPResultParamError = 89
176 | LDAPResultNoMemory = 90
177 | LDAPResultConnectError = 91
178 | LDAPResultNotSupported = 92
179 | LDAPResultControlNotFound = 93
180 | LDAPResultNoResultsReturned = 94
181 | LDAPResultMoreResultsToReturn = 95
182 | LDAPResultClientLoop = 96
183 | LDAPResultReferralLimitExceeded = 97
184 | LDAPResultInvalidResponse = 100
185 | LDAPResultAmbiguousResponse = 101
186 | LDAPResultTLSNotSupported = 112
187 | LDAPResultIntermediateResponse = 113
188 | LDAPResultUnknownType = 114
189 | LDAPResultCanceled = 118
190 | LDAPResultNoSuchOperation = 119
191 | LDAPResultTooLate = 120
192 | LDAPResultCannotCancel = 121
193 | LDAPResultAssertionFailed = 122
194 | LDAPResultAuthorizationDenied = 123
195 | LDAPResultSyncRefreshRequired = 4096
196 |
197 | ErrorNetwork = 200
198 | ErrorFilterCompile = 201
199 | ErrorFilterDecompile = 202
200 | ErrorDebugging = 203
201 | ErrorUnexpectedMessage = 204
202 | ErrorUnexpectedResponse = 205
203 | ErrorEmptyPassword = 206
204 | )
205 |
206 | // LDAPResultCodeMap contains string descriptions for LDAP error codes
207 | var LDAPResultCodeMap = map[uint16]string{
208 | LDAPResultSuccess: "Success",
209 | LDAPResultOperationsError: "Operations Error",
210 | LDAPResultProtocolError: "Protocol Error",
211 | LDAPResultTimeLimitExceeded: "Time Limit Exceeded",
212 | LDAPResultSizeLimitExceeded: "Size Limit Exceeded",
213 | LDAPResultCompareFalse: "Compare False",
214 | LDAPResultCompareTrue: "Compare True",
215 | LDAPResultAuthMethodNotSupported: "Auth Method Not Supported",
216 | LDAPResultStrongAuthRequired: "Strong Auth Required",
217 | LDAPResultReferral: "Referral",
218 | LDAPResultAdminLimitExceeded: "Admin Limit Exceeded",
219 | LDAPResultUnavailableCriticalExtension: "Unavailable Critical Extension",
220 | LDAPResultConfidentialityRequired: "Confidentiality Required",
221 | LDAPResultSaslBindInProgress: "Sasl Bind In Progress",
222 | LDAPResultNoSuchAttribute: "No Such Attribute",
223 | LDAPResultUndefinedAttributeType: "Undefined Attribute Type",
224 | LDAPResultInappropriateMatching: "Inappropriate Matching",
225 | LDAPResultConstraintViolation: "Constraint Violation",
226 | LDAPResultAttributeOrValueExists: "Attribute Or Value Exists",
227 | LDAPResultInvalidAttributeSyntax: "Invalid Attribute Syntax",
228 | LDAPResultNoSuchObject: "No Such Object",
229 | LDAPResultAliasProblem: "Alias Problem",
230 | LDAPResultInvalidDNSyntax: "Invalid DN Syntax",
231 | LDAPResultIsLeaf: "Is Leaf",
232 | LDAPResultAliasDereferencingProblem: "Alias Dereferencing Problem",
233 | LDAPResultInappropriateAuthentication: "Inappropriate Authentication",
234 | LDAPResultInvalidCredentials: "Invalid Credentials",
235 | LDAPResultInsufficientAccessRights: "Insufficient Access Rights",
236 | LDAPResultBusy: "Busy",
237 | LDAPResultUnavailable: "Unavailable",
238 | LDAPResultUnwillingToPerform: "Unwilling To Perform",
239 | LDAPResultLoopDetect: "Loop Detect",
240 | LDAPResultSortControlMissing: "Sort Control Missing",
241 | LDAPResultOffsetRangeError: "Result Offset Range Error",
242 | LDAPResultNamingViolation: "Naming Violation",
243 | LDAPResultObjectClassViolation: "Object Class Violation",
244 | LDAPResultResultsTooLarge: "Results Too Large",
245 | LDAPResultNotAllowedOnNonLeaf: "Not Allowed On Non Leaf",
246 | LDAPResultNotAllowedOnRDN: "Not Allowed On RDN",
247 | LDAPResultEntryAlreadyExists: "Entry Already Exists",
248 | LDAPResultObjectClassModsProhibited: "Object Class Mods Prohibited",
249 | LDAPResultAffectsMultipleDSAs: "Affects Multiple DSAs",
250 | LDAPResultVirtualListViewErrorOrControlError: "Failed because of a problem related to the virtual list view",
251 | LDAPResultOther: "Other",
252 | LDAPResultServerDown: "Cannot establish a connection",
253 | LDAPResultLocalError: "An error occurred",
254 | LDAPResultEncodingError: "LDAP encountered an error while encoding",
255 | LDAPResultDecodingError: "LDAP encountered an error while decoding",
256 | LDAPResultTimeout: "LDAP timeout while waiting for a response from the server",
257 | LDAPResultAuthUnknown: "The auth method requested in a bind request is unknown",
258 | LDAPResultFilterError: "An error occurred while encoding the given search filter",
259 | LDAPResultUserCanceled: "The user canceled the operation",
260 | LDAPResultParamError: "An invalid parameter was specified",
261 | LDAPResultNoMemory: "Out of memory error",
262 | LDAPResultConnectError: "A connection to the server could not be established",
263 | LDAPResultNotSupported: "An attempt has been made to use a feature not supported LDAP",
264 | LDAPResultControlNotFound: "The controls required to perform the requested operation were not found",
265 | LDAPResultNoResultsReturned: "No results were returned from the server",
266 | LDAPResultMoreResultsToReturn: "There are more results in the chain of results",
267 | LDAPResultClientLoop: "A loop has been detected. For example when following referrals",
268 | LDAPResultReferralLimitExceeded: "The referral hop limit has been exceeded",
269 | LDAPResultCanceled: "Operation was canceled",
270 | LDAPResultNoSuchOperation: "Server has no knowledge of the operation requested for cancellation",
271 | LDAPResultTooLate: "Too late to cancel the outstanding operation",
272 | LDAPResultCannotCancel: "The identified operation does not support cancellation or the cancel operation cannot be performed",
273 | LDAPResultAssertionFailed: "An assertion control given in the LDAP operation evaluated to false causing the operation to not be performed",
274 | LDAPResultSyncRefreshRequired: "Refresh Required",
275 | LDAPResultInvalidResponse: "Invalid Response",
276 | LDAPResultAmbiguousResponse: "Ambiguous Response",
277 | LDAPResultTLSNotSupported: "Tls Not Supported",
278 | LDAPResultIntermediateResponse: "Intermediate Response",
279 | LDAPResultUnknownType: "Unknown Type",
280 | LDAPResultAuthorizationDenied: "Authorization Denied",
281 |
282 | ErrorNetwork: "Network Error",
283 | ErrorFilterCompile: "Filter Compile Error",
284 | ErrorFilterDecompile: "Filter Decompile Error",
285 | ErrorDebugging: "Debugging Error",
286 | ErrorUnexpectedMessage: "Unexpected Message",
287 | ErrorUnexpectedResponse: "Unexpected Response",
288 | ErrorEmptyPassword: "Empty password not allowed by the client",
289 | }
290 |
291 | // Error holds LDAP error information
292 | type Error struct {
293 | // Err is the underlying error
294 | Err error
295 | // ResultCode is the LDAP error code
296 | ResultCode uint16
297 | // MatchedDN is the matchedDN returned if any
298 | MatchedDN string
299 | // Packet is the returned packet if any
300 | Packet *ber.Packet
301 | }
302 |
303 | func (e *Error) Error() string {
304 | return fmt.Sprintf("LDAP Result Code %d %q: %s", e.ResultCode, LDAPResultCodeMap[e.ResultCode], e.Err.Error())
305 | }
306 |
307 | func (e *Error) Unwrap() error { return e.Err }
308 |
309 | // GetLDAPError creates an Error out of a BER packet representing a LDAPResult
310 | // The return is an error object. It can be casted to a Error structure.
311 | // This function returns nil if resultCode in the LDAPResult sequence is success(0).
312 | func GetLDAPError(packet *ber.Packet) error {
313 | if packet == nil {
314 | return &Error{ResultCode: ErrorUnexpectedResponse, Err: fmt.Errorf("Empty packet")}
315 | }
316 |
317 | if len(packet.Children) >= 2 {
318 | response := packet.Children[1]
319 | if response == nil {
320 | return &Error{ResultCode: ErrorUnexpectedResponse, Err: fmt.Errorf("Empty response in packet"), Packet: packet}
321 | }
322 | if response.ClassType == ber.ClassApplication && response.TagType == ber.TypeConstructed && len(response.Children) >= 3 {
323 | if ber.Type(response.Children[0].Tag) == ber.Type(ber.TagInteger) || ber.Type(response.Children[0].Tag) == ber.Type(ber.TagEnumerated) {
324 | resultCode := uint16(response.Children[0].Value.(int64))
325 | if resultCode == 0 { // No error
326 | return nil
327 | }
328 |
329 | if ber.Type(response.Children[1].Tag) == ber.Type(ber.TagOctetString) &&
330 | ber.Type(response.Children[2].Tag) == ber.Type(ber.TagOctetString) {
331 | return &Error{
332 | ResultCode: resultCode,
333 | MatchedDN: response.Children[1].Value.(string),
334 | Err: fmt.Errorf("%v", response.Children[2].Value),
335 | Packet: packet,
336 | }
337 | }
338 | }
339 | }
340 | }
341 |
342 | return &Error{ResultCode: ErrorNetwork, Err: fmt.Errorf("Invalid packet format"), Packet: packet}
343 | }
344 |
345 | // NewError creates an LDAP error with the given code and underlying error
346 | func NewError(resultCode uint16, err error) error {
347 | return &Error{ResultCode: resultCode, Err: err}
348 | }
349 |
350 | /*
351 | func AddDefaultLDAPResponseDescriptions(packet *ber.Packet) error {
352 | resultCode := uint16(LDAPResultSuccess)
353 | matchedDN := ""
354 | description := "Success"
355 | if err := GetLDAPError(packet); err != nil {
356 | resultCode = err.(*Error).ResultCode
357 | matchedDN = err.(*Error).MatchedDN
358 | description = "Error Message"
359 | }
360 |
361 | packet.Children[1].Children[0].Description = "Result Code (" + LDAPResultCodeMap[resultCode] + ")"
362 | packet.Children[1].Children[1].Description = "Matched DN (" + matchedDN + ")"
363 | packet.Children[1].Children[2].Description = description
364 | if len(packet.Children[1].Children) > 3 {
365 | packet.Children[1].Children[3].Description = "Referral"
366 | }
367 | if len(packet.Children) == 3 {
368 | return AddControlDescriptions(packet.Children[2])
369 | }
370 | return nil
371 | }
372 |
373 | // Adds descriptions to an LDAP Response packet for debugging
374 | func AddLDAPDescriptions(packet *ber.Packet) (err error) {
375 | defer func() {
376 | if r := recover(); r != nil {
377 | err = NewError(ErrorDebugging, fmt.Errorf("ldap: cannot process packet to Add descriptions: %s", r))
378 | }
379 | }()
380 | packet.Description = "LDAP Response"
381 | packet.Children[0].Description = "Message ID"
382 |
383 | application := uint8(packet.Children[1].Tag)
384 | packet.Children[1].Description = ApplicationMap[application]
385 |
386 | switch application {
387 | case ApplicationBindRequest:
388 | err = AddRequestDescriptions(packet)
389 | case ApplicationBindResponse:
390 | err = AddDefaultLDAPResponseDescriptions(packet)
391 | case ApplicationUnbindRequest:
392 | err = AddRequestDescriptions(packet)
393 | case ApplicationSearchRequest:
394 | err = AddRequestDescriptions(packet)
395 | case ApplicationSearchResultEntry:
396 | packet.Children[1].Children[0].Description = "Object Name"
397 | packet.Children[1].Children[1].Description = "Attributes"
398 | for _, child := range packet.Children[1].Children[1].Children {
399 | child.Description = "Attribute"
400 | child.Children[0].Description = "Attribute Name"
401 | child.Children[1].Description = "Attribute Values"
402 | for _, grandchild := range child.Children[1].Children {
403 | grandchild.Description = "Attribute Value"
404 | }
405 | }
406 | if len(packet.Children) == 3 {
407 | err = AddControlDescriptions(packet.Children[2])
408 | }
409 | case ApplicationSearchResultDone:
410 | err = AddDefaultLDAPResponseDescriptions(packet)
411 | case ApplicationModifyRequest:
412 | err = AddRequestDescriptions(packet)
413 | case ApplicationModifyResponse:
414 | case ApplicationAddRequest:
415 | err = AddRequestDescriptions(packet)
416 | case ApplicationAddResponse:
417 | case ApplicationDelRequest:
418 | err = AddRequestDescriptions(packet)
419 | case ApplicationDelResponse:
420 | case ApplicationModifyDNRequest:
421 | err = AddRequestDescriptions(packet)
422 | case ApplicationModifyDNResponse:
423 | case ApplicationCompareRequest:
424 | err = AddRequestDescriptions(packet)
425 | case ApplicationCompareResponse:
426 | case ApplicationAbandonRequest:
427 | err = AddRequestDescriptions(packet)
428 | case ApplicationSearchResultReference:
429 | case ApplicationExtendedRequest:
430 | err = AddRequestDescriptions(packet)
431 | case ApplicationExtendedResponse:
432 | }
433 |
434 | return err
435 | }
436 |
437 | func AddRequestDescriptions(packet *ber.Packet) error {
438 | packet.Description = "LDAP Request"
439 | packet.Children[0].Description = "Message ID"
440 | packet.Children[1].Description = ApplicationMap[uint8(packet.Children[1].Tag)]
441 | if len(packet.Children) == 3 {
442 | return AddControlDescriptions(packet.Children[2])
443 | }
444 | return nil
445 | }
446 |
447 | func AddControlDescriptions(packet *ber.Packet) error {
448 | packet.Description = "Controls"
449 | for _, child := range packet.Children {
450 | var value *ber.Packet
451 | controlType := ""
452 | child.Description = "Control"
453 | switch len(child.Children) {
454 | case 0:
455 | // at least one child is required for control type
456 | return fmt.Errorf("at least one child is required for control type")
457 |
458 | case 1:
459 | // just type, no criticality or value
460 | controlType = child.Children[0].Value.(string)
461 | child.Children[0].Description = "Control Type (" + ControlTypeMap[controlType] + ")"
462 |
463 | case 2:
464 | controlType = child.Children[0].Value.(string)
465 | child.Children[0].Description = "Control Type (" + ControlTypeMap[controlType] + ")"
466 | // Children[1] could be criticality or value (both are optional)
467 | // duck-type on whether this is a boolean
468 | if _, ok := child.Children[1].Value.(bool); ok {
469 | child.Children[1].Description = "Criticality"
470 | } else {
471 | child.Children[1].Description = "Control Value"
472 | value = child.Children[1]
473 | }
474 |
475 | case 3:
476 | // criticality and value present
477 | controlType = child.Children[0].Value.(string)
478 | child.Children[0].Description = "Control Type (" + ControlTypeMap[controlType] + ")"
479 | child.Children[1].Description = "Criticality"
480 | child.Children[2].Description = "Control Value"
481 | value = child.Children[2]
482 |
483 | default:
484 | // more than 3 children is invalid
485 | return fmt.Errorf("more than 3 children for control packet found")
486 | }
487 |
488 | if value == nil {
489 | continue
490 | }
491 | switch controlType {
492 | case ControlTypePaging:
493 | value.Description += " (Paging)"
494 | if value.Value != nil {
495 | valueChildren, err := ber.DecodePacketErr(value.Data.Bytes())
496 | if err != nil {
497 | return fmt.Errorf("failed to decode data bytes: %s", err)
498 | }
499 | value.Data.Truncate(0)
500 | value.Value = nil
501 | valueChildren.Children[1].Value = valueChildren.Children[1].Data.Bytes()
502 | value.AppendChild(valueChildren)
503 | }
504 | value.Children[0].Description = "Real Search Control Value"
505 | value.Children[0].Children[0].Description = "Paging Size"
506 | value.Children[0].Children[1].Description = "Cookie"
507 |
508 | case ControlTypeBeheraPasswordPolicy:
509 | value.Description += " (Password Policy - Behera Draft)"
510 | if value.Value != nil {
511 | valueChildren, err := ber.DecodePacketErr(value.Data.Bytes())
512 | if err != nil {
513 | return fmt.Errorf("failed to decode data bytes: %s", err)
514 | }
515 | value.Data.Truncate(0)
516 | value.Value = nil
517 | value.AppendChild(valueChildren)
518 | }
519 | sequence := value.Children[0]
520 | for _, child := range sequence.Children {
521 | if child.Tag == 0 {
522 | // Warning
523 | warningPacket := child.Children[0]
524 | val, err := ber.ParseInt64(warningPacket.Data.Bytes())
525 | if err != nil {
526 | return fmt.Errorf("failed to decode data bytes: %s", err)
527 | }
528 | if warningPacket.Tag == 0 {
529 | // timeBeforeExpiration
530 | value.Description += " (TimeBeforeExpiration)"
531 | warningPacket.Value = val
532 | } else if warningPacket.Tag == 1 {
533 | // graceAuthNsRemaining
534 | value.Description += " (GraceAuthNsRemaining)"
535 | warningPacket.Value = val
536 | }
537 | } else if child.Tag == 1 {
538 | // Error
539 | bs := child.Data.Bytes()
540 | if len(bs) != 1 || bs[0] > 8 {
541 | return fmt.Errorf("failed to decode data bytes: %s", "invalid PasswordPolicyResponse enum value")
542 | }
543 | val := int8(bs[0])
544 | child.Description = "Error"
545 | child.Value = val
546 | }
547 | }
548 | }
549 | }
550 | return nil
551 | }
552 | */
553 |
--------------------------------------------------------------------------------
/parser/validation.go:
--------------------------------------------------------------------------------
1 | package parser
2 |
3 | import (
4 | "fmt"
5 | "regexp"
6 | "strings"
7 | )
8 |
9 | // IsOID checks if a string matches OID pattern (numbers separated by dots)
10 | func IsOID(s string) bool {
11 | oidPattern := regexp.MustCompile(`^(?i:oid\.)?\d+(\.\d+)* *$`)
12 | return oidPattern.MatchString(s)
13 | }
14 |
15 | // Gets the token format for an attribute
16 | func GetAttributeTokenFormat(attributeName string) (LDAPTokenFormat, error) {
17 | if context, exists := AttrContexts[strings.ToLower(attributeName)]; exists {
18 | return context.Format, nil
19 | }
20 |
21 | return TokenStringUnicode, fmt.Errorf("Error: attribute format not found for attribute '%s'", attributeName)
22 | }
23 |
--------------------------------------------------------------------------------
/proxy.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bufio"
5 | "crypto/tls"
6 | "fmt"
7 | "net"
8 | "strings"
9 |
10 | "github.com/Macmod/ldapx/log"
11 | "github.com/Macmod/ldapx/parser"
12 | ber "github.com/go-asn1-ber/asn1-ber"
13 | "h12.io/socks"
14 | )
15 |
16 | func startProxyLoop(listener net.Listener) {
17 | for {
18 | select {
19 | case <-shutdownChan:
20 | return
21 | default:
22 | conn, err := listener.Accept()
23 | if err != nil {
24 | // log.Log.Printf("[-] Failed to accept connection: %v\n", err)
25 | continue
26 | }
27 | go handleLDAPConnection(conn)
28 | }
29 | }
30 | }
31 |
32 | func reconnectTarget() error {
33 | // Close the existing target connection
34 | if targetConn != nil {
35 | targetConn.Close()
36 | }
37 |
38 | // Connect to the new target
39 | var err error
40 | targetConn, err = connect(targetLDAPAddr)
41 | if err != nil {
42 | return fmt.Errorf("failed to connect to target LDAP server: %v", err)
43 | }
44 |
45 | return nil
46 | }
47 |
48 | func connect(addr string) (net.Conn, error) {
49 | var conn net.Conn
50 | var err error
51 | var dialer net.Dialer
52 |
53 | if socksServer != "" {
54 | dialSocksProxy := socks.Dial(socksServer)
55 |
56 | // First establish connection through SOCKS proxy
57 | conn, err = dialSocksProxy("tcp", addr)
58 | if err != nil {
59 | return nil, err
60 | }
61 |
62 | if ldaps {
63 | // Wrap the SOCKS connection with TLS
64 | tlsConn := tls.Client(conn, insecureTlsConfig)
65 | if err = tlsConn.Handshake(); err != nil {
66 | conn.Close()
67 | return nil, err
68 | }
69 | conn = tlsConn
70 | }
71 | } else {
72 | // Original non-proxy connection logic
73 | if ldaps {
74 | conn, err = tls.DialWithDialer(&dialer, "tcp", addr, insecureTlsConfig)
75 | } else {
76 | conn, err = net.Dial("tcp", addr)
77 | }
78 | }
79 |
80 | return conn, err
81 | }
82 |
83 | func handleLDAPConnection(conn net.Conn) {
84 | defer conn.Close()
85 |
86 | // Connect to target conn
87 | var err error
88 | targetConn, err = connect(targetLDAPAddr)
89 |
90 | if err != nil {
91 | fmt.Println("")
92 | log.Log.Printf("Failed to connect to target LDAP server: %v\n", err)
93 | return
94 | }
95 | defer targetConn.Close()
96 |
97 | targetConnReader := bufio.NewReader(targetConn)
98 | targetConnWriter := bufio.NewWriter(targetConn)
99 |
100 | done := make(chan struct{}) // Channel to signal when either goroutine is done
101 |
102 | connReader := bufio.NewReader(conn)
103 | connWriter := bufio.NewWriter(conn)
104 |
105 | sendPacketForward := func(packet *ber.Packet) {
106 | if _, err := targetConnWriter.Write(packet.Bytes()); err != nil {
107 | fmt.Printf("\n")
108 | log.Log.Printf("[-] Error forwarding LDAP request: %v\n", err)
109 | return
110 | }
111 | globalStats.Lock()
112 | globalStats.Forward.PacketsSent++
113 | globalStats.Forward.BytesSent += uint64(len(packet.Bytes()))
114 | globalStats.Unlock()
115 |
116 | if err := targetConnWriter.Flush(); err != nil {
117 | fmt.Printf("\n")
118 | log.Log.Printf("[-] Error flushing LDAP response: %v\n", err)
119 | return
120 | }
121 | }
122 |
123 | sendPacketReverse := func(packet *ber.Packet) {
124 | responseBytes := packet.Bytes()
125 | if _, err := connWriter.Write(responseBytes); err != nil {
126 | log.Log.Printf("[-] Error sending response back to client: %v\n", err)
127 | return
128 | }
129 | globalStats.Lock()
130 | globalStats.Reverse.PacketsSent++
131 | globalStats.Reverse.BytesSent += uint64(len(responseBytes))
132 | globalStats.Unlock()
133 |
134 | connWriter.Flush()
135 | }
136 |
137 | go func() {
138 | // Close `done` channel when done to signal response goroutine to exit
139 | defer close(done)
140 |
141 | var searchRequestMap = make(map[string]*ber.Packet)
142 |
143 | for {
144 | packet, err := ber.ReadPacket(connReader)
145 | if err != nil {
146 | fmt.Println("")
147 | log.Log.Printf("[-] Error reading LDAP request: %v\n", err)
148 | return
149 | }
150 |
151 | fmt.Println("\n" + strings.Repeat("─", 55))
152 |
153 | if verbFwd > 1 {
154 | log.Log.Printf("[DEBUG] Packet Dump (Received From Client)")
155 | ber.PrintPacket(packet)
156 | }
157 |
158 | globalStats.Lock()
159 | globalStats.Forward.PacketsReceived++
160 | globalStats.Forward.BytesReceived += uint64(len(packet.Bytes()))
161 | application := uint8(packet.Children[1].Tag)
162 | globalStats.Forward.CountsByType[int(application)]++
163 | globalStats.Unlock()
164 |
165 | reqMessageID := packet.Children[0].Value.(int64)
166 | applicationText, ok := parser.ApplicationMap[application]
167 | if !ok {
168 | applicationText = fmt.Sprintf("Unknown Application '%d'", application)
169 | }
170 |
171 | if verbFwd > 0 {
172 | log.Log.Printf("[C->T] [%d - %s]\n", reqMessageID, applicationText)
173 | }
174 |
175 | switch application {
176 | case parser.ApplicationSearchRequest:
177 | if interceptSearch {
178 | log.Log.Printf("[+] Search Request Intercepted (%d)\n", reqMessageID)
179 | packet = ProcessSearchRequest(packet, searchRequestMap)
180 | }
181 | case parser.ApplicationModifyRequest:
182 | if interceptModify {
183 | log.Log.Printf("[+] Modify Request Intercepted (%d)\n", reqMessageID)
184 | packet = ProcessModifyRequest(packet)
185 | }
186 | case parser.ApplicationAddRequest:
187 | if interceptAdd {
188 | log.Log.Printf("[+] Add Request Intercepted (%d)\n", reqMessageID)
189 | packet = ProcessAddRequest(packet)
190 | }
191 | case parser.ApplicationDelRequest:
192 | if interceptDelete {
193 | log.Log.Printf("[+] Delete Request Intercepted (%d)\n", reqMessageID)
194 | packet = ProcessDeleteRequest(packet)
195 | }
196 | case parser.ApplicationModifyDNRequest:
197 | if interceptModifyDN {
198 | log.Log.Printf("[+] ModifyDN Request Intercepted (%d)\n", reqMessageID)
199 | packet = ProcessModifyDNRequest(packet)
200 | }
201 | }
202 |
203 | sendPacketForward(packet)
204 |
205 | if verbFwd > 1 {
206 | log.Log.Printf("[DEBUG] Packet Dump (Sent To Target)")
207 | ber.PrintPacket(packet)
208 | }
209 | }
210 | }()
211 |
212 | go func() {
213 | for {
214 | select {
215 | case <-done:
216 | return // Exit if the request goroutine is done
217 | default:
218 | responsePacket, err := ber.ReadPacket(targetConnReader)
219 | if err != nil {
220 | fmt.Println("")
221 | log.Log.Printf("[-] Error reading LDAP response: %v\n", err)
222 | return
223 | }
224 |
225 | globalStats.Lock()
226 | globalStats.Reverse.PacketsReceived++
227 | globalStats.Reverse.BytesReceived += uint64(len(responsePacket.Bytes()))
228 | application := uint8(responsePacket.Children[1].Tag)
229 | globalStats.Reverse.CountsByType[int(application)]++
230 | globalStats.Unlock()
231 |
232 | respMessageID := responsePacket.Children[0].Value.(int64)
233 | applicationText, ok := parser.ApplicationMap[application]
234 | if !ok {
235 | applicationText = fmt.Sprintf("Unknown Application '%d'", application)
236 | }
237 |
238 | sendPacketReverse(responsePacket)
239 |
240 | if verbRev > 0 {
241 | log.Log.Printf("[C<-T] [%d - %s] (%d bytes)\n", respMessageID, applicationText, len(responsePacket.Bytes()))
242 |
243 | if verbRev > 1 {
244 | log.Log.Printf("[DEBUG] Packet Dump (Received From Target)")
245 | ber.PrintPacket(responsePacket)
246 | }
247 | }
248 | }
249 | }
250 | }()
251 |
252 | <-done
253 | }
254 |
--------------------------------------------------------------------------------
/shell.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "sort"
7 | "strconv"
8 | "strings"
9 |
10 | "github.com/Macmod/ldapx/log"
11 | "github.com/Macmod/ldapx/middlewares"
12 | "github.com/Macmod/ldapx/parser"
13 | "github.com/c-bata/go-prompt"
14 | )
15 |
16 | var suggestions = []prompt.Suggest{
17 | {Text: "set", Description: "Set a configuration parameter"},
18 | {Text: "show", Description: "Show current configuration"},
19 | {Text: "help", Description: "Show help message"},
20 | {Text: "exit", Description: "Exit the program"},
21 | {Text: "clear", Description: "Clear a configuration parameter"},
22 | {Text: "test", Description: "Test an LDAP query through the middlewares"},
23 | {Text: "version", Description: "Show version information"},
24 | }
25 |
26 | var setParamSuggestions = []prompt.Suggest{
27 | {Text: "basedn", Description: "Set basedn middleware chain"},
28 | {Text: "filter", Description: "Set filter middleware chain"},
29 | {Text: "attrlist", Description: "Set attributes list middleware chain"},
30 | {Text: "attrentries", Description: "Set attributes entries middleware chain"},
31 | {Text: "target", Description: "Set target LDAP server address"},
32 | {Text: "ldaps", Description: "Set LDAPS connection mode (true/false)"},
33 | {Text: "option", Description: "Set a middleware option"},
34 | {Text: "verbfwd", Description: "Set forward verbosity level"},
35 | {Text: "verbrev", Description: "Set reverse verbosity level"},
36 | {Text: "isearch", Description: "Set search operation interception (true/false)"},
37 | {Text: "imodify", Description: "Set modify operation interception (true/false)"},
38 | {Text: "iadd", Description: "Set add operation interception (true/false)"},
39 | {Text: "idelete", Description: "Set delete operation interception (true/false)"},
40 | {Text: "imodifydn", Description: "Set modifydn operation interception (true/false)"},
41 | {Text: "socks", Description: "Set the SOCKS server to use for the target connection"},
42 | }
43 |
44 | var clearParamSuggestions = []prompt.Suggest{
45 | {Text: "basedn", Description: "Clear basedn middleware chain"},
46 | {Text: "filter", Description: "Clear filter middleware chain"},
47 | {Text: "attrlist", Description: "Clear attribute list middleware chain"},
48 | {Text: "attrentries", Description: "Clear attributes entries middleware chain"},
49 | {Text: "stats", Description: "Clear statistics"},
50 | {Text: "isearch", Description: "Clear search operation interception"},
51 | {Text: "imodify", Description: "Clear modify operation interception"},
52 | {Text: "iadd", Description: "Clear add operation interception"},
53 | {Text: "idelete", Description: "Clear delete operation interception"},
54 | {Text: "imodifydn", Description: "Clear modifydn operation interception"},
55 | {Text: "socks", Description: "Clear configured SOCKS server"},
56 | }
57 |
58 | var showParamSuggestions = []prompt.Suggest{
59 | {Text: "basedn", Description: "Show basedn middleware chain"},
60 | {Text: "filter", Description: "Show filter middleware chain"},
61 | {Text: "attrlist", Description: "Show attributes list middleware chain"},
62 | {Text: "attrentries", Description: "Show attributes entries middleware chain"},
63 | {Text: "testbasedn", Description: "Show BaseDN to use for the `test` command"},
64 | {Text: "testattrlist", Description: "Show attributes list to use for the `test` command"},
65 | {Text: "target", Description: "Show target address to connect upon receiving a connection"},
66 | {Text: "ldaps", Description: "Show LDAPS connection mode"},
67 | {Text: "option", Description: "Show current middleware options"},
68 | {Text: "stats", Description: "Show packet statistics"},
69 | {Text: "verbfwd", Description: "Show forward verbosity level"},
70 | {Text: "verbrev", Description: "Show reverse verbosity level"},
71 | {Text: "isearch", Description: "Show search operation interception status"},
72 | {Text: "imodify", Description: "Show modify operation interception status"},
73 | {Text: "iadd", Description: "Show add operation interception status"},
74 | {Text: "idelete", Description: "Show delete operation interception status"},
75 | {Text: "imodifydn", Description: "Show modifydn operation interception status"},
76 | {Text: "socks", Description: "Show configured SOCKS server"},
77 | }
78 |
79 | var helpParamSuggestions = []prompt.Suggest{
80 | {Text: "basedn", Description: "Show available basedn middlewares"},
81 | {Text: "filter", Description: "Show available filter middlewares"},
82 | {Text: "attrlist", Description: "Show available attributes list middlewares"},
83 | {Text: "attrentries", Description: "Show available attributes entries middlewares"},
84 | {Text: "testbasedn", Description: "Show testbasedn parameter info"},
85 | {Text: "testattrlist", Description: "Show testattrlist parameter info"},
86 | {Text: "target", Description: "Show target parameter info"},
87 | {Text: "ldaps", Description: "Show LDAPS parameter info"},
88 | {Text: "option", Description: "Show option parameter info"},
89 | {Text: "stats", Description: "Show stats parameter info"},
90 | {Text: "verbfwd", Description: "Show forward verbosity parameter info"},
91 | {Text: "verbrev", Description: "Show reverse verbosity parameter info"},
92 | {Text: "socks", Description: "Show socks parameter info"},
93 | }
94 |
95 | var testBaseDN = "DC=test,DC=local"
96 | var testAttrList = []string{"cn", "objectClass", "sAMAccountName"}
97 |
98 | func completer(in prompt.Document) []prompt.Suggest {
99 | w := in.GetWordBeforeCursor()
100 |
101 | args := strings.Split(in.TextBeforeCursor(), " ")
102 | if len(args) == 1 && len(args[0]) > 0 {
103 | return prompt.FilterHasPrefix(suggestions, w, true)
104 | }
105 |
106 | if len(args) > 2 {
107 | return []prompt.Suggest{}
108 | }
109 |
110 | switch args[0] {
111 | case "set":
112 | return prompt.FilterHasPrefix(setParamSuggestions, w, true)
113 | case "clear":
114 | return prompt.FilterHasPrefix(clearParamSuggestions, w, true)
115 | case "show":
116 | return prompt.FilterHasPrefix(showParamSuggestions, w, true)
117 | case "help":
118 | return prompt.FilterHasPrefix(helpParamSuggestions, w, true)
119 | default:
120 | return []prompt.Suggest{}
121 | }
122 | }
123 |
124 | func executor(in string) {
125 | in = strings.TrimSpace(in)
126 | blocks := strings.Split(in, " ")
127 |
128 | if len(blocks) == 0 || blocks[0] == "" {
129 | fmt.Println("No command provided. Type 'help' to see available commands.")
130 | return
131 | }
132 |
133 | switch blocks[0] {
134 | case "exit":
135 | shutdownProgram()
136 | case "clear":
137 | if len(blocks) < 2 {
138 | updateFilterChain("")
139 | updateBaseDNChain("")
140 | updateAttrListChain("")
141 | updateAttrEntriesChain("")
142 | clearStatistics()
143 | fmt.Printf("Middleware chains and statistics cleared.\n")
144 | return
145 | }
146 | handleClearCommand(blocks[1])
147 | case "set":
148 | if len(blocks) < 3 {
149 | fmt.Println("Usage: set ")
150 | return
151 | }
152 | handleSetCommand(blocks[1], blocks[2:])
153 | case "show":
154 | if len(blocks) > 1 {
155 | handleShowCommand(blocks[1])
156 | } else {
157 | handleShowCommand("")
158 | }
159 | case "help":
160 | if len(blocks) > 1 {
161 | showHelp(blocks[1])
162 | } else {
163 | showHelp()
164 | }
165 | case "test":
166 | if len(blocks) < 2 {
167 | fmt.Println("Usage: test ")
168 | return
169 | }
170 | handleTestCommand(strings.Join(blocks[1:], " "))
171 | case "version":
172 | fmt.Printf("ldapx %s\n", version)
173 | default:
174 | fmt.Printf("Unknown command: '%s'\n", blocks[0])
175 | }
176 | }
177 |
178 | func RunShell() {
179 | p := prompt.New(
180 | executor,
181 | completer,
182 | prompt.OptionPrefix("ldapx> "),
183 | prompt.OptionTitle("ldapx"),
184 | )
185 |
186 | p.Run()
187 | }
188 |
189 | func handleClearCommand(param string) {
190 | switch param {
191 | case "filter":
192 | updateFilterChain("")
193 | fmt.Printf("Middleware chain Filter cleared.\n")
194 | case "basedn":
195 | updateBaseDNChain("")
196 | fmt.Printf("Middleware chain BaseDN cleared.\n")
197 | case "attrlist":
198 | updateAttrListChain("")
199 | fmt.Printf("Middleware chain AttrList cleared.\n")
200 | case "attrentries":
201 | updateAttrEntriesChain("")
202 | fmt.Printf("Middleware chain AttrEntries cleared.\n")
203 | case "stats":
204 | clearStatistics()
205 | fmt.Println("Statistics cleared.")
206 | case "isearch":
207 | interceptSearch = false
208 | fmt.Printf("Search interception cleared.\n")
209 | case "imodify":
210 | interceptModify = false
211 | fmt.Printf("Modify interception cleared.\n")
212 | case "iadd":
213 | interceptAdd = false
214 | fmt.Printf("Add interception cleared.\n")
215 | case "idelete":
216 | interceptDelete = false
217 | fmt.Printf("Delete interception cleared.\n")
218 | case "imodifydn":
219 | interceptModifyDN = false
220 | fmt.Printf("ModifyDN interception cleared.\n")
221 | case "socks":
222 | socksServer = ""
223 | fmt.Printf("SOCKS server cleared.\n")
224 | default:
225 | fmt.Printf("Unknown parameter: %s\n", param)
226 | }
227 | }
228 |
229 | func handleSetCommand(param string, values []string) {
230 | value := strings.Join(values, " ")
231 | switch param {
232 | case "filter":
233 | updateFilterChain(value)
234 | fmt.Printf("Middleware chain Filter updated:\n")
235 | showChainConfig("Filter", filterChain, filterMidFlags)
236 | case "basedn":
237 | updateBaseDNChain(value)
238 | fmt.Printf("Middleware chain BaseDN updated:\n")
239 | showChainConfig("BaseDN", baseChain, baseDNMidFlags)
240 | case "attrlist":
241 | updateAttrListChain(value)
242 | fmt.Printf("Middleware chain AttrList updated:\n")
243 | showChainConfig("AttrList", attrChain, attrListMidFlags)
244 | case "attrentries":
245 | updateAttrEntriesChain(value)
246 | fmt.Printf("Middleware chain AttrEntries updated:\n")
247 | showChainConfig("AttrEntries", entriesChain, attrEntriesMidFlags)
248 | case "testbasedn":
249 | testBaseDN = value
250 | fmt.Printf("Test BaseDN set to: %s\n", testBaseDN)
251 | case "testattrlist":
252 | testAttrList = strings.Split(value, ",")
253 | for i := range testAttrList {
254 | testAttrList[i] = strings.TrimSpace(testAttrList[i])
255 | }
256 | fmt.Printf("Test attributes list set to: %v\n", testAttrList)
257 | case "target":
258 | targetLDAPAddr = value
259 | fmt.Printf("Target LDAP server address set to: %s\n", targetLDAPAddr)
260 | /*
261 | fmt.Println("Connecting to the new target...")
262 | err := reconnectTarget()
263 | if err != nil {
264 | fmt.Printf("Failed to connect to the new target: %v\n", err)
265 | } else {
266 | fmt.Println("Successfully connected to the new target.")
267 | }
268 | */
269 | case "option":
270 | if len(values) != 1 {
271 | fmt.Println("Usage: set option =")
272 | return
273 | }
274 | options.Set(values[0])
275 |
276 | SetupMiddlewaresMap()
277 |
278 | fmt.Printf("Option set: %s\n", values[0])
279 | case "verbfwd":
280 | if len(values) != 1 {
281 | fmt.Println("Usage: set verbfwd ")
282 | return
283 | }
284 | level, err := strconv.ParseUint(values[0], 10, 64)
285 | if err != nil {
286 | fmt.Printf("Invalid verbosity level: %s\n", values[0])
287 | return
288 | }
289 | verbFwd = uint(level)
290 | fmt.Printf("Forward verbosity level set to: %d\n", verbFwd)
291 | case "verbrev":
292 | if len(values) != 1 {
293 | fmt.Println("Usage: set verbrev ")
294 | return
295 | }
296 | level, err := strconv.ParseUint(values[0], 10, 64)
297 | if err != nil {
298 | fmt.Printf("Invalid verbosity level: %s\n", values[0])
299 | return
300 | }
301 | verbRev = uint(level)
302 | fmt.Printf("Reverse verbosity level set to: %d\n", verbRev)
303 | case "ldaps":
304 | if len(values) != 1 {
305 | fmt.Println("Usage: set ldaps ")
306 | return
307 | }
308 | ldapsValue, err := strconv.ParseBool(values[0])
309 | if err != nil {
310 | fmt.Printf("Invalid boolean value: %s\n", values[0])
311 | return
312 | }
313 | ldaps = ldapsValue
314 | fmt.Printf("LDAPS mode set to: %v\n", ldaps)
315 | case "isearch":
316 | if len(values) != 1 {
317 | fmt.Println("Usage: set isearch ")
318 | return
319 | }
320 | val, err := strconv.ParseBool(values[0])
321 | if err != nil {
322 | fmt.Printf("Invalid boolean value: %s\n", values[0])
323 | return
324 | }
325 | interceptSearch = val
326 | fmt.Printf("Search interception set to: %v\n", interceptSearch)
327 | case "imodify":
328 | if len(values) != 1 {
329 | fmt.Println("Usage: set imodify ")
330 | return
331 | }
332 | val, err := strconv.ParseBool(values[0])
333 | if err != nil {
334 | fmt.Printf("Invalid boolean value: %s\n", values[0])
335 | return
336 | }
337 | interceptModify = val
338 | fmt.Printf("Modify interception set to: %v\n", interceptModify)
339 | case "iadd":
340 | if len(values) != 1 {
341 | fmt.Println("Usage: set iadd ")
342 | return
343 | }
344 | val, err := strconv.ParseBool(values[0])
345 | if err != nil {
346 | fmt.Printf("Invalid boolean value: %s\n", values[0])
347 | return
348 | }
349 | interceptAdd = val
350 | fmt.Printf("Add interception set to: %v\n", interceptAdd)
351 | case "idelete":
352 | if len(values) != 1 {
353 | fmt.Println("Usage: set idelete ")
354 | return
355 | }
356 | val, err := strconv.ParseBool(values[0])
357 | if err != nil {
358 | fmt.Printf("Invalid boolean value: %s\n", values[0])
359 | return
360 | }
361 | interceptDelete = val
362 | fmt.Printf("Delete interception set to: %v\n", interceptDelete)
363 | case "imodifydn":
364 | if len(values) != 1 {
365 | fmt.Println("Usage: set imodifydn ")
366 | return
367 | }
368 | val, err := strconv.ParseBool(values[0])
369 | if err != nil {
370 | fmt.Printf("Invalid boolean value: %s\n", values[0])
371 | return
372 | }
373 | interceptModifyDN = val
374 | fmt.Printf("ModifyDN interception set to: %v\n", interceptModifyDN)
375 | case "socks":
376 | if len(values) != 1 {
377 | fmt.Println("Usage: set socks ")
378 | return
379 | }
380 | socksServer = values[0]
381 | default:
382 | fmt.Printf("Unknown parameter for 'set': %s\n", param)
383 | }
384 | }
385 |
386 | func handleShowCommand(param string) {
387 | if param == "" {
388 | showGlobalConfig()
389 | showChainConfig("Filter", filterChain, filterMidFlags)
390 | showChainConfig("BaseDN", baseChain, baseDNMidFlags)
391 | showChainConfig("AttrList", attrChain, attrListMidFlags)
392 | showChainConfig("AttrEntries", entriesChain, attrEntriesMidFlags)
393 | return
394 | }
395 |
396 | switch param {
397 | case "global":
398 | showGlobalConfig()
399 | case "filter":
400 | showChainConfig("Filter", filterChain, filterMidFlags)
401 | case "basedn":
402 | showChainConfig("BaseDN", baseChain, baseDNMidFlags)
403 | case "attrlist":
404 | showChainConfig("AttrList", attrChain, attrListMidFlags)
405 | case "attrentries":
406 | showChainConfig("AttrEntries", entriesChain, attrEntriesMidFlags)
407 | case "testbasedn":
408 | fmt.Println(testBaseDN)
409 | case "testattrlist":
410 | fmt.Println(testAttrList)
411 | case "target":
412 | fmt.Println(targetLDAPAddr)
413 | case "ldaps":
414 | fmt.Printf("LDAPS mode: %v\n", ldaps)
415 | case "options", "option":
416 | showOptions()
417 | case "stats":
418 | showStatistics()
419 | case "verbfwd":
420 | fmt.Printf("Forward verbosity level: %d\n", verbFwd)
421 | case "verbrev":
422 | fmt.Printf("Reverse verbosity level: %d\n", verbRev)
423 | case "socks":
424 | fmt.Printf("SOCKS proxy: '%s'\n", socksServer)
425 | default:
426 | fmt.Printf("Unknown parameter for 'show': '%s'\n", param)
427 | }
428 | }
429 | func showOptions() {
430 | fmt.Println("[Middleware Options]")
431 | for _, key := range middlewares.DefaultOptionsKeys {
432 | defaultValue := middlewares.DefaultOptions[key]
433 | if value, ok := options.Get(key); ok {
434 | fmt.Printf(" %s = %s (default = %s)\n", key, value, defaultValue)
435 | } else {
436 | fmt.Printf(" %s = %s\n", key, defaultValue)
437 | }
438 | }
439 | fmt.Println("")
440 | }
441 |
442 | func showChainConfig(name string, chain string, flags map[rune]string) {
443 | fmt.Printf("[%s chain]\n", name)
444 | if chain == "" {
445 | fmt.Println(" (empty)")
446 | fmt.Println("")
447 | return
448 | }
449 |
450 | fmt.Printf(" Chain: '%s'\n", chain)
451 | for i, c := range chain {
452 | if middlewareName, exists := flags[c]; exists {
453 | indent := strings.Repeat(" ", i)
454 | fmt.Printf(" %s|> %s (%c)\n", indent, middlewareName, c)
455 | }
456 | }
457 |
458 | fmt.Println("")
459 | }
460 |
461 | func printMiddlewareFlags(midFlags map[rune]string) {
462 | var flags []rune
463 | for flag := range midFlags {
464 | flags = append(flags, flag)
465 | }
466 | sort.Slice(flags, func(i, j int) bool {
467 | return flags[i] < flags[j]
468 | })
469 | for _, flag := range flags {
470 | fmt.Printf(" %c - %s\n", flag, midFlags[flag])
471 | }
472 | }
473 |
474 | func showHelp(args ...string) {
475 | if len(args) == 0 {
476 | fmt.Println("Available commands:")
477 | fmt.Println(" set Set a configuration parameter")
478 | fmt.Println(" clear [] Clear a configuration parameter or all")
479 | fmt.Println(" show [] Show a configuration parameter or all")
480 | fmt.Println(" help [] Show this help message or parameter-specific help")
481 | fmt.Println(" exit Exit the program")
482 | fmt.Println(" test Simulate an LDAP query through the middlewares without sending it")
483 | fmt.Println("\nParameters:")
484 | fmt.Println(" basedn - BaseDN middleware chain")
485 | fmt.Println(" filter - Filter middleware chain")
486 | fmt.Println(" attrlist - Attributes list middleware chain")
487 | fmt.Println(" attrentries - AttrEntries middleware chain")
488 | fmt.Println(" testbasedn - BaseDN to use for the `test` command")
489 | fmt.Println(" testattrlist - Attributes list to use for the `test` command (separated by commas)")
490 | fmt.Println(" target - Target address to connect upon receiving a connection")
491 | fmt.Println(" ldaps - Enable/disable LDAPS connection mode (true/false)")
492 | fmt.Println(" stats - Packet statistics")
493 | fmt.Println(" option - Middleware options")
494 | fmt.Println(" verbfwd - Forward verbosity level")
495 | fmt.Println(" verbrev - Reverse verbosity level")
496 | fmt.Println(" isearch - Search operation interception mode (true/false)")
497 | fmt.Println(" imodify - Modify operation interception mode (true/false)")
498 | fmt.Println(" iadd - Add operation interception mode (true/false)")
499 | fmt.Println(" idelete - Delete operation interception mode (true/false)")
500 | fmt.Println(" imodifydn - ModifyDN operation interception (true/false)")
501 | fmt.Println(" socks - SOCKS proxy address to use for the target connection")
502 | fmt.Println("\nUse 'help ' for detailed information about specific parameters")
503 | fmt.Println("")
504 | return
505 | }
506 |
507 | switch args[0] {
508 | case "filter":
509 | fmt.Println("Possible Filter middlewares:")
510 | printMiddlewareFlags(filterMidFlags)
511 | case "basedn":
512 | fmt.Println("Possible BaseDN middlewares:")
513 | printMiddlewareFlags(baseDNMidFlags)
514 | case "attrlist":
515 | fmt.Println("Possible AttrList middlewares:")
516 | printMiddlewareFlags(attrListMidFlags)
517 | case "attrentries":
518 | fmt.Println("Possible AttrEntries middlewares:")
519 | printMiddlewareFlags(attrEntriesMidFlags)
520 | case "testbasedn":
521 | fmt.Println("testbasedn - BaseDN to use for the `test` command")
522 | case "testattrlist":
523 | fmt.Println("testattrlist - Attributes list to use for the `test` command (separated by commas)")
524 | case "target":
525 | fmt.Println("target - Target address to connect upon receiving a connection (can only be set or shown)")
526 | case "ldaps":
527 | fmt.Println("ldaps - Enable/disable LDAPS connection mode (true/false)")
528 | case "stats":
529 | fmt.Println("stats - Packet statistics (cannot be set, only shown or cleared)")
530 | case "option":
531 | fmt.Println("option - Middleware options that can be set / shown / cleared (KEY=VALUE)")
532 | case "verbfwd":
533 | fmt.Println("verbfwd - Forward verbosity level (0-3)")
534 | fmt.Println(" 0: No verbosity")
535 | fmt.Println(" 1: Show metadata for all requests")
536 | fmt.Println(" 2: Show packet dumps for all requests")
537 | case "verbrev":
538 | fmt.Println("verbrev - Reverse verbosity level (0-3)")
539 | fmt.Println(" 0: No verbosity")
540 | fmt.Println(" 1: Show metadata for all responses")
541 | fmt.Println(" 2: Show packet dump for all responses")
542 | case "socks":
543 | fmt.Println("socks - SOCKS proxy address in the schema://host:port format")
544 | default:
545 | fmt.Printf("Unknown parameter: %s\n", args[0])
546 | }
547 | fmt.Println("")
548 | }
549 | func showGlobalConfig() {
550 | fmt.Printf("[Global settings]\n")
551 | fmt.Printf(" Forward Verbosity: %d\n", verbFwd)
552 | fmt.Printf(" Reverse Verbosity: %d\n", verbRev)
553 | fmt.Printf(" Listen address: %s\n", proxyLDAPAddr)
554 | fmt.Printf(" Target address: %s\n", targetLDAPAddr)
555 | fmt.Printf(" Target LDAPS: %t\n", ldaps)
556 | fmt.Printf(" SOCKS proxy: '%s'\n", socksServer)
557 | fmt.Printf("\n[Interceptions]\n")
558 | fmt.Printf(" Search: %t\n", interceptSearch)
559 | fmt.Printf(" Modify: %t\n", interceptModify)
560 | fmt.Printf(" Add: %t\n", interceptAdd)
561 | fmt.Printf(" Delete: %t\n", interceptDelete)
562 | fmt.Printf(" ModifyDN: %t\n", interceptModifyDN)
563 | fmt.Printf("\n[Test settings]\n")
564 | fmt.Printf(" Test BaseDN: '%s'\n", testBaseDN)
565 | testAttrs, _ := json.Marshal(testAttrList)
566 | fmt.Printf(" Test Attributes: %s\n", testAttrs)
567 | fmt.Println("")
568 | }
569 |
570 | func handleTestCommand(query string) {
571 | fmt.Printf("%s\n", strings.Repeat("─", 55))
572 | log.Log.Printf("[+] Simulated LDAP Search\n")
573 | log.Log.Printf("[+] Input: %s\n", query)
574 |
575 | filter, err := parser.QueryToFilter(query)
576 | if err != nil {
577 | red.Printf("Error compiling query: %v\n", err)
578 | return
579 | }
580 |
581 | parsed, err := parser.FilterToQuery(filter)
582 | if err != nil {
583 | red.Printf("Unknown error: %v\n", err)
584 | return
585 | }
586 |
587 | blue.Printf("Input Request:\n")
588 | blue.Printf(" BaseDN: %s\n", testBaseDN)
589 | blue.Printf(" Attributes: %v\n", testAttrList)
590 | blue.Printf(" Filter: %s\n", parsed)
591 |
592 | // Transform using current middleware chains
593 | newFilter, newBaseDN, newAttrs := TransformSearchRequest(
594 | filter,
595 | testBaseDN,
596 | testAttrList,
597 | )
598 |
599 | newParsed, err := parser.FilterToQuery(newFilter)
600 | if err != nil {
601 | red.Printf("Unknown error: '%v'", err)
602 | }
603 |
604 | green.Printf("Output Request:\n")
605 | green.Printf(" BaseDN: %s\n", newBaseDN)
606 | green.Printf(" Attributes: %v\n", newAttrs)
607 | green.Printf(" Filter: %v\n", newParsed)
608 | }
609 |
610 | func showStatistics() {
611 | fmt.Println("[Client -> Target]")
612 | fmt.Printf(" Packets Received: %d\n", globalStats.Forward.PacketsReceived)
613 | fmt.Printf(" Packets Sent: %d\n", globalStats.Forward.PacketsSent)
614 | fmt.Printf(" Bytes Received: %d\n", globalStats.Forward.BytesReceived)
615 | fmt.Printf(" Bytes Sent: %d\n", globalStats.Forward.BytesSent)
616 | fmt.Println(" Counts by Type:")
617 | for appType, count := range globalStats.Forward.CountsByType {
618 | appName, ok := parser.ApplicationMap[uint8(appType)]
619 | if !ok {
620 | appName = fmt.Sprintf("Unknown (%d)", appType)
621 | }
622 | fmt.Printf(" %s: %d\n", appName, count)
623 | }
624 |
625 | fmt.Println("\n[Client <- Target]")
626 | fmt.Printf(" Packets Received: %d\n", globalStats.Reverse.PacketsReceived)
627 | fmt.Printf(" Packets Sent: %d\n", globalStats.Reverse.PacketsSent)
628 | fmt.Printf(" Bytes Received: %d\n", globalStats.Reverse.BytesReceived)
629 | fmt.Printf(" Bytes Sent: %d\n", globalStats.Reverse.BytesSent)
630 | fmt.Println(" Counts by Type:")
631 | for appType, count := range globalStats.Reverse.CountsByType {
632 | appName, ok := parser.ApplicationMap[uint8(appType)]
633 | if !ok {
634 | appName = fmt.Sprintf("Unknown (%d)", appType)
635 | }
636 | fmt.Printf(" %s: %d\n", appName, count)
637 | }
638 | }
639 |
640 | func clearStatistics() {
641 | globalStats = Stats{
642 | Forward: struct {
643 | PacketsReceived uint64
644 | PacketsSent uint64
645 | BytesReceived uint64
646 | BytesSent uint64
647 | CountsByType map[int]uint64
648 | }{
649 | CountsByType: make(map[int]uint64),
650 | },
651 | Reverse: struct {
652 | PacketsReceived uint64
653 | PacketsSent uint64
654 | BytesReceived uint64
655 | BytesSent uint64
656 | CountsByType map[int]uint64
657 | }{
658 | CountsByType: make(map[int]uint64),
659 | },
660 | }
661 | }
662 |
--------------------------------------------------------------------------------