├── .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 | 
2 |
3 |
4 | A tool that allows you to extract a client-specific wordlist from the LDAP of an Active Directory.
5 |
6 |
7 |
8 |
9 |
10 |
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 | 
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 |
--------------------------------------------------------------------------------