diff --git a/.gitignore b/.gitignore index 4eb918f..5c159e3 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,10 @@ venv* *.7z *.pprof *.exe -*.dit + *SECURITY -*SYSTEM \ No newline at end of file +/lib/* +*.test +*.prof +*.out +/test/python/* \ No newline at end of file diff --git a/cmd/dumpSecrets.go b/cmd/dumpSecrets.go index aa2b774..5e1fa01 100644 --- a/cmd/dumpSecrets.go +++ b/cmd/dumpSecrets.go @@ -16,6 +16,7 @@ type Settings struct { Outfile string NoPrint bool Stream bool + History bool } func GoSecretsDump(s Settings) error { @@ -28,14 +29,14 @@ func GoSecretsDump(s Settings) error { if s.Outfile != "" { fmt.Println("Writing to file ", s.Outfile) if s.Stream { - fileStreamWriter(dataChan, s) + go fileStreamWriter(dataChan, s) } else { - fileWriter(dataChan, s) + go fileWriter(dataChan, s) } } else { - consoleWriter(dataChan, s) + go consoleWriter(dataChan, s) } - return nil + return dr.Dump() } func consoleWriter(val <-chan ditreader.DumpedHash, s Settings) { @@ -68,6 +69,9 @@ func consoleWriter(val <-chan ditreader.DumpedHash, s Settings) { hs.WriteString(append.String()) } hs.WriteString("\n") + if s.History { + hs.WriteString(dh.HistoryString()) + } //pts = dh.Supp.HashString() + "\n" } fmt.Print(hs.String()) @@ -114,7 +118,9 @@ func fileWriter(val <-chan ditreader.DumpedHash, s Settings) { kerbs.WriteString(append.String()) kerbs.WriteString("\n") } - + if s.History { + hs.WriteString(dh.HistoryString()) + } //pts = dh.Supp.HashString() + "\n" plaintext.WriteString(pts.String()) } diff --git a/main.go b/main.go index 3f1e236..71cc5ae 100644 --- a/main.go +++ b/main.go @@ -8,7 +8,7 @@ import ( "github.com/C-Sto/gosecretsdump/cmd" ) -const version = "0.1.0" +const version = "0.2.0" func main() { @@ -26,6 +26,7 @@ func main() { flag.BoolVar(&s.NoPrint, "noprint", false, "Don't print output to screen (probably use this with the -out flag)") flag.BoolVar(&s.Stream, "stream", false, "Stream to files rather than writing in a block. Can be much slower.") flag.BoolVar(&vers, "version", false, "Print version and exit") + flag.BoolVar(&s.History, "history", false, "Include Password History") flag.Parse() if vers { @@ -35,7 +36,10 @@ func main() { flag.Usage() os.Exit(1) } - cmd.GoSecretsDump(s) + e := cmd.GoSecretsDump(s) + if e != nil { + panic(e) + } } //info dumped out of https://github.com/SecureAuthCorp/impacket/blob/master/impacket/examples/secretsdump.py diff --git a/pkg/ditreader/constants.go b/pkg/ditreader/constants.go index 384eb50..c7ec00a 100644 --- a/pkg/ditreader/constants.go +++ b/pkg/ditreader/constants.go @@ -55,3 +55,6 @@ var accTypes = map[int32]string{ 0x30000001: "SAM_MACHINE_ACCOUNT", 0x30000002: "SAM_TRUST_ACCOUNT", } + +var emptyNT = []byte{0x31, 0xd6, 0xcf, 0xe0, 0xd1, 0x6a, 0xe9, 0x31, 0xb7, 0x3c, 0x59, 0xd7, 0xe0, 0xc0, 0x89, 0xc0} +var emptyLM = []byte{0xaa, 0xd3, 0xb4, 0x35, 0xb5, 0x14, 0x04, 0xee, 0xaa, 0xd3, 0xb4, 0x35, 0xb5, 0x14, 0x04, 0xee} diff --git a/pkg/ditreader/crypto.go b/pkg/ditreader/crypto.go index 899695c..53c0911 100644 --- a/pkg/ditreader/crypto.go +++ b/pkg/ditreader/crypto.go @@ -111,10 +111,17 @@ func decryptAES(key, value, iv []byte) ([]byte, error) { } type CryptedHashW16 struct { - Header [8]byte - KeyMaterial [16]byte - Unknown uint32 - EncrypedHash [32]byte + Header [8]byte + KeyMaterial [16]byte + Unknown uint32 + EncryptedHash [32]byte +} + +type CryptedHashW16History struct { + Header [8]byte + KeyMaterial [16]byte + Unknown uint32 + EncryptedHash []byte } func NewCryptedHashW16(data []byte) CryptedHashW16 { @@ -129,7 +136,24 @@ func NewCryptedHashW16(data []byte) CryptedHashW16 { r.Unknown = binary.LittleEndian.Uint32(data[:4]) data = data[4:] - copy(r.EncrypedHash[:], data[:32]) + copy(r.EncryptedHash[:], data[:32]) + + return r +} + +func NewCryptedHashW16History(data []byte) CryptedHashW16History { + r := CryptedHashW16History{} + //data := make([]byte, len(inData)) + //copy(data, inData) + copy(r.Header[:], data[:8]) + data = data[8:] + copy(r.KeyMaterial[:], data[:16]) + data = data[16:] + + r.Unknown = binary.LittleEndian.Uint32(data[:4]) + data = data[4:] + r.EncryptedHash = make([]byte, len(data)) + copy(r.EncryptedHash[:], data[:]) return r } diff --git a/pkg/ditreader/ditreader.go b/pkg/ditreader/ditreader.go index f96124c..1dbe261 100644 --- a/pkg/ditreader/ditreader.go +++ b/pkg/ditreader/ditreader.go @@ -6,7 +6,6 @@ import ( "crypto/rc4" "fmt" "os" - "runtime" "sync" "github.com/C-Sto/gosecretsdump/pkg/systemreader" @@ -16,7 +15,6 @@ import ( //New Creates a new dit dumper func New(system, ntds string) (DitReader, error) { - parallel := false r := DitReader{ isRemote: false, history: false, @@ -31,21 +29,20 @@ func New(system, ntds string) (DitReader, error) { printUserStatus: false, systemHiveLocation: system, ntdsFileLocation: ntds, - db: esent.Esedb{}.Init(ntds), - userData: make(chan DumpedHash, 500), + //db: esent.Esedb{}.Init(ntds), + userData: make(chan DumpedHash, 500), } - if parallel { - for i := 0; i < runtime.NumCPU()-1; i++ { - go r.decryptWorker() - } - } var err error + r.db, err = esent.Esedb{}.Init(ntds) + if err != nil { + return r, err + } r.cursor, err = r.db.OpenTable("datatable") if err != nil { return r, err } - go r.dump() //start dumping the file immediately output will be put into the output channel as it comes + //go r.dump() //start dumping the file immediately output will be put into the output channel as it comes return r, err } diff --git a/pkg/ditreader/dumpedInfo.go b/pkg/ditreader/dumpedInfo.go index 8d51837..6d69f07 100644 --- a/pkg/ditreader/dumpedInfo.go +++ b/pkg/ditreader/dumpedInfo.go @@ -83,6 +83,58 @@ type DumpedHash struct { Enabled bool UAC uacFlags Supp SuppInfo + History PwdHistory +} + +type PwdHistory struct { + LmHist [][]byte + NTHist [][]byte +} + +func (d DumpedHash) HistoryStrings() []string { + r := make([]string, 0, len(d.History.NTHist)) + for i, v := range d.History.LmHist { + r = append(r, fmt.Sprintf("%s_history%d:%s:%s:%s:::", + d.Username, + i, + d.Rid, + hex.EncodeToString(v), + hex.EncodeToString(emptyNT), + )) + } + for i, v := range d.History.NTHist { + r = append(r, fmt.Sprintf("%s_history%d:%s:%s:%s:::", + d.Username, + i, + d.Rid, + hex.EncodeToString(emptyLM), + hex.EncodeToString(v), + )) + } + return r +} + +func (d DumpedHash) HistoryString() string { + r := strings.Builder{} + for i, v := range d.History.LmHist { + r.WriteString(fmt.Sprintf("%s_history%d:%s:%s:%s:::\n", + d.Username, + i, + d.Rid, + hex.EncodeToString(v), + hex.EncodeToString(emptyNT), + )) + } + for i, v := range d.History.NTHist { + r.WriteString(fmt.Sprintf("%s_history%d:%s:%s:%s:::\n", + d.Username, + i, + d.Rid, + hex.EncodeToString(emptyLM), + hex.EncodeToString(v), + )) + } + return r.String() } func (d DumpedHash) HashString() string { diff --git a/pkg/ditreader/records.go b/pkg/ditreader/records.go index 57e295b..789c2eb 100644 --- a/pkg/ditreader/records.go +++ b/pkg/ditreader/records.go @@ -32,7 +32,7 @@ func (d *DitReader) DecryptRecord(record esent.Esent_record) (DumpedHash, error) if bytes.Compare(encryptedLM.Header[:4], []byte("\x13\x00\x00\x00")) == 0 { encryptedLMW := NewCryptedHashW16(b) pekIndex := encryptedLMW.Header - tmpLM, err = decryptAES(d.pek[pekIndex[4]], encryptedLMW.EncrypedHash[:16], encryptedLMW.KeyMaterial[:]) + tmpLM, err = decryptAES(d.pek[pekIndex[4]], encryptedLMW.EncryptedHash[:16], encryptedLMW.KeyMaterial[:]) if err != nil { return dh, err } @@ -48,7 +48,7 @@ func (d *DitReader) DecryptRecord(record esent.Esent_record) (DumpedHash, error) } } else { //hard coded empty lm hash - dh.LMHash, _ = hex.DecodeString("aad3b435b51404eeaad3b435b51404ee") + dh.LMHash = emptyLM //, _ = hex.DecodeString("aad3b435b51404eeaad3b435b51404ee") } //nt hash @@ -61,7 +61,7 @@ func (d *DitReader) DecryptRecord(record esent.Esent_record) (DumpedHash, error) if bytes.Compare(encryptedNT.Header[:4], []byte("\x13\x00\x00\x00")) == 0 { encryptedNTW := NewCryptedHashW16(v) pekIndex := encryptedNTW.Header - tmpNT, err = decryptAES(d.pek[pekIndex[4]], encryptedNTW.EncrypedHash[:16], encryptedNTW.KeyMaterial[:]) + tmpNT, err = decryptAES(d.pek[pekIndex[4]], encryptedNTW.EncryptedHash[:16], encryptedNTW.KeyMaterial[:]) if err != nil { return dh, err } @@ -77,7 +77,7 @@ func (d *DitReader) DecryptRecord(record esent.Esent_record) (DumpedHash, error) } } else { //hard coded empty NTLM hash - dh.NTHash, _ = hex.DecodeString("31D6CFE0D16AE931B73C59D7E0C089C0") + dh.NTHash = emptyNT //, _ = hex.DecodeString("31D6CFE0D16AE931B73C59D7E0C089C0") } //username @@ -90,10 +90,66 @@ func (d *DitReader) DecryptRecord(record esent.Esent_record) (DumpedHash, error) dh.Username = fmt.Sprintf("%s", v) } + //Password history LM + if !d.noLMHash { + if v, _ := record.GetBytVal(nlmPwdHistory); v != nil && len(v) > 0 { //&& len(v) > 0 { + ch, err := NewCryptedHash(v) + if err != nil { + return dh, err + } + tmphst := []byte{} + tmphst, err = d.removeRC4(ch) + if err != nil { + return dh, err + } + + for i := 16; i < len(tmphst); i += 16 { + hst1 := tmphst[i : i+16] + hst2, err := removeDES(hst1, dh.Rid) + dh.History.LmHist = append(dh.History.LmHist, hst2) + if err != nil { + return dh, err + } + } + } + } + + //password history NT + if v, _ := record.GetBytVal(nntPwdHistory); v != nil && len(v) > 0 { //&& len(v) > 0 { + ch, err := NewCryptedHash(v) + if err != nil { + return dh, err + } + tmphst := []byte{} + if bytes.Compare(ch.Header[:4], []byte("\x13\x00\x00\x00")) == 0 { + encryptedNTW := NewCryptedHashW16History(v) + pekIndex := encryptedNTW.Header + tmphst, err = decryptAES(d.pek[pekIndex[4]], encryptedNTW.EncryptedHash[:], encryptedNTW.KeyMaterial[:]) + if err != nil { + return dh, err + } + } else { + tmphst, err = d.removeRC4(ch) + if err != nil { + return dh, err + } + } + for i := 16; i < len(tmphst); i += 16 { + hst1 := tmphst[i : i+16] + hst2, err := removeDES(hst1, dh.Rid) + if err != nil { + return dh, err + } + dh.History.NTHist = append(dh.History.NTHist, hst2) + } + + } + //check if account is enabled if v, _ := record.GetLongVal(nuserAccountControl); v != 0 { // record.Column[nuserAccountControl"]].Long; v != 0 { dh.UAC = decodeUAC(int(v)) } + //check if cleartext exists if val, _ := record.GetBytVal(nsupplementalCredentials); len(val) > 24 { //if val := record.Column[nsupplementalCredentials"]]; len(val.BytVal) > 24 { var err error diff --git a/pkg/esent/ese.go b/pkg/esent/ese.go index c8eb297..2a3fd27 100644 --- a/pkg/esent/ese.go +++ b/pkg/esent/ese.go @@ -35,7 +35,7 @@ var stringCodePages = map[uint32]string{ 1252: "cp1252", } //standin for const lookup/enum thing -func (e Esedb) Init(fn string) Esedb { +func (e Esedb) Init(fn string) (Esedb, error) { //create the esedb structure r := Esedb{ filename: fn, @@ -45,8 +45,8 @@ func (e Esedb) Init(fn string) Esedb { } //'mount' the database (parse the file) - r.mountDb(fn) - return r + err := r.mountDb(fn) + return r, err } //OpenTable opens a table, and returns a cursor pointing to the current parsing state @@ -108,14 +108,17 @@ func (e *Esedb) OpenTable(s string) (*Cursor, error) { return &r, nil } -func (e *Esedb) mountDb(filename string) { +func (e *Esedb) mountDb(filename string) (err error) { //the first page is the dbheader - e.loadPages(filename) - + err = e.loadPages(filename) + if err != nil { + return + } // this was a gross way of working out how many pages the file has... - //this is where everything actually gets parsed out e.parseCatalog(CATALOG_PAGE_NUMBER) //4 ? + + return } func (e *Esedb) parseCatalog(pagenum uint32) error { diff --git a/pyTest.sh b/pyTest.sh index ad0b860..c077418 100755 --- a/pyTest.sh +++ b/pyTest.sh @@ -1,6 +1,13 @@ source venv/bin/activate + +if [[ ! -d "./impacket" ]] +then + git clone https://github.com/SecureAuthCorp/impacket +fi + cd impacket python setup.py install cd .. -python impacket/examples/secretsdump.py -system test/Big/SYSTEM -ntds test/Big/ntds.dit LOCAL +#python impacket/examples/secretsdump.py -system test/Big/SYSTEM -ntds test/Big/ntds.dit LOCAL +python impacket/examples/secretsdump.py -system test/2016/SYSTEM -ntds test/2016/ntds.dit LOCAL -history deactivate \ No newline at end of file diff --git a/test/acuracy_test.go b/test/acuracy_test.go new file mode 100644 index 0000000..a0188db --- /dev/null +++ b/test/acuracy_test.go @@ -0,0 +1,67 @@ +package test + +import ( + "io/ioutil" + "strings" + "testing" + + "github.com/C-Sto/gosecretsdump/pkg/ditreader" +) + +func TestProgram(t *testing.T) { + dr, err := ditreader.New("./system", "./ntds.dit") + if err != nil { + t.Fatal(err) + } + //dr := ditreader.New("../big/registry/SYSTEM", "../big/Active Directory/ntds.dit") + //handle any output + dataChan := dr.GetOutChan() + go dr.Dump() + i := 0 + for range dataChan { + i++ + } + if i != 39 { + t.Fatal("Did not recover all users. Expected 39, got ", i) + } +} + +func TestGetHashes(t *testing.T) { + //get valid output files + s, e := ioutil.ReadFile("impacket-out/2016/2016.ntds") + if e != nil { + t.Error("Could not read from 2016 file") + } + corretkerb := make(map[string]bool, len(s)) + sa := strings.Split(string(s), "\n") + for _, v := range sa { + if v != "" { + corretkerb[v] = true + } + } + + dr, err := ditreader.New("./2016/system", "./2016/ntds.dit") + if err != nil { + t.Fatal(err) + } + go dr.Dump() + dataChan := dr.GetOutChan() + for ok := range dataChan { + //ensure it exists (don't find values that are not in impacket.. yet) + if _, found := corretkerb[ok.HashString()]; !found { + t.Errorf("found unexpected value: %s", ok.HashString()) + } + //check history too + for _, h := range ok.HistoryStrings() { + if _, found := corretkerb[h]; !found { + t.Errorf("found unexpected value: %s", h) + } + delete(corretkerb, h) + } + //ensure we don't miss any that impacket finds + delete(corretkerb, ok.HashString()) + } + if len(corretkerb) > 0 { + t.Errorf("Expected empty map. Unfound hashes: %+v", corretkerb) + } +} diff --git a/test/bench/bench.go b/test/bench/bench.go new file mode 100644 index 0000000..e69de29 diff --git a/test/bench/dumpsecrets_test.go b/test/bench/dumpsecrets_test.go index 6ebbaa0..14aaa9a 100644 --- a/test/bench/dumpsecrets_test.go +++ b/test/bench/dumpsecrets_test.go @@ -16,23 +16,6 @@ func BenchmarkBigProgram(t *testing.B) { } }*/ -func TestProgram(t *testing.T) { - dr, err := ditreader.New("../system", "../ntds.dit") - if err != nil { - t.Fatal(err) - } - //dr := ditreader.New("../big/registry/SYSTEM", "../big/Active Directory/ntds.dit") - //handle any output - dataChan := dr.GetOutChan() - i := 0 - for range dataChan { - i++ - } - if i != 39 { - t.Fatal("Did not recover all users. Expected 39, got ", i) - } -} - func BenchmarkProgram(t *testing.B) { t.ReportAllocs() for i := 0; i < t.N; i++ { diff --git a/test/impacket-out/2016/2016.kerberos b/test/impacket-out/2016/2016.kerberos new file mode 100644 index 0000000..8c5b137 --- /dev/null +++ b/test/impacket-out/2016/2016.kerberos @@ -0,0 +1,9 @@ +WIN-K97I9JS0MQ0$:aes256-cts-hmac-sha1-96:eb17251816833c6aa41adfbcc3e561a8c4ac09cd8432d1f699404091eba0e242 +WIN-K97I9JS0MQ0$:aes128-cts-hmac-sha1-96:447a4bffe6c5be6337fabdffaf2775ec +WIN-K97I9JS0MQ0$:des-cbc-md5:e6b5a2ec6b944052 +krbtgt:aes256-cts-hmac-sha1-96:3d8ecf6154bf3a6296096cc72b257ea64d490e48da22352cd7cd95dfbb1ac06b +krbtgt:aes128-cts-hmac-sha1-96:1b7702abe2cd8d78e3fa4d1466e91a71 +krbtgt:des-cbc-md5:4fd5e0e398621608 +camtest123:aes256-cts-hmac-sha1-96:f773fe8693823158418b711ce935ec6222f81cbe8f6705faa41c7a0993b2dc98 +camtest123:aes128-cts-hmac-sha1-96:7d38a681c4047ceaa265b52ec725880f +camtest123:des-cbc-md5:ce673886a1019d1c diff --git a/test/impacket-out/2016/2016.ntds b/test/impacket-out/2016/2016.ntds new file mode 100644 index 0000000..b599a04 --- /dev/null +++ b/test/impacket-out/2016/2016.ntds @@ -0,0 +1,11 @@ +Administrator:500:aad3b435b51404eeaad3b435b51404ee:986ced7b028e25984c4e2ad171d9ded5::: +Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0::: +DefaultAccount:503:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0::: +WIN-K97I9JS0MQ0$:1000:aad3b435b51404eeaad3b435b51404ee:1abb49fcab0cb1a491850c2348eac619::: +krbtgt:502:aad3b435b51404eeaad3b435b51404ee:be0aa069cf8f5de187f72a4cb7bbd926::: +krbtgt_history0:502:aad3b435b51404eeaad3b435b51404ee:b5ca59b606a13445af2043409d2c0086::: +camtest123:1103:aad3b435b51404eeaad3b435b51404ee:766b62d3db023f90443469d86393ca66::: +camtest123_history0:1103:aad3b435b51404eeaad3b435b51404ee:c9ab9d08cc7da5a55d8a82d869e01ea8::: +camtest123_history1:1103:aad3b435b51404eeaad3b435b51404ee:02151f5a54ba5a016ee42da5de832457::: +camtest123_history2:1103:aad3b435b51404eeaad3b435b51404ee:c8f55e0c6d01af1f57ee3493e87a59f5::: +camtest123_history3:1103:aad3b435b51404eeaad3b435b51404ee:c63407eac237a49a7e559f453cc6a4df::: diff --git a/test/test.go b/test/test.go new file mode 100644 index 0000000..56e5404 --- /dev/null +++ b/test/test.go @@ -0,0 +1 @@ +package test