├── .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 | ![GitHub Release](https://img.shields.io/github/v/release/Macmod/ldapx) ![](https://img.shields.io/github/go-mod/go-version/Macmod/ldapx) ![](https://img.shields.io/github/languages/code-size/Macmod/ldapx) ![](https://img.shields.io/github/license/Macmod/ldapx) ![](https://img.shields.io/github/actions/workflow/status/Macmod/ldapx/release.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/Macmod/ldapx)](https://goreportcard.com/report/github.com/Macmod/ldapx) ![GitHub Downloads](https://img.shields.io/github/downloads/Macmod/ldapx/total) [Twitter Follow](https://twitter.com/MacmodSec) 4 | 5 | ![Logo](https://raw.githubusercontent.com/Macmod/ldapx/main/images/ldapx-ai-logo.jpg) 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 | ![Demo1](https://github.com/Macmod/ldapx/blob/main/images/demo1.png) 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 | ![Demo2](https://github.com/Macmod/ldapx/blob/main/images/demo2.png) 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 | --------------------------------------------------------------------------------