diff --git a/go.mod b/go.mod index 554f4c0e5..05269b1ab 100644 --- a/go.mod +++ b/go.mod @@ -93,7 +93,6 @@ require ( github.com/projectdiscovery/utils v0.0.77 github.com/projectdiscovery/wappalyzergo v0.0.109 github.com/redis/go-redis/v9 v9.1.0 - github.com/ropnop/gokrb5/v8 v8.0.0-20201111231119-729746023c02 github.com/sashabaranov/go-openai v1.15.3 github.com/stretchr/testify v1.8.4 github.com/zmap/zgrab2 v0.1.8-0.20230806160807-97ba87c0e706 @@ -153,14 +152,14 @@ require ( github.com/google/go-github/v30 v30.1.0 // indirect github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect - github.com/hashicorp/go-uuid v1.0.2 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/hashicorp/go-version v1.6.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.6 // indirect github.com/hbakhtiyor/strsim v0.0.0-20190107154042-4d2bbb273edf // indirect github.com/jcmturner/aescts/v2 v2.0.0 // indirect github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect - github.com/jcmturner/gofork v1.0.0 // indirect - github.com/jcmturner/rpc/v2 v2.0.2 // indirect + github.com/jcmturner/gofork v1.7.6 // indirect + github.com/jcmturner/rpc/v2 v2.0.3 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/kataras/jwt v0.1.10 // indirect @@ -316,6 +315,7 @@ require ( github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/imdario/mergo v0.3.16 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/jcmturner/gokrb5/v8 v8.4.4 github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/labstack/gommon v0.4.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect diff --git a/go.sum b/go.sum index a5cc5f731..eace032c7 100644 --- a/go.sum +++ b/go.sum @@ -504,7 +504,7 @@ github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= -github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= @@ -529,8 +529,9 @@ github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerX github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= @@ -569,12 +570,14 @@ github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFK github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= -github.com/jcmturner/gofork v1.0.0 h1:J7uCkflzTEhUZ64xqKnkDxq3kzc96ajM1Gli5ktUem8= -github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= +github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= -github.com/jcmturner/rpc/v2 v2.0.2 h1:gMB4IwRXYsWw4Bc6o/az2HJgFUA1ffSh90i26ZJ6Xl0= -github.com/jcmturner/rpc/v2 v2.0.2/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= @@ -908,8 +911,6 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= -github.com/ropnop/gokrb5/v8 v8.0.0-20201111231119-729746023c02 h1:Nk74A6E84pynxLN74hIrQ7Q3cS0/0L5I7coOLNSFAMs= -github.com/ropnop/gokrb5/v8 v8.0.0-20201111231119-729746023c02/go.mod h1:OGEfzIZJs5m/VgAb1BvWR8fH17RTQWx84HTB1koGf9s= github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -1139,7 +1140,6 @@ golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -1150,6 +1150,7 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= diff --git a/pkg/js/devtools/bindgen/cmd/bindgen/main.go b/pkg/js/devtools/bindgen/cmd/bindgen/main.go index cb849c76f..09f493452 100644 --- a/pkg/js/devtools/bindgen/cmd/bindgen/main.go +++ b/pkg/js/devtools/bindgen/cmd/bindgen/main.go @@ -16,12 +16,14 @@ var ( dir string generatedDir string targetModules string + goOnly bool ) func main() { flag.StringVar(&dir, "dir", "libs", "directory to process") flag.StringVar(&generatedDir, "out", "generated", "directory to output generated files") flag.StringVar(&targetModules, "target", "", "target modules to generate") + flag.BoolVar(&goOnly, "go", false, "generate only go files") flag.Parse() log.SetFlags(0) if !fileutil.FolderExists(dir) { @@ -52,9 +54,11 @@ func process() error { } prefixed := "lib" + module - err = data.WriteJSTemplate(filepath.Join(generatedDir, "js/"+prefixed), module) - if err != nil { - return fmt.Errorf("could not write js template: %v", err) + if !goOnly { + err = data.WriteJSTemplate(filepath.Join(generatedDir, "js/"+prefixed), module) + if err != nil { + return fmt.Errorf("could not write js template: %v", err) + } } err = data.WriteGoTemplate(path.Join(generatedDir, "go/"+prefixed), module) if err != nil { diff --git a/pkg/js/devtools/bindgen/generator.go b/pkg/js/devtools/bindgen/generator.go index 8c471e35a..5656e419a 100644 --- a/pkg/js/devtools/bindgen/generator.go +++ b/pkg/js/devtools/bindgen/generator.go @@ -345,6 +345,10 @@ func (d *TemplateData) handleStarExpr(v *ast.StarExpr) string { } func (d *TemplateData) collectTypeFromExternal(pkg *types.Package, pkgName, name string) { + if pkgName == "goja" { + // no need to attempt to collect types from goja ( this is metadata ) + return + } extra := PackageTypeExtra{ Fields: make(map[string]string), } diff --git a/pkg/js/generated/go/libkerberos/kerberos.go b/pkg/js/generated/go/libkerberos/kerberos.go index 2b1fe4913..2dc2da320 100644 --- a/pkg/js/generated/go/libkerberos/kerberos.go +++ b/pkg/js/generated/go/libkerberos/kerberos.go @@ -15,16 +15,25 @@ func init() { module.Set( gojs.Objects{ // Functions + "ASRepToHashcat": lib_kerberos.ASRepToHashcat, + "CheckKrbError": lib_kerberos.CheckKrbError, + "NewKerberosClient": lib_kerberos.NewKerberosClient, + "NewKerberosClientFromString": lib_kerberos.NewKerberosClientFromString, + "SendToKDC": lib_kerberos.SendToKDC, + "TGStoHashcat": lib_kerberos.TGStoHashcat, // Var and consts // Types (value type) + "Client": lib_kerberos.NewKerberosClient, "EnumerateUserResponse": func() lib_kerberos.EnumerateUserResponse { return lib_kerberos.EnumerateUserResponse{} }, - "KerberosClient": func() lib_kerberos.KerberosClient { return lib_kerberos.KerberosClient{} }, + "TGS": func() lib_kerberos.TGS { return lib_kerberos.TGS{} }, + "Config": func() lib_kerberos.Config { return lib_kerberos.Config{} }, // Types (pointer type) - "NewEnumerateUserResponse": func() *lib_kerberos.EnumerateUserResponse { return &lib_kerberos.EnumerateUserResponse{} }, - "NewKerberosClient": func() *lib_kerberos.KerberosClient { return &lib_kerberos.KerberosClient{} }, + // "NewClient": func() *lib_kerberos.Client { return &lib_kerberos.Client{} }, + // "NewEnumerateUserResponse": func() *lib_kerberos.EnumerateUserResponse { return &lib_kerberos.EnumerateUserResponse{} }, + // "NewTGS": func() *lib_kerberos.TGS { return &lib_kerberos.TGS{} }, }, ).Register() } diff --git a/pkg/js/libs/kerberos/kerberos.go b/pkg/js/libs/kerberos/kerberos.go deleted file mode 100644 index 954dd0ceb..000000000 --- a/pkg/js/libs/kerberos/kerberos.go +++ /dev/null @@ -1,193 +0,0 @@ -package kerberos - -import ( - "encoding/hex" - "fmt" - "html/template" - "strings" - - "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate" - kclient "github.com/ropnop/gokrb5/v8/client" - kconfig "github.com/ropnop/gokrb5/v8/config" - "github.com/ropnop/gokrb5/v8/iana/errorcode" - "github.com/ropnop/gokrb5/v8/messages" -) - -// Client is a kerberos client -type KerberosClient struct{} - -type kerberosEnumUserOpts struct { - realm string - config *kconfig.Config - kdcs map[int]string -} - -// Taken from kerbrute: https://github.com/ropnop/kerbrute/blob/master/session/session.go - -const krb5ConfigTemplateDNS = `[libdefaults] -dns_lookup_kdc = true -default_realm = {{.Realm}} -` - -const krb5ConfigTemplateKDC = `[libdefaults] -default_realm = {{.Realm}} -[realms] -{{.Realm}} = { - kdc = {{.DomainController}} - admin_server = {{.DomainController}} -} -` - -func buildKrb5Template(realm, domainController string) string { - data := map[string]interface{}{ - "Realm": realm, - "DomainController": domainController, - } - var kTemplate string - if domainController == "" { - kTemplate = krb5ConfigTemplateDNS - } else { - kTemplate = krb5ConfigTemplateKDC - } - t := template.Must(template.New("krb5ConfigString").Parse(kTemplate)) - builder := &strings.Builder{} - if err := t.Execute(builder, data); err != nil { - panic(err) - } - return builder.String() -} - -func newKerbrosEnumUserOpts(domain, domainController string) (*kerberosEnumUserOpts, error) { - realm := strings.ToUpper(domain) - configstring := buildKrb5Template(realm, domainController) - Config, err := kconfig.NewFromString(configstring) - if err != nil { - return nil, err - } - _, kdcs, err := Config.GetKDCs(realm, false) - if err != nil { - err = fmt.Errorf("couldn't find any KDCs for realm %s. Please specify a Domain Controller", realm) - return nil, err - } - return &kerberosEnumUserOpts{realm: realm, config: Config, kdcs: kdcs}, nil -} - -// EnumerateUserResponse is the response from EnumerateUser -type EnumerateUserResponse struct { - Valid bool - ASREPHash string -} - -// EnumerateUser returns true if the user exists in the domain -// -// If the user is not found, false is returned. -// If the user is found, true is returned. Optionally, the AS-REP -// hash is also returned if discovered. -func (c *KerberosClient) EnumerateUser(domain, controller string, username string) (EnumerateUserResponse, error) { - - resp := EnumerateUserResponse{} - - if !protocolstate.IsHostAllowed(domain) { - // host is not valid according to network policy - return resp, protocolstate.ErrHostDenied.Msgf(domain) - } - - opts, err := newKerbrosEnumUserOpts(domain, controller) - if err != nil { - return resp, err - } - cl := kclient.NewWithPassword(username, opts.realm, "foobar", opts.config, kclient.DisablePAFXFAST(true)) - defer cl.Destroy() - - req, err := messages.NewASReqForTGT(cl.Credentials.Domain(), cl.Config, cl.Credentials.CName()) - if err != nil { - return resp, err - } - b, err := req.Marshal() - if err != nil { - return resp, err - } - rb, err := cl.SendToKDC(b, opts.realm) - if err == nil { - var ASRep messages.ASRep - err = ASRep.Unmarshal(rb) - if err != nil { - // something went wrong, it's not a valid response - return resp, err - } - hashcatString, _ := asRepToHashcat(ASRep) - resp.Valid = true - resp.ASREPHash = hashcatString - return resp, nil - } - e, ok := err.(messages.KRBError) - if !ok { - return resp, nil - } - switch e.ErrorCode { - case errorcode.KDC_ERR_C_PRINCIPAL_UNKNOWN: - return resp, nil - case errorcode.KDC_ERR_PREAUTH_REQUIRED: - resp.Valid = true - return resp, nil - default: - return resp, err - - } -} - -func asRepToHashcat(asrep messages.ASRep) (string, error) { - return fmt.Sprintf("$krb5asrep$%d$%s@%s:%s$%s", - asrep.EncPart.EType, - asrep.CName.PrincipalNameString(), - asrep.CRealm, - hex.EncodeToString(asrep.EncPart.Cipher[:16]), - hex.EncodeToString(asrep.EncPart.Cipher[16:])), nil -} - -type TGS struct { - Ticket messages.Ticket - Hash string -} - -func (c *KerberosClient) GetServiceTicket(domain, controller string, username, password string, target, spn string) (TGS, error) { - var tgs TGS - - if !protocolstate.IsHostAllowed(domain) { - // host is not valid according to network policy - return tgs, protocolstate.ErrHostDenied.Msgf(domain) - } - - opts, err := newKerbrosEnumUserOpts(domain, controller) - if err != nil { - return tgs, err - } - cl := kclient.NewWithPassword(username, opts.realm, password, opts.config, kclient.DisablePAFXFAST(true)) - defer cl.Destroy() - - ticket, _, err := cl.GetServiceTicket(spn) - if err != nil { - return tgs, err - } - - hashcat, err := tgsToHashcat(ticket, target) - if err != nil { - return tgs, err - } - - return TGS{ - Ticket: ticket, - Hash: hashcat, - }, nil -} - -func tgsToHashcat(tgs messages.Ticket, username string) (string, error) { - return fmt.Sprintf("$krb5tgs$%d$*%s$%s$%s*$%s$%s", - tgs.EncPart.EType, - username, - tgs.Realm, - strings.Join(tgs.SName.NameString[:], "/"), - hex.EncodeToString(tgs.EncPart.Cipher[:16]), - hex.EncodeToString(tgs.EncPart.Cipher[16:]), - ), nil -} diff --git a/pkg/js/libs/kerberos/kerberosx.go b/pkg/js/libs/kerberos/kerberosx.go new file mode 100644 index 000000000..b9c95e06f --- /dev/null +++ b/pkg/js/libs/kerberos/kerberosx.go @@ -0,0 +1,313 @@ +package kerberos + +import ( + "strings" + + "github.com/dop251/goja" + kclient "github.com/jcmturner/gokrb5/v8/client" + kconfig "github.com/jcmturner/gokrb5/v8/config" + "github.com/jcmturner/gokrb5/v8/iana/errorcode" + "github.com/jcmturner/gokrb5/v8/messages" + "github.com/projectdiscovery/nuclei/v3/pkg/js/utils" + "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate" + ConversionUtil "github.com/projectdiscovery/utils/conversion" +) + +// Known Issues: +// Hardcoded timeout in gokrb5 library +// TGT / Session Handling not exposed + +// EnumerateUserResponse is the response from EnumerateUser +type EnumerateUserResponse struct { + Valid bool `json:"valid"` + ASREPHash string `json:"asrep_hash"` + Error string `json:"error"` +} + +// TGS is the response from GetServiceTicket +type TGS struct { + Ticket messages.Ticket `json:"ticket"` + Hash string `json:"hash"` + ErrMsg string `json:"error"` +} + +// Config is extra configuration for the kerberos client +type Config struct { + ip string + timeout int // in seconds +} + +func (c *Config) SetIPAddress(ip string) *Config { + c.ip = ip + return c +} + +func (c *Config) SetTimeout(timeout int) *Config { + c.timeout = timeout + return c +} + +// Example Values for jargons +// Realm: ACME.COM (Authentical zone / security area) +// Domain: acme.com (Public website / domain) +// DomainController: dc.acme.com (Domain Controller / Active Directory Server) +// KDC: kdc.acme.com (Key Distribution Center / Authentication Server) + +// Updated Package definations and structure +type Client struct { + nj *utils.NucleiJS // helper functions/bindings + Krb5Config *kconfig.Config + Realm string + config Config +} + +// Constructor for KerberosClient +// if controller is empty a dns lookup for default kdc server will be performed +// Signature: Client(domain, {controller}) +// @param domain: string +// @param controller: string (optional) +// When controller is empty or not given krb5 will perform a DNS lookup for the default KDC server +// and retrieve its address from the DNS server +func NewKerberosClient(call goja.ConstructorCall, runtime *goja.Runtime) *goja.Object { + // setup nucleijs utils + c := &Client{nj: utils.NewNucleiJS(runtime)} + c.nj.ObjectSig = "Client(domain, controller)" // will be included in error messages + + // get arguments (type assertion is efficient than reflection) + // when accepting type as input like net.Conn we can use utils.GetArg + domain, _ := c.nj.GetArg(call.Arguments, 0).(string) + controller, _ := c.nj.GetArg(call.Arguments, 1).(string) + + // validate arguments + c.nj.Require(domain != "", "domain cannot be empty") + + cfg := kconfig.New() + + if controller != "" { + // validate controller hostport + if !protocolstate.IsHostAllowed(controller) { + c.nj.Throw("domain controller address blacklisted by network policy") + } + + tmp := strings.Split(controller, ":") + if len(tmp) == 1 { + tmp = append(tmp, "88") + } + realm := strings.ToUpper(domain) + cfg.LibDefaults.DefaultRealm = realm // set default realm + cfg.Realms = []kconfig.Realm{ + { + Realm: realm, + KDC: []string{tmp[0] + ":" + tmp[1]}, + AdminServer: []string{tmp[0] + ":" + tmp[1]}, + KPasswdServer: []string{tmp[0] + ":464"}, // default password server port + }, + } + cfg.DomainRealm = make(kconfig.DomainRealm) + } else { + // if controller is empty use DNS lookup + cfg.LibDefaults.DNSLookupKDC = true + cfg.LibDefaults.DefaultRealm = strings.ToUpper(domain) + cfg.DomainRealm = make(kconfig.DomainRealm) + } + c.Krb5Config = cfg + c.Realm = strings.ToUpper(domain) + + // Link Constructor to Client and return + return utils.LinkConstructor(call, runtime, c) +} + +// NewKerberosClientFromString creates a new kerberos client from a string +// by parsing krb5.conf +// @param cfg: string +// Example krb5.conf: +// [libdefaults] +// default_realm = ACME.COM +// dns_lookup_kdc = true +func NewKerberosClientFromString(cfg string) (*Client, error) { + config, err := kconfig.NewFromString(cfg) + if err != nil { + return nil, err + } + return &Client{Krb5Config: config}, nil +} + +// SetConfig sets additional config for the kerberos client +// Signature: SetConfig(cfg) +// @param cfg: @Config +// Note: as of now ip and timeout overrides are only supported +// in EnumerateUser due to fastdialer but can be extended to other methods currently +func (c *Client) SetConfig(cfg *Config) { + if cfg == nil { + c.nj.Throw("config cannot be nil") + } + c.config = *cfg +} + +// EnumerateUser and attempt to get AS-REP hash by disabling PA-FX-FAST +// Signature: EnumerateUser(username, {password}) +// @param username: string +func (c *Client) EnumerateUser(username string) (EnumerateUserResponse, error) { + c.nj.Require(c.Krb5Config != nil, "Kerberos client not initialized") + password := "password" + // client does not actually attempt connection it manages state here + client := kclient.NewWithPassword(username, c.Realm, password, c.Krb5Config, kclient.DisablePAFXFAST(true)) + defer client.Destroy() + + // generate ASReq hash + req, err := messages.NewASReqForTGT(client.Credentials.Domain(), client.Config, client.Credentials.CName()) + c.nj.HandleError(err, "failed to generate TGT request") + + // marshal request + b, err := req.Marshal() + c.nj.HandleError(err, "failed to marshal TGT request") + + data, err := SendToKDC(c, string(b)) + rb := ConversionUtil.Bytes(data) + + if err == nil { + var ASRep messages.ASRep + resp := EnumerateUserResponse{Valid: true} + err = ASRep.Unmarshal(rb) + if err != nil { + resp.Error = err.Error() + return resp, nil + } + hashcatString, _ := ASRepToHashcat(ASRep) + resp.ASREPHash = hashcatString + return resp, nil + } + + resp := EnumerateUserResponse{} + e, ok := err.(messages.KRBError) + if !ok { + return resp, err + } + if e.ErrorCode == errorcode.KDC_ERR_PREAUTH_REQUIRED { + resp.Valid = true + resp.Error = errorcode.Lookup(e.ErrorCode) + return resp, nil + } + resp.Error = errorcode.Lookup(e.ErrorCode) + return resp, nil +} + +// GetServiceTicket returns a TGS for a given user, password, target and SPN +// Signature: GetServiceTicket(User, Pass, Target, SPN) +// @param User: string +// @param Pass: string +// @param SPN: string Service Principal Name +func (c *Client) GetServiceTicket(User, Pass, SPN string) (TGS, error) { + c.nj.Require(c.Krb5Config != nil, "Kerberos client not initialized") + c.nj.Require(User != "", "User cannot be empty") + c.nj.Require(Pass != "", "Pass cannot be empty") + c.nj.Require(SPN != "", "SPN cannot be empty") + + if len(c.Krb5Config.Realms) > 0 { + // this means dc address was given + for _, r := range c.Krb5Config.Realms { + for _, kdc := range r.KDC { + if !protocolstate.IsHostAllowed(kdc) { + c.nj.Throw("KDC address %v blacklisted by network policy", kdc) + } + } + for _, kpasswd := range r.KPasswdServer { + if !protocolstate.IsHostAllowed(kpasswd) { + c.nj.Throw("Kpasswd address %v blacklisted by network policy", kpasswd) + } + } + } + } else { + // here net.Dialer is used instead of fastdialer hence get possible addresses + // and check if they are allowed by network policy + _, kdcs, _ := c.Krb5Config.GetKDCs(c.Realm, true) + for _, v := range kdcs { + if !protocolstate.IsHostAllowed(v) { + c.nj.Throw("KDC address %v blacklisted by network policy", v) + } + } + } + + // client does not actually attempt connection it manages state here + client := kclient.NewWithPassword(User, c.Realm, Pass, c.Krb5Config, kclient.DisablePAFXFAST(true)) + defer client.Destroy() + + resp := TGS{} + + ticket, _, err := client.GetServiceTicket(SPN) + resp.Ticket = ticket + if err != nil { + if code, ok := err.(messages.KRBError); ok { + resp.ErrMsg = errorcode.Lookup(code.ErrorCode) + return resp, err + } + return resp, err + } + // convert AS-REP to hashcat format + hashcat, err := TGStoHashcat(ticket, c.Realm) + if err != nil { + if code, ok := err.(messages.KRBError); ok { + resp.ErrMsg = errorcode.Lookup(code.ErrorCode) + return resp, err + } + return resp, err + } + resp.Ticket = ticket + resp.Hash = hashcat + return resp, nil +} + +// // GetASREP returns AS-REP for a given user and password +// // it contains Client's TGT , Principal and Session Key +// // Signature: GetASREP(User, Pass) +// // @param User: string +// // @param Pass: string +// func (c *Client) GetASREP(User, Pass string) messages.ASRep { +// c.nj.Require(c.Krb5Config != nil, "Kerberos client not initialized") +// c.nj.Require(User != "", "User cannot be empty") +// c.nj.Require(Pass != "", "Pass cannot be empty") + +// if len(c.Krb5Config.Realms) > 0 { +// // this means dc address was given +// for _, r := range c.Krb5Config.Realms { +// for _, kdc := range r.KDC { +// if !protocolstate.IsHostAllowed(kdc) { +// c.nj.Throw("KDC address blacklisted by network policy") +// } +// } +// for _, kpasswd := range r.KPasswdServer { +// if !protocolstate.IsHostAllowed(kpasswd) { +// c.nj.Throw("Kpasswd address blacklisted by network policy") +// } +// } +// } +// } else { +// // here net.Dialer is used instead of fastdialer hence get possible addresses +// // and check if they are allowed by network policy +// _, kdcs, _ := c.Krb5Config.GetKDCs(c.Realm, true) +// for _, v := range kdcs { +// if !protocolstate.IsHostAllowed(v) { +// c.nj.Throw("KDC address blacklisted by network policy") +// } +// } +// } + +// // login to get TGT +// cl := kclient.NewWithPassword(User, c.Realm, Pass, c.Krb5Config, kclient.DisablePAFXFAST(true)) +// defer cl.Destroy() + +// // generate ASReq +// ASReq, err := messages.NewASReqForTGT(cl.Credentials.Domain(), cl.Config, cl.Credentials.CName()) +// c.nj.HandleError(err, "failed to generate TGT request") + +// // exchange AS-REQ for AS-REP +// resp, err := cl.ASExchange(c.Realm, ASReq, 0) +// c.nj.HandleError(err, "failed to exchange AS-REQ") + +// // try to decrypt encrypted parts of the response and TGT +// key, err := resp.DecryptEncPart(cl.Credentials) +// if err == nil { +// _ = resp.Ticket.Decrypt(key) +// } +// return resp +// } diff --git a/pkg/js/libs/kerberos/sendtokdc.go b/pkg/js/libs/kerberos/sendtokdc.go new file mode 100644 index 000000000..e1bc27026 --- /dev/null +++ b/pkg/js/libs/kerberos/sendtokdc.go @@ -0,0 +1,206 @@ +package kerberos + +// the following code is adapted from the original library +// https://github.com/jcmturner/gokrb5/blob/855dbc707a37a21467aef6c0245fcf3328dc39ed/v8/client/network.go +// it is copied here because the library does not export "SendToKDC()" + +import ( + "context" + "encoding/binary" + "encoding/hex" + "fmt" + "io" + "net" + "strings" + "time" + + "github.com/jcmturner/gokrb5/v8/messages" + "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate" +) + +// sendtokdc.go deals with actual sending and receiving responses from KDC +// SendToKDC sends a message to the KDC and returns the response. +func SendToKDC(kclient *Client, msg string) (string, error) { + if kclient == nil || kclient.nj == nil || kclient.Krb5Config == nil || kclient.Realm == "" { + return "", fmt.Errorf("kerberos client is not initialized") + } + if kclient.config.timeout == 0 { + kclient.config.timeout = 5 // default timeout 5 seconds + } + var response []byte + var err error + + response, err = sendToKDCTcp(kclient, msg) + if err == nil { + // if it related to tcp + bin, err := CheckKrbError(response) + if err == nil { + return string(bin), nil + } + // if it is krb error no need to do udp + if e, ok := err.(messages.KRBError); ok { + return string(response), e + } + } + + // fallback to udp + response, err = sendToKDCUdp(kclient, msg) + if err == nil { + // if it related to udp + bin, err := CheckKrbError(response) + if err == nil { + return string(bin), nil + } + } + return string(response), err +} + +// sendToKDCTcp sends a message to the KDC via TCP. +func sendToKDCTcp(kclient *Client, msg string) ([]byte, error) { + _, kdcs, err := kclient.Krb5Config.GetKDCs(kclient.Realm, true) + kclient.nj.HandleError(err, "error getting KDCs") + kclient.nj.Require(len(kdcs) > 0, "no KDCs found") + + var errs []string + for i := 1; i <= len(kdcs); i++ { + host, port, err := net.SplitHostPort(kdcs[i]) + if err == nil && kclient.config.ip != "" { + // use that ip address instead of realm/domain for resolving + host = kclient.config.ip + } + tcpConn, err := protocolstate.Dialer.Dial(context.TODO(), "tcp", net.JoinHostPort(host, port)) + if err != nil { + errs = append(errs, fmt.Sprintf("error establishing connection to %s: %v", kdcs[i], err)) + continue + } + defer tcpConn.Close() + _ = tcpConn.SetDeadline(time.Now().Add(time.Duration(kclient.config.timeout) * time.Second)) //read and write deadline + rb, err := sendTCP(tcpConn.(*net.TCPConn), []byte(msg)) + if err != nil { + errs = append(errs, fmt.Sprintf("error sending to %s: %v", kdcs[i], err)) + continue + } + return rb, nil + } + if len(errs) > 0 { + return nil, fmt.Errorf("error sending to a KDC: %s", strings.Join(errs, "; ")) + } + return nil, nil +} + +// sendToKDCUdp sends a message to the KDC via UDP. +func sendToKDCUdp(kclient *Client, msg string) ([]byte, error) { + _, kdcs, err := kclient.Krb5Config.GetKDCs(kclient.Realm, true) + kclient.nj.HandleError(err, "error getting KDCs") + kclient.nj.Require(len(kdcs) > 0, "no KDCs found") + + var errs []string + for i := 1; i <= len(kdcs); i++ { + host, port, err := net.SplitHostPort(kdcs[i]) + if err == nil && kclient.config.ip != "" { + // use that ip address instead of realm/domain for resolving + host = kclient.config.ip + } + udpConn, err := protocolstate.Dialer.Dial(context.TODO(), "udp", net.JoinHostPort(host, port)) + if err != nil { + errs = append(errs, fmt.Sprintf("error establishing connection to %s: %v", kdcs[i], err)) + continue + } + defer udpConn.Close() + _ = udpConn.SetDeadline(time.Now().Add(time.Duration(kclient.config.timeout) * time.Second)) //read and write deadline + rb, err := sendUDP(udpConn.(*net.UDPConn), []byte(msg)) + if err != nil { + errs = append(errs, fmt.Sprintf("error sending to %s: %v", kdcs[i], err)) + continue + } + return rb, nil + } + if len(errs) > 0 { + // fallback to tcp + return nil, fmt.Errorf("error sending to a KDC: %s", strings.Join(errs, "; ")) + } + return nil, nil +} + +// sendUDP sends bytes to connection over UDP. +func sendUDP(conn *net.UDPConn, b []byte) ([]byte, error) { + var r []byte + defer conn.Close() + _, err := conn.Write(b) + if err != nil { + return r, fmt.Errorf("error sending to (%s): %v", conn.RemoteAddr().String(), err) + } + udpbuf := make([]byte, 4096) + n, _, err := conn.ReadFrom(udpbuf) + r = udpbuf[:n] + if err != nil { + return r, fmt.Errorf("sending over UDP failed to %s: %v", conn.RemoteAddr().String(), err) + } + if len(r) < 1 { + return r, fmt.Errorf("no response data from %s", conn.RemoteAddr().String()) + } + return r, nil +} + +// sendTCP sends bytes to connection over TCP. +func sendTCP(conn *net.TCPConn, b []byte) ([]byte, error) { + defer conn.Close() + var r []byte + // RFC 4120 7.2.2 specifies the first 4 bytes indicate the length of the message in big endian order. + hb := make([]byte, 4) + binary.BigEndian.PutUint32(hb, uint32(len(b))) + b = append(hb, b...) + + _, err := conn.Write(b) + if err != nil { + return r, fmt.Errorf("error sending to KDC (%s): %v", conn.RemoteAddr().String(), err) + } + + sh := make([]byte, 4) + _, err = conn.Read(sh) + if err != nil { + return r, fmt.Errorf("error reading response size header: %v", err) + } + s := binary.BigEndian.Uint32(sh) + + rb := make([]byte, s) + _, err = io.ReadFull(conn, rb) + if err != nil { + return r, fmt.Errorf("error reading response: %v", err) + } + if len(rb) < 1 { + return r, fmt.Errorf("no response data from KDC %s", conn.RemoteAddr().String()) + } + return rb, nil +} + +// CheckKrbError checks if the response bytes from the KDC are a KRBError. +func CheckKrbError(b []byte) ([]byte, error) { + var KRBErr messages.KRBError + if err := KRBErr.Unmarshal(b); err == nil { + return b, KRBErr + } + return b, nil +} + +// TGStoHashcat converts a TGS to a hashcat format. +func TGStoHashcat(tgs messages.Ticket, username string) (string, error) { + return fmt.Sprintf("$krb5tgs$%d$*%s$%s$%s*$%s$%s", + tgs.EncPart.EType, + username, + tgs.Realm, + strings.Join(tgs.SName.NameString[:], "/"), + hex.EncodeToString(tgs.EncPart.Cipher[:16]), + hex.EncodeToString(tgs.EncPart.Cipher[16:]), + ), nil +} + +// ASRepToHashcat converts an AS-REP message to a hashcat format +func ASRepToHashcat(asrep messages.ASRep) (string, error) { + return fmt.Sprintf("$krb5asrep$%d$%s@%s:%s$%s", + asrep.EncPart.EType, + asrep.CName.PrincipalNameString(), + asrep.CRealm, + hex.EncodeToString(asrep.EncPart.Cipher[:16]), + hex.EncodeToString(asrep.EncPart.Cipher[16:])), nil +} diff --git a/pkg/js/utils/nucleijs.go b/pkg/js/utils/nucleijs.go new file mode 100644 index 000000000..d3d456081 --- /dev/null +++ b/pkg/js/utils/nucleijs.go @@ -0,0 +1,158 @@ +package utils + +import ( + "fmt" + "reflect" + "strings" + "sync" + + "github.com/dop251/goja" +) + +// temporary on demand runtime to throw errors when vm is not available +var ( + tmpRuntime *goja.Runtime + runtimeInit func() = sync.OnceFunc(func() { + tmpRuntime = goja.New() + }) +) + +func getRuntime() *goja.Runtime { + runtimeInit() + return tmpRuntime +} + +// NucleiJS is js bindings that handles goja runtime related issue +// and allows setting a defer statements to close resources +type NucleiJS struct { + vm *goja.Runtime + ObjectSig string +} + +// NewNucleiJS creates a new nucleijs instance +func NewNucleiJS(vm *goja.Runtime) *NucleiJS { + return &NucleiJS{vm: vm} +} + +// internal runtime getter +func (j *NucleiJS) runtime() *goja.Runtime { + if j == nil { + return getRuntime() + } + return j.vm +} + +// see: https://arc.net/l/quote/wpenftpc for throwing docs + +// ThrowError throws an error in goja runtime if is not nil +func (j *NucleiJS) ThrowError(err error) { + if err == nil { + return + } + panic(j.runtime().ToValue(err.Error())) +} + +// HandleError handles error and throws a +func (j *NucleiJS) HandleError(err error, msg ...string) { + if err == nil { + return + } + if len(msg) == 0 { + j.ThrowError(err) + } + j.Throw(fmt.Sprintf("%s: %s", strings.Join(msg, ":"), err.Error())) +} + +// Throw throws an error in goja runtime +func (j *NucleiJS) Throw(format string, args ...interface{}) { + panic(j.runtime().ToValue(fmt.Sprintf(format, args...))) +} + +// GetArg returns argument at index from goja runtime if not found throws error +func (j *NucleiJS) GetArg(args []goja.Value, index int) any { + if index >= len(args) { + j.Throw("Missing argument at index %v: %v", index, j.ObjectSig) + } + val := args[index] + if goja.IsUndefined(val) { + j.Throw("Missing argument at index %v: %v", index, j.ObjectSig) + } + return val.Export() +} + +// GetArgSafe returns argument at index from goja runtime if not found returns default value +func (j *NucleiJS) GetArgSafe(args []goja.Value, index int, defaultValue any) any { + if index >= len(args) { + return defaultValue + } + val := args[index] + if goja.IsUndefined(val) { + return defaultValue + } + return val.Export() +} + +// Require throws an error if expression is false +func (j *NucleiJS) Require(expr bool, msg string) { + if !expr { + j.Throw(msg) + } +} + +// LinkConstructor links a type with invocation doing this allows +// usage of instance of type in js +func LinkConstructor[T any](call goja.ConstructorCall, vm *goja.Runtime, obj T) *goja.Object { + instance := vm.ToValue(obj).(*goja.Object) + _ = instance.SetPrototype(call.This.Prototype()) + return instance +} + +// GetStructType gets a type defined in go and passed as argument from goja runtime if not found throws error +// Donot use this unless you are accepting a struct type from constructor +func GetStructType[T any](nj *NucleiJS, args []goja.Value, index int, FuncSig string) T { + if nj == nil { + nj = &NucleiJS{} + } + if index >= len(args) { + if FuncSig == "" { + nj.Throw("Missing argument at index %v", index) + } + nj.Throw("Missing arguments expected : %v", FuncSig) + } + value := args[index] + // validate type + var ptr T + expected := reflect.ValueOf(ptr).Type() + argType := expected.Name() + valueType := value.ExportType().Name() + + if argType != valueType { + nj.Throw("Type Mismatch expected %v got %v", argType, valueType) + } + + ptrValue := reflect.New(expected).Elem() + ptrValue.Set(reflect.ValueOf(value.Export())) + + return ptrValue.Interface().(T) +} + +// GetStructTypeSafe gets an type defined in go and passed as argument from goja runtime if not found returns default value +// Donot use this unless you are accepting a struct type from constructor +func GetStructTypeSafe[T any](nj *NucleiJS, args []goja.Value, index int, defaultValue T) T { + if nj == nil { + nj = &NucleiJS{} + } + if index >= len(args) { + return defaultValue + } + value := args[index] + // validate type + var ptr T + argType := reflect.ValueOf(ptr).Type().Name() + valueType := value.ExportType().Name() + + if argType != valueType { + return defaultValue + } + return value.ToObject(nj.runtime()).Export().(T) +} diff --git a/pkg/protocols/common/protocolstate/headless.go b/pkg/protocols/common/protocolstate/headless.go index 0b58d5e32..755d367b9 100644 --- a/pkg/protocols/common/protocolstate/headless.go +++ b/pkg/protocols/common/protocolstate/headless.go @@ -1,6 +1,7 @@ package protocolstate import ( + "net" "strings" "github.com/go-rod/rod" @@ -81,6 +82,24 @@ func IsHostAllowed(targetUrl string) bool { if NetworkPolicy == nil { return true } + sepCount := strings.Count(targetUrl, ":") + if sepCount > 1 { + // most likely a ipv6 address (parse url and validate host) + return NetworkPolicy.Validate(targetUrl) + } + if sepCount == 1 { + host, _, _ := net.SplitHostPort(targetUrl) + if _, ok := NetworkPolicy.ValidateHost(host); !ok { + return false + } + return true + // portInt, _ := strconv.Atoi(port) + // fixme: broken port validation logic in networkpolicy + // if !NetworkPolicy.ValidatePort(portInt) { + // return false + // } + } + // just a hostname or ip without port _, ok := NetworkPolicy.ValidateHost(targetUrl) return ok }