├── README.md ├── go.mod ├── go.sum ├── main.go └── winrmntlm ├── encryption.go └── ntlmssp ├── authenticate_message.go ├── avpair.go ├── bindings.go ├── challenge_message.go ├── client.go ├── crypto.go ├── domain_posix.go ├── domain_windows.go ├── flags.go ├── http ├── client.go ├── crypto.go ├── mime.go └── util.go ├── message_header.go ├── negotiate_message.go ├── ntlm.go ├── payload.go ├── security.go ├── unicode.go ├── util.go ├── version.go ├── version_posix.go ├── version_windows.go └── workstation.go /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # winrm-PTH 4 | Golang implement WinRM client with PTH (pass the hash) 5 | 6 | #### Table of Contents 7 |
    8 |
  1. 9 | Info 10 |
  2. 11 |
  3. How it works?
  4. 12 |
  5. Screenshots
  6. 13 |
  7. References
  8. 14 |
  9. References
  10. 15 |
16 | 17 | ## Info 18 | 19 | This is an example of Golang implement WinRM client with PTH. 20 | 21 |

(back to top)

22 | 23 | ## How it works? 24 | 25 | In [ntlmssp](https://github.com/bodgit/ntlmssp), it always encodes the password to md4 hash in function `ntowfV1`, then we can make a simple logic detection to check if the user passes a hash into it, with that "patch", we can give hash into `ntlmssp` to make the auth structures. 26 | 27 | The [winrm](https://github.com/masterzen/winrm) always use `DefaultClientCompatibilityLevel`, so we don't need to do lots of changes in `ntlmssp`. 28 | 29 | Why do we need `encryption.go`? 30 | 31 | - Because in Windows, winrm is always set `AllowUnencrypted` to false, and the library [winrm](https://github.com/masterzen/winrm) only support `AllowUnencrypted`, which means we can't auth into `winrm` without `winrm set winrm/config/service @{AllowUnencrypted="true"}`, this is not make sense. 32 | - With `encryption.go`, we can auth into `winrm` even without `winrm set winrm/config/service @{AllowUnencrypted="true"}` 33 | 34 |

(back to top)

35 | 36 | ## Screenshots 37 | - Without `AllowUnencrypted` in PTH 38 | ![image](https://github.com/XiaoliChan/winrm-PTH/assets/30458572/12f94519-6451-471d-9880-80eb4cd4fc28) 39 | 40 |

(back to top)

41 | 42 | ## Known bugs 43 | - Hash + AllowUnencrypted="true" == Not working 44 | 45 | ## References 46 | - https://github.com/CalypsoSys/winrmntlm 47 | - https://github.com/masterzen/winrm 48 | - https://github.com/CalypsoSys/bobwinrm 49 | - https://github.com/bodgit/ntlmssp 50 | 51 |

(back to top)

52 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/bodgit/windows v1.0.1 7 | github.com/go-logr/logr v1.3.0 8 | github.com/hashicorp/go-cleanhttp v0.5.2 9 | github.com/masterzen/winrm v0.0.0-20231227165926-e811dad5ac77 10 | github.com/tidwall/transform v0.0.0-20201103190739-32f242e2dbde 11 | golang.org/x/crypto v0.17.0 12 | golang.org/x/text v0.14.0 13 | ) 14 | 15 | require ( 16 | github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect 17 | github.com/ChrisTrenkamp/goxpath v0.0.0-20210404020558-97928f7e12b6 // indirect 18 | github.com/bodgit/ntlmssp v0.0.0-20231122144230-2b2bca29f22b // indirect 19 | github.com/gofrs/uuid v4.4.0+incompatible // indirect 20 | github.com/hashicorp/go-uuid v1.0.3 // indirect 21 | github.com/jcmturner/aescts/v2 v2.0.0 // indirect 22 | github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect 23 | github.com/jcmturner/gofork v1.7.6 // indirect 24 | github.com/jcmturner/goidentity/v6 v6.0.1 // indirect 25 | github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect 26 | github.com/jcmturner/rpc/v2 v2.0.3 // indirect 27 | github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786 // indirect 28 | golang.org/x/net v0.18.0 // indirect 29 | ) 30 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= 2 | github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= 3 | github.com/ChrisTrenkamp/goxpath v0.0.0-20210404020558-97928f7e12b6 h1:w0E0fgc1YafGEh5cROhlROMWXiNoZqApk2PDN0M1+Ns= 4 | github.com/ChrisTrenkamp/goxpath v0.0.0-20210404020558-97928f7e12b6/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4= 5 | github.com/bodgit/ntlmssp v0.0.0-20231122144230-2b2bca29f22b h1:/rxp4dz0wtk+sumWvQpaUft0yM+YqPegRgKxqU3MXBE= 6 | github.com/bodgit/ntlmssp v0.0.0-20231122144230-2b2bca29f22b/go.mod h1:t46HDKvw4bCyAsVTayprrRiC9UItu18q9Zo29vTGN10= 7 | github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= 8 | github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= 13 | github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 14 | github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= 15 | github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 16 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 17 | github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= 18 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 19 | github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= 20 | github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= 21 | github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 22 | github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 23 | github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 24 | github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= 25 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 26 | github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= 27 | github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= 28 | github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= 29 | github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= 30 | github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= 31 | github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= 32 | github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= 33 | github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= 34 | github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= 35 | github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= 36 | github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= 37 | github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= 38 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 39 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 40 | github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786 h1:2ZKn+w/BJeL43sCxI2jhPLRv73oVVOjEKZjKkflyqxg= 41 | github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786/go.mod h1:kCEbxUJlNDEBNbdQMkPSp6yaKcRXVI6f4ddk8Riv4bc= 42 | github.com/masterzen/winrm v0.0.0-20231227165926-e811dad5ac77 h1:psY7rHKhnfqjTEgkleIYpF1vVxVfYsUYFTO/cL5Z6xM= 43 | github.com/masterzen/winrm v0.0.0-20231227165926-e811dad5ac77/go.mod h1:otHfftEJdo9JWGoq9GcJRaeNLp/uhqNq8JOk5lL+8Ks= 44 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 45 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 46 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 47 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 48 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 49 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 50 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 51 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 52 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 53 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 54 | github.com/tidwall/transform v0.0.0-20201103190739-32f242e2dbde h1:AMNpJRc7P+GTwVbl8DkK2I9I8BBUzNiHuH/tlxrpan0= 55 | github.com/tidwall/transform v0.0.0-20201103190739-32f242e2dbde/go.mod h1:MvrEmduDUz4ST5pGZ7CABCnOU5f3ZiOAZzT6b1A6nX8= 56 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 57 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 58 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 59 | golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= 60 | golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= 61 | golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= 62 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 63 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 64 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 65 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 66 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 67 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 68 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 69 | golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= 70 | golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= 71 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 72 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 73 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 74 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 75 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 76 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 77 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 78 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 79 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 80 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 81 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 82 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 83 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 84 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 85 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 86 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 87 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 88 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 89 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 90 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 91 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 92 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 93 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 94 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 95 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 96 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 97 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 98 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "main/winrmntlm" 9 | 10 | "github.com/masterzen/winrm" 11 | ) 12 | 13 | func main() { 14 | runExec_winrmntlm("192.168.183.253", 5985, false, "administrator", "e91d2eafde47de62c6c49a012b3a6af1") // works // unsupported action 15 | } 16 | 17 | func runExec_winrmntlm(address string, port int, https bool, userName string, password string) { 18 | endpoint := winrm.NewEndpoint(address, port, https, true, nil, nil, nil, 0) 19 | 20 | params := winrm.DefaultParameters 21 | enc, _ := winrmntlm.NewEncryption("ntlm", userName, password, endpoint, true) // true is means if password is hash, else false 22 | params.TransportDecorator = func() winrm.Transporter { return enc } 23 | 24 | client, err := winrm.NewClientWithParameters(endpoint, userName, password, params) 25 | if err != nil { 26 | fmt.Println(err) 27 | } 28 | 29 | exitCode, err := client.RunWithContext(context.Background(), "ipconfig /all", os.Stdout, os.Stderr) 30 | fmt.Printf("%d\n%v\nn", exitCode, err) 31 | if err != nil { 32 | _ = exitCode 33 | fmt.Println(err) 34 | } else { 35 | fmt.Println("Command Test Ok") 36 | } 37 | 38 | wmiQuery := `select * from Win32_ComputerSystem` 39 | psCommand := fmt.Sprintf(`$FormatEnumerationLimit=-1; Get-WmiObject -Query "%s" | Out-String -Width 4096`, wmiQuery) 40 | stdOut, stdErr, exitCode, _ := client.RunPSWithContext(context.Background(), psCommand) 41 | 42 | fmt.Println(stdOut) 43 | fmt.Println(stdErr) 44 | fmt.Println(exitCode) 45 | } 46 | -------------------------------------------------------------------------------- /winrmntlm/encryption.go: -------------------------------------------------------------------------------- 1 | package winrmntlm 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | "io" 9 | 10 | "net/http" 11 | "net/url" 12 | "strconv" 13 | "strings" 14 | 15 | "main/winrmntlm/ntlmssp" 16 | 17 | ntlmhttp "main/winrmntlm/ntlmssp/http" 18 | 19 | "github.com/masterzen/winrm" 20 | "github.com/masterzen/winrm/soap" 21 | ) 22 | 23 | type Encryption struct { 24 | username string 25 | password string 26 | pass_is_hash bool 27 | endpoint *winrm.Endpoint 28 | ntlm *winrm.ClientNTLM 29 | protocol string 30 | protocolString []byte 31 | httpClient *http.Client 32 | ntlmClient *ntlmssp.Client 33 | ntlmhttp *ntlmhttp.Client 34 | } 35 | 36 | const ( 37 | sixTenKB = 16384 38 | mimeBoundary = "--Encrypted Boundary" 39 | defaultCipher = "RC4-HMAC-NTLM" 40 | boundaryLength = len(mimeBoundary) 41 | ) 42 | 43 | /* 44 | Encrypted Message Types 45 | When using Encryption, there are three options available 46 | 47 | 1. Negotiate/SPNEGO 48 | 49 | 2. Kerberos 50 | 51 | 3. CredSSP 52 | 53 | protocol: The protocol string used for the particular auth protocol 54 | 55 | The auth protocol used, will determine the wrapping and unwrapping method plus 56 | the protocol string to use. Currently only NTLM is supported 57 | 58 | based on the python code from https://pypi.org/project/pywinrm/ 59 | 60 | see https://github.com/diyan/pywinrm/blob/master/winrm/encryption.py 61 | 62 | uses the most excellent NTLM library from https://github.com/bodgit/ntlmssp 63 | */ 64 | func NewEncryption(protocol string, username string, password string, endpoint *winrm.Endpoint, pass_is_hash bool) (*Encryption, error) { 65 | encryption := &Encryption{ 66 | username: username, 67 | password: password, 68 | pass_is_hash: pass_is_hash, 69 | endpoint: endpoint, 70 | ntlm: &winrm.ClientNTLM{}, 71 | protocol: protocol, 72 | } 73 | 74 | switch protocol { 75 | case "ntlm": 76 | encryption.protocolString = []byte("application/HTTP-SPNEGO-session-encrypted") 77 | return encryption, nil 78 | /* credssp and kerberos is currently unimplemented, leave holder for future to keep in sync with python implementation 79 | case "credssp": 80 | encryption.protocolString = []byte("application/HTTP-CredSSP-session-encrypted") 81 | case "kerberos": // kerberos is currently unimplemented, leave holder for future to keep in sync with python implementation 82 | encryption.protocolString = []byte("application/HTTP-SPNEGO-session-encrypted") 83 | */ 84 | } 85 | 86 | return nil, fmt.Errorf("Encryption for protocol '%s' not supported in winrm", protocol) 87 | } 88 | 89 | func (e *Encryption) Transport(endpoint *winrm.Endpoint) error { 90 | e.httpClient = &http.Client{} 91 | return e.ntlm.Transport(endpoint) 92 | } 93 | 94 | func (e *Encryption) Post(client *winrm.Client, message *soap.SoapMessage) (string, error) { 95 | var userName, domain string 96 | if strings.Contains(e.username, "@") { 97 | parts := strings.Split(e.username, "@") 98 | domain = parts[1] 99 | userName = parts[0] 100 | } else if strings.Contains(e.username, "\\") { 101 | parts := strings.Split(e.username, "\\") 102 | domain = parts[0] 103 | userName = parts[1] 104 | } else { 105 | userName = e.username 106 | } 107 | 108 | e.ntlmClient, _ = ntlmssp.NewClient(ntlmssp.SetUserInfo(userName, e.password, e.pass_is_hash), ntlmssp.SetDomain(domain), ntlmssp.SetVersion(ntlmssp.DefaultVersion())) 109 | e.ntlmhttp, _ = ntlmhttp.NewClient(e.httpClient, e.ntlmClient) 110 | 111 | var err error 112 | if err = e.PrepareRequest(client, e.url()); err == nil { 113 | return e.PrepareEncryptedRequest(client, e.url(), []byte(message.String())) 114 | } else { 115 | return e.ntlm.Post(client, message) 116 | } 117 | } 118 | 119 | func (e *Encryption) url() string { 120 | var scheme string 121 | if e.endpoint.HTTPS { 122 | scheme = "https" 123 | } else { 124 | scheme = "http" 125 | } 126 | 127 | return fmt.Sprintf("%s://%s:%d/wsman", scheme, e.endpoint.Host, e.endpoint.Port) 128 | } 129 | 130 | func (e *Encryption) PrepareRequest(client *winrm.Client, endpoint string) error { 131 | req, err := http.NewRequest("POST", endpoint, nil) 132 | if err != nil { 133 | return err 134 | } 135 | 136 | req.Header.Set("User-Agent", "WinRM client") 137 | req.Header.Set("Content-Length", "0") 138 | req.Header.Set("Content-Type", "application/soap+xml;charset=UTF-8") 139 | req.Header.Set("Connection", "Keep-Alive") 140 | 141 | resp, err := e.ntlmhttp.Do(req) 142 | if err != nil { 143 | return fmt.Errorf("unknown error %w", err) 144 | } 145 | 146 | if resp.StatusCode != 200 { 147 | return fmt.Errorf("http error %d", resp.StatusCode) 148 | } 149 | 150 | return nil 151 | } 152 | 153 | /* 154 | Creates a prepared request to send to the server with an encrypted message 155 | and correct headers 156 | 157 | :param endpoint: The endpoint/server to prepare requests to 158 | :param message: The unencrypted message to send to the server 159 | :return: A prepared request that has an decrypted message 160 | */ 161 | func (e *Encryption) PrepareEncryptedRequest(client *winrm.Client, endpoint string, message []byte) (string, error) { 162 | url, err := url.Parse(endpoint) 163 | if err != nil { 164 | return "", err 165 | } 166 | host := strings.Split(url.Hostname(), ":")[0] 167 | 168 | var content_type string 169 | var encrypted_message []byte 170 | 171 | if e.protocol == "credssp" && len(message) > sixTenKB { 172 | content_type = "multipart/x-multi-encrypted" 173 | encrypted_message = []byte{} 174 | message_chunks := [][]byte{} 175 | for i := 0; i < len(message); i += sixTenKB { 176 | message_chunks = append(message_chunks, message[i:i+sixTenKB]) 177 | } 178 | for _, message_chunk := range message_chunks { 179 | encrypted_chunk := e.encryptMessage(message_chunk, host) 180 | encrypted_message = append(encrypted_message, encrypted_chunk...) 181 | } 182 | } else { 183 | content_type = "multipart/encrypted" 184 | encrypted_message = e.encryptMessage(message, host) 185 | } 186 | 187 | encrypted_message = append(encrypted_message, []byte(mimeBoundary)...) 188 | encrypted_message = append(encrypted_message, []byte("--\r\n")...) 189 | 190 | req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(encrypted_message)) 191 | if err != nil { 192 | return "", err 193 | } 194 | 195 | req.Header.Set("User-Agent", "WinRM client") 196 | req.Header.Set("Connection", "Keep-Alive") 197 | req.Header.Set("Content-Length", fmt.Sprintf("%d", len(encrypted_message))) 198 | req.Header.Set("Content-Type", fmt.Sprintf(`%s;protocol="%s";boundary="Encrypted Boundary"`, content_type, e.protocolString)) 199 | 200 | resp, err := e.ntlmhttp.Do(req) 201 | if err != nil { 202 | return "", fmt.Errorf("unknown error %w", err) 203 | } 204 | 205 | body, err := e.ParseEncryptedResponse(resp) 206 | 207 | return string(body), err 208 | } 209 | 210 | /* 211 | Takes in the encrypted response from the server and decrypts it 212 | 213 | :param response: The response that needs to be decrytped 214 | :return: The unencrypted message from the server 215 | */ 216 | func (e *Encryption) ParseEncryptedResponse(response *http.Response) ([]byte, error) { 217 | contentType := response.Header.Get("Content-Type") 218 | if strings.Contains(contentType, fmt.Sprintf(`protocol="%s"`, e.protocolString)) { 219 | return e.decryptResponse(response, response.Request.URL.Hostname()) 220 | } 221 | body, err := io.ReadAll(response.Body) 222 | response.Body.Close() 223 | if err != nil { 224 | return nil, err 225 | } 226 | return body, nil 227 | } 228 | 229 | func (e *Encryption) encryptMessage(message []byte, host string) []byte { 230 | encryptedStream, _ := e.buildMessage(message, host) 231 | 232 | messagePayload := bytes.Join([][]byte{ 233 | []byte(mimeBoundary), 234 | []byte("\r\n"), 235 | []byte(fmt.Sprintf("\tContent-Type: %s\r\n", string(e.protocolString))), 236 | []byte(fmt.Sprintf("\tOriginalContent: type=application/soap+xml;charset=UTF-8;Length=%d\r\n", len(message))), 237 | []byte(mimeBoundary), 238 | []byte("\r\n"), 239 | []byte("\tContent-Type: application/octet-stream\r\n"), 240 | encryptedStream, 241 | }, []byte{}) 242 | 243 | return messagePayload 244 | } 245 | 246 | func deleteEmpty(b [][]byte) [][]byte { 247 | var r [][]byte 248 | for _, by := range b { 249 | if len(by) != 0 { 250 | r = append(r, by) 251 | } 252 | } 253 | return r 254 | } 255 | 256 | // tried using pkg.go.dev/mime/multipart here but parsing fails with with 257 | // because in the header we have "\tContent-Type: application/HTTP-SPNEGO-session-encrypted\r\n" 258 | // on call to textproto.ReadMIMEHeader 259 | // because of "The first line cannot start with a leading space." 260 | func (e *Encryption) decryptResponse(response *http.Response, host string) ([]byte, error) { 261 | body, _ := io.ReadAll(response.Body) 262 | parts := deleteEmpty(bytes.Split(body, []byte(fmt.Sprintf("%s\r\n", mimeBoundary)))) 263 | var message []byte 264 | 265 | for i := 0; i < len(parts); i += 2 { 266 | header := parts[i] 267 | payload := parts[i+1] 268 | 269 | expectedLengthStr := bytes.SplitAfter(header, []byte("Length="))[1] 270 | expectedLength, err := strconv.Atoi(string(bytes.TrimSpace(expectedLengthStr))) 271 | if err != nil { 272 | return nil, err 273 | } 274 | 275 | // remove the end MIME block if it exists 276 | if bytes.HasSuffix(payload, []byte(fmt.Sprintf("%s--\r\n", mimeBoundary))) { 277 | payload = payload[:len(payload)-boundaryLength-4] 278 | } 279 | encryptedData := bytes.ReplaceAll(payload, []byte("\tContent-Type: application/octet-stream\r\n"), []byte{}) 280 | decryptedMessage, err := e.decryptMessage(encryptedData, host) 281 | if err != nil { 282 | return nil, err 283 | } 284 | 285 | actualLength := int(len(decryptedMessage)) 286 | if actualLength != expectedLength { 287 | return nil, errors.New("encrypted length from server does not match the expected size, message has been tampered with") 288 | } 289 | 290 | message = append(message, decryptedMessage...) 291 | } 292 | 293 | return message, nil 294 | } 295 | 296 | func (e *Encryption) decryptMessage(encryptedData []byte, host string) ([]byte, error) { 297 | switch e.protocol { 298 | case "ntlm": 299 | return e.decryptNtlmMessage(encryptedData, host) 300 | /* credssp and kerberos is currently unimplemented, leave holder for future to keep in sync with python implementation 301 | case "credssp": 302 | return e.decryptCredsspMessage(encryptedData, host) 303 | case "kerberos": 304 | return e.decryptKerberosMessage(encryptedData, host) 305 | */ 306 | default: 307 | return nil, errors.New("Encryption for protocol " + e.protocol + " not supported in pywinrm") 308 | } 309 | } 310 | 311 | func (e *Encryption) decryptNtlmMessage(encryptedData []byte, host string) ([]byte, error) { 312 | signatureLength := int(binary.LittleEndian.Uint32(encryptedData[:4])) 313 | signature := encryptedData[4 : signatureLength+4] 314 | encryptedMessage := encryptedData[signatureLength+4:] 315 | 316 | message, err := e.ntlmClient.SecuritySession().Unwrap(encryptedMessage, signature) 317 | if err != nil { 318 | return nil, err 319 | } 320 | return message, nil 321 | } 322 | 323 | /* credssp and kerberos is currently unimplemented, leave holder for future to keep in sync with python implementation 324 | func (e *Encryption) decryptCredsspMessage(encryptedData []byte, host string) ([]byte, error) { 325 | // // TODO 326 | // encryptedMessage := encryptedData[4:] 327 | 328 | // credsspContext, ok := e.session.Auth.Contexts()[host] 329 | // if !ok { 330 | // return nil, fmt.Errorf("credssp context not found for host: %s", host) 331 | // } 332 | 333 | // message, err := credsspContext.Unwrap(encryptedMessage) 334 | // if err != nil { 335 | // return nil, err 336 | // } 337 | // return message, nil 338 | } 339 | 340 | func (enc *Encryption) decryptKerberosMessage(encryptedData []byte, host string) ([]byte, error) { 341 | // //TODO 342 | // signatureLength := binary.LittleEndian.Uint32(encryptedData[0:4]) 343 | // signature := encryptedData[4 : 4+signatureLength] 344 | // encryptedMessage := encryptedData[4+signatureLength:] 345 | 346 | // message, err := enc.session.Auth.UnwrapWinrm(host, encryptedMessage, signature) 347 | // if err != nil { 348 | // return nil, err 349 | // } 350 | 351 | // return message, nil 352 | } 353 | */ 354 | 355 | func (e *Encryption) buildMessage(encryptedData []byte, host string) ([]byte, error) { 356 | switch e.protocol { 357 | case "ntlm": 358 | return e.buildNTLMMessage(encryptedData, host) 359 | /* credssp and kerberos is currently unimplemented, leave holder for future to keep in sync with python implementation 360 | case "credssp": 361 | return e.buildCredSSPMessage(encryptedData, host) 362 | case "kerberos": 363 | return e.buildKerberosMessage(encryptedData, host) 364 | */ 365 | default: 366 | return nil, errors.New("Encryption for protocol " + e.protocol + " not supported in pywinrm") 367 | } 368 | } 369 | 370 | func (enc *Encryption) buildNTLMMessage(message []byte, host string) ([]byte, error) { 371 | if enc.ntlmClient.SecuritySession() == nil { 372 | return nil, nil 373 | } 374 | sealedMessage, signature, err := enc.ntlmClient.SecuritySession().Wrap(message) 375 | if err != nil { 376 | return nil, err 377 | } 378 | 379 | buf := new(bytes.Buffer) 380 | if err = binary.Write(buf, binary.LittleEndian, uint32(len(signature))); err != nil { 381 | return nil, err 382 | } 383 | 384 | buf.Write(signature) 385 | buf.Write(sealedMessage) 386 | 387 | return buf.Bytes(), nil 388 | } 389 | 390 | /* credssp and kerberos is currently unimplemented, leave holder for future to keep in sync with python implementation 391 | func (e *Encryption) buildCredSSPMessage(message []byte, host string) ([]byte, error) { 392 | // //TODO 393 | // context := e.session.Auth.Contexts[host] 394 | // sealedMessage := context.Wrap(message) 395 | 396 | // cipherNegotiated := context.TLSConnection.ConnectionState().CipherSuite.Name 397 | // trailerLength := e.getCredSSPTrailerLength(len(message), cipherNegotiated) 398 | 399 | // trailer := make([]byte, 4) 400 | // binary.LittleEndian.PutUint32(trailer, uint32(trailerLength)) 401 | 402 | // return append(trailer, sealedMessage...), nil 403 | } 404 | 405 | func (e *Encryption) buildKerberosMessage(message []byte, host string) ([]byte, error) { 406 | // //TODO 407 | // sealedMessage, signature := e.session.Auth.WrapWinrm(host, message) 408 | 409 | // signatureLength := make([]byte, 4) 410 | // binary.LittleEndian.PutUint32(signatureLength, uint32(len(signature))) 411 | 412 | // return append(append(signatureLength, signature...), sealedMessage...), nil 413 | } 414 | 415 | func (e *Encryption) getCredSSPTrailerLength(messageLength int, cipherSuite string) int { 416 | var trailerLength int 417 | 418 | if match, _ := regexp.MatchString("^.*-GCM-[\\w\\d]*$", cipherSuite); match { 419 | trailerLength = 16 420 | } else { 421 | hashAlgorithm := cipherSuite[strings.LastIndex(cipherSuite, "-")+1:] 422 | var hashLength int 423 | 424 | if hashAlgorithm == "MD5" { 425 | hashLength = 16 426 | } else if hashAlgorithm == "SHA" { 427 | hashLength = 20 428 | } else if hashAlgorithm == "SHA256" { 429 | hashLength = 32 430 | } else if hashAlgorithm == "SHA384" { 431 | hashLength = 48 432 | } else { 433 | hashLength = 0 434 | } 435 | 436 | prePadLength := messageLength + hashLength 437 | paddingLength := 0 438 | 439 | if strings.Contains(cipherSuite, "RC4") { 440 | paddingLength = 0 441 | } else if strings.Contains(cipherSuite, "DES") || strings.Contains(cipherSuite, "3DES") { 442 | paddingLength = 8 - (prePadLength % 8) 443 | 444 | } else { 445 | // AES is a 128 bit block cipher 446 | paddingLength = 16 - (prePadLength % 16) 447 | } 448 | 449 | trailerLength = (prePadLength + paddingLength) - messageLength 450 | } 451 | return trailerLength 452 | } 453 | */ 454 | -------------------------------------------------------------------------------- /winrmntlm/ntlmssp/authenticate_message.go: -------------------------------------------------------------------------------- 1 | package ntlmssp 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "sort" 10 | "unsafe" 11 | ) 12 | 13 | var ( 14 | errInvalidAuthenticateMessage = errors.New("invalid NTLM authenticate message") 15 | ) 16 | 17 | type authenticateMessageFields struct { 18 | messageHeader 19 | LmChallengeResponseFields payload 20 | NtChallengeResponseFields payload 21 | DomainNameFields payload 22 | UserNameFields payload 23 | WorkstationFields payload 24 | EncryptedRandomSessionKeyFields payload 25 | NegotiateFlags uint32 26 | } 27 | 28 | func (m *authenticateMessageFields) IsValid() bool { 29 | return m.messageHeader.IsValid() && m.MessageType == ntLmAuthenticate 30 | } 31 | 32 | type authenticateMessage struct { 33 | authenticateMessageFields 34 | LmChallengeResponse []uint8 35 | NtChallengeResponse []uint8 36 | DomainName string 37 | UserName string 38 | Workstation string 39 | EncryptedRandomSessionKey []uint8 40 | Version *Version 41 | MIC []uint8 42 | 43 | // These aren't transmitted, but necessary in order to compute the MIC 44 | ExportedSessionKey []uint8 45 | TargetInfo targetInfo 46 | } 47 | 48 | func (m *authenticateMessage) UpdateMIC(preamble []byte) error { 49 | if v, ok := m.TargetInfo.Get(msvAvFlags); ok { 50 | flags := binary.LittleEndian.Uint32(v) 51 | if flags&msvAvFlagMICProvided != 0 { 52 | // Add an all-zero MIC first 53 | m.MIC = zeroBytes(16) 54 | 55 | b, err := m.Marshal() 56 | if err != nil { 57 | return err 58 | } 59 | 60 | // Now compute the actual MIC 61 | m.MIC = hmacMD5(m.ExportedSessionKey, concat(preamble, b)) 62 | } 63 | } 64 | 65 | return nil 66 | } 67 | 68 | func (m *authenticateMessage) Marshal() ([]byte, error) { 69 | offset := int(unsafe.Sizeof(m.authenticateMessageFields) + unsafe.Sizeof(*m.Version)) 70 | if m.MIC != nil { 71 | offset += len(m.MIC) 72 | } 73 | 74 | var domainName, userName, workstation []byte 75 | if ntlmsspNegotiateUnicode.IsSet(m.NegotiateFlags) { 76 | var err error 77 | domainName, err = utf16FromString(m.DomainName) 78 | if err != nil { 79 | return nil, err 80 | } 81 | userName, err = utf16FromString(m.UserName) 82 | if err != nil { 83 | return nil, err 84 | } 85 | workstation, err = utf16FromString(m.Workstation) 86 | if err != nil { 87 | return nil, err 88 | } 89 | } else { 90 | domainName = []byte(m.DomainName) 91 | userName = []byte(m.UserName) 92 | workstation = []byte(m.Workstation) 93 | } 94 | 95 | // Microsoft MS-NLMP spec doesn't write out the payloads in the same 96 | // order as they are defined in the struct. Copy the behaviour just so 97 | // the various test values can be used 98 | 99 | m.DomainNameFields = newPayload(len(domainName), &offset) 100 | m.UserNameFields = newPayload(len(userName), &offset) 101 | m.WorkstationFields = newPayload(len(workstation), &offset) 102 | 103 | m.LmChallengeResponseFields = newPayload(len(m.LmChallengeResponse), &offset) 104 | m.NtChallengeResponseFields = newPayload(len(m.NtChallengeResponse), &offset) 105 | 106 | m.EncryptedRandomSessionKeyFields = newPayload(len(m.EncryptedRandomSessionKey), &offset) 107 | 108 | var version = &Version{} 109 | 110 | m.NegotiateFlags = ntlmsspNegotiateVersion.Unset(m.NegotiateFlags) 111 | if m.Version != nil { 112 | m.NegotiateFlags = ntlmsspNegotiateVersion.Set(m.NegotiateFlags) 113 | version = m.Version 114 | } 115 | 116 | b := bytes.Buffer{} 117 | if err := binary.Write(&b, binary.LittleEndian, &m.authenticateMessageFields); err != nil { 118 | return nil, err 119 | } 120 | 121 | v, err := version.marshal() 122 | if err != nil { 123 | return nil, err 124 | } 125 | b.Write(v) 126 | 127 | if m.MIC != nil { 128 | b.Write(m.MIC) 129 | } 130 | 131 | b.Write(domainName) 132 | b.Write(userName) 133 | b.Write(workstation) 134 | 135 | b.Write(m.LmChallengeResponse) 136 | b.Write(m.NtChallengeResponse) 137 | 138 | b.Write(m.EncryptedRandomSessionKey) 139 | 140 | return b.Bytes(), nil 141 | } 142 | 143 | func (m *authenticateMessage) Unmarshal(b []byte) error { 144 | reader := bytes.NewReader(b) 145 | 146 | err := binary.Read(reader, binary.LittleEndian, &m.authenticateMessageFields) 147 | if err != nil { 148 | return err 149 | } 150 | 151 | if !m.authenticateMessageFields.IsValid() { 152 | return errInvalidAuthenticateMessage 153 | } 154 | 155 | var offsets []int 156 | 157 | for _, x := range []struct { 158 | payload *payload 159 | bytes *[]uint8 160 | }{ 161 | { 162 | &m.LmChallengeResponseFields, 163 | &m.LmChallengeResponse, 164 | }, 165 | { 166 | &m.NtChallengeResponseFields, 167 | &m.NtChallengeResponse, 168 | }, 169 | { 170 | &m.EncryptedRandomSessionKeyFields, 171 | &m.EncryptedRandomSessionKey, 172 | }, 173 | } { 174 | offsets = append(offsets, int(x.payload.Offset)) 175 | 176 | if x.payload.Len > 0 { 177 | *x.bytes, err = x.payload.ReadValue(reader) 178 | if err != nil { 179 | return err 180 | } 181 | } 182 | } 183 | 184 | for _, x := range []struct { 185 | payload *payload 186 | string *string 187 | }{ 188 | { 189 | &m.DomainNameFields, 190 | &m.DomainName, 191 | }, 192 | { 193 | &m.UserNameFields, 194 | &m.UserName, 195 | }, 196 | { 197 | &m.WorkstationFields, 198 | &m.Workstation, 199 | }, 200 | } { 201 | offsets = append(offsets, int(x.payload.Offset)) 202 | 203 | if x.payload.Len > 0 { 204 | *x.string, err = x.payload.ReadString(reader, ntlmsspNegotiateUnicode.IsSet(m.NegotiateFlags)) 205 | if err != nil { 206 | return err 207 | } 208 | } 209 | } 210 | 211 | if ntlmsspNegotiateVersion.IsSet(m.NegotiateFlags) { 212 | v := Version{} 213 | if err := v.unmarshal(reader); err != nil { 214 | return err 215 | } 216 | m.Version = &v 217 | } else { 218 | // Seek past the version 219 | if _, err := reader.Seek(int64(unsafe.Sizeof(*m.Version)), io.SeekCurrent); err != nil { 220 | return err 221 | } 222 | } 223 | 224 | pos, err := reader.Seek(0, io.SeekCurrent) 225 | if err != nil { 226 | return err 227 | } 228 | 229 | // Sort the offsets into ascending order 230 | sort.Ints(offsets) 231 | 232 | // If there's a gap between the end of the version where we are and 233 | // the first payload offset then treat the gap as a MIC 234 | if length := offsets[0] - int(pos); length > 0 { 235 | m.MIC = make([]byte, length) 236 | n, err := reader.Read(m.MIC) 237 | if err != nil { 238 | return err 239 | } 240 | if n != length { 241 | return fmt.Errorf("expected %d bytes, only read %d", length, n) 242 | } 243 | } 244 | 245 | return nil 246 | } 247 | -------------------------------------------------------------------------------- /winrmntlm/ntlmssp/avpair.go: -------------------------------------------------------------------------------- 1 | package ntlmssp 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/gob" 7 | "fmt" 8 | ) 9 | 10 | type avID uint16 11 | 12 | const ( 13 | msvAvEOL avID = iota 14 | msvAvNbComputerName 15 | msvAvNbDomainName 16 | msvAvDNSComputerName 17 | msvAvDNSDomainName 18 | msvAvDNSTreeName 19 | msvAvFlags 20 | msvAvTimestamp 21 | msvAvSingleHost 22 | msvAvTargetName 23 | msvChannelBindings 24 | ) 25 | 26 | const ( 27 | msvAvFlagAuthenticationConstrained uint32 = 1 << iota 28 | msvAvFlagMICProvided 29 | msvAvFlagUntrustedSPNSource 30 | ) 31 | 32 | type avPair struct { 33 | ID avID 34 | Length uint16 35 | } 36 | 37 | type targetInfo struct { 38 | Pairs map[avID][]uint8 39 | Order []avID 40 | } 41 | 42 | func newTargetInfo() targetInfo { 43 | return targetInfo{ 44 | Pairs: make(map[avID][]uint8), 45 | Order: []avID{}, 46 | } 47 | } 48 | 49 | func (t *targetInfo) Get(id avID) ([]uint8, bool) { 50 | v, ok := t.Pairs[id] 51 | return v, ok 52 | } 53 | 54 | func (t *targetInfo) Set(id avID, value []uint8) { 55 | if id == msvAvEOL { 56 | return 57 | } 58 | if _, ok := t.Get(id); !ok { 59 | t.Order = append(t.Order, id) 60 | } 61 | t.Pairs[id] = value 62 | } 63 | 64 | func (t *targetInfo) Del(id avID) { 65 | delete(t.Pairs, id) 66 | j := 0 67 | for _, n := range t.Order { 68 | if n != id { 69 | t.Order[j] = n 70 | j++ 71 | } 72 | } 73 | t.Order = t.Order[:j] 74 | } 75 | 76 | func (t *targetInfo) Len() int { 77 | return len(t.Pairs) 78 | } 79 | 80 | func init() { 81 | gob.Register(targetInfo{}) 82 | } 83 | 84 | func (t *targetInfo) Clone() (*targetInfo, error) { 85 | b := bytes.Buffer{} 86 | enc := gob.NewEncoder(&b) 87 | dec := gob.NewDecoder(&b) 88 | if err := enc.Encode(*t); err != nil { 89 | return nil, err 90 | } 91 | var copy targetInfo 92 | if err := dec.Decode(©); err != nil { 93 | return nil, err 94 | } 95 | return ©, nil 96 | } 97 | 98 | func (t *targetInfo) Marshal() ([]byte, error) { 99 | b := bytes.Buffer{} 100 | 101 | for _, k := range t.Order { 102 | if k == msvAvEOL { 103 | continue 104 | } 105 | 106 | v := t.Pairs[k] 107 | 108 | if err := binary.Write(&b, binary.LittleEndian, &avPair{k, uint16(len(v))}); err != nil { 109 | return nil, err 110 | } 111 | 112 | b.Write(v) 113 | } 114 | 115 | // Append required MsvAvEOL pair 116 | if err := binary.Write(&b, binary.LittleEndian, &avPair{msvAvEOL, 0}); err != nil { 117 | return nil, err 118 | } 119 | 120 | return b.Bytes(), nil 121 | } 122 | 123 | func (t *targetInfo) Unmarshal(b []byte) error { 124 | reader := bytes.NewReader(b) 125 | 126 | for { 127 | var pair avPair 128 | 129 | if err := binary.Read(reader, binary.LittleEndian, &pair); err != nil { 130 | return err 131 | } 132 | 133 | if pair.ID == msvAvEOL { 134 | break 135 | } 136 | 137 | value := make([]byte, pair.Length) 138 | n, err := reader.Read(value) 139 | if err != nil { 140 | return err 141 | } 142 | if n != int(pair.Length) { 143 | return fmt.Errorf("expected %d bytes, only read %d", pair.Length, n) 144 | } 145 | 146 | t.Set(pair.ID, value) 147 | } 148 | 149 | return nil 150 | } 151 | -------------------------------------------------------------------------------- /winrmntlm/ntlmssp/bindings.go: -------------------------------------------------------------------------------- 1 | package ntlmssp 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | ) 7 | 8 | const ( 9 | // TLSServerEndPoint is defined in RFC 5929 and stores the hash of the server certificate. 10 | TLSServerEndPoint string = "tls-server-end-point" 11 | ) 12 | 13 | // ChannelBindings models the GSS-API channel bindings defined in RFC 2744. 14 | type ChannelBindings struct { 15 | InitiatorAddrtype uint32 16 | InitiatorAddress []uint8 17 | AcceptorAddrtype uint32 18 | AcceptorAddress []uint8 19 | ApplicationData []uint8 20 | } 21 | 22 | func (c *ChannelBindings) marshal() ([]byte, error) { 23 | b := bytes.Buffer{} 24 | 25 | if err := binary.Write(&b, binary.LittleEndian, &c.InitiatorAddrtype); err != nil { 26 | return nil, err 27 | } 28 | 29 | length := uint32(len(c.InitiatorAddress)) 30 | if err := binary.Write(&b, binary.LittleEndian, &length); err != nil { 31 | return nil, err 32 | } 33 | 34 | b.Write(c.InitiatorAddress) 35 | 36 | if err := binary.Write(&b, binary.LittleEndian, &c.AcceptorAddrtype); err != nil { 37 | return nil, err 38 | } 39 | 40 | length = uint32(len(c.AcceptorAddress)) 41 | if err := binary.Write(&b, binary.LittleEndian, &length); err != nil { 42 | return nil, err 43 | } 44 | 45 | b.Write(c.AcceptorAddress) 46 | 47 | length = uint32(len(c.ApplicationData)) 48 | if err := binary.Write(&b, binary.LittleEndian, &length); err != nil { 49 | return nil, err 50 | } 51 | 52 | b.Write(c.ApplicationData) 53 | 54 | return b.Bytes(), nil 55 | } 56 | -------------------------------------------------------------------------------- /winrmntlm/ntlmssp/challenge_message.go: -------------------------------------------------------------------------------- 1 | package ntlmssp 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "unsafe" 8 | ) 9 | 10 | var ( 11 | errInvalidChallengeMessage = errors.New("invalid NTLM challenge message") 12 | ) 13 | 14 | type challengeMessageFields struct { 15 | messageHeader 16 | TargetNameFields payload 17 | NegotiateFlags uint32 18 | ServerChallenge [8]uint8 19 | _ [8]uint8 20 | TargetInfoFields payload 21 | } 22 | 23 | func (m *challengeMessageFields) IsValid() bool { 24 | return m.messageHeader.IsValid() && m.MessageType == ntLmChallenge 25 | } 26 | 27 | type challengeMessage struct { 28 | challengeMessageFields 29 | TargetName string 30 | TargetInfo targetInfo 31 | Version *Version 32 | } 33 | 34 | func (m *challengeMessage) Marshal() ([]byte, error) { 35 | offset := int(unsafe.Sizeof(m.challengeMessageFields) + unsafe.Sizeof(*m.Version)) 36 | 37 | m.NegotiateFlags = ntlmsspRequestTarget.Unset(m.NegotiateFlags) 38 | if m.TargetName != "" { 39 | m.NegotiateFlags = ntlmsspRequestTarget.Set(m.NegotiateFlags) 40 | } 41 | 42 | m.NegotiateFlags = ntlmsspNegotiateTargetInfo.Unset(m.NegotiateFlags) 43 | if m.TargetInfo.Len() > 0 { 44 | m.NegotiateFlags = ntlmsspNegotiateTargetInfo.Set(m.NegotiateFlags) 45 | } 46 | 47 | var targetName []byte 48 | if ntlmsspNegotiateUnicode.IsSet(m.NegotiateFlags) { 49 | var err error 50 | targetName, err = utf16FromString(m.TargetName) 51 | if err != nil { 52 | return nil, err 53 | } 54 | } else { 55 | targetName = []byte(m.TargetName) 56 | } 57 | 58 | m.TargetNameFields = newPayload(len(targetName), &offset) 59 | 60 | targetInfo, err := m.TargetInfo.Marshal() 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | m.TargetInfoFields = newPayload(len(targetInfo), &offset) 66 | 67 | var version = &Version{} 68 | 69 | m.NegotiateFlags = ntlmsspNegotiateVersion.Unset(m.NegotiateFlags) 70 | if m.Version != nil { 71 | m.NegotiateFlags = ntlmsspNegotiateVersion.Set(m.NegotiateFlags) 72 | version = m.Version 73 | } 74 | 75 | b := bytes.Buffer{} 76 | if err := binary.Write(&b, binary.LittleEndian, &m.challengeMessageFields); err != nil { 77 | return nil, err 78 | } 79 | 80 | v, err := version.marshal() 81 | if err != nil { 82 | return nil, err 83 | } 84 | b.Write(v) 85 | 86 | b.Write(targetName) 87 | b.Write(targetInfo) 88 | 89 | return b.Bytes(), nil 90 | } 91 | 92 | func (m *challengeMessage) Unmarshal(b []byte) error { 93 | reader := bytes.NewReader(b) 94 | 95 | err := binary.Read(reader, binary.LittleEndian, &m.challengeMessageFields) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | if !m.challengeMessageFields.IsValid() { 101 | return errInvalidChallengeMessage 102 | } 103 | 104 | if ntlmsspRequestTarget.IsSet(m.NegotiateFlags) && m.TargetNameFields.Len > 0 { 105 | m.TargetName, err = m.TargetNameFields.ReadString(reader, ntlmsspNegotiateUnicode.IsSet(m.NegotiateFlags)) 106 | if err != nil { 107 | return err 108 | } 109 | } 110 | 111 | m.TargetInfo = newTargetInfo() 112 | 113 | if ntlmsspNegotiateTargetInfo.IsSet(m.NegotiateFlags) && m.TargetInfoFields.Len > 0 { 114 | v, err := m.TargetInfoFields.ReadValue(reader) 115 | if err != nil { 116 | return err 117 | } 118 | 119 | if err := m.TargetInfo.Unmarshal(v); err != nil { 120 | return err 121 | } 122 | } 123 | 124 | if ntlmsspNegotiateVersion.IsSet(m.NegotiateFlags) { 125 | v := Version{} 126 | if err := v.unmarshal(reader); err != nil { 127 | return err 128 | } 129 | m.Version = &v 130 | } 131 | 132 | return nil 133 | } 134 | -------------------------------------------------------------------------------- /winrmntlm/ntlmssp/client.go: -------------------------------------------------------------------------------- 1 | package ntlmssp 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type lmCompatibilityLevel int 9 | 10 | const ( 11 | DefaultClientCompatibilityLevel lmCompatibilityLevel = 3 12 | ) 13 | 14 | type Client struct { 15 | compatibilityLevel lmCompatibilityLevel 16 | defaultFlags uint32 17 | domain string 18 | password string 19 | pass_is_hash bool 20 | username string 21 | workstation string 22 | version *Version 23 | 24 | negotiatedFlags uint32 25 | negotiateMessage []byte 26 | challengeMessage []byte 27 | complete bool 28 | securitySession *SecuritySession 29 | } 30 | 31 | func realClientChallenge() ([]byte, error) { 32 | return nonce(8) 33 | } 34 | 35 | func realExportedSessionKey() ([]byte, error) { 36 | return nonce(16) 37 | } 38 | 39 | var ( 40 | generateClientChallenge = realClientChallenge 41 | generateExportedSessionKey = realExportedSessionKey 42 | ) 43 | 44 | func NewClient(options ...func(*Client) error) (*Client, error) { 45 | c := &Client{} 46 | 47 | // Set the defaults 48 | if err := c.SetOption(SetCompatibilityLevel(DefaultClientCompatibilityLevel)); err != nil { 49 | return nil, err 50 | } 51 | 52 | if err := c.SetOption(options...); err != nil { 53 | return nil, err 54 | } 55 | 56 | return c, nil 57 | } 58 | 59 | func defaultClientFlags() uint32 { 60 | flags := uint32(0) 61 | 62 | flags = ntlmsspNegotiateUnicode.Set(flags) 63 | flags = ntlmsspNegotiateSign.Set(flags) 64 | flags = ntlmsspNegotiateSeal.Set(flags) 65 | flags = ntlmsspNegotiateAlwaysSign.Set(flags) 66 | flags = ntlmsspNegotiateTargetInfo.Set(flags) 67 | flags = ntlmsspNegotiate128.Set(flags) 68 | flags = ntlmsspNegotiateKeyExch.Set(flags) 69 | flags = ntlmsspNegotiate56.Set(flags) 70 | 71 | return flags 72 | } 73 | 74 | func (c *Client) SetOption(options ...func(*Client) error) error { 75 | for _, option := range options { 76 | if err := option(c); err != nil { 77 | return err 78 | } 79 | } 80 | return nil 81 | } 82 | 83 | func SetCompatibilityLevel(level lmCompatibilityLevel) func(*Client) error { 84 | return func(c *Client) error { 85 | flags := defaultClientFlags() 86 | 87 | // Ideally these should be constants if there's official naming for them 88 | switch level { 89 | case 0: // LM Auth and NTLMv1 Auth 90 | flags = ntlmsspNegotiateLMKey.Set(flags) 91 | flags = ntlmsspNegotiateNTLM.Set(flags) 92 | case 1: // LM Auth and NTLMv1 Auth with Extended Session Security (NTLM2) 93 | flags = ntlmsspNegotiateNTLM.Set(flags) 94 | flags = ntlmsspNegotiateExtendedSessionsecurity.Set(flags) 95 | case 2: // NTLMv1 Auth with Extended Session Security (NTLM2) 96 | fallthrough 97 | case 3, 4, 5: // NTLMv2 Auth 98 | flags = ntlmsspNegotiateExtendedSessionsecurity.Set(flags) 99 | default: 100 | return fmt.Errorf("invalid compatibility level(0-5): %d", level) 101 | } 102 | 103 | c.compatibilityLevel = level 104 | c.defaultFlags = flags 105 | 106 | return nil 107 | } 108 | } 109 | 110 | func SetDomain(domain string) func(*Client) error { 111 | return func(c *Client) error { 112 | c.domain = strings.TrimSpace(domain) 113 | return nil 114 | } 115 | } 116 | 117 | func SetUserInfo(username, password string, pass_is_hash bool) func(*Client) error { 118 | return func(c *Client) error { 119 | c.username = username 120 | c.password = password 121 | c.pass_is_hash = pass_is_hash 122 | return nil 123 | } 124 | } 125 | 126 | func SetWorkstation(workstation string) func(*Client) error { 127 | return func(c *Client) error { 128 | c.workstation = strings.TrimSpace(workstation) 129 | return nil 130 | } 131 | } 132 | 133 | func SetVersion(version *Version) func(*Client) error { 134 | return func(c *Client) error { 135 | c.version = version 136 | return nil 137 | } 138 | } 139 | 140 | func (c *Client) Authenticate(input []byte, bindings *ChannelBindings) ([]byte, error) { 141 | if input != nil { 142 | return c.processChallengeMessage(input, bindings) 143 | } 144 | return c.newNegotiateMessage() 145 | } 146 | 147 | func (c *Client) newNegotiateMessage() ([]byte, error) { 148 | m := &negotiateMessage{ 149 | negotiateMessageFields: negotiateMessageFields{ 150 | messageHeader: newMessageHeader(ntLmNegotiate), 151 | NegotiateFlags: c.defaultFlags, 152 | }, 153 | DomainName: c.domain, 154 | Workstation: c.workstation, 155 | Version: c.version, 156 | } 157 | 158 | b, err := m.Marshal() 159 | if err != nil { 160 | return nil, err 161 | } 162 | 163 | // Store the message bytes in case we need to generate a MIC 164 | c.negotiateMessage = b 165 | 166 | return b, nil 167 | } 168 | 169 | func (c *Client) processChallengeMessage(input []byte, bindings *ChannelBindings) ([]byte, error) { 170 | cm := &challengeMessage{} 171 | if err := cm.Unmarshal(input); err != nil { 172 | return nil, err 173 | } 174 | 175 | // Store the message bytes in case we need to generate a MIC 176 | c.challengeMessage = input 177 | 178 | c.negotiatedFlags = cm.NegotiateFlags 179 | if ntlmsspNegotiateUnicode.IsSet(c.negotiatedFlags) { 180 | c.negotiatedFlags = ntlmNegotiateOEM.Unset(c.negotiatedFlags) 181 | } 182 | 183 | // Set anonymous flag 184 | if c.username == "" && c.password == "" { 185 | c.negotiatedFlags = ntlmsspAnonymous.Set(c.negotiatedFlags) 186 | } 187 | 188 | clientChallenge, err := generateClientChallenge() 189 | if err != nil { 190 | return nil, err 191 | } 192 | 193 | lmChallengeResponse, err := lmChallengeResponse(c.negotiatedFlags, c.compatibilityLevel, clientChallenge, c.username, c.password, c.domain, c.pass_is_hash, cm) 194 | if err != nil { 195 | return nil, err 196 | } 197 | 198 | targetInfo, err := cm.TargetInfo.Clone() 199 | if err != nil { 200 | return nil, err 201 | } 202 | 203 | ntChallengeResponse, keyExchangeKey, err := ntChallengeResponse(c.negotiatedFlags, c.compatibilityLevel, clientChallenge, c.username, c.password, c.domain, c.pass_is_hash, cm, lmChallengeResponse, *targetInfo, bindings) 204 | if err != nil { 205 | return nil, err 206 | } 207 | 208 | var encryptedRandomSessionKey, exportedSessionKey []byte 209 | 210 | if ntlmsspNegotiateKeyExch.IsSet(c.negotiatedFlags) { 211 | exportedSessionKey, err = generateExportedSessionKey() 212 | if err != nil { 213 | return nil, err 214 | } 215 | 216 | encryptedRandomSessionKey, err = encryptRC4K(keyExchangeKey, exportedSessionKey) 217 | if err != nil { 218 | return nil, err 219 | } 220 | } else { 221 | exportedSessionKey = keyExchangeKey 222 | } 223 | 224 | if ntlmsspNegotiateSeal.IsSet(c.negotiatedFlags) || ntlmsspNegotiateSign.IsSet(c.negotiatedFlags) { 225 | c.securitySession, err = newSecuritySession(c.negotiatedFlags, exportedSessionKey, sourceClient) 226 | if err != nil { 227 | return nil, err 228 | } 229 | } 230 | 231 | am := &authenticateMessage{ 232 | authenticateMessageFields: authenticateMessageFields{ 233 | messageHeader: newMessageHeader(ntLmAuthenticate), 234 | NegotiateFlags: c.negotiatedFlags, 235 | }, 236 | LmChallengeResponse: lmChallengeResponse, 237 | NtChallengeResponse: ntChallengeResponse, 238 | DomainName: c.domain, 239 | UserName: c.username, 240 | Workstation: c.workstation, 241 | EncryptedRandomSessionKey: encryptedRandomSessionKey, 242 | Version: c.version, 243 | // Needed for computing the MIC 244 | ExportedSessionKey: exportedSessionKey, 245 | TargetInfo: *targetInfo, 246 | } 247 | 248 | if err := am.UpdateMIC(concat(c.negotiateMessage, c.challengeMessage)); err != nil { 249 | return nil, err 250 | } 251 | 252 | b, err := am.Marshal() 253 | if err != nil { 254 | return nil, err 255 | } 256 | 257 | // Mark transaction as complete 258 | c.complete = true 259 | 260 | return b, nil 261 | } 262 | 263 | func (c *Client) Complete() bool { 264 | return c.complete 265 | } 266 | 267 | func (c *Client) SecuritySession() *SecuritySession { 268 | return c.securitySession 269 | } 270 | -------------------------------------------------------------------------------- /winrmntlm/ntlmssp/crypto.go: -------------------------------------------------------------------------------- 1 | package ntlmssp 2 | 3 | import ( 4 | "bytes" 5 | "crypto/des" 6 | "crypto/hmac" 7 | "crypto/md5" 8 | "crypto/rand" 9 | "crypto/rc4" 10 | "encoding/binary" 11 | "errors" 12 | "hash/crc32" 13 | 14 | "golang.org/x/crypto/md4" 15 | ) 16 | 17 | func nonce(length int) ([]byte, error) { 18 | result := make([]byte, length) 19 | if _, err := rand.Read(result); err != nil { 20 | return nil, err 21 | } 22 | 23 | return result, nil 24 | } 25 | 26 | func createDESKey(b []byte) ([]byte, error) { 27 | if len(b) < 7 { 28 | return nil, errors.New("need at least 7 bytes") 29 | } 30 | 31 | key := zeroBytes(8) 32 | 33 | key[0] = b[0] 34 | key[1] = b[0]<<7 | b[1]>>1 35 | key[2] = b[1]<<6 | b[2]>>2 36 | key[3] = b[2]<<5 | b[3]>>3 37 | key[4] = b[3]<<4 | b[4]>>4 38 | key[5] = b[4]<<3 | b[5]>>5 39 | key[6] = b[5]<<2 | b[6]>>6 40 | key[7] = b[6] << 1 41 | 42 | // Calculate odd parity 43 | for i, x := range key { 44 | key[i] = (x & 0xfe) | ((((x >> 1) ^ (x >> 2) ^ (x >> 3) ^ (x >> 4) ^ (x >> 5) ^ (x >> 6) ^ (x >> 7)) ^ 0x01) & 0x01) 45 | } 46 | 47 | return key, nil 48 | } 49 | 50 | func encryptDES(k, d []byte) ([]byte, error) { 51 | key, err := createDESKey(k) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | cipher, err := des.NewCipher(key) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | result := make([]byte, len(d)) 62 | cipher.Encrypt(result, d) 63 | 64 | return result, nil 65 | } 66 | 67 | func encryptDESL(k, d []byte) ([]byte, error) { 68 | b := bytes.Buffer{} 69 | 70 | padded := zeroPad(k, 21) 71 | 72 | for _, i := range []int{0, 7, 14} { 73 | result, err := encryptDES(padded[i:], d) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | if _, err := b.Write(result); err != nil { 79 | return nil, err 80 | } 81 | } 82 | 83 | return b.Bytes(), nil 84 | } 85 | 86 | func initRC4(k []byte) (*rc4.Cipher, error) { 87 | cipher, err := rc4.NewCipher(k) 88 | if err != nil { 89 | return nil, err 90 | } 91 | return cipher, nil 92 | } 93 | 94 | func encryptRC4(cipher *rc4.Cipher, d []byte) []byte { 95 | result := make([]byte, len(d)) 96 | cipher.XORKeyStream(result, d) 97 | return result 98 | } 99 | 100 | func encryptRC4K(k, d []byte) ([]byte, error) { 101 | cipher, err := initRC4(k) 102 | if err != nil { 103 | return nil, err 104 | } 105 | return encryptRC4(cipher, d), nil 106 | } 107 | 108 | func hashMD4(b []byte) []byte { 109 | md4 := md4.New() 110 | md4.Write(b) 111 | 112 | return md4.Sum(nil) 113 | } 114 | 115 | func hashMD5(b []byte) []byte { 116 | md5 := md5.New() 117 | md5.Write(b) 118 | 119 | return md5.Sum(nil) 120 | } 121 | 122 | func hmacMD5(k, m []byte) []byte { 123 | mac := hmac.New(md5.New, k) 124 | mac.Write(m) 125 | 126 | return mac.Sum(nil) 127 | } 128 | 129 | func hashCRC32(b []byte) []byte { 130 | checksum := make([]byte, 4) 131 | binary.LittleEndian.PutUint32(checksum, crc32.ChecksumIEEE(b)) 132 | return checksum 133 | } 134 | -------------------------------------------------------------------------------- /winrmntlm/ntlmssp/domain_posix.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package ntlmssp 4 | 5 | // DefaultDomain returns the Windows domain that the host is joined to. This 6 | // will never be successful on non-Windows as there's no standard API. 7 | func DefaultDomain() (string, error) { 8 | return "", nil 9 | } 10 | -------------------------------------------------------------------------------- /winrmntlm/ntlmssp/domain_windows.go: -------------------------------------------------------------------------------- 1 | package ntlmssp 2 | 3 | import ( 4 | "syscall" 5 | "unsafe" 6 | ) 7 | 8 | // DefaultDomain returns the Windows domain that the host is joined to. This 9 | // will never be successful on non-Windows as there's no standard API. 10 | func DefaultDomain() (string, error) { 11 | var domain *uint16 12 | var status uint32 13 | err := syscall.NetGetJoinInformation(nil, &domain, &status) 14 | if err != nil { 15 | return "", err 16 | } 17 | defer syscall.NetApiBufferFree((*byte)(unsafe.Pointer(domain))) 18 | 19 | // Not joined to a domain 20 | if status != syscall.NetSetupDomainName { 21 | return "", nil 22 | } 23 | 24 | return syscall.UTF16ToString((*[1024]uint16)(unsafe.Pointer(domain))[:]), nil 25 | } 26 | -------------------------------------------------------------------------------- /winrmntlm/ntlmssp/flags.go: -------------------------------------------------------------------------------- 1 | package ntlmssp 2 | 3 | import "strings" 4 | 5 | type negotiateFlag uint32 6 | 7 | const ( 8 | ntlmsspNegotiateUnicode negotiateFlag = 1 << iota 9 | ntlmNegotiateOEM 10 | ntlmsspRequestTarget 11 | _ 12 | ntlmsspNegotiateSign 13 | ntlmsspNegotiateSeal 14 | ntlmsspNegotiateDatagram 15 | ntlmsspNegotiateLMKey 16 | _ 17 | ntlmsspNegotiateNTLM 18 | _ 19 | ntlmsspAnonymous 20 | ntlmsspNegotiateOEMDomainSupplied 21 | ntlmsspNegotiateOEMWorkstationSupplied 22 | _ 23 | ntlmsspNegotiateAlwaysSign 24 | ntlmsspTargetTypeDomain 25 | ntlmsspTargetTypeServer 26 | _ 27 | ntlmsspNegotiateExtendedSessionsecurity 28 | ntlmsspNegotiateIdentity 29 | _ 30 | ntlmsspRequestNonNTSessionKey 31 | ntlmsspNegotiateTargetInfo 32 | _ 33 | ntlmsspNegotiateVersion 34 | _ 35 | _ 36 | _ 37 | ntlmsspNegotiate128 38 | ntlmsspNegotiateKeyExch 39 | ntlmsspNegotiate56 40 | ) 41 | 42 | func (f negotiateFlag) Set(flags uint32) uint32 { 43 | return flags | uint32(f) 44 | } 45 | 46 | func (f negotiateFlag) IsSet(flags uint32) bool { 47 | return (flags & uint32(f)) != 0 48 | } 49 | 50 | func (f negotiateFlag) Unset(flags uint32) uint32 { 51 | return flags & ^uint32(f) 52 | } 53 | 54 | func (f negotiateFlag) String() string { 55 | strings := map[negotiateFlag]string{ 56 | ntlmsspNegotiateUnicode: "NTLMSSP_NEGOTIATE_UNICODE", 57 | ntlmNegotiateOEM: "NTLM_NEGOTIATE_OEM", 58 | ntlmsspRequestTarget: "NTLMSSP_REQUEST_TARGET", 59 | ntlmsspNegotiateSign: "NTLMSSP_NEGOTIATE_SIGN", 60 | ntlmsspNegotiateSeal: "NTLMSSP_NEGOTIATE_SEAL", 61 | ntlmsspNegotiateDatagram: "NTLMSSP_NEGOTIATE_DATAGRAM", 62 | ntlmsspNegotiateLMKey: "NTLMSSP_NEGOTIATE_LM_KEY", 63 | ntlmsspNegotiateNTLM: "NTLMSSP_NEGOTIATE_NTLM", 64 | ntlmsspAnonymous: "NTLMSSP_ANONYMOUS", 65 | ntlmsspNegotiateOEMDomainSupplied: "NTLMSSP_NEGOTIATE_OEM_DOMAIN_SUPPLIED", 66 | ntlmsspNegotiateOEMWorkstationSupplied: "NTLMSSP_NEGOTIATE_OEM_WORKSTATION_SUPPLIED", 67 | ntlmsspNegotiateAlwaysSign: "NTLMSSP_NEGOTIATE_ALWAYS_SIGN", 68 | ntlmsspTargetTypeDomain: "NTLMSSP_TARGET_TYPE_DOMAIN", 69 | ntlmsspTargetTypeServer: "NTLMSSP_TARGET_TYPE_SERVER", 70 | ntlmsspNegotiateExtendedSessionsecurity: "NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY", 71 | ntlmsspNegotiateIdentity: "NTLMSSP_NEGOTIATE_IDENTITY", 72 | ntlmsspRequestNonNTSessionKey: "NTLMSSP_REQUEST_NON_NT_SESSION_KEY", 73 | ntlmsspNegotiateTargetInfo: "NTLMSSP_NEGOTIATE_TARGET_INFO", 74 | ntlmsspNegotiateVersion: "NTLMSSP_NEGOTIATE_VERSION", 75 | ntlmsspNegotiate128: "NTLMSSP_NEGOTIATE_128", 76 | ntlmsspNegotiateKeyExch: "NTLMSSP_NEGOTIATE_KEY_EXCH", 77 | ntlmsspNegotiate56: "NTLMSSP_NEGOTIATE_56", 78 | } 79 | 80 | return strings[f] 81 | } 82 | 83 | func flagsToString(flags uint32) string { 84 | var set []string 85 | 86 | for _, f := range [...]negotiateFlag{ 87 | ntlmsspNegotiateUnicode, 88 | ntlmNegotiateOEM, 89 | ntlmsspRequestTarget, 90 | ntlmsspNegotiateSign, 91 | ntlmsspNegotiateSeal, 92 | ntlmsspNegotiateDatagram, 93 | ntlmsspNegotiateLMKey, 94 | ntlmsspNegotiateNTLM, 95 | ntlmsspAnonymous, 96 | ntlmsspNegotiateOEMDomainSupplied, 97 | ntlmsspNegotiateOEMWorkstationSupplied, 98 | ntlmsspNegotiateAlwaysSign, 99 | ntlmsspTargetTypeDomain, 100 | ntlmsspTargetTypeServer, 101 | ntlmsspNegotiateExtendedSessionsecurity, 102 | ntlmsspNegotiateIdentity, 103 | ntlmsspRequestNonNTSessionKey, 104 | ntlmsspNegotiateTargetInfo, 105 | ntlmsspNegotiateVersion, 106 | ntlmsspNegotiate128, 107 | ntlmsspNegotiateKeyExch, 108 | ntlmsspNegotiate56, 109 | } { 110 | if f.IsSet(flags) { 111 | set = append(set, f.String()) 112 | } 113 | } 114 | 115 | return strings.Join(set, " | ") 116 | } 117 | -------------------------------------------------------------------------------- /winrmntlm/ntlmssp/http/client.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "encoding/binary" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "net/http" 11 | "net/http/cookiejar" 12 | "net/textproto" 13 | "net/url" 14 | "strings" 15 | 16 | "main/winrmntlm/ntlmssp" 17 | 18 | "github.com/go-logr/logr" 19 | "github.com/hashicorp/go-cleanhttp" 20 | ) 21 | 22 | var ( 23 | httpAuthenticateHeader = textproto.CanonicalMIMEHeaderKey("WWW-Authenticate") 24 | ) 25 | 26 | type Client struct { 27 | http *http.Client 28 | ntlm *ntlmssp.Client 29 | encryption bool 30 | sendCBT bool 31 | logger logr.Logger 32 | } 33 | 34 | type teeReadCloser struct { 35 | io.Reader 36 | io.Closer 37 | } 38 | 39 | func NewClient(httpClient *http.Client, ntlmClient *ntlmssp.Client, options ...func(*Client) error) (*Client, error) { 40 | if httpClient == nil { 41 | httpClient = cleanhttp.DefaultClient() 42 | } 43 | if httpClient.Jar == nil { 44 | httpClient.Jar, _ = cookiejar.New(nil) 45 | } 46 | if httpClient.Transport != nil && httpClient.Transport.(*http.Transport).DisableKeepAlives { 47 | return nil, errors.New("NTLM cannot work without keepalives") 48 | } 49 | 50 | // FIXME CheckRedirect 51 | 52 | if ntlmClient == nil { 53 | domain, err := ntlmssp.DefaultDomain() 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | workstation, err := ntlmssp.DefaultWorkstation() 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | ntlmClient, _ = ntlmssp.NewClient(ntlmssp.SetDomain(domain), ntlmssp.SetWorkstation(workstation), ntlmssp.SetVersion(ntlmssp.DefaultVersion())) 64 | } 65 | 66 | c := &Client{ 67 | http: httpClient, 68 | ntlm: ntlmClient, 69 | logger: logr.Discard(), 70 | } 71 | 72 | if err := c.SetOption(options...); err != nil { 73 | return nil, err 74 | } 75 | 76 | return c, nil 77 | } 78 | 79 | func (c *Client) SetOption(options ...func(*Client) error) error { 80 | for _, option := range options { 81 | if err := option(c); err != nil { 82 | return err 83 | } 84 | } 85 | return nil 86 | } 87 | 88 | func SendCBT(value bool) func(*Client) error { 89 | return func(c *Client) error { 90 | c.sendCBT = value 91 | return nil 92 | } 93 | } 94 | 95 | func Encryption(value bool) func(*Client) error { 96 | return func(c *Client) error { 97 | c.encryption = value 98 | return nil 99 | } 100 | } 101 | 102 | func Logger(logger logr.Logger) func(*Client) error { 103 | return func(c *Client) error { 104 | c.logger = logger 105 | return nil 106 | } 107 | } 108 | 109 | func (c *Client) wrap(req *http.Request) error { 110 | if session := c.ntlm.SecuritySession(); c.ntlm.Complete() && c.encryption && session != nil && req.Body != nil { 111 | 112 | contentType := req.Header.Get(contentTypeHeader) 113 | if contentType == "" { 114 | return errors.New("no Content-Type header") 115 | } 116 | 117 | body, err := io.ReadAll(req.Body) 118 | if err != nil { 119 | return err 120 | } 121 | 122 | sealed, signature, err := session.Wrap(body) 123 | if err != nil { 124 | return err 125 | } 126 | 127 | length := make([]byte, 4) 128 | binary.LittleEndian.PutUint32(length, uint32(len(signature))) 129 | 130 | body, newContentType, err := Wrap(concat(length, signature, sealed), contentType) 131 | if err != nil { 132 | return err 133 | } 134 | 135 | req.Body = io.NopCloser(bytes.NewBuffer(body)) 136 | req.Header.Set(contentTypeHeader, newContentType) 137 | } 138 | 139 | return nil 140 | } 141 | 142 | func (c *Client) unwrap(resp *http.Response) error { 143 | if session := c.ntlm.SecuritySession(); c.ntlm.Complete() && c.encryption && session != nil && resp.Body != nil { 144 | 145 | contentType := resp.Header.Get(contentTypeHeader) 146 | if contentType == "" { 147 | return errors.New("no Content-Type header") 148 | } 149 | 150 | sealed, err := io.ReadAll(resp.Body) 151 | if err != nil { 152 | return err 153 | } 154 | defer resp.Body.Close() 155 | 156 | data, newContentType, err := Unwrap(sealed, contentType) 157 | if err != nil { 158 | return err 159 | } 160 | 161 | length := binary.LittleEndian.Uint32(data[:4]) 162 | 163 | signature := make([]byte, length) 164 | copy(signature, data[4:4+length]) 165 | 166 | body, err := session.Unwrap(data[4+length:], signature) 167 | if err != nil { 168 | return err 169 | } 170 | 171 | resp.Body = io.NopCloser(bytes.NewBuffer(body)) 172 | resp.Header.Set(contentTypeHeader, newContentType) 173 | resp.Header.Set("Content-Length", fmt.Sprint(len(body))) 174 | } 175 | 176 | return nil 177 | } 178 | 179 | func (c *Client) Do(req *http.Request) (resp *http.Response, err error) { 180 | 181 | // Potentially replace the request body with a signed and sealed copy 182 | if err := c.wrap(req); err != nil { 183 | return nil, err 184 | } 185 | 186 | var body bytes.Buffer 187 | 188 | if req.Body != nil { 189 | tr := io.TeeReader(req.Body, &body) 190 | req.Body = teeReadCloser{tr, req.Body} 191 | } 192 | 193 | c.logger.Info("request", req) 194 | 195 | resp, err = c.http.Do(req) 196 | if err != nil { 197 | return nil, err 198 | } 199 | 200 | if c.ntlm.Complete() || resp.StatusCode != http.StatusUnauthorized { 201 | // Potentially unseal and check signature 202 | if err := c.unwrap(resp); err != nil { 203 | return nil, err 204 | } 205 | 206 | return resp, nil 207 | } 208 | 209 | for i := 0; i < 2; i++ { 210 | 211 | ok, input, err := isAuthenticationMethod(resp.Header, "Negotiate") 212 | if err != nil { 213 | return nil, err 214 | } 215 | 216 | if !ok { 217 | return resp, nil 218 | } 219 | 220 | var cbt *ntlmssp.ChannelBindings 221 | 222 | if c.sendCBT && resp.TLS != nil { 223 | cbt = generateChannelBindings(resp.TLS.PeerCertificates[0]) // Presume it's the first one? 224 | } 225 | 226 | b, err := c.ntlm.Authenticate(input, cbt) 227 | if err != nil { 228 | return nil, err 229 | } 230 | req.Header.Set("Authorization", "Negotiate "+base64.StdEncoding.EncodeToString(b)) 231 | 232 | if req.Body != nil { 233 | req.Body = io.NopCloser(&body) 234 | } 235 | 236 | c.logger.Info("request", req) 237 | 238 | resp, err = c.http.Do(req) 239 | if err != nil { 240 | return nil, err 241 | } 242 | 243 | if resp.StatusCode != http.StatusUnauthorized { 244 | return resp, nil 245 | } 246 | } 247 | 248 | return resp, nil 249 | } 250 | 251 | func isAuthenticationMethod(headers http.Header, method string) (bool, []byte, error) { 252 | if h, ok := headers[httpAuthenticateHeader]; ok { 253 | for _, x := range h { 254 | if x == method { 255 | return true, nil, nil 256 | } 257 | if strings.HasPrefix(x, method+" ") { 258 | parts := strings.SplitN(x, " ", 2) 259 | if len(parts) < 2 { 260 | return true, nil, errors.New("malformed " + method + " header value") 261 | } 262 | b, err := base64.StdEncoding.DecodeString(parts[1]) 263 | return true, b, err 264 | } 265 | } 266 | } 267 | return false, nil, nil 268 | } 269 | 270 | func (c *Client) Get(url string) (resp *http.Response, err error) { 271 | req, err := http.NewRequest("GET", url, nil) 272 | if err != nil { 273 | return nil, err 274 | } 275 | return c.Do(req) 276 | } 277 | 278 | func (c *Client) Head(url string) (resp *http.Response, err error) { 279 | req, err := http.NewRequest("HEAD", url, nil) 280 | if err != nil { 281 | return nil, err 282 | } 283 | return c.Do(req) 284 | } 285 | 286 | func (c *Client) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) { 287 | req, err := http.NewRequest("POST", url, body) 288 | if err != nil { 289 | return nil, err 290 | } 291 | req.Header.Set("Content-Type", contentType) 292 | return c.Do(req) 293 | } 294 | 295 | func (c *Client) PostForm(url string, data url.Values) (resp *http.Response, err error) { 296 | return c.Post(url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode())) 297 | } 298 | -------------------------------------------------------------------------------- /winrmntlm/ntlmssp/http/crypto.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "crypto" 5 | "crypto/x509" 6 | "hash" 7 | 8 | "main/winrmntlm/ntlmssp" 9 | ) 10 | 11 | func signatureAlgorithmHash(algo x509.SignatureAlgorithm) crypto.Hash { 12 | for _, details := range []struct { 13 | algo x509.SignatureAlgorithm 14 | hash crypto.Hash 15 | }{ 16 | {x509.MD2WithRSA, crypto.Hash(0)}, 17 | {x509.MD5WithRSA, crypto.MD5}, 18 | {x509.SHA1WithRSA, crypto.SHA1}, 19 | {x509.SHA256WithRSA, crypto.SHA256}, 20 | {x509.SHA384WithRSA, crypto.SHA384}, 21 | {x509.SHA512WithRSA, crypto.SHA512}, 22 | {x509.DSAWithSHA1, crypto.SHA1}, 23 | {x509.DSAWithSHA256, crypto.SHA256}, 24 | {x509.ECDSAWithSHA1, crypto.SHA1}, 25 | {x509.ECDSAWithSHA256, crypto.SHA256}, 26 | {x509.ECDSAWithSHA384, crypto.SHA384}, 27 | {x509.ECDSAWithSHA512, crypto.SHA512}, 28 | {x509.SHA256WithRSAPSS, crypto.SHA256}, 29 | {x509.SHA384WithRSAPSS, crypto.SHA384}, 30 | {x509.SHA512WithRSAPSS, crypto.SHA512}, 31 | } { 32 | if details.algo == algo { 33 | return details.hash 34 | } 35 | } 36 | return crypto.Hash(0) 37 | } 38 | 39 | func generateCertificateHash(cert *x509.Certificate) []byte { 40 | algorithm := signatureAlgorithmHash(cert.SignatureAlgorithm) 41 | 42 | var hash hash.Hash 43 | 44 | switch algorithm { 45 | case crypto.Hash(0): 46 | return nil 47 | case crypto.MD5, crypto.SHA1: 48 | hash = crypto.SHA256.New() 49 | default: 50 | hash = algorithm.New() 51 | } 52 | 53 | hash.Write(cert.Raw) 54 | 55 | return hash.Sum(nil) 56 | } 57 | 58 | func generateChannelBindings(cert *x509.Certificate) *ntlmssp.ChannelBindings { 59 | b := generateCertificateHash(cert) 60 | if b == nil { 61 | return nil 62 | } 63 | 64 | return &ntlmssp.ChannelBindings{ 65 | ApplicationData: concat([]byte(ntlmssp.TLSServerEndPoint+":"), b), 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /winrmntlm/ntlmssp/http/mime.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "mime/multipart" 10 | "net/textproto" 11 | "strings" 12 | 13 | "github.com/tidwall/transform" 14 | ) 15 | 16 | const ( 17 | mimeBoundary string = "Encrypted Boundary" 18 | mimeProtocol string = "application/HTTP-SPNEGO-session-encrypted" 19 | dashBoundary string = "--" + mimeBoundary 20 | contentTypeHeader string = "Content-Type" 21 | contentTypeValue string = "multipart/encrypted;protocol=\"" + mimeProtocol + "\";boundary=\"" + mimeBoundary + "\"" 22 | octetStream string = "application/octet-stream" 23 | ) 24 | 25 | func isHeader(line []byte) bool { 26 | b := make([]byte, len(line)) 27 | copy(b, line) 28 | b = append(b, '\r', '\n') 29 | r := textproto.NewReader(bufio.NewReader(bytes.NewBuffer(b))) 30 | _, err := r.ReadMIMEHeader() 31 | if err != nil { 32 | return false 33 | } 34 | return true 35 | } 36 | 37 | func toWSMV(r io.Reader) io.Reader { 38 | br := bufio.NewReader(r) 39 | return transform.NewTransformer(func() ([]byte, error) { 40 | for { 41 | line, err := br.ReadBytes('\n') 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | switch { 47 | case bytes.Equal(line, []byte{'\r', '\n'}): 48 | break 49 | case bytes.HasPrefix(line, []byte(dashBoundary)): 50 | return line, nil 51 | case isHeader(line): 52 | return append([]byte{'\t'}, line...), nil 53 | default: 54 | next, err := br.Peek(len(dashBoundary)) 55 | if err != nil { 56 | return nil, err 57 | } 58 | if bytes.Equal(next, []byte(dashBoundary)) { 59 | return line[:len(line)-2], nil 60 | } 61 | return line, nil 62 | } 63 | } 64 | }) 65 | } 66 | 67 | func fromWSMV(r io.Reader) io.Reader { 68 | br := bufio.NewReader(r) 69 | header := false 70 | return transform.NewTransformer(func() ([]byte, error) { 71 | for { 72 | line, err := br.ReadBytes('\n') 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | switch { 78 | case bytes.HasPrefix(line, []byte{'\t'}) && isHeader(line[1:]): 79 | header = true 80 | return line[1:], nil 81 | default: 82 | if header { 83 | line = append([]byte{'\r', '\n'}, line...) 84 | header = false 85 | } 86 | if bytes.Contains(line[1:], []byte(dashBoundary)) { 87 | for i := 0; i < len(line); i++ { 88 | if bytes.HasPrefix(line[i:], []byte(dashBoundary)) { 89 | line = append(line[:i], append([]byte{'\r', '\n'}, line[i:]...)...) 90 | break 91 | } 92 | } 93 | } 94 | return line, nil 95 | } 96 | } 97 | }) 98 | } 99 | 100 | func Wrap(input []byte, ct string) ([]byte, string, error) { 101 | b := &bytes.Buffer{} 102 | 103 | writer := multipart.NewWriter(b) 104 | if err := writer.SetBoundary(mimeBoundary); err != nil { 105 | return nil, "", err 106 | } 107 | 108 | header := make(textproto.MIMEHeader) 109 | header.Set(contentTypeHeader, mimeProtocol) 110 | // Using Set() will canonicalise this to "Originalcontent" which may cause problems 111 | header["OriginalContent"] = []string{fmt.Sprintf("type=%s;Length=%d", ct, len(input))} 112 | 113 | if _, err := writer.CreatePart(header); err != nil { 114 | return nil, "", err 115 | } 116 | 117 | body := make(textproto.MIMEHeader) 118 | body.Set(contentTypeHeader, octetStream) 119 | 120 | part, err := writer.CreatePart(body) 121 | if err != nil { 122 | return nil, "", err 123 | } 124 | 125 | if _, err := io.Copy(part, bytes.NewBuffer(input)); err != nil { 126 | return nil, "", err 127 | } 128 | 129 | writer.Close() 130 | 131 | output, err := io.ReadAll(toWSMV(b)) 132 | if err != nil { 133 | return nil, "", err 134 | } 135 | 136 | return output, contentTypeValue, nil 137 | } 138 | 139 | func Unwrap(input []byte, ct string) ([]byte, string, error) { 140 | if ct != contentTypeValue { 141 | return nil, "", errors.New("incorrect Content-Type value") 142 | } 143 | 144 | reader := multipart.NewReader(fromWSMV(bytes.NewBuffer(input)), mimeBoundary) 145 | output := bytes.Buffer{} 146 | 147 | var originalContent string 148 | 149 | Loop: 150 | for i := 0; true; i++ { 151 | part, err := reader.NextPart() 152 | switch err { 153 | case nil: 154 | break 155 | case io.EOF: 156 | break Loop 157 | default: 158 | return nil, "", err 159 | } 160 | 161 | switch i { 162 | case 0: 163 | if part.Header.Get(contentTypeHeader) != mimeProtocol { 164 | return nil, "", errors.New("incorrect Content-Type value") 165 | } 166 | if originalContent = part.Header.Get("OriginalContent"); originalContent == "" { 167 | return nil, "", errors.New("missing OriginalContent header") 168 | } 169 | case 1: 170 | if part.Header.Get(contentTypeHeader) != octetStream { 171 | return nil, "", errors.New("incorrect Content-Type value") 172 | } 173 | if _, err := output.ReadFrom(part); err != nil { 174 | return nil, "", err 175 | } 176 | default: 177 | return nil, "", errors.New("additional MIME parts encountered") 178 | } 179 | } 180 | 181 | // TODO Better way of parsing this 182 | parts := strings.Split(originalContent, ";") 183 | 184 | return output.Bytes(), parts[0][5:] + ";" + parts[1], nil 185 | } 186 | -------------------------------------------------------------------------------- /winrmntlm/ntlmssp/http/util.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import "bytes" 4 | 5 | func concat(bs ...[]byte) []byte { 6 | return bytes.Join(bs, nil) 7 | } 8 | -------------------------------------------------------------------------------- /winrmntlm/ntlmssp/message_header.go: -------------------------------------------------------------------------------- 1 | package ntlmssp 2 | 3 | import "bytes" 4 | 5 | const ( 6 | ntLmNegotiate uint32 = iota + 1 7 | ntLmChallenge 8 | ntLmAuthenticate 9 | ) 10 | 11 | type messageHeader struct { 12 | Signature [8]byte 13 | MessageType uint32 14 | } 15 | 16 | var signature = [8]byte{'N', 'T', 'L', 'M', 'S', 'S', 'P', 0} 17 | 18 | func (h messageHeader) IsValid() bool { 19 | return bytes.Equal(h.Signature[:], signature[:]) && 20 | h.MessageType >= ntLmNegotiate && h.MessageType <= ntLmAuthenticate 21 | } 22 | 23 | func newMessageHeader(messageType uint32) messageHeader { 24 | return messageHeader{signature, messageType} 25 | } 26 | -------------------------------------------------------------------------------- /winrmntlm/ntlmssp/negotiate_message.go: -------------------------------------------------------------------------------- 1 | package ntlmssp 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "unsafe" 8 | ) 9 | 10 | var ( 11 | errInvalidNegotiateMessage = errors.New("invalid NTLM negotiate message") 12 | ) 13 | 14 | type negotiateMessageFields struct { 15 | messageHeader 16 | NegotiateFlags uint32 17 | DomainNameFields payload 18 | WorkstationFields payload 19 | } 20 | 21 | func (m *negotiateMessageFields) IsValid() bool { 22 | return m.messageHeader.IsValid() && m.MessageType == ntLmNegotiate 23 | } 24 | 25 | type negotiateMessage struct { 26 | negotiateMessageFields 27 | DomainName string 28 | Workstation string 29 | Version *Version 30 | } 31 | 32 | func (m *negotiateMessage) Marshal() ([]byte, error) { 33 | offset := int(unsafe.Sizeof(m.negotiateMessageFields) + unsafe.Sizeof(*m.Version)) 34 | 35 | m.NegotiateFlags = ntlmsspNegotiateOEMDomainSupplied.Unset(m.NegotiateFlags) 36 | if m.DomainName != "" { 37 | m.NegotiateFlags = ntlmsspNegotiateOEMDomainSupplied.Set(m.NegotiateFlags) 38 | } 39 | 40 | m.NegotiateFlags = ntlmsspNegotiateOEMWorkstationSupplied.Unset(m.NegotiateFlags) 41 | if m.Workstation != "" { 42 | m.NegotiateFlags = ntlmsspNegotiateOEMWorkstationSupplied.Set(m.NegotiateFlags) 43 | } 44 | 45 | // Always OEM 46 | m.DomainNameFields = newPayload(len(m.DomainName), &offset) 47 | m.WorkstationFields = newPayload(len(m.Workstation), &offset) 48 | 49 | var version = &Version{} 50 | 51 | m.NegotiateFlags = ntlmsspNegotiateVersion.Unset(m.NegotiateFlags) 52 | if m.Version != nil { 53 | m.NegotiateFlags = ntlmsspNegotiateVersion.Set(m.NegotiateFlags) 54 | version = m.Version 55 | } 56 | 57 | b := bytes.Buffer{} 58 | if err := binary.Write(&b, binary.LittleEndian, &m.negotiateMessageFields); err != nil { 59 | return nil, err 60 | } 61 | 62 | v, err := version.marshal() 63 | if err != nil { 64 | return nil, err 65 | } 66 | b.Write(v) 67 | 68 | // Always OEM 69 | // XXX Should they be forced uppercase? 70 | b.WriteString(m.DomainName + m.Workstation) 71 | 72 | return b.Bytes(), nil 73 | } 74 | 75 | func (m *negotiateMessage) Unmarshal(b []byte) error { 76 | reader := bytes.NewReader(b) 77 | 78 | err := binary.Read(reader, binary.LittleEndian, &m.negotiateMessageFields) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | if !m.negotiateMessageFields.IsValid() { 84 | return errInvalidNegotiateMessage 85 | } 86 | 87 | if ntlmsspNegotiateOEMDomainSupplied.IsSet(m.NegotiateFlags) && m.DomainNameFields.Len > 0 { 88 | m.DomainName, err = m.DomainNameFields.ReadString(reader, false) // Always OEM 89 | if err != nil { 90 | return err 91 | } 92 | } 93 | 94 | if ntlmsspNegotiateOEMWorkstationSupplied.IsSet(m.NegotiateFlags) && m.WorkstationFields.Len > 0 { 95 | m.Workstation, err = m.WorkstationFields.ReadString(reader, false) // Always OEM 96 | if err != nil { 97 | return err 98 | } 99 | } 100 | 101 | if ntlmsspNegotiateVersion.IsSet(m.NegotiateFlags) { 102 | v := Version{} 103 | if err := v.unmarshal(reader); err != nil { 104 | return err 105 | } 106 | m.Version = &v 107 | } 108 | 109 | return nil 110 | } 111 | -------------------------------------------------------------------------------- /winrmntlm/ntlmssp/ntlm.go: -------------------------------------------------------------------------------- 1 | package ntlmssp 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "strings" 8 | "time" 9 | 10 | "github.com/bodgit/windows" 11 | "golang.org/x/crypto/md4" 12 | ) 13 | 14 | const ( 15 | lmCiphertext string = "KGS!@#$%" 16 | ) 17 | 18 | func realCurrentTime() ([]byte, error) { 19 | ft := windows.NsecToFiletime(time.Now().UnixNano()) 20 | 21 | b := bytes.Buffer{} 22 | if err := binary.Write(&b, binary.LittleEndian, ft); err != nil { 23 | return nil, err 24 | } 25 | 26 | return b.Bytes(), nil 27 | } 28 | 29 | var currentTime = realCurrentTime 30 | 31 | func lmowfV1(password string) ([]byte, error) { 32 | b := bytes.Buffer{} 33 | 34 | padded := zeroPad([]byte(strings.ToUpper(password)), 14) 35 | 36 | for _, i := range []int{0, 7} { 37 | result, err := encryptDES(padded[i:], []byte(lmCiphertext)) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | b.Write(result) 43 | } 44 | 45 | return b.Bytes(), nil 46 | } 47 | 48 | func ntowfV1(password string) ([]byte, error) { 49 | b, err := utf16FromString(password) 50 | if err != nil { 51 | return nil, err 52 | } 53 | return hashMD4(b), nil 54 | } 55 | 56 | func ntowfV2(username, password, domain string, pass_is_hash bool) ([]byte, error) { 57 | var k []byte 58 | m, err := utf16FromString(strings.ToUpper(username) + domain) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | if pass_is_hash { 64 | k, err = hex.DecodeString(password) 65 | } else { 66 | k, err = ntowfV1(password) 67 | } 68 | 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | return hmacMD5(k, m), nil 74 | } 75 | 76 | func lmV1WithSessionSecurityResponse(clientChallenge []byte) []byte { 77 | return zeroPad(clientChallenge, 24) 78 | } 79 | 80 | func lmV1Response(password string, serverChallenge []byte) ([]byte, error) { 81 | lmHash, err := lmowfV1(password) 82 | if err != nil { 83 | return nil, err 84 | } 85 | return encryptDESL(lmHash, serverChallenge) 86 | } 87 | 88 | func lmV2Response(username, password, domain string, pass_is_hash bool, serverChallenge, clientChallenge []byte) ([]byte, error) { 89 | ntlmHash, err := ntowfV2(username, password, domain, pass_is_hash) 90 | if err != nil { 91 | return nil, err 92 | } 93 | return concat(hmacMD5(ntlmHash, concat(serverChallenge, clientChallenge)), clientChallenge), nil 94 | } 95 | 96 | func ntlmV1Response(password string, serverChallenge []byte) ([]byte, []byte, error) { 97 | ntlmHash, err := ntowfV1(password) 98 | if err != nil { 99 | return nil, nil, err 100 | } 101 | 102 | response, err := encryptDESL(ntlmHash, serverChallenge) 103 | if err != nil { 104 | return nil, nil, err 105 | } 106 | 107 | return response, hashMD4(ntlmHash), nil 108 | } 109 | 110 | func ntlm2Response(password string, serverChallenge, clientChallenge []byte) ([]byte, []byte, error) { 111 | ntlmHash, err := ntowfV1(password) 112 | if err != nil { 113 | return nil, nil, err 114 | } 115 | 116 | response, err := encryptDESL(ntlmHash, hashMD5(concat(serverChallenge, clientChallenge))[:8]) 117 | if err != nil { 118 | return nil, nil, err 119 | } 120 | 121 | return response, hashMD4(ntlmHash), nil 122 | } 123 | 124 | func ntlmV2Temp(timestamp []byte, clientChallenge []byte, targetInfo targetInfo) ([]byte, error) { 125 | b, err := targetInfo.Marshal() 126 | if err != nil { 127 | return nil, err 128 | } 129 | 130 | return concat([]byte{0x01}, []byte{0x01}, zeroBytes(6), timestamp, clientChallenge, zeroBytes(4), b, zeroBytes(4)), nil 131 | } 132 | 133 | func ntlmV2Response(username, password, domain string, pass_is_hash bool, serverChallenge, clientChallenge []byte, timestamp []byte, targetInfo targetInfo) ([]byte, []byte, error) { 134 | ntlmHash, err := ntowfV2(username, password, domain, pass_is_hash) 135 | if err != nil { 136 | return nil, nil, err 137 | } 138 | 139 | temp, err := ntlmV2Temp(timestamp, clientChallenge, targetInfo) 140 | if err != nil { 141 | return nil, nil, err 142 | } 143 | 144 | ntProofStr := hmacMD5(ntlmHash, concat(serverChallenge, temp)) 145 | 146 | return concat(ntProofStr, temp), hmacMD5(ntlmHash, ntProofStr), nil 147 | } 148 | 149 | func ntlmV1ExchangeKey(flags uint32, sessionBaseKey []byte, serverChallenge []byte, lmChallengeResponse []byte, lmHash []byte) ([]byte, error) { 150 | switch { 151 | case ntlmsspNegotiateExtendedSessionsecurity.IsSet(flags): 152 | return hmacMD5(sessionBaseKey, concat(serverChallenge, lmChallengeResponse[:8])), nil 153 | case ntlmsspNegotiateLMKey.IsSet(flags): 154 | b := bytes.Buffer{} 155 | 156 | for _, k := range [][]byte{lmHash[:7], concat(lmHash[7:8], bytes.Repeat([]byte{0xbd}, 6))} { 157 | result, err := encryptDES(k, lmChallengeResponse[:8]) 158 | if err != nil { 159 | return nil, err 160 | } 161 | 162 | b.Write(result) 163 | } 164 | 165 | return b.Bytes(), nil 166 | case ntlmsspRequestNonNTSessionKey.IsSet(flags): 167 | return zeroPad(lmHash[:8], 16), nil 168 | default: 169 | return sessionBaseKey, nil 170 | } 171 | } 172 | 173 | func lmChallengeResponse(flags uint32, level lmCompatibilityLevel, clientChallenge []byte, username, password, domain string, pass_is_hash bool, cm *challengeMessage) ([]byte, error) { 174 | switch { 175 | case ntlmsspAnonymous.IsSet(flags): 176 | return zeroBytes(1), nil 177 | case ntlmsspNegotiateExtendedSessionsecurity.IsSet(flags) && level < 3: 178 | // LMv1 with session security 179 | return lmV1WithSessionSecurityResponse(clientChallenge), nil 180 | case level < 2: 181 | // LMv1 response 182 | return lmV1Response(password, cm.ServerChallenge[:]) 183 | case level == 2: 184 | // NTLMv1 response 185 | response, _, err := ntlmV1Response(password, cm.ServerChallenge[:]) 186 | return response, err 187 | default: 188 | // LMv2 response 189 | if _, ok := cm.TargetInfo.Get(msvAvTimestamp); ok { 190 | return zeroBytes(24), nil 191 | } 192 | return lmV2Response(username, password, domain, pass_is_hash, cm.ServerChallenge[:], clientChallenge) 193 | } 194 | } 195 | 196 | func ntChallengeResponse(flags uint32, level lmCompatibilityLevel, clientChallenge []byte, username, password, domain string, pass_is_hash bool, cm *challengeMessage, lmChallengeResponse []byte, targetInfo targetInfo, channelBindings *ChannelBindings) ([]byte, []byte, error) { 197 | switch { 198 | case ntlmsspAnonymous.IsSet(flags): 199 | return []byte{}, zeroBytes(md4.Size), nil 200 | case level < 3: 201 | var response, sessionBaseKey []byte 202 | var err error 203 | 204 | if ntlmsspNegotiateExtendedSessionsecurity.IsSet(flags) { 205 | // NTLMv1 authentication with NTLM2 206 | response, sessionBaseKey, err = ntlm2Response(password, cm.ServerChallenge[:], clientChallenge) 207 | } else { 208 | // NTLMv1 authentication 209 | response, sessionBaseKey, err = ntlmV1Response(password, cm.ServerChallenge[:]) 210 | } 211 | if err != nil { 212 | return nil, nil, err 213 | } 214 | 215 | lmHash, err := lmowfV1(password) 216 | if err != nil { 217 | return nil, nil, err 218 | } 219 | 220 | keyExchangeKey, err := ntlmV1ExchangeKey(flags, sessionBaseKey, cm.ServerChallenge[:], lmChallengeResponse, lmHash) 221 | if err != nil { 222 | return nil, nil, err 223 | } 224 | 225 | return response, keyExchangeKey, nil 226 | default: 227 | // NTLMv2 authentication 228 | timestamp, ok := targetInfo.Get(msvAvTimestamp) 229 | if ok { 230 | var flags uint32 231 | if v, ok := targetInfo.Get(msvAvFlags); ok { 232 | flags = binary.LittleEndian.Uint32(v) 233 | flags |= msvAvFlagMICProvided 234 | } else { 235 | flags = msvAvFlagMICProvided 236 | } 237 | v := make([]byte, 4) 238 | binary.LittleEndian.PutUint32(v, flags) 239 | targetInfo.Set(msvAvFlags, v) 240 | } else { 241 | var err error 242 | timestamp, err = currentTime() 243 | if err != nil { 244 | return nil, nil, err 245 | } 246 | } 247 | 248 | if channelBindings != nil { 249 | b, err := channelBindings.marshal() 250 | if err != nil { 251 | return nil, nil, err 252 | } 253 | targetInfo.Set(msvChannelBindings, hashMD5(b)) 254 | } 255 | 256 | return ntlmV2Response(username, password, domain, pass_is_hash, cm.ServerChallenge[:], clientChallenge, timestamp, targetInfo) 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /winrmntlm/ntlmssp/payload.go: -------------------------------------------------------------------------------- 1 | package ntlmssp 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | type payload struct { 10 | Len uint16 11 | MaxLen uint16 12 | Offset uint32 13 | } 14 | 15 | func newPayload(length int, offset *int) payload { 16 | p := payload{ 17 | Len: uint16(length), 18 | MaxLen: uint16(length), 19 | Offset: uint32(*offset), 20 | } 21 | 22 | *offset += length 23 | 24 | return p 25 | } 26 | 27 | func (p *payload) ReadValue(reader *bytes.Reader) ([]byte, error) { 28 | // Remember where we are 29 | pos, err := reader.Seek(0, io.SeekCurrent) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | // Seek to where the payload is 35 | _, err = reader.Seek(int64(p.Offset), io.SeekStart) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | // Read the value 41 | b := make([]byte, p.Len) 42 | n, err := reader.Read(b) 43 | if err != nil { 44 | return nil, err 45 | } 46 | if n != int(p.Len) { 47 | return nil, fmt.Errorf("expected %d bytes, only read %d", p.Len, n) 48 | } 49 | 50 | // Seek back to where we were 51 | _, err = reader.Seek(pos, io.SeekStart) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | return b, nil 57 | } 58 | 59 | func (p *payload) ReadString(reader *bytes.Reader, utf16 bool) (string, error) { 60 | b, err := p.ReadValue(reader) 61 | if err != nil { 62 | return "", err 63 | } 64 | 65 | if utf16 { 66 | s, err := utf16ToString(b) 67 | if err != nil { 68 | return "", err 69 | } 70 | return s, nil 71 | } 72 | 73 | return string(b), nil 74 | } 75 | -------------------------------------------------------------------------------- /winrmntlm/ntlmssp/security.go: -------------------------------------------------------------------------------- 1 | package ntlmssp 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rc4" 6 | "encoding/binary" 7 | "errors" 8 | ) 9 | 10 | type SecuritySession struct { 11 | negotiateFlags, outgoingSeqNum, incomingSeqNum uint32 12 | outgoingSigningKey, incomingSigningKey []byte 13 | outgoingHandle, incomingHandle *rc4.Cipher 14 | } 15 | 16 | var ( 17 | clientSigning = concat([]byte("session key to client-to-server signing key magic constant"), []byte{0x00}) 18 | serverSigning = concat([]byte("session key to server-to-client signing key magic constant"), []byte{0x00}) 19 | clientSealing = concat([]byte("session key to client-to-server sealing key magic constant"), []byte{0x00}) 20 | serverSealing = concat([]byte("session key to server-to-client sealing key magic constant"), []byte{0x00}) 21 | ) 22 | 23 | type securitySource int 24 | 25 | const ( 26 | sourceClient securitySource = iota 27 | sourceServer 28 | ) 29 | 30 | func newSecuritySession(flags uint32, exportedSessionKey []byte, source securitySource) (*SecuritySession, error) { 31 | s := &SecuritySession{ 32 | negotiateFlags: flags, 33 | } 34 | 35 | clientSealingKey := sealKey(s.negotiateFlags, exportedSessionKey, clientSealing) 36 | serverSealingKey := sealKey(s.negotiateFlags, exportedSessionKey, serverSealing) 37 | 38 | var err error 39 | 40 | switch source { 41 | case sourceClient: 42 | s.outgoingSigningKey = signKey(exportedSessionKey, clientSigning) 43 | s.incomingSigningKey = signKey(exportedSessionKey, serverSigning) 44 | 45 | s.outgoingHandle, err = initRC4(clientSealingKey) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | s.incomingHandle, err = initRC4(serverSealingKey) 51 | if err != nil { 52 | return nil, err 53 | } 54 | case sourceServer: 55 | s.outgoingSigningKey = signKey(exportedSessionKey, serverSigning) 56 | s.incomingSigningKey = signKey(exportedSessionKey, clientSigning) 57 | 58 | s.outgoingHandle, err = initRC4(serverSealingKey) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | s.incomingHandle, err = initRC4(clientSealingKey) 64 | if err != nil { 65 | return nil, err 66 | } 67 | default: 68 | return nil, errors.New("unknown source") 69 | } 70 | 71 | return s, nil 72 | } 73 | 74 | func (s *SecuritySession) Wrap(b []byte) ([]byte, []byte, error) { 75 | m := append(b[:0:0], b...) 76 | switch { 77 | case ntlmsspNegotiateSeal.IsSet(s.negotiateFlags): 78 | m = encryptRC4(s.outgoingHandle, m) 79 | fallthrough 80 | case ntlmsspNegotiateSign.IsSet(s.negotiateFlags): 81 | // Signature is always created on the original unencrypted message 82 | signature, err := calculateSignature(b, s.negotiateFlags, s.outgoingSigningKey, s.outgoingSeqNum, s.outgoingHandle) 83 | if err != nil { 84 | return nil, nil, err 85 | } 86 | 87 | s.outgoingSeqNum++ 88 | 89 | return m, signature, nil 90 | default: 91 | return m, nil, nil 92 | } 93 | } 94 | 95 | func (s *SecuritySession) Unwrap(b, signature []byte) ([]byte, error) { 96 | m := append(b[:0:0], b...) 97 | switch { 98 | case ntlmsspNegotiateSeal.IsSet(s.negotiateFlags): 99 | m = encryptRC4(s.incomingHandle, m) 100 | fallthrough 101 | case ntlmsspNegotiateSign.IsSet(s.negotiateFlags): 102 | // Signature is checked after any decryption 103 | expected, err := calculateSignature(m, s.negotiateFlags, s.incomingSigningKey, s.incomingSeqNum, s.incomingHandle) 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | offset := 8 109 | if ntlmsspNegotiateExtendedSessionsecurity.IsSet(s.negotiateFlags) { 110 | offset = 4 111 | } 112 | 113 | // Compare the checksum portion of the signature 114 | if !bytes.Equal(signature[offset:12], expected[offset:12]) { 115 | return nil, errors.New("checksum does not match") 116 | } 117 | 118 | // Compare the sequence number portion of the signature 119 | if !bytes.Equal(signature[12:16], expected[12:16]) { 120 | return nil, errors.New("sequence number does not match") 121 | } 122 | 123 | s.incomingSeqNum++ 124 | 125 | fallthrough 126 | default: 127 | return m, nil 128 | } 129 | } 130 | 131 | func signKey(exportedSessionKey, constant []byte) []byte { 132 | return hashMD5(concat(exportedSessionKey, constant)) 133 | } 134 | 135 | func sealKey(flags uint32, exportedSessionKey, constant []byte) []byte { 136 | switch { 137 | case ntlmsspNegotiateExtendedSessionsecurity.IsSet(flags) && ntlmsspNegotiate128.IsSet(flags): 138 | // NTLM2 with 128-bit key 139 | return hashMD5(concat(exportedSessionKey, constant)) 140 | case ntlmsspNegotiateExtendedSessionsecurity.IsSet(flags) && ntlmsspNegotiate56.IsSet(flags): 141 | // NTLM2 with 56-bit key 142 | return hashMD5(concat(exportedSessionKey[:7], constant)) 143 | case ntlmsspNegotiateExtendedSessionsecurity.IsSet(flags): 144 | // NTLM2 with 40-bit key 145 | return hashMD5(concat(exportedSessionKey[:5], constant)) 146 | case ntlmsspNegotiateLMKey.IsSet(flags) && ntlmsspNegotiate56.IsSet(flags): 147 | // NTLM1 with 56-bit key 148 | return concat(exportedSessionKey[:7], []byte{0xa0}) 149 | case ntlmsspNegotiateLMKey.IsSet(flags): 150 | // NTLM1 with 40-bit key 151 | return concat(exportedSessionKey[:5], []byte{0xe5, 0x38, 0xb0}) 152 | default: 153 | return exportedSessionKey 154 | } 155 | } 156 | 157 | func calculateSignature(message []byte, flags uint32, signingKey []byte, sequenceNumber uint32, handle *rc4.Cipher) ([]byte, error) { 158 | var version uint32 = 1 159 | 160 | b := bytes.Buffer{} 161 | if err := binary.Write(&b, binary.LittleEndian, &version); err != nil { 162 | return nil, err 163 | } 164 | 165 | seqNum := make([]byte, 4) 166 | binary.LittleEndian.PutUint32(seqNum, sequenceNumber) 167 | 168 | switch { 169 | case ntlmsspNegotiateExtendedSessionsecurity.IsSet(flags): 170 | checksum := hmacMD5(signingKey, concat(seqNum, message))[:8] 171 | 172 | if ntlmsspNegotiateKeyExch.IsSet(flags) { 173 | checksum = encryptRC4(handle, checksum) 174 | } 175 | 176 | // Checksum 177 | b.Write(checksum) 178 | 179 | // Sequence Number 180 | b.Write(seqNum) 181 | default: 182 | // The MS-NLMP specification doesn't match the examples. The 183 | // examples write out the encrypted random pad in the signature 184 | // whereas the pseudo code writes out zeroes. 185 | 186 | // RandomPad of 0 187 | _ = encryptRC4(handle, zeroBytes(4)) 188 | b.Write(zeroBytes(4)) 189 | 190 | // Checksum 191 | b.Write(encryptRC4(handle, hashCRC32(message))) 192 | 193 | encryptedSeqNum := encryptRC4(handle, zeroBytes(4)) 194 | 195 | for i := 0; i < 4; i++ { 196 | encryptedSeqNum[i] ^= seqNum[i] 197 | } 198 | 199 | // Sequence Number 200 | b.Write(encryptedSeqNum) 201 | } 202 | 203 | return b.Bytes(), nil 204 | } 205 | -------------------------------------------------------------------------------- /winrmntlm/ntlmssp/unicode.go: -------------------------------------------------------------------------------- 1 | package ntlmssp 2 | 3 | import "golang.org/x/text/encoding/unicode" 4 | 5 | func utf16FromString(s string) ([]byte, error) { 6 | b, err := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder().Bytes([]byte(s)) 7 | if err != nil { 8 | return nil, err 9 | } 10 | 11 | return b, nil 12 | } 13 | 14 | func utf16ToString(b []byte) (string, error) { 15 | s, err := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewDecoder().Bytes(b) 16 | if err != nil { 17 | return "", err 18 | } 19 | 20 | return string(s), nil 21 | } 22 | -------------------------------------------------------------------------------- /winrmntlm/ntlmssp/util.go: -------------------------------------------------------------------------------- 1 | package ntlmssp 2 | 3 | import "bytes" 4 | 5 | func concat(bs ...[]byte) []byte { 6 | return bytes.Join(bs, nil) 7 | } 8 | 9 | func zeroBytes(length int) []byte { 10 | return make([]byte, length, length) 11 | } 12 | 13 | func zeroPad(s []byte, length int) []byte { 14 | d := zeroBytes(length) 15 | copy(d, s) 16 | 17 | return d 18 | } 19 | -------------------------------------------------------------------------------- /winrmntlm/ntlmssp/version.go: -------------------------------------------------------------------------------- 1 | package ntlmssp 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "unsafe" 7 | ) 8 | 9 | const ( 10 | // NTLMSSPRevisionW2K3 is the current revision of NTLM. 11 | NTLMSSPRevisionW2K3 uint8 = 0x0f 12 | ) 13 | 14 | // Version models the NTLM version passed in the various messages. 15 | type Version struct { 16 | ProductMajorVersion uint8 17 | ProductMinorVersion uint8 18 | ProductBuild uint16 19 | _ [3]uint8 20 | NTLMRevisionCurrent uint8 21 | } 22 | 23 | func (v *Version) marshal() ([]byte, error) { 24 | dest := make([]byte, 0, unsafe.Sizeof(Version{})) 25 | buffer := bytes.NewBuffer(dest) 26 | 27 | if err := binary.Write(buffer, binary.LittleEndian, v); err != nil { 28 | return nil, err 29 | } 30 | 31 | return buffer.Bytes(), nil 32 | } 33 | 34 | func (v *Version) unmarshal(reader *bytes.Reader) error { 35 | if err := binary.Read(reader, binary.LittleEndian, v); err != nil { 36 | return err 37 | } 38 | 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /winrmntlm/ntlmssp/version_posix.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package ntlmssp 4 | 5 | // DefaultVersion returns a pointer to a NTLM Version struct for the OS which 6 | // will be populated on Windows or nil otherwise. 7 | func DefaultVersion() *Version { 8 | return nil 9 | } 10 | -------------------------------------------------------------------------------- /winrmntlm/ntlmssp/version_windows.go: -------------------------------------------------------------------------------- 1 | package ntlmssp 2 | 3 | import ( 4 | "syscall" 5 | ) 6 | 7 | // DefaultVersion returns a pointer to a NTLM Version struct for the OS which 8 | // will be populated on Windows or nil otherwise. 9 | func DefaultVersion() *Version { 10 | dll := syscall.MustLoadDLL("kernel32.dll") 11 | p := dll.MustFindProc("GetVersion") 12 | v, _, _ := p.Call() 13 | 14 | return &Version{ 15 | ProductMajorVersion: uint8(v), 16 | ProductMinorVersion: uint8(v >> 8), 17 | ProductBuild: uint16(v >> 16), 18 | NTLMRevisionCurrent: NTLMSSPRevisionW2K3, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /winrmntlm/ntlmssp/workstation.go: -------------------------------------------------------------------------------- 1 | package ntlmssp 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | ) 7 | 8 | // DefaultWorkstation returns the current workstation name. 9 | func DefaultWorkstation() (string, error) { 10 | hostname, err := os.Hostname() 11 | if err != nil { 12 | return "", err 13 | } 14 | 15 | return strings.ToUpper(hostname), nil 16 | } 17 | --------------------------------------------------------------------------------