├── .github ├── banner.png ├── example.png └── workflows │ ├── commit.yaml │ ├── release.yaml │ └── auto_prefix_issues.yml ├── go.mod ├── core ├── ExtractDescriptionsOfAllObjects.go ├── ExtractNamesOfAllObjects.go ├── ExtractServicePrincipalNames.go ├── ExtractADSites.go ├── ExtractTrustedDomains.go └── wordlist.go ├── README.md ├── main.go └── go.sum /.github/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheManticoreProject/LDAPWordlistHarvester/HEAD/.github/banner.png -------------------------------------------------------------------------------- /.github/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheManticoreProject/LDAPWordlistHarvester/HEAD/.github/example.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/TheManticoreProject/LDAPWordlistHarvester 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/TheManticoreProject/Manticore v1.0.2 7 | github.com/TheManticoreProject/goopts v1.2.4 8 | ) 9 | 10 | require ( 11 | github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect 12 | github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect 13 | github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect 14 | github.com/go-ldap/ldap/v3 v3.4.11 // indirect 15 | github.com/google/uuid v1.6.0 // indirect 16 | github.com/hashicorp/go-uuid v1.0.3 // indirect 17 | github.com/jcmturner/aescts/v2 v2.0.0 // indirect 18 | github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect 19 | github.com/jcmturner/gofork v1.7.6 // indirect 20 | github.com/jcmturner/goidentity/v6 v6.0.1 // indirect 21 | github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect 22 | github.com/jcmturner/rpc/v2 v2.0.3 // indirect 23 | golang.org/x/crypto v0.37.0 // indirect 24 | golang.org/x/net v0.39.0 // indirect 25 | ) 26 | -------------------------------------------------------------------------------- /.github/workflows/commit.yaml: -------------------------------------------------------------------------------- 1 | name: Build on commit 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | name: Build Release Assets 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | os: [linux, windows, darwin] 16 | arch: [amd64, arm64, 386] 17 | binaryname: [LDAPWordlistHarvester] 18 | # Exclude incompatible couple of GOOS and GOARCH values 19 | exclude: 20 | - os: darwin 21 | arch: 386 22 | 23 | env: 24 | GO111MODULE: 'on' 25 | CGO_ENABLED: '0' 26 | 27 | steps: 28 | - name: Checkout Repository 29 | uses: actions/checkout@v3 30 | 31 | - name: Set up Go 32 | uses: actions/setup-go@v4 33 | with: 34 | go-version: '1.24.0' 35 | 36 | - name: Build Binary 37 | env: 38 | GOOS: ${{ matrix.os }} 39 | GOARCH: ${{ matrix.arch }} 40 | run: | 41 | mkdir -p build 42 | ls -lha 43 | OUTPUT_PATH="./build/${{ matrix.binaryname }}-${{ matrix.os }}-${{ matrix.arch }}" 44 | # Build the binary 45 | go build -ldflags="-s -w" -o $OUTPUT_PATH${{ matrix.os == 'windows' && '.exe' || '' }} 46 | -------------------------------------------------------------------------------- /core/ExtractDescriptionsOfAllObjects.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/TheManticoreProject/Manticore/logger" 8 | "github.com/TheManticoreProject/Manticore/network/ldap" 9 | ) 10 | 11 | // ExtractDescriptionsOfAllObjects extracts the descriptions of all objects from the LDAP server. 12 | // It uses the ldap.Session to query the LDAP server and get the descriptions of all objects. 13 | // It then adds the descriptions to the wordlist. 14 | func ExtractDescriptionsOfAllObjects(ldapSession ldap.Session, wordlist *Wordlist) { 15 | logger.Info("Extracting descriptions of all objects from LDAP...") 16 | 17 | query := "(objectClass=*)" 18 | ldapResults, err := ldapSession.QueryWholeSubtree("", query, []string{"description"}) 19 | if err != nil { 20 | logger.Error(fmt.Sprintf("Error querying LDAP: %s", err)) 21 | return 22 | } 23 | 24 | candidates := make([]string, 0) 25 | for _, entry := range ldapResults { 26 | // Process description 27 | descriptions := entry.GetEqualFoldAttributeValues("description") 28 | if len(descriptions) > 0 { 29 | // If description is a slice 30 | words := strings.Split(strings.Join(descriptions, " "), " ") 31 | candidates = append(candidates, words...) 32 | } else { 33 | // If description is a single string 34 | for _, description := range descriptions { 35 | words := strings.Split(description, " ") 36 | candidates = append(candidates, words...) 37 | } 38 | } 39 | } 40 | 41 | nwords := wordlist.AddUniqueWords(candidates) 42 | 43 | logger.Info(fmt.Sprintf(" └──[+] Added %d unique words to wordlist.", nwords)) 44 | } 45 | -------------------------------------------------------------------------------- /core/ExtractNamesOfAllObjects.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/TheManticoreProject/Manticore/logger" 8 | "github.com/TheManticoreProject/Manticore/network/ldap" 9 | ) 10 | 11 | // ExtractNamesOfAllObjects extracts the names of all objects from the LDAP server. 12 | // It uses the ldap.Session to query the LDAP server and get the names of all objects. 13 | // It then adds the names to the wordlist. 14 | func ExtractNamesOfAllObjects(ldapSession ldap.Session, wordlist *Wordlist) { 15 | logger.Info("Extracting names of all objects from LDAP...") 16 | 17 | query := "(objectClass=*)" 18 | ldapResults, err := ldapSession.QueryWholeSubtree("", query, []string{"name"}) 19 | if err != nil { 20 | logger.Error(fmt.Sprintf("Error querying LDAP: %s", err)) 21 | return 22 | } 23 | 24 | candidates := make([]string, 0) 25 | for _, entry := range ldapResults { 26 | // Process name 27 | if len(entry.GetEqualFoldAttributeValues("name")) > 0 { // If name is a slice 28 | names := make([]string, 0) 29 | for _, name := range entry.GetEqualFoldAttributeValues("name") { 30 | if len(name) > 0 { 31 | names = append(names, name) 32 | } 33 | } 34 | words := strings.Split(strings.Join(names, " "), " ") 35 | candidates = append(candidates, words...) 36 | } else { 37 | // If name is a single string 38 | words := strings.Split(entry.GetEqualFoldAttributeValues("name")[0], " ") 39 | candidates = append(candidates, words...) 40 | } 41 | } 42 | 43 | nwords := wordlist.AddUniqueWords(candidates) 44 | 45 | logger.Info(fmt.Sprintf(" └──[+] Added %d unique words to wordlist.", nwords)) 46 | } 47 | -------------------------------------------------------------------------------- /core/ExtractServicePrincipalNames.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/TheManticoreProject/Manticore/logger" 8 | "github.com/TheManticoreProject/Manticore/network/ldap" 9 | ) 10 | 11 | // ExtractServicePrincipalNames extracts the service principal names from the LDAP server. 12 | // It uses the ldap.Session to query the LDAP server and get the service principal names. 13 | // It then adds the service principal names to the wordlist. 14 | func ExtractServicePrincipalNames(ldapSession ldap.Session, wordlist *Wordlist) { 15 | logger.Info("Extracting service principal names from LDAP...") 16 | 17 | query := "(objectClass=*)" 18 | ldapResults, err := ldapSession.QueryWholeSubtree("", query, []string{"servicePrincipalName"}) 19 | if err != nil { 20 | logger.Error(fmt.Sprintf("Error querying LDAP: %s", err)) 21 | return 22 | } 23 | 24 | candidates := make([]string, 0) 25 | for _, entry := range ldapResults { 26 | // Process service principal name 27 | servicePrincipalNames := entry.GetEqualFoldAttributeValues("servicePrincipalName") 28 | if len(servicePrincipalNames) > 0 { 29 | // If service principal name is a slice 30 | words := strings.Split(strings.Join(servicePrincipalNames, "/"), "/") 31 | candidates = append(candidates, words...) 32 | } else { 33 | // If service principal name is a single string 34 | for _, servicePrincipalName := range servicePrincipalNames { 35 | words := strings.Split(servicePrincipalName, "/") 36 | candidates = append(candidates, words...) 37 | } 38 | } 39 | } 40 | 41 | nwords := wordlist.AddUniqueWords(candidates) 42 | 43 | logger.Info(fmt.Sprintf(" └──[+] Added %d unique words to wordlist.", nwords)) 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Build and Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | name: Build Release Assets 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | matrix: 14 | os: [linux, windows, darwin] 15 | arch: [amd64, arm64, 386] 16 | binaryname: [LDAPWordlistHarvester] 17 | # Exclude incompatible couple of GOOS and GOARCH values 18 | exclude: 19 | - os: darwin 20 | arch: 386 21 | 22 | env: 23 | GO111MODULE: 'on' 24 | CGO_ENABLED: '0' 25 | 26 | steps: 27 | - name: Checkout Repository 28 | uses: actions/checkout@v3 29 | 30 | - name: Set up Go 31 | uses: actions/setup-go@v4 32 | with: 33 | go-version: '1.24.0' 34 | 35 | - name: Build Binary 36 | env: 37 | GOOS: ${{ matrix.os }} 38 | GOARCH: ${{ matrix.arch }} 39 | run: | 40 | mkdir -p bin 41 | ls -lha 42 | OUTPUT_PATH="./build/${{ matrix.binaryname }}-${{ matrix.os }}-${{ matrix.arch }}" 43 | # Build the binary 44 | go build -ldflags="-s -w" -o $OUTPUT_PATH${{ matrix.os == 'windows' && '.exe' || '' }} 45 | 46 | - name: Prepare Release Assets 47 | if: ${{ success() }} 48 | run: | 49 | mkdir -p ./release/ 50 | cp ./build/${{ matrix.binaryname }}-* ./release/ 51 | 52 | - name: Upload the Release binaries 53 | uses: svenstaro/upload-release-action@v2 54 | with: 55 | repo_token: ${{ secrets.GITHUB_TOKEN }} 56 | tag: ${{ github.ref }} 57 | file: ./release/${{ matrix.binaryname }}-* 58 | file_glob: true 59 | -------------------------------------------------------------------------------- /core/ExtractADSites.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/TheManticoreProject/Manticore/logger" 8 | "github.com/TheManticoreProject/Manticore/network/ldap" 9 | ) 10 | 11 | // ExtractADSites extracts the AD Sites from the LDAP server. 12 | // It uses the ldap.Session to query the LDAP server and get the AD Sites. 13 | // It then adds the AD Sites to the wordlist. 14 | func ExtractADSites(ldapSession ldap.Session, wordlist *Wordlist) { 15 | // Extract AD Sites 16 | 17 | logger.Info("Extracting AD Sites from LDAP...") 18 | 19 | query := "(objectClass=site)" 20 | ldapResults, err := ldapSession.QueryWholeSubtree("", query, []string{"name", "description"}) 21 | if err != nil { 22 | logger.Error(fmt.Sprintf("Error querying LDAP: %s", err)) 23 | return 24 | } 25 | 26 | candidates := make([]string, 0) 27 | for _, entry := range ldapResults { 28 | // Process description 29 | if len(entry.GetEqualFoldAttributeValues("description")) > 0 { 30 | // If description is a slice 31 | words := strings.Split(strings.Join(entry.GetEqualFoldAttributeValues("description"), " "), " ") 32 | candidates = append(candidates, words...) 33 | } else { 34 | // If description is a single string 35 | words := strings.Split(entry.GetEqualFoldAttributeValues("description")[0], " ") 36 | candidates = append(candidates, words...) 37 | } 38 | 39 | // Process name 40 | if len(entry.GetEqualFoldAttributeValues("name")) > 0 { 41 | // If name is a slice 42 | names := make([]string, 0) 43 | for _, name := range entry.GetEqualFoldAttributeValues("name") { 44 | if len(name) > 0 { 45 | names = append(names, name) 46 | } 47 | } 48 | words := strings.Split(strings.Join(names, " "), " ") 49 | candidates = append(candidates, words...) 50 | } else { 51 | // If name is a single string 52 | words := strings.Split(entry.GetEqualFoldAttributeValues("name")[0], " ") 53 | candidates = append(candidates, words...) 54 | } 55 | } 56 | 57 | nwords := wordlist.AddUniqueWords(candidates) 58 | 59 | logger.Info(fmt.Sprintf(" └──[+] Added %d unique words to wordlist.", nwords)) 60 | } 61 | -------------------------------------------------------------------------------- /core/ExtractTrustedDomains.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/TheManticoreProject/Manticore/logger" 8 | "github.com/TheManticoreProject/Manticore/network/ldap" 9 | ) 10 | 11 | // ExtractTrustedDomains extracts the trusted domains from the LDAP server. 12 | // It uses the ldap.Session to query the LDAP server and get the trusted domains. 13 | // It then adds the trusted domains to the wordlist. 14 | func ExtractTrustedDomains(ldapSession ldap.Session, wordlist *Wordlist) { 15 | logger.Info("Extracting trusted domains from LDAP...") 16 | 17 | query := "(objectClass=trustedDomain)" 18 | ldapResults, err := ldapSession.QueryWholeSubtree("", query, []string{"name", "description"}) 19 | if err != nil { 20 | logger.Error(fmt.Sprintf("Error querying LDAP: %s", err)) 21 | return 22 | } 23 | 24 | candidates := make([]string, 0) 25 | for _, entry := range ldapResults { 26 | // Process description 27 | if len(entry.GetEqualFoldAttributeValues("description")) > 0 { 28 | // If description is a slice 29 | words := strings.Split(strings.Join(entry.GetEqualFoldAttributeValues("description"), " "), " ") 30 | candidates = append(candidates, words...) 31 | } else { 32 | // If description is a single string 33 | words := strings.Split(entry.GetEqualFoldAttributeValues("description")[0], " ") 34 | candidates = append(candidates, words...) 35 | } 36 | 37 | // Process name 38 | if len(entry.GetEqualFoldAttributeValues("name")) > 0 { 39 | // If name is a slice 40 | names := make([]string, 0) 41 | for _, name := range entry.GetEqualFoldAttributeValues("name") { 42 | if len(name) > 0 { 43 | names = append(names, name) 44 | } 45 | } 46 | words := strings.Split(strings.Join(names, " "), " ") 47 | candidates = append(candidates, words...) 48 | } else { 49 | // If name is a single string 50 | words := strings.Split(entry.GetEqualFoldAttributeValues("name")[0], " ") 51 | candidates = append(candidates, words...) 52 | } 53 | } 54 | 55 | nwords := wordlist.AddUniqueWords(candidates) 56 | 57 | logger.Info(fmt.Sprintf(" └──[+] Added %d unique words to wordlist.", nwords)) 58 | } 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](./.github/banner.png) 2 | 3 |

4 | A tool that allows you to extract a client-specific wordlist from the LDAP of an Active Directory. 5 |
6 | Build and Release 7 | GitHub release (latest by date) 8 | Go Report Card 9 | 10 | YouTube Channel Subscribers 11 |
12 |

13 | 14 | ## Features 15 | 16 | - [x] Extract a client-specific wordlist from the LDAP of an Active Directory. 17 | - [x] Support for LDAPS and LDAP. 18 | - [x] Choose the output file with the option `--output`. 19 | 20 | ## Usage 21 | 22 | ``` 23 | $ ./LDAPWordlistHarvester -h 24 | LDAPWordlistHarvester - by Remi GASCOU (Podalirius) @ TheManticoreProject - v1.0.0 25 | 26 | Usage: LDAPWordlistHarvester --domain --username [--password ] [--hashes ] [--debug] [--output ] --dc-ip [--ldap-port ] [--use-ldaps] [--use-kerberos] 27 | 28 | Authentication: 29 | -d, --domain Active Directory domain to authenticate to. 30 | -u, --username User to authenticate as. 31 | -p, --password Password to authenticate with. (default: "") 32 | -H, --hashes NT/LM hashes, format is LMhash:NThash. (default: "") 33 | 34 | Configuration: 35 | -d, --debug Debug mode. (default: false) 36 | -o, --output Output file. (default: "wordlist.txt") 37 | 38 | LDAP Connection Settings: 39 | -dc, --dc-ip IP Address of the domain controller or KDC (Key Distribution Center) for Kerberos. If omitted, it will use the domain part (FQDN) specified in the identity parameter. 40 | -lp, --ldap-port Port number to connect to LDAP server. (default: 389) 41 | -L, --use-ldaps Use LDAPS instead of LDAP. (default: false) 42 | -k, --use-kerberos Use Kerberos instead of NTLM. (default: false) 43 | ``` 44 | 45 | ## Demonstration 46 | 47 | ``` 48 | ./LDAPWordlistHarvester --domain "MANTICORE" --username "Administrator" --password 'Admin123!' --dc-ip 192.168.56.101 49 | ``` 50 | 51 | ![](./.github/example.png) 52 | 53 | ## Contributing 54 | 55 | Pull requests are welcome. Feel free to open an issue if you want to add other features. 56 | 57 | ## Credits 58 | - [Remi GASCOU (Podalirius)](https://github.com/p0dalirius) for the creation of the python version [pyLDAPWordlistHarvester](https://github.com/p0dalirius/pyLDAPWordlistHarvester) project before creating it in Go in TheManticoreProject. 59 | 60 | -------------------------------------------------------------------------------- /core/wordlist.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/TheManticoreProject/Manticore/logger" 10 | ) 11 | 12 | // Wordlist is a struct that contains the path to the wordlist file and the list of words. 13 | type Wordlist struct { 14 | Path string 15 | Wordlist []string 16 | } 17 | 18 | // NewWordlist creates a new Wordlist struct with the given path. 19 | // It initializes the Wordlist struct with the given path and an empty list of words. 20 | func NewWordlist(wordlistPath string) *Wordlist { 21 | return &Wordlist{ 22 | Path: wordlistPath, 23 | Wordlist: make([]string, 0), 24 | } 25 | } 26 | 27 | // AddUniqueWords adds unique words from the provided candidates slice to the Wordlist. 28 | // 29 | // This method iterates over each word in the candidates slice and checks if it already exists 30 | // in the Wordlist. If a word does not exist in the Wordlist, it is added to the Wordlist. 31 | // 32 | // Parameters: 33 | // 34 | // candidates []string: A slice of words to be added to the Wordlist. 35 | // 36 | // Example usage: 37 | // 38 | // wordlist := NewWordlist("path/to/wordlist.txt") 39 | // wordlist.Wordlist = []string{"existingWord1", "existingWord2"} 40 | // candidates := []string{"newWord1", "existingWord1", "newWord2"} 41 | // wordlist.AddUniqueWords(candidates) 42 | // // wordlist.Wordlist now contains: ["existingWord1", "existingWord2", "newWord1", "newWord2"] 43 | // 44 | // Note: 45 | // - This method does not add duplicate words to the Wordlist. 46 | // - The comparison is case-sensitive, meaning "Word" and "word" are considered different. 47 | func (w *Wordlist) AddUniqueWords(candidates []string) int { 48 | addedWords := 0 49 | for _, word := range candidates { 50 | found := false 51 | for _, existing := range w.Wordlist { 52 | if word == existing { 53 | found = true 54 | break 55 | } 56 | } 57 | if !found { 58 | for _, existing := range w.Wordlist { 59 | if strings.EqualFold(word, existing) { 60 | found = true 61 | break 62 | } 63 | } 64 | } 65 | if !found { 66 | w.Wordlist = append(w.Wordlist, word) 67 | addedWords++ 68 | } 69 | } 70 | return addedWords 71 | } 72 | 73 | // Write writes the wordlist to the file specified by the Path field of the Wordlist struct. 74 | // It creates a new file if it does not exist, or truncates the file if it does exist. 75 | // 76 | // Returns an error if the file cannot be created or if there is an error during writing. 77 | // 78 | // Example usage: 79 | // 80 | // wordlist := NewWordlist("path/to/wordlist.txt") 81 | // wordlist.Wordlist = []string{"word1", "word2", "word3"} 82 | // err := wordlist.Write() 83 | // if err != nil { 84 | // fmt.Printf("Error writing wordlist: %v\n", err) 85 | // } 86 | func (w *Wordlist) Write() error { 87 | file, err := os.Create(w.Path) 88 | if err != nil { 89 | return fmt.Errorf("failed to create wordlist file: %v", err) 90 | } 91 | defer file.Close() 92 | 93 | writer := bufio.NewWriter(file) 94 | for _, word := range w.Wordlist { 95 | _, err := writer.WriteString(word + "\n") 96 | if err != nil { 97 | return fmt.Errorf("failed to write to wordlist: %v", err) 98 | } 99 | } 100 | writer.Flush() 101 | logger.Info(fmt.Sprintf("Wordlist written to: %s (%d words)", w.Path, len(w.Wordlist))) 102 | 103 | return nil 104 | } 105 | -------------------------------------------------------------------------------- /.github/workflows/auto_prefix_issues.yml: -------------------------------------------------------------------------------- 1 | name: Auto‑prefix & Label Issues 2 | 3 | on: 4 | issues: 5 | types: [opened, edited] 6 | schedule: 7 | - cron: '0 0 * * *' # every day at midnight UTC 8 | 9 | jobs: 10 | prefix_and_label: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Ensure labels exist, then prefix titles & add labels 14 | uses: actions/github-script@v6 15 | with: 16 | script: | 17 | const owner = context.repo.owner; 18 | const repo = context.repo.repo; 19 | 20 | // 1. Ensure required labels exist 21 | const required = [ 22 | { name: 'bug', color: 'd73a4a', description: 'Something isn\'t working' }, 23 | { name: 'enhancement', color: 'a2eeef', description: 'New feature or request' } 24 | ]; 25 | 26 | // Fetch current labels in the repo 27 | const { data: existingLabels } = await github.rest.issues.listLabelsForRepo({ 28 | owner, repo, per_page: 100 29 | }); 30 | const existingNames = new Set(existingLabels.map(l => l.name)); 31 | 32 | // Create any missing labels 33 | for (const lbl of required) { 34 | if (!existingNames.has(lbl.name)) { 35 | await github.rest.issues.createLabel({ 36 | owner, 37 | repo, 38 | name: lbl.name, 39 | color: lbl.color, 40 | description: lbl.description 41 | }); 42 | console.log(`Created label "${lbl.name}"`); 43 | } 44 | } 45 | 46 | // 2. Fetch all open issues 47 | const issues = await github.paginate( 48 | github.rest.issues.listForRepo, 49 | { owner, repo, state: 'open', per_page: 100 } 50 | ); 51 | 52 | // 3. Keyword sets 53 | const enhancementWords = ["add", "added", "improve", "improved"]; 54 | const bugWords = ["bug", "error", "problem", "crash", "failed", "fix", "fixed"]; 55 | 56 | // 4. Process each issue 57 | for (const issue of issues) { 58 | const origTitle = issue.title; 59 | const lower = origTitle.toLowerCase(); 60 | 61 | // skip if already prefixed 62 | if (/^\[(bug|enhancement)\]/i.test(origTitle)) continue; 63 | 64 | let prefix, labelToAdd; 65 | if (enhancementWords.some(w => lower.includes(w))) { 66 | prefix = "[enhancement]"; 67 | labelToAdd = "enhancement"; 68 | } else if (bugWords.some(w => lower.includes(w))) { 69 | prefix = "[bug]"; 70 | labelToAdd = "bug"; 71 | } 72 | 73 | if (prefix) { 74 | // update title 75 | await github.rest.issues.update({ 76 | owner, repo, issue_number: issue.number, 77 | title: `${prefix} ${origTitle}` 78 | }); 79 | console.log(`Prefixed title of #${issue.number}`); 80 | 81 | // add label 82 | await github.rest.issues.addLabels({ 83 | owner, repo, issue_number: issue.number, 84 | labels: [labelToAdd] 85 | }); 86 | console.log(`Added label "${labelToAdd}" to #${issue.number}`); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/TheManticoreProject/LDAPWordlistHarvester/core" 7 | "github.com/TheManticoreProject/Manticore/logger" 8 | "github.com/TheManticoreProject/Manticore/network/ldap" 9 | "github.com/TheManticoreProject/Manticore/windows/credentials" 10 | "github.com/TheManticoreProject/goopts/parser" 11 | ) 12 | 13 | var ( 14 | // Configuration 15 | debug bool 16 | 17 | // Authentication 18 | authDomain string 19 | authUsername string 20 | authPassword string 21 | authHashes string 22 | 23 | // LDAP Connection Settin mode string 24 | domainController string 25 | ldapPort int 26 | useLdaps bool 27 | useKerberos bool 28 | outputFile string 29 | ) 30 | 31 | func parseArgs() { 32 | ap := parser.ArgumentsParser{ 33 | Banner: "LDAPWordlistHarvester - by Remi GASCOU (Podalirius) @ TheManticoreProject - v1.0.0", 34 | } 35 | ap.SetOptShowBannerOnHelp(true) 36 | ap.SetOptShowBannerOnRun(true) 37 | 38 | // Configuration flags 39 | group_config, err := ap.NewArgumentGroup("Configuration") 40 | if err != nil { 41 | fmt.Printf("[error] Error creating ArgumentGroup: %s\n", err) 42 | } else { 43 | group_config.NewBoolArgument(&debug, "", "--debug", false, "Debug mode.") 44 | group_config.NewStringArgument(&outputFile, "-o", "--output", "wordlist.txt", false, "Output file.") 45 | } 46 | // LDAP Connection Settings 47 | group_ldapSettings, err := ap.NewArgumentGroup("LDAP Connection Settings") 48 | if err != nil { 49 | fmt.Printf("[error] Error creating ArgumentGroup: %s\n", err) 50 | } else { 51 | group_ldapSettings.NewStringArgument(&domainController, "-dc", "--dc-ip", "", true, "IP Address of the domain controller or KDC (Key Distribution Center) for Kerberos. If omitted, it will use the domain part (FQDN) specified in the identity parameter.") 52 | group_ldapSettings.NewTcpPortArgument(&ldapPort, "-lp", "--ldap-port", 389, false, "Port number to connect to LDAP server.") 53 | group_ldapSettings.NewBoolArgument(&useLdaps, "-L", "--use-ldaps", false, "Use LDAPS instead of LDAP.") 54 | group_ldapSettings.NewBoolArgument(&useKerberos, "-k", "--use-kerberos", false, "Use Kerberos instead of NTLM.") 55 | } 56 | // Authentication flags 57 | group_auth, err := ap.NewArgumentGroup("Authentication") 58 | if err != nil { 59 | fmt.Printf("[error] Error creating ArgumentGroup: %s\n", err) 60 | } else { 61 | group_auth.NewStringArgument(&authDomain, "-d", "--domain", "", true, "Active Directory domain to authenticate to.") 62 | group_auth.NewStringArgument(&authUsername, "-u", "--username", "", true, "User to authenticate as.") 63 | group_auth.NewStringArgument(&authPassword, "-p", "--password", "", false, "Password to authenticate with.") 64 | group_auth.NewStringArgument(&authHashes, "-H", "--hashes", "", false, "NT/LM hashes, format is LMhash:NThash.") 65 | } 66 | 67 | ap.Parse() 68 | } 69 | 70 | func main() { 71 | parseArgs() 72 | 73 | creds, err := credentials.NewCredentials(authDomain, authUsername, authPassword, authHashes) 74 | if err != nil { 75 | fmt.Printf("[error] Error creating credentials: %s\n", err) 76 | return 77 | } 78 | 79 | ldapSession := ldap.Session{} 80 | ldapSession.InitSession(domainController, ldapPort, creds, useLdaps, useKerberos) 81 | success, err := ldapSession.Connect() 82 | if !success { 83 | logger.Warn(fmt.Sprintf("Error performing LDAP search: %s\n", err)) 84 | return 85 | } 86 | 87 | wordlist := core.NewWordlist(outputFile) 88 | 89 | core.ExtractADSites(ldapSession, wordlist) 90 | 91 | core.ExtractNamesOfAllObjects(ldapSession, wordlist) 92 | 93 | core.ExtractDescriptionsOfAllObjects(ldapSession, wordlist) 94 | 95 | core.ExtractServicePrincipalNames(ldapSession, wordlist) 96 | 97 | wordlist.Write() 98 | 99 | logger.Print("Done") 100 | } 101 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= 2 | github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= 3 | github.com/TheManticoreProject/Manticore v1.0.2 h1:VNFMrchf7krBrjgC07D72K9XVOcd15ne/XH7rKJAviM= 4 | github.com/TheManticoreProject/Manticore v1.0.2/go.mod h1:2YzwHihKSODuo1YP0UuHL/AgcwkQLtc5j/ihuvke4/k= 5 | github.com/TheManticoreProject/goopts v1.2.4 h1:HTR+i6ZfcwTPSDltENayQvTH2fZP6cDx0GTBUp/RuPM= 6 | github.com/TheManticoreProject/goopts v1.2.4/go.mod h1:NaM3hXXCeN+x9ZSlkS6Bm8i8Lfqe28/rveBsfrMUrAo= 7 | github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= 8 | github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= 13 | github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= 14 | github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU= 15 | github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM= 16 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 17 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 18 | github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= 19 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 20 | github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= 21 | github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= 22 | github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 23 | github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= 24 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 25 | github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= 26 | github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= 27 | github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= 28 | github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= 29 | github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= 30 | github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= 31 | github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= 32 | github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= 33 | github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= 34 | github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= 35 | github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= 36 | github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= 37 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 38 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 39 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 40 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 41 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 42 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 43 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 44 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 45 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 46 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 47 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 48 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 49 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 50 | golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= 51 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 52 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 53 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 54 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 55 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 56 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 57 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 58 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 59 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 60 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 61 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 62 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 63 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 64 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 65 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 66 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 67 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 68 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 69 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 70 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 71 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 72 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 73 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 74 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 75 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 76 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 77 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 78 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 79 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 80 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 81 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 82 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 83 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 84 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 85 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 86 | --------------------------------------------------------------------------------