-
Notifications
You must be signed in to change notification settings - Fork 1
/
ad.go
168 lines (135 loc) · 3.29 KB
/
ad.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
package main
import (
"bufio"
"log"
"os"
"sort"
"strings"
"github.com/C-Sto/gosecretsdump/pkg/ditreader"
)
// AD stores the users and password hashes from Active Directory
type AD struct {
hashes map[string]string
reuse []*ReusedPassword
}
// NewAD returns a new AD instance
func NewAD() *AD {
ad := AD{}
ad.hashes = make(map[string]string)
return &ad
}
// LoadHashesFromCSV loads the password hashes from a CSV file (comma separated)
func (ad *AD) LoadHashesFromCSV(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
// split at ;
parts := strings.Split(line, ",")
if len(parts) != 2 {
log.Print("Ignoring line: " + line)
continue
}
ad.hashes[parts[0]] = strings.ToLower(parts[1])
}
err = scanner.Err()
if err != nil {
return err
}
return nil
}
// LoadHashesFromSecretsdump loads the password hashes from a text file with the secretsdump output
func (ad *AD) LoadHashesFromSecretsdump(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
// split at :
parts := strings.Split(line, ":")
if len(parts) != 7 {
continue
}
ad.hashes[parts[0]] = strings.ToLower(parts[3])
}
err = scanner.Err()
if err != nil {
return err
}
return nil
}
// LoadHashesFromNTDS loads the password hashes from a file
func (ad *AD) LoadHashesFromNTDS(ntdsFile string, systemFile string) error {
dr, err := ditreader.New(systemFile, ntdsFile)
if err != nil {
return err
}
data := dr.GetOutChan()
for d := range data {
// check dh.UAC.AccountDisable ?
line := d.HashString()
parts := strings.Split(line, ":")
if len(parts) != 7 {
log.Print("Ignoring line: " + line)
continue
}
ad.hashes[parts[0]] = strings.ToLower(parts[3])
}
return nil
}
// Analyze runs the analysis of the collected password hashs
func (ad *AD) Analyze() {
ad.findReusedPasswords()
}
// ReusedPasswords returns the reused passwords and according users
func (ad *AD) ReusedPasswords() []*ReusedPassword {
return ad.reuse
}
// FindReusedPasswords searches for accounts that have the same password hash
func (ad *AD) findReusedPasswords() {
// a hashtable is used to easily check, whether the hash was found already or not
store := make(map[string]*ReusedPassword)
// search
for user, hash := range ad.hashes {
// ignore empty ntlm hash
if hash == "31d6cfe0d16ae931b73c59d7e0c089c0" {
continue
}
// store
_, reused := store[hash]
if reused {
store[hash].Add(user)
} else {
store[hash] = NewReusedPassword(user, hash)
}
}
// count how many passwords are used more than once (so we know how big the result list is)
numReuses := 0
for _, reusedpassword := range store {
if reusedpassword.Count > 1 {
numReuses++
}
}
// filter and convert to output format (list instead of hashmap)
result := make([]*ReusedPassword, numReuses)
counter := 0
for _, reusedpassword := range store {
if reusedpassword.Count > 1 {
result[counter] = reusedpassword
counter++
}
}
// sort
sort.Slice(result, func(i, j int) bool {
return result[i].Count > result[j].Count
})
// done
ad.reuse = result
}