diff --git a/cmd/root.go b/cmd/root.go index 82ffbc4..84cbd5c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -8,8 +8,10 @@ import ( "os" "strings" "time" - + "net" + "context" "github.com/sensepost/godoh/dnsclient" + "github.com/sensepost/godoh/utils" log "github.com/sirupsen/logrus" @@ -23,6 +25,9 @@ var dnsDomain string var dnsProviderName string var dnsProvider dnsclient.Client var validateSSL bool +var proxyAddr string +var proxyUsername string +var proxyPassword string // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ @@ -54,6 +59,7 @@ func init() { cobra.OnInitialize(validateDNSDomain) cobra.OnInitialize(seedRand) cobra.OnInitialize(configureSSLValidation) + cobra.OnInitialize(configureProxy) // if the DNS domain was configured at compile time, remove the flag if dnsDomain == "" { @@ -66,8 +72,12 @@ func init() { "Preferred DNS provider to use. [possible: googlefront, google, cloudflare, quad9, raw]") rootCmd.PersistentFlags().BoolVarP(&validateSSL, "validate-certificate", "K", false, "Validate DoH provider SSL certificates") + rootCmd.PersistentFlags().StringVarP(&proxyAddr, "proxy", "", "", "Use NTLM proxy, i.e hostname:port") + rootCmd.PersistentFlags().StringVarP(&proxyUsername, "proxy-username", "", "", "NTLM proxy username to use (blank: attempt to use running user's credentials) ") + rootCmd.PersistentFlags().StringVarP(&proxyPassword, "proxy-password", "", "", "NTLM proxy password to use (blank: attempt to use running user's credentials) ") } + func seedRand() { rand.Seed(time.Now().UTC().UnixNano()) } @@ -108,6 +118,42 @@ func validateDNSProvider() { log.Infof("Using `%s` as preferred provider\n", dnsProviderName) } +func configureProxy() { + + if proxyAddr!="" { + + if ( (proxyUsername =="" && proxyPassword !="")||(proxyUsername !="" && proxyPassword =="") ) { + log.Fatalf("Proxy username or password were not provided") + } + + dialContext := (&net.Dialer{ + KeepAlive: 30 * time.Second, + Timeout: 30 * time.Second, + }).DialContext + + ntlmDialContext := func(ctx context.Context, network, address string) (net.Conn, error) { + conn, err := dialContext(ctx, network, proxyAddr) + if err != nil { + return conn, err + } + log.Infof("Attempting to inject NTLM authentication") + err = utils.ProxySetup(conn, address, proxyUsername,proxyPassword) + if err != nil { + log.Fatalf("Failed to inject NTLM authentication: %v.", err) + return conn, err + } + return conn, err + } + http.DefaultTransport.(*http.Transport).Proxy=nil + http.DefaultTransport.(*http.Transport).DialContext=ntlmDialContext + + } else { + if (proxyUsername !="" || proxyPassword !="") { + log.Fatalf("Proxy address not set, however proxy credentials were provided") + } + } +} + func configureSSLValidation() { if !validateSSL { http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} diff --git a/utils/go-ntlm-auth/LICENSE.txt b/utils/go-ntlm-auth/LICENSE.txt new file mode 100644 index 0000000..6c047fd --- /dev/null +++ b/utils/go-ntlm-auth/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 G-Research + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/utils/go-ntlm-auth/README.md b/utils/go-ntlm-auth/README.md new file mode 100644 index 0000000..5a9befc --- /dev/null +++ b/utils/go-ntlm-auth/README.md @@ -0,0 +1,16 @@ +# go-ntlm-auth + +This is an implementation of NTLM for Go that was implemented using Windows SSPI +(Security Support Provider Interface). It differs from other implemenations of +NTLM as it uses the default credentials of the account that the application is +running with, meaning the username and password does not have to be provided in +plain text. + +## Usage Notes + +This is currently only implemented for Windows. + +### References +https://github.com/G-Research/go-ntlm-auth - original author +https://github.com/denisenkom/go-mssqldb - sspi_windows.go used as ntlm_windows.go to perform Win32 calls to SSPI +https://github.com/github/git-lfs/blob/master/lfs/ntlm.go - git-lfs used as basis for NTLM authentication logic diff --git a/utils/go-ntlm-auth/ntlm/ntlm.go b/utils/go-ntlm-auth/ntlm/ntlm.go new file mode 100644 index 0000000..080ea7c --- /dev/null +++ b/utils/go-ntlm-auth/ntlm/ntlm.go @@ -0,0 +1,21 @@ +package ntlm + +import ( + "encoding/base64" + "fmt" +) + +func ParseChallengeResponse(header string) ([]byte, error) { + if len(header) < 6 { + return nil, fmt.Errorf("Invalid NTLM challenge response: %q", header) + } + + //parse out the "NTLM " at the beginning of the response + challenge := header[5:] + val, err := base64.StdEncoding.DecodeString(challenge) + + if err != nil { + return nil, err + } + return []byte(val), nil +} diff --git a/utils/go-ntlm-auth/ntlm/ntlmAuthenticator.go b/utils/go-ntlm-auth/ntlm/ntlmAuthenticator.go new file mode 100644 index 0000000..754857d --- /dev/null +++ b/utils/go-ntlm-auth/ntlm/ntlmAuthenticator.go @@ -0,0 +1,8 @@ +package ntlm + +// NtlmAuthenticator defines interface to provide methods to get byte arrays required for NTLM authentication +type NtlmAuthenticator interface { + GetNegotiateBytes() ([]byte, error) + GetResponseBytes([]byte) ([]byte, error) + ReleaseContext() +} diff --git a/utils/go-ntlm-auth/ntlm/ntlm_other.go b/utils/go-ntlm-auth/ntlm/ntlm_other.go new file mode 100644 index 0000000..3c9ef91 --- /dev/null +++ b/utils/go-ntlm-auth/ntlm/ntlm_other.go @@ -0,0 +1,13 @@ +// +build !windows + +package ntlm + +// NTLM authentication is only currently implemented on Windows +func GetDefaultCredentialsAuth() (NtlmAuthenticator, bool) { + return nil, false +} + +func GetAuth(user, password, service, workstation string) (NtlmAuthenticator, bool) { + return nil, false +} + diff --git a/utils/go-ntlm-auth/ntlm/ntlm_test.go b/utils/go-ntlm-auth/ntlm/ntlm_test.go new file mode 100644 index 0000000..980ff3b --- /dev/null +++ b/utils/go-ntlm-auth/ntlm/ntlm_test.go @@ -0,0 +1,42 @@ +package ntlm + +import ( + "encoding/base64" + "strings" + "testing" +) + +func TestNtlmHeaderParseValid(t *testing.T) { + header := "NTLM " + base64.StdEncoding.EncodeToString([]byte("Some data")) + bytes, err := ParseChallengeResponse(header) + + if err != nil { + t.Fatalf("Unexpected exception!") + } + + // Check NTLM has been stripped from response + if strings.HasPrefix(string(bytes), "NTLM") { + t.Fatalf("Response contains NTLM prefix!") + } +} + +func TestNtlmHeaderParseInvalidLength(t *testing.T) { + header := "NTL" + ret, err := ParseChallengeResponse(header) + if ret != nil { + t.Errorf("Unexpected challenge response: %v", ret) + } + + if err == nil { + t.Errorf("Expected error, got none!") + } +} + +func TestNtlmHeaderParseInvalid(t *testing.T) { + header := base64.StdEncoding.EncodeToString([]byte("NTLM I am a moose")) + _, err := ParseChallengeResponse(header) + + if err == nil { + t.Fatalf("Expected error, got none!") + } +} diff --git a/utils/go-ntlm-auth/ntlm/ntlm_windows.go b/utils/go-ntlm-auth/ntlm/ntlm_windows.go new file mode 100644 index 0000000..c12d055 --- /dev/null +++ b/utils/go-ntlm-auth/ntlm/ntlm_windows.go @@ -0,0 +1,314 @@ +/* +Copyright (c) 2012 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +// +build windows + +package ntlm + +// Windows implementation of NTLM authentication using SSPI (Security Support Provider Interface) + +import ( + "fmt" + "strings" + "syscall" + "unsafe" +) + +const ( + SEC_E_OK = 0 + SECPKG_CRED_OUTBOUND = 2 + SEC_WINNT_AUTH_IDENTITY_UNICODE = 2 + ISC_REQ_DELEGATE = 0x00000001 + ISC_REQ_REPLAY_DETECT = 0x00000004 + ISC_REQ_SEQUENCE_DETECT = 0x00000008 + ISC_REQ_CONFIDENTIALITY = 0x00000010 + ISC_REQ_CONNECTION = 0x00000800 + SECURITY_NETWORK_DREP = 0 + SEC_I_CONTINUE_NEEDED = 0x00090312 + SEC_I_COMPLETE_NEEDED = 0x00090313 + SEC_I_COMPLETE_AND_CONTINUE = 0x00090314 + SECBUFFER_VERSION = 0 + SECBUFFER_TOKEN = 2 + NTLMBUF_LEN = 12000 +) + +const ISC_REQ = ISC_REQ_CONFIDENTIALITY | + ISC_REQ_REPLAY_DETECT | + ISC_REQ_SEQUENCE_DETECT | + ISC_REQ_CONNECTION | + ISC_REQ_DELEGATE + +type SecurityFunctionTable struct { + dwVersion uint32 + EnumerateSecurityPackages uintptr + QueryCredentialsAttributes uintptr + AcquireCredentialsHandle uintptr + FreeCredentialsHandle uintptr + Reserved2 uintptr + InitializeSecurityContext uintptr + AcceptSecurityContext uintptr + CompleteAuthToken uintptr + DeleteSecurityContext uintptr + ApplyControlToken uintptr + QueryContextAttributes uintptr + ImpersonateSecurityContext uintptr + RevertSecurityContext uintptr + MakeSignature uintptr + VerifySignature uintptr + FreeContextBuffer uintptr + QuerySecurityPackageInfo uintptr + Reserved3 uintptr + Reserved4 uintptr + Reserved5 uintptr + Reserved6 uintptr + Reserved7 uintptr + Reserved8 uintptr + QuerySecurityContextToken uintptr + EncryptMessage uintptr + DecryptMessage uintptr +} + +type SEC_WINNT_AUTH_IDENTITY struct { + User *uint16 + UserLength uint32 + Domain *uint16 + DomainLength uint32 + Password *uint16 + PasswordLength uint32 + Flags uint32 +} + +type TimeStamp struct { + LowPart uint32 + HighPart int32 +} + +type SecHandle struct { + dwLower uintptr + dwUpper uintptr +} + +type SecBuffer struct { + cbBuffer uint32 + BufferType uint32 + pvBuffer *byte +} + +type SecBufferDesc struct { + ulVersion uint32 + cBuffers uint32 + pBuffers *SecBuffer +} + +type SSPIAuth struct { + Domain string + UserName string + Password string + Service string + cred SecHandle + ctxt SecHandle +} + +var ( + initialized = false + sec_fn *SecurityFunctionTable +) + +func initialize() { + + secur32dll := syscall.NewLazyDLL("secur32.dll") + initSecurityInterface := secur32dll.NewProc("InitSecurityInterfaceW") + + ptr, _, _ := initSecurityInterface.Call() + sec_fn = (*SecurityFunctionTable)(unsafe.Pointer(ptr)) + + initialized = true +} + +func GetDefaultCredentialsAuth() (NtlmAuthenticator, bool) { + return GetAuth("", "", "", "") +} + +//Sensepost modification from getAuth to GetAuth +func GetAuth(user, password, service, workstation string) (NtlmAuthenticator, bool) { + if !initialized { + initialize() + } + + if user == "" { + return &SSPIAuth{Service: service}, true + } + if !strings.ContainsRune(user, '\\') { + return nil, false + } + domain_user := strings.SplitN(user, "\\", 2) + return &SSPIAuth{ + Domain: domain_user[0], + UserName: domain_user[1], + Password: password, + Service: service, + }, true +} + +func (auth *SSPIAuth) GetNegotiateBytes() ([]byte, error) { + var identity *SEC_WINNT_AUTH_IDENTITY + if auth.UserName != "" { + identity = &SEC_WINNT_AUTH_IDENTITY{ + Flags: SEC_WINNT_AUTH_IDENTITY_UNICODE, + Password: syscall.StringToUTF16Ptr(auth.Password), + PasswordLength: uint32(len(auth.Password)), + Domain: syscall.StringToUTF16Ptr(auth.Domain), + DomainLength: uint32(len(auth.Domain)), + User: syscall.StringToUTF16Ptr(auth.UserName), + UserLength: uint32(len(auth.UserName)), + } + } + var ts TimeStamp + sec_ok, _, _ := syscall.Syscall9(sec_fn.AcquireCredentialsHandle, + 9, + 0, + uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("NTLM"))), //'NTLM' or 'Negotiate' for Kerberos + SECPKG_CRED_OUTBOUND, + 0, + uintptr(unsafe.Pointer(identity)), + 0, + 0, + uintptr(unsafe.Pointer(&auth.cred)), + uintptr(unsafe.Pointer(&ts))) + if sec_ok != SEC_E_OK { + return nil, fmt.Errorf("AcquireCredentialsHandle failed %x", sec_ok) + } + + var buf SecBuffer + var desc SecBufferDesc + desc.ulVersion = SECBUFFER_VERSION + desc.cBuffers = 1 + desc.pBuffers = &buf + + outbuf := make([]byte, NTLMBUF_LEN) + buf.cbBuffer = NTLMBUF_LEN + buf.BufferType = SECBUFFER_TOKEN + buf.pvBuffer = &outbuf[0] + + var attrs uint32 + sec_ok, _, _ = syscall.Syscall12(sec_fn.InitializeSecurityContext, + 12, + uintptr(unsafe.Pointer(&auth.cred)), + 0, + uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(auth.Service))), + ISC_REQ, + 0, + SECURITY_NETWORK_DREP, + 0, + 0, + uintptr(unsafe.Pointer(&auth.ctxt)), + uintptr(unsafe.Pointer(&desc)), + uintptr(unsafe.Pointer(&attrs)), + uintptr(unsafe.Pointer(&ts))) + if sec_ok == SEC_I_COMPLETE_AND_CONTINUE || + sec_ok == SEC_I_COMPLETE_NEEDED { + syscall.Syscall6(sec_fn.CompleteAuthToken, + 2, + uintptr(unsafe.Pointer(&auth.ctxt)), + uintptr(unsafe.Pointer(&desc)), + 0, 0, 0, 0) + } else if sec_ok != SEC_E_OK && + sec_ok != SEC_I_CONTINUE_NEEDED { + syscall.Syscall6(sec_fn.FreeCredentialsHandle, + 1, + uintptr(unsafe.Pointer(&auth.cred)), + 0, 0, 0, 0, 0) + return nil, fmt.Errorf("InitialBytes InitializeSecurityContext failed %x", sec_ok) + } + return outbuf[:buf.cbBuffer], nil +} + +func (auth *SSPIAuth) GetResponseBytes(bytes []byte) ([]byte, error) { + var in_buf, out_buf SecBuffer + var in_desc, out_desc SecBufferDesc + + in_desc.ulVersion = SECBUFFER_VERSION + in_desc.cBuffers = 1 + in_desc.pBuffers = &in_buf + + out_desc.ulVersion = SECBUFFER_VERSION + out_desc.cBuffers = 1 + out_desc.pBuffers = &out_buf + + in_buf.BufferType = SECBUFFER_TOKEN + in_buf.pvBuffer = &bytes[0] + in_buf.cbBuffer = uint32(len(bytes)) + + outbuf := make([]byte, NTLMBUF_LEN) + out_buf.BufferType = SECBUFFER_TOKEN + out_buf.pvBuffer = &outbuf[0] + out_buf.cbBuffer = NTLMBUF_LEN + + var attrs uint32 + var ts TimeStamp + sec_ok, _, _ := syscall.Syscall12(sec_fn.InitializeSecurityContext, + 12, + uintptr(unsafe.Pointer(&auth.cred)), + uintptr(unsafe.Pointer(&auth.ctxt)), + uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(auth.Service))), + ISC_REQ, + 0, + SECURITY_NETWORK_DREP, + uintptr(unsafe.Pointer(&in_desc)), + 0, + uintptr(unsafe.Pointer(&auth.ctxt)), + uintptr(unsafe.Pointer(&out_desc)), + uintptr(unsafe.Pointer(&attrs)), + uintptr(unsafe.Pointer(&ts))) + if sec_ok == SEC_I_COMPLETE_AND_CONTINUE || + sec_ok == SEC_I_COMPLETE_NEEDED { + syscall.Syscall6(sec_fn.CompleteAuthToken, + 2, + uintptr(unsafe.Pointer(&auth.ctxt)), + uintptr(unsafe.Pointer(&out_desc)), + 0, 0, 0, 0) + } else if sec_ok != SEC_E_OK && + sec_ok != SEC_I_CONTINUE_NEEDED { + return nil, fmt.Errorf("NextBytes InitializeSecurityContext failed %x", sec_ok) + } + + return outbuf[:out_buf.cbBuffer], nil +} + +func (auth *SSPIAuth) ReleaseContext() { + syscall.Syscall6(sec_fn.DeleteSecurityContext, + 1, + uintptr(unsafe.Pointer(&auth.ctxt)), + 0, 0, 0, 0, 0) + syscall.Syscall6(sec_fn.FreeCredentialsHandle, + 1, + uintptr(unsafe.Pointer(&auth.cred)), + 0, 0, 0, 0, 0) +} diff --git a/utils/proxy.go b/utils/proxy.go new file mode 100644 index 0000000..e794d52 --- /dev/null +++ b/utils/proxy.go @@ -0,0 +1,102 @@ +package utils + +// Originally from github.com/anynines/go-ntlm-client-using-dialcontext/ntlm, +// reimplemented to add support for credentials being passed by commandline + +import ( + "bufio" + "encoding/base64" + "errors" + "io/ioutil" + "net" + "net/http" + "net/url" + "strings" + _ "unsafe" + + ntlmauth "github.com/sensepost/godoh/utils/go-ntlm-auth/ntlm" +) + +func ProxySetup(conn net.Conn, targetAddr string, username string, password string) error { + var auth ntlmauth.NtlmAuthenticator + var authOk bool + if username == "" { + auth, authOk = ntlmauth.GetDefaultCredentialsAuth() + } else { + auth, authOk = ntlmauth.GetAuth(username,password,"","") + } + + if !authOk { + return errors.New("Failed to set NTLM auth") + } + + negotiateMessageBytes, err := auth.GetNegotiateBytes() + if err != nil { + return errors.New("Failed to get NTLM negotiaten bytes") + } + defer auth.ReleaseContext() + + negotiateMsg := base64.StdEncoding.EncodeToString(negotiateMessageBytes) + + hdr := make(http.Header) + hdr.Set("Proxy-Connection", "Keep-Alive") + hdr.Set("Proxy-Authorization", "NTLM "+negotiateMsg) + connectReq := &http.Request{ + Method: "CONNECT", + URL: &url.URL{Opaque: targetAddr}, + Host: targetAddr, + Header: hdr, + } + + connectReq.Write(conn) + + // Read response. + br := bufio.NewReader(conn) + resp, err := http.ReadResponse(br, connectReq) + if err != nil { + return err + } + _, err = ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + resp.Body.Close() + + if resp.StatusCode != 407 { + f := strings.SplitN(resp.Status, " ", 2) + return errors.New(f[1]) + } + + // decode challenge + challengeMessage, err := ntlmauth.ParseChallengeResponse(resp.Header.Get("Proxy-Authenticate")) + if err != nil { + return err + } + + challengeBytes, err := auth.GetResponseBytes(challengeMessage) + if err != nil { + return err + } + + authMsg := base64.StdEncoding.EncodeToString(challengeBytes) + hdr.Set("Proxy-Authorization", "NTLM "+authMsg) + connectReq = &http.Request{ + Method: "CONNECT", + URL: &url.URL{Opaque: targetAddr}, + Host: targetAddr, + Header: hdr, + } + connectReq.Write(conn) + + // Read response. + resp, err = http.ReadResponse(br, connectReq) + if err != nil { + return err + } + if resp.StatusCode != 200 { + f := strings.SplitN(resp.Status, " ", 2) + return errors.New(f[1]) + } + + return nil +}