diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..867cb18 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,39 @@ +# This workflow will build binaries for Linux and Windows x64 architecture + +name: release + +on: + release: + types: [published] + +jobs: + build: + permissions: + contents: write # to allow upload of release assets + runs-on: ubuntu-latest + steps: + - name: checkout repo + uses: actions/checkout@v4 + + - name: set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.20' + + - name: install dependencies + run: go get + + - name: build project + run: make + + - name: publish binaries to release + run: | + ASSET_NAME_1="go-secdump" + ASSET_NAME_2="go-secdump.exe" + UPLOAD_URL="https://uploads.github.com/repos/${{ github.repository }}/releases/${{ github.event.release.id }}/assets" + + # Upload asset 1 + curl -sSL -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" -H "Content-Type: application/octet-stream" --data-binary @"$ASSET_NAME_1" "$UPLOAD_URL?name=$ASSET_NAME_1" + + # Upload asset 2 + curl -sSL -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" -H "Content-Type: application/octet-stream" --data-binary @"$ASSET_NAME_2" "$UPLOAD_URL?name=$ASSET_NAME_2" diff --git a/Makefile b/Makefile index 456ff88..90a047c 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ all: - GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o go-secdump - GOOS=windows GOARCH=386 go build -ldflags "-s -w" -o go-secdump.exe . + GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "-s -w" -o go-secdump + GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "-s -w" -o go-secdump.exe . clean: rm -f go-secdump diff --git a/go.mod b/go.mod index 381ef07..aa6e1b5 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module github.com/jfjallid/go-secdump go 1.19 require ( - github.com/jfjallid/go-smb v0.2.5 - github.com/jfjallid/golog v0.3.2 + github.com/jfjallid/go-smb v0.2.6 + github.com/jfjallid/golog v0.3.3 golang.org/x/crypto v0.6.0 golang.org/x/term v0.5.0 ) diff --git a/go.sum b/go.sum index ab35ce7..9ea3eed 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ -github.com/jfjallid/go-smb v0.2.5 h1:J3ubQCBqI0FPczM7Tgn36MAG0H2w+lUCCA3q6b7/5k8= -github.com/jfjallid/go-smb v0.2.5/go.mod h1:Kew0ysf3f+GtnBzpU7jiZmZ4ohp4ZHE7z2PMnXXG7IM= -github.com/jfjallid/golog v0.3.2 h1:i3fKI9AUOnIvPmp/1qtLAbjaS2l3SjdjaE4izzpBwvc= -github.com/jfjallid/golog v0.3.2/go.mod h1:19Q/zg5OgPPd0xhFllokPnMzthzhFPZmiAGAokE7k58= +github.com/jfjallid/go-smb v0.2.6 h1:kF0UH7wXjqnqSrISc0vEpLkJFfsz4Bnr/jlXAobrwAM= +github.com/jfjallid/go-smb v0.2.6/go.mod h1:Kew0ysf3f+GtnBzpU7jiZmZ4ohp4ZHE7z2PMnXXG7IM= +github.com/jfjallid/golog v0.3.3 h1:dY6qf8wTxJ9OwBPVTadVRDmt/6MVXSWwXrxaGMMyXsU= +github.com/jfjallid/golog v0.3.3/go.mod h1:19Q/zg5OgPPd0xhFllokPnMzthzhFPZmiAGAokE7k58= golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= diff --git a/main.go b/main.go index d292846..048c779 100644 --- a/main.go +++ b/main.go @@ -39,7 +39,7 @@ import ( ) var log = golog.Get("") -var release string = "0.1.1" +var release string = "0.1.2" func startRemoteRegistry(session *smb.Connection, share string) (err error) { f, err := session.OpenFile(share, "svcctl") diff --git a/sam.go b/sam.go index 2bc441d..aadadf3 100644 --- a/sam.go +++ b/sam.go @@ -103,6 +103,80 @@ func (self *dpapi_system) unmarshal(data []byte) { copy(self.UserKey[:], data[24:44]) } +type sam_key_data_aes struct { + Revision uint32 + Length uint32 + ChecksumLen uint32 + DataLen uint32 + Salt [16]byte + Data [32]byte +} + +type sam_key_data struct { + Revision uint32 + Length uint32 + Salt [16]byte + Key [16]byte + Checksum [16]byte + _ uint64 +} + +type domain_account_f struct { // 104 bytes of fixed length fields + Revision uint16 + _ uint32 // Unknown + _ uint16 // Unknown + CreationTime uint64 + DomainModifiedAccount uint64 + MaxPasswordAge uint64 + MinPasswordAge uint64 + ForceLogoff uint64 + LockoutDuration uint64 + LockoutObservationWindow uint64 + ModifiedCountAtLastPromotion uint64 + NextRid uint32 + PasswordProperties uint32 + MinPasswordLength uint16 + PasswordHistoryLength uint16 + LockoutThreshold uint16 + _ uint16 // Unknown + ServerState uint32 + ServerRole uint32 + UasCompatibilityRequired uint32 + _ uint32 // Unknown + Data []byte +} + +func (self *domain_account_f) unmarshal(data []byte) (err error) { + if len(data) < 104 { + err = fmt.Errorf("Not enough data to unmarshal a DOMAIN_ACCOUNT_F") + log.Errorln(err) + return + } + + self.Revision = binary.LittleEndian.Uint16(data[:2]) + self.CreationTime = binary.LittleEndian.Uint64(data[8:16]) + self.DomainModifiedAccount = binary.LittleEndian.Uint64(data[16:24]) + self.MaxPasswordAge = binary.LittleEndian.Uint64(data[24:32]) + self.MinPasswordAge = binary.LittleEndian.Uint64(data[32:40]) + self.ForceLogoff = binary.LittleEndian.Uint64(data[40:48]) + self.LockoutDuration = binary.LittleEndian.Uint64(data[48:56]) + self.LockoutObservationWindow = binary.LittleEndian.Uint64(data[56:64]) + self.ModifiedCountAtLastPromotion = binary.LittleEndian.Uint64(data[64:72]) + self.NextRid = binary.LittleEndian.Uint32(data[72:76]) + self.PasswordProperties = binary.LittleEndian.Uint32(data[76:80]) + self.MinPasswordLength = binary.LittleEndian.Uint16(data[80:82]) + self.PasswordHistoryLength = binary.LittleEndian.Uint16(data[82:84]) + self.LockoutThreshold = binary.LittleEndian.Uint16(data[84:86]) + self.ServerState = binary.LittleEndian.Uint32(data[88:92]) + self.ServerRole = binary.LittleEndian.Uint32(data[92:96]) + self.UasCompatibilityRequired = binary.LittleEndian.Uint32(data[96:100]) + if len(data) > 104 { + self.Data = make([]byte, len(data[104:])) + copy(self.Data, data[104:]) + } + return +} + type nl_record struct { UserLength uint16 DomainNameLength uint16 @@ -418,35 +492,6 @@ func getBootKey(rpccon *msrrp.RPCCon, base []byte) (result []byte, err error) { return } -func getEncryptedsysKey(rpccon *msrrp.RPCCon, base, f []byte) (encsysKey []byte, err error) { - - val, err := IsWin10After1607(GetOSVersionBuild(rpccon, base)) - if err != nil { - return - } - if val { - log.Debugf("Getting AES encrypted sysKey from F[0x88]\n") - encsysKey = f[0x88 : 0x88+16] - } else { - encsysKey = f[0x80 : 0x80+16] - } - return -} - -func getEncryptedsysKeyIV(rpccon *msrrp.RPCCon, base, f []byte) (sysKeyIV []byte, err error) { - val, err := IsWin10After1607(GetOSVersionBuild(rpccon, base)) - if err != nil { - return - } - if val { - log.Debugf("Getting sysKey IV from F[0x78]\n") - sysKeyIV = f[0x78 : 0x78+16] - } else { - sysKeyIV = f[0x70 : 0x70+16] - } - return -} - func getSysKey(rpccon *msrrp.RPCCon, base []byte) (sysKey []byte, err error) { var tmpSysKey []byte _, err = getBootKey(rpccon, base) @@ -459,7 +504,7 @@ func getSysKey(rpccon *msrrp.RPCCon, base []byte) (sysKey []byte, err error) { return } - f, err := rpccon.QueryValue(hSubKey, "F") + fBytes, err := rpccon.QueryValue(hSubKey, "F") if err != nil { log.Errorln(err) rpccon.CloseKeyHandle(hSubKey) @@ -468,61 +513,97 @@ func getSysKey(rpccon *msrrp.RPCCon, base []byte) (sysKey []byte, err error) { rpccon.CloseKeyHandle(hSubKey) - encsysKey, err := getEncryptedsysKey(rpccon, base, f) - if err != nil { - return - } - sysKeyIV, err := getEncryptedsysKeyIV(rpccon, base, f) + f := &domain_account_f{} + err = f.unmarshal(fBytes) if err != nil { + log.Errorln(err) + rpccon.CloseKeyHandle(hSubKey) return } - if f[0] == 0x3 { - tmpSysKey, err = DecryptAESSysKey(BootKey, encsysKey, sysKeyIV) + var encSysKey []byte + var sysKeyIV []byte + sysKey = make([]byte, 16) + + if f.Revision == 3 { + // AES + samAesData := sam_key_data_aes{} + err = binary.Read(bytes.NewReader(f.Data), binary.LittleEndian, &samAesData) + if err != nil { + log.Errorln(err) + rpccon.CloseKeyHandle(hSubKey) + return + } + sysKeyIV = samAesData.Salt[:] + encSysKey = samAesData.Data[:samAesData.DataLen] + tmpSysKey, err = DecryptAESSysKey(BootKey, encSysKey, sysKeyIV) + copy(sysKey, tmpSysKey) + } else if f.Revision == 2 { + // RC4 + samData := &sam_key_data{} + err = binary.Read(bytes.NewReader(f.Data), binary.LittleEndian, samData) + if err != nil { + log.Errorln(err) + rpccon.CloseKeyHandle(hSubKey) + return + } + + sysKeyIV = samData.Salt[:] + // For RC4, also check the checksum so we should XOR 32 bytes instead of just 16 + encSysKey = append(samData.Key[:], samData.Checksum[:]...) + tmpSysKey, err = DecryptRC4SysKey(BootKey, encSysKey, sysKeyIV) + // Verify checksum + input := []byte{} + input = append(input, tmpSysKey[:16]...) + input = append(input, s2...) + input = append(input, tmpSysKey[:16]...) + input = append(input, s1...) + checksum := md5.Sum(input) + if bytes.Compare(checksum[:], tmpSysKey[16:]) != 0 { + err = fmt.Errorf("Syskey checksum failed. Could be that a Syskey startup password is in use.") + log.Errorln(err) + return + } + copy(sysKey, tmpSysKey[:16]) } else { - tmpSysKey, err = DecryptRC4SysKey(BootKey, encsysKey, sysKeyIV) - } - if err != nil { + err = fmt.Errorf("Unknown revision of DOMAIN_ACCOUNT_F") log.Errorln(err) return } - sysKey = make([]byte, len(tmpSysKey)) - copy(sysKey, tmpSysKey) + log.Noticef("SysKey: 0x%x\n", sysKey) return } -func DecryptRC4SysKey(bootkey, encSyskey, syskeyIV []byte) (syskey []byte, err error) { +func DecryptRC4SysKey(bootKey, encSysKey, sysKeyIV []byte) (sysKey []byte, err error) { // Building the decryption key for the Syskey input := []byte{} - input = append(input, syskeyIV...) + input = append(input, sysKeyIV...) input = append(input, s1...) - input = append(input, bootkey...) + input = append(input, bootKey...) input = append(input, s2...) - // Building syskey RC4 enc key with: one byte from F[0x70], String1, Bootkey, String2 rc4key := md5.Sum(input) - log.Debugf("Syskey RC4 enc key: md5(%x %x %x %x)\n", syskeyIV, s1, bootkey, s2) + log.Debugf("Syskey RC4 enc key: md5(%x %q %x %q)\n", sysKeyIV, s1, bootKey, s2) log.Debugf("Syskey RC4 enc key: %x\n", rc4key) c1, err := rc4.NewCipher(rc4key[:]) if err != nil { log.Errorln("Failed to init RC4 key") return } - syskey = make([]byte, 16) - c1.XORKeyStream(syskey, encSyskey) - log.Debugf("Syskey: %x\n", syskey) + sysKey = make([]byte, 32) + c1.XORKeyStream(sysKey, encSysKey) return } -func DecryptAESSysKey(bootkey, encSyskey, syskeyIV []byte) (syskey []byte, err error) { - syskey = make([]byte, 16) - a1, err := aes.NewCipher(bootkey) +func DecryptAESSysKey(bootKey, encSysKey, sysKeyIV []byte) (sysKey []byte, err error) { + sysKey = make([]byte, len(encSysKey)) + a1, err := aes.NewCipher(bootKey) if err != nil { log.Errorln("Failed to init AES key") return } - c1 := cipher.NewCBCDecrypter(a1, syskeyIV) - c1.CryptBlocks(syskey, encSyskey) + c1 := cipher.NewCBCDecrypter(a1, sysKeyIV) + c1.CryptBlocks(sysKey, encSysKey) return } @@ -532,6 +613,14 @@ func getNTHash(rpccon *msrrp.RPCCon, base []byte, rids []string) (result []UserC // Some entires have empty passwords or hash retrieval fails for some reason. // In those cases I skip to the next entry. I increment the ctr first thing // instead of at the end of the loop to make sure it happens + + // Determine OS version once + osBuild, osVersion, isServer, err := GetOSVersionBuild(rpccon, base) + if err != nil { + log.Errorln(err) + return + } + cntr := -1 for _, ridStr := range rids { cntr += 1 @@ -632,7 +721,7 @@ func getNTHash(rpccon *msrrp.RPCCon, base []byte, rids []string) (result []UserC szNT := binary.LittleEndian.Uint32(v[0xac:]) offsetHashStruct := binary.LittleEndian.Uint32(v[0xa8:]) + 0xcc //log.Debugf("Size of SAM Hash structure for user %s: %d located at offset: 0x%x\n", name, szNT, offsetHashStruct) - preWin11, err2 := IsBetweenWinXPWin10(GetOSVersionBuild(rpccon, base)) + preWin11, err2 := IsBetweenWinXPWin10(osBuild, osVersion, isServer) if err2 != nil { log.Errorln(err2) continue @@ -644,7 +733,7 @@ func getNTHash(rpccon *msrrp.RPCCon, base []byte, rids []string) (result []UserC result[cntr].AES = false result[cntr].Data = v[offsetNTHash : offsetNTHash+16] } else { - afterAnniversary, err2 := IsWin10After1607(GetOSVersionBuild(rpccon, base)) + afterAnniversary, err2 := IsWin10After1607(osBuild, osVersion) if err2 != nil { log.Errorln(err2) continue @@ -669,7 +758,7 @@ func getNTHash(rpccon *msrrp.RPCCon, base []byte, rids []string) (result []UserC result[cntr].AES = false result[cntr].Data = []byte{} } else { - log.Warningf("Unknown Hash type for %x with length 0x%x with OS: %s\n", rid, szNT, GetOsVersionName(GetOSVersionBuild(rpccon, base))) + log.Warningf("Unknown Hash type for %x with length 0x%x with OS: %s\n", rid, szNT, osNameMap[GetOSVersion(osBuild, osVersion, isServer)]) } } } @@ -1046,13 +1135,15 @@ func GetCachedHashes(rpccon *msrrp.RPCCon, base []byte) (result []string, err er return } -func GetOSVersionBuild(rpccon *msrrp.RPCCon, base []byte) (build int, version float64, err error) { +func GetOSVersionBuild(rpccon *msrrp.RPCCon, base []byte) (build int, version float64, server bool, err error) { hSubKey, err := rpccon.OpenSubKey(base, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`) if err != nil { log.Noticef("Failed to open registry key CurrentVersion with error: %v\n", err) return } - defer rpccon.CloseKeyHandle(hSubKey) + defer func(h []byte) { + rpccon.CloseKeyHandle(h) + }(hSubKey) value, err := rpccon.QueryValueString(hSubKey, "CurrentBuild") if err != nil { @@ -1078,5 +1169,24 @@ func GetOSVersionBuild(rpccon *msrrp.RPCCon, base []byte) (build int, version fl return } + hSubKey, err = rpccon.OpenSubKey(base, `SYSTEM\CurrentControlSet\Control\ProductOptions`) + if err != nil { + log.Noticef("Failed to open registry key ProductOptions with error: %v\n", err) + return + } + defer func(h []byte) { + rpccon.CloseKeyHandle(h) + }(hSubKey) + + value, err = rpccon.QueryValueString(hSubKey, "ProductType") + if err != nil { + log.Errorln(err) + return + } + + if strings.Compare(value, "ServerNT") == 0 { + server = true + } + return } diff --git a/utils.go b/utils.go index ffb9b4e..77ea43a 100644 --- a/utils.go +++ b/utils.go @@ -30,79 +30,105 @@ import ( "crypto/sha256" "encoding/binary" "math/bits" + "strconv" ) const ( WIN_UNKNOWN = iota WINXP + WIN_SERVER_2003 + WIN_VISTA + WIN_SERVER_2008 WIN7 + WIN_SERVER_2008_R2 WIN8 + WIN_SERVER_2012 WIN81 + WIN_SERVER_2012_R2 WIN10 + WIN_SERVER_2016 + WIN_SERVER_2019 + WIN_SERVER_2022 + WIN11 ) -func GetOSVersion(build int, version float64) (os byte, err error) { +var osNameMap = map[byte]string{ + WIN_UNKNOWN: "Windows Unknown", + WINXP: "Windows XP", + WIN_VISTA: "Windows Vista", + WIN7: "Windows 7", + WIN8: "Windows 8", + WIN81: "Windows 8.1", + WIN10: "Windows 10", + WIN11: "Windows 11", + WIN_SERVER_2003: "Windows Server 2003", + WIN_SERVER_2008: "Windows Server 2008", + WIN_SERVER_2008_R2: "Windows Server 2008 R2", + WIN_SERVER_2012: "Windows Server 2012", + WIN_SERVER_2012_R2: "Windows Server 2012 R2", + WIN_SERVER_2016: "Windows Server 2016", + WIN_SERVER_2019: "Windows Server 2019", + WIN_SERVER_2022: "Windows Server 2022", +} - if (build >= 7000) && (build < 7999) { - log.Debugf("Windows 7\n") - os = WIN7 - } else if (build >= 9000) && (build < 9999) { - if version < 6.3 { - log.Debugf("Windows 8\n") - os = WIN8 - } else { - //log.Debugf("Windows 8.1\n") - os = WIN81 - } - } else if (build >= 10000) && (build < 18363) { - log.Debugf("Windows 10\n") - os = WIN10 - } else { - if (version < 5.2) && (version > 5.0) { - os = WINXP - log.Debugf("Windows XP?\n") - } else { +func GetOSVersion(currentBuild int, currentVersion float64, server bool) (os byte) { + + currentVersionStr := strconv.FormatFloat(currentVersion, 'f', 1, 64) + if server { + switch { + case currentBuild >= 3790 && currentBuild < 6001: + os = WIN_SERVER_2003 + case currentBuild >= 6001 && currentBuild < 7601: + os = WIN_SERVER_2008 + case currentBuild >= 7601 && currentBuild < 9200: + os = WIN_SERVER_2008_R2 + case currentBuild >= 9200 && currentBuild < 9600: + os = WIN_SERVER_2012 + case currentBuild >= 9200 && currentBuild < 14393: + os = WIN_SERVER_2012_R2 + case currentBuild >= 14393 && currentBuild < 17763: + os = WIN_SERVER_2016 + case currentBuild >= 17763 && currentBuild < 20348: + os = WIN_SERVER_2019 + case currentBuild >= 20348: + os = WIN_SERVER_2022 + default: + log.Debugf("Unknown server version of Windows with CurrentBuild %d and CurrentVersion %f\n", currentBuild, currentVersion) + os = WIN_UNKNOWN + } + } else { + switch currentVersionStr { + case "5.1": + os = WINXP + case "6.0": + // Windows Vista but it shares CurrentVersion and CurrentBuild with Windows Server 2008 + os = WIN_VISTA + case "6.1": + // Windows 7 but it shares CurrentVersion and CurrentBuild with Windows Server 2008 R2 + os = WIN7 + case "6.2": + // Windows 8 but it shares CurrentVersion and CurrentBuild with Windows Server 2012 + os = WIN8 + case "6.3": + // Windows 8.1 but it shares CurrentVersion and CurrentBuild with Windows Server 2012 R2 + os = WIN81 + case "10.0": + if currentBuild < 22000 { + os = WIN10 + } else { + os = WIN11 + } + default: + log.Debugf("Unknown version of Windows with CurrentBuild %d and CurrentVersion %f\n", currentBuild, currentVersion) os = WIN_UNKNOWN - log.Debugf("Unknown OS\n") } } - log.Debugf("OS Version: %d\n", os) + + log.Debugf("OS Version: %s\n", osNameMap[os]) return } -func GetOsVersionName(build int, version float64, inErr error) string { - if inErr != nil { - log.Errorln(inErr) - return "Unknown OS" - } - os, err := GetOSVersion(build, version) - if err != nil { - log.Errorln(err) - return "Unknown OS" - } - result := "Unknown OS" - switch os { - case WIN_UNKNOWN: - break - case WINXP: - result = "Windows XP" - case WIN7: - result = "Windows 7" - case WIN8: - result = "Windows 8" - case WIN81: - result = "Windows 8.1" - case WIN10: - result = "Windows 10" - } - - return result -} - -func IsWin10After1607(build int, version float64, inErr error) (value bool, err error) { - if inErr != nil { - return false, inErr - } +func IsWin10After1607(build int, version float64) (value bool, err error) { if build >= 14393 { value = true } else { @@ -111,14 +137,8 @@ func IsWin10After1607(build int, version float64, inErr error) (value bool, err return } -func IsBetweenWinXPWin10(build int, version float64, inErr error) (value bool, err error) { - if inErr != nil { - return false, inErr - } - os, err := GetOSVersion(build, version) - if err != nil { - return - } +func IsBetweenWinXPWin10(build int, version float64, isServer bool) (value bool, err error) { + os := GetOSVersion(build, version, isServer) if (WINXP <= os) && (os <= WIN10) { value = true } else {