From 77e243078880ded04b421e8b00c461d6189cb515 Mon Sep 17 00:00:00 2001 From: 5amu Date: Tue, 16 Jan 2024 10:22:45 +0100 Subject: [PATCH 1/7] switch dependency for kerberos in js module to upstream --- go.mod | 9 +- go.sum | 21 +-- pkg/js/libs/kerberos/kerberos.go | 15 ++- pkg/js/libs/kerberos/sendtokdc.go | 209 ++++++++++++++++++++++++++++++ 4 files changed, 235 insertions(+), 19 deletions(-) create mode 100644 pkg/js/libs/kerberos/sendtokdc.go diff --git a/go.mod b/go.mod index 597c6c8f5..d5d218dc1 100644 --- a/go.mod +++ b/go.mod @@ -71,6 +71,7 @@ require ( github.com/go-sql-driver/mysql v1.6.0 github.com/h2non/filetype v1.1.3 github.com/hirochachacha/go-smb2 v1.1.0 + github.com/jcmturner/gokrb5 v8.4.4+incompatible github.com/labstack/echo/v4 v4.10.2 github.com/lib/pq v1.10.1 github.com/mholt/archiver v3.1.1+incompatible @@ -93,7 +94,6 @@ require ( github.com/projectdiscovery/utils v0.0.73-0.20240110205148-46f474b2947f 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 +153,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 @@ -315,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 68112782c..b1f854120 100644 --- a/go.sum +++ b/go.sum @@ -505,7 +505,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= @@ -530,8 +530,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= @@ -572,12 +573,16 @@ 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.4.4+incompatible h1:aX4yX9Lwq0U7yurW6pzRH5JJYDwK0hWIPBTTWfWBOLQ= +github.com/jcmturner/gokrb5 v8.4.4+incompatible/go.mod h1:0Q5eFyVvYsEsZ8xl1A/jUqhXvxUp/X9ELrJm+zieq5E= +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= @@ -907,8 +912,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= @@ -1137,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-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -1149,6 +1151,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 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= diff --git a/pkg/js/libs/kerberos/kerberos.go b/pkg/js/libs/kerberos/kerberos.go index 954dd0ceb..bec79aa98 100644 --- a/pkg/js/libs/kerberos/kerberos.go +++ b/pkg/js/libs/kerberos/kerberos.go @@ -6,15 +6,17 @@ import ( "html/template" "strings" + 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/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 KerberosClient struct { + client *kclient.Client +} type kerberosEnumUserOpts struct { realm string @@ -107,7 +109,8 @@ func (c *KerberosClient) EnumerateUser(domain, controller string, username strin if err != nil { return resp, err } - rb, err := cl.SendToKDC(b, opts.realm) + c.client = cl + rb, err := c.SendToKDC(b, opts.realm) if err == nil { var ASRep messages.ASRep err = ASRep.Unmarshal(rb) diff --git a/pkg/js/libs/kerberos/sendtokdc.go b/pkg/js/libs/kerberos/sendtokdc.go new file mode 100644 index 000000000..d638e5206 --- /dev/null +++ b/pkg/js/libs/kerberos/sendtokdc.go @@ -0,0 +1,209 @@ +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 ( + "encoding/binary" + "fmt" + "io" + "net" + "strings" + "time" + + "github.com/jcmturner/gokrb5/iana/errorcode" + "github.com/jcmturner/gokrb5/v8/messages" +) + +// SendToKDC performs network actions to send data to the KDC. +func (cl *KerberosClient) SendToKDC(b []byte, realm string) ([]byte, error) { + var rb []byte + if cl.client.Config.LibDefaults.UDPPreferenceLimit == 1 { + //1 means we should always use TCP + rb, errtcp := cl.sendKDCTCP(realm, b) + if errtcp != nil { + if e, ok := errtcp.(messages.KRBError); ok { + return rb, e + } + return rb, fmt.Errorf("communication error with KDC via TCP: %v", errtcp) + } + return rb, nil + } + if len(b) <= cl.client.Config.LibDefaults.UDPPreferenceLimit { + //Try UDP first, TCP second + rb, errudp := cl.sendKDCUDP(realm, b) + if errudp != nil { + if e, ok := errudp.(messages.KRBError); ok && e.ErrorCode != errorcode.KRB_ERR_RESPONSE_TOO_BIG { + // Got a KRBError from KDC + // If this is not a KRB_ERR_RESPONSE_TOO_BIG we will return immediately otherwise will try TCP. + return rb, e + } + // Try TCP + r, errtcp := cl.sendKDCTCP(realm, b) + if errtcp != nil { + if e, ok := errtcp.(messages.KRBError); ok { + // Got a KRBError + return r, e + } + return r, fmt.Errorf("failed to communicate with KDC. Attempts made with UDP (%v) and then TCP (%v)", errudp, errtcp) + } + rb = r + } + return rb, nil + } + //Try TCP first, UDP second + rb, errtcp := cl.sendKDCTCP(realm, b) + if errtcp != nil { + if e, ok := errtcp.(messages.KRBError); ok { + // Got a KRBError from KDC so returning and not trying UDP. + return rb, e + } + rb, errudp := cl.sendKDCUDP(realm, b) + if errudp != nil { + if e, ok := errudp.(messages.KRBError); ok { + // Got a KRBError + return rb, e + } + return rb, fmt.Errorf("failed to communicate with KDC. Attempts made with TCP (%v) and then UDP (%v)", errtcp, errudp) + } + } + return rb, nil +} + +// sendKDCUDP sends bytes to the KDC via UDP. +func (cl *KerberosClient) sendKDCUDP(realm string, b []byte) ([]byte, error) { + var r []byte + _, kdcs, err := cl.client.Config.GetKDCs(realm, false) + if err != nil { + return r, err + } + r, err = dialSendUDP(kdcs, b) + if err != nil { + return r, err + } + return checkForKRBError(r) +} + +// dialSendUDP establishes a UDP connection to a KDC. +func dialSendUDP(kdcs map[int]string, b []byte) ([]byte, error) { + var errs []string + for i := 1; i <= len(kdcs); i++ { + conn, err := net.DialTimeout("udp", kdcs[i], 5*time.Second) + if err != nil { + errs = append(errs, fmt.Sprintf("error establishing connection to %s: %v", kdcs[i], err)) + continue + } + if err := conn.SetDeadline(time.Now().Add(5 * time.Second)); err != nil { + errs = append(errs, fmt.Sprintf("error setting deadline on connection to %s: %v", kdcs[i], err)) + continue + } + // conn is guaranteed to be a UDPConn + rb, err := sendUDP(conn.(*net.UDPConn), b) + if err != nil { + errs = append(errs, fmt.Sprintf("error sneding to %s: %v", kdcs[i], err)) + continue + } + return rb, nil + } + return nil, fmt.Errorf("error sending to a KDC: %s", strings.Join(errs, "; ")) +} + +// 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 +} + +// sendKDCTCP sends bytes to the KDC via TCP. +func (cl *KerberosClient) sendKDCTCP(realm string, b []byte) ([]byte, error) { + var r []byte + _, kdcs, err := cl.client.Config.GetKDCs(realm, true) + if err != nil { + return r, err + } + r, err = dialSendTCP(kdcs, b) + if err != nil { + return r, err + } + return checkForKRBError(r) +} + +// dialKDCTCP establishes a TCP connection to a KDC. +func dialSendTCP(kdcs map[int]string, b []byte) ([]byte, error) { + var errs []string + for i := 1; i <= len(kdcs); i++ { + conn, err := net.DialTimeout("tcp", kdcs[i], 5*time.Second) + if err != nil { + errs = append(errs, fmt.Sprintf("error establishing connection to %s: %v", kdcs[i], err)) + continue + } + if err := conn.SetDeadline(time.Now().Add(5 * time.Second)); err != nil { + errs = append(errs, fmt.Sprintf("error setting deadline on connection to %s: %v", kdcs[i], err)) + continue + } + // conn is guaranteed to be a TCPConn + rb, err := sendTCP(conn.(*net.TCPConn), b) + if err != nil { + errs = append(errs, fmt.Sprintf("error sneding to %s: %v", kdcs[i], err)) + continue + } + return rb, nil + } + return nil, fmt.Errorf("error sending to a KDC: %s", strings.Join(errs, "; ")) +} + +// 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 +} + +// checkForKRBError checks if the response bytes from the KDC are a KRBError. +func checkForKRBError(b []byte) ([]byte, error) { + var KRBErr messages.KRBError + if err := KRBErr.Unmarshal(b); err == nil { + return b, KRBErr + } + return b, nil +} From 3b5ce39e86e37895154d8250ce78440a6277b415 Mon Sep 17 00:00:00 2001 From: 5amu Date: Tue, 16 Jan 2024 10:24:26 +0100 Subject: [PATCH 2/7] make protocolstate.IsHostAllowed check the domaincontroller, not the domain --- pkg/js/libs/kerberos/kerberos.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/js/libs/kerberos/kerberos.go b/pkg/js/libs/kerberos/kerberos.go index bec79aa98..b81d21c1a 100644 --- a/pkg/js/libs/kerberos/kerberos.go +++ b/pkg/js/libs/kerberos/kerberos.go @@ -89,9 +89,9 @@ func (c *KerberosClient) EnumerateUser(domain, controller string, username strin resp := EnumerateUserResponse{} - if !protocolstate.IsHostAllowed(domain) { + if !protocolstate.IsHostAllowed(controller) { // host is not valid according to network policy - return resp, protocolstate.ErrHostDenied.Msgf(domain) + return resp, protocolstate.ErrHostDenied.Msgf(controller) } opts, err := newKerbrosEnumUserOpts(domain, controller) @@ -156,9 +156,9 @@ type TGS struct { func (c *KerberosClient) GetServiceTicket(domain, controller string, username, password string, target, spn string) (TGS, error) { var tgs TGS - if !protocolstate.IsHostAllowed(domain) { + if !protocolstate.IsHostAllowed(controller) { // host is not valid according to network policy - return tgs, protocolstate.ErrHostDenied.Msgf(domain) + return tgs, protocolstate.ErrHostDenied.Msgf(controller) } opts, err := newKerbrosEnumUserOpts(domain, controller) From 2c2cc2774aef5479ca0d830318eac3b536b2e5d6 Mon Sep 17 00:00:00 2001 From: Tarun Koyalwar Date: Tue, 23 Jan 2024 04:11:04 +0530 Subject: [PATCH 3/7] feat: introduce nucleijs utils --- pkg/js/devtools/bindgen/cmd/bindgen/main.go | 10 +- pkg/js/devtools/bindgen/generator.go | 4 + pkg/js/generated/go/libkerberos/kerberos.go | 8 +- pkg/js/libs/kerberos/kerberosx.go | 56 ++++++++ pkg/js/utils/nucleijs.go | 146 ++++++++++++++++++++ 5 files changed, 220 insertions(+), 4 deletions(-) create mode 100644 pkg/js/libs/kerberos/kerberosx.go create mode 100644 pkg/js/utils/nucleijs.go 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 921812406..b88fec3dc 100644 --- a/pkg/js/devtools/bindgen/generator.go +++ b/pkg/js/devtools/bindgen/generator.go @@ -346,6 +346,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..98624445f 100644 --- a/pkg/js/generated/go/libkerberos/kerberos.go +++ b/pkg/js/generated/go/libkerberos/kerberos.go @@ -15,16 +15,22 @@ func init() { module.Set( gojs.Objects{ // Functions + "Client": lib_kerberos.NewKerberosClient, // Var and consts // Types (value type) + // "Client": func() lib_kerberos.Client { return lib_kerberos.Client{} }, "EnumerateUserResponse": func() lib_kerberos.EnumerateUserResponse { return lib_kerberos.EnumerateUserResponse{} }, "KerberosClient": func() lib_kerberos.KerberosClient { return lib_kerberos.KerberosClient{} }, + "ServiceOptions": func() lib_kerberos.ServiceOptions { return lib_kerberos.ServiceOptions{} }, + "TGS": func() lib_kerberos.TGS { return lib_kerberos.TGS{} }, // Types (pointer type) + "NewClient": func() *lib_kerberos.Client { return &lib_kerberos.Client{} }, "NewEnumerateUserResponse": func() *lib_kerberos.EnumerateUserResponse { return &lib_kerberos.EnumerateUserResponse{} }, - "NewKerberosClient": func() *lib_kerberos.KerberosClient { return &lib_kerberos.KerberosClient{} }, + "NewServiceOptions": func() *lib_kerberos.ServiceOptions { return &lib_kerberos.ServiceOptions{} }, + "NewTGS": func() *lib_kerberos.TGS { return &lib_kerberos.TGS{} }, }, ).Register() } diff --git a/pkg/js/libs/kerberos/kerberosx.go b/pkg/js/libs/kerberos/kerberosx.go new file mode 100644 index 000000000..86cfe9408 --- /dev/null +++ b/pkg/js/libs/kerberos/kerberosx.go @@ -0,0 +1,56 @@ +package kerberos + +import ( + "fmt" + + "github.com/dop251/goja" + "github.com/projectdiscovery/nuclei/v3/pkg/js/utils" +) + +// Updated Package definations and structure +type Client struct { + nj *utils.NucleiJS // helper functions/bindings +} + +// Constructor for KerberosClient +// creates client object and can be created using new +// var client = new kerberos.Client(domain,controller); +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") + c.nj.Require(controller != "", "controller cannot be empty") + + // Link Constructor to Client and return + return utils.LinkConstructor(call, runtime, c) +} + +// EnumerateUserResponse is the response from EnumerateUsers +func (c *Client) EnumerateUser(username string) (EnumerateUserResponse, error) { + return EnumerateUserResponse{}, nil +} + +type ServiceOptions struct { + Username string + Password string + Target string + SPN string +} + +func (c *Client) GetServiceTicket(sv ServiceOptions) (TGS, error) { + fmt.Printf("get service ticket %v\n", sv) + return TGS{}, nil +} + +// prefer using string or hex over byte array in javascript modules +func (c *Client) Send(data string) (string, error) { + return "", nil +} diff --git a/pkg/js/utils/nucleijs.go b/pkg/js/utils/nucleijs.go new file mode 100644 index 000000000..2b4d44ef5 --- /dev/null +++ b/pkg/js/utils/nucleijs.go @@ -0,0 +1,146 @@ +package utils + +import ( + "fmt" + "reflect" + "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 +func (j *NucleiJS) ThrowError(err error) { + if err == nil { + return + } + panic(j.runtime().ToValue(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) +} From 06d9de3a2734042b58022b7595ff52b8d965b2aa Mon Sep 17 00:00:00 2001 From: Tarun Koyalwar <45962551+tarunKoyalwar@users.noreply.github.com> Date: Mon, 5 Feb 2024 23:21:04 +0530 Subject: [PATCH 4/7] refactor kerberos with nucleijs helper --- pkg/js/generated/go/libkerberos/kerberos.go | 19 +- pkg/js/libs/kerberos/kerberos.go | 196 ----------------- pkg/js/libs/kerberos/kerberosx.go | 203 ++++++++++++++++-- pkg/js/libs/kerberos/sendtokdc.go | 223 ++++++++++---------- pkg/js/utils/nucleijs.go | 14 +- 5 files changed, 319 insertions(+), 336 deletions(-) delete mode 100644 pkg/js/libs/kerberos/kerberos.go diff --git a/pkg/js/generated/go/libkerberos/kerberos.go b/pkg/js/generated/go/libkerberos/kerberos.go index 98624445f..2dc2da320 100644 --- a/pkg/js/generated/go/libkerberos/kerberos.go +++ b/pkg/js/generated/go/libkerberos/kerberos.go @@ -15,22 +15,25 @@ func init() { module.Set( gojs.Objects{ // Functions - "Client": lib_kerberos.NewKerberosClient, + "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": func() lib_kerberos.Client { return lib_kerberos.Client{} }, + "Client": lib_kerberos.NewKerberosClient, "EnumerateUserResponse": func() lib_kerberos.EnumerateUserResponse { return lib_kerberos.EnumerateUserResponse{} }, - "KerberosClient": func() lib_kerberos.KerberosClient { return lib_kerberos.KerberosClient{} }, - "ServiceOptions": func() lib_kerberos.ServiceOptions { return lib_kerberos.ServiceOptions{} }, "TGS": func() lib_kerberos.TGS { return lib_kerberos.TGS{} }, + "Config": func() lib_kerberos.Config { return lib_kerberos.Config{} }, // Types (pointer type) - "NewClient": func() *lib_kerberos.Client { return &lib_kerberos.Client{} }, - "NewEnumerateUserResponse": func() *lib_kerberos.EnumerateUserResponse { return &lib_kerberos.EnumerateUserResponse{} }, - "NewServiceOptions": func() *lib_kerberos.ServiceOptions { return &lib_kerberos.ServiceOptions{} }, - "NewTGS": func() *lib_kerberos.TGS { return &lib_kerberos.TGS{} }, + // "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 b81d21c1a..000000000 --- a/pkg/js/libs/kerberos/kerberos.go +++ /dev/null @@ -1,196 +0,0 @@ -package kerberos - -import ( - "encoding/hex" - "fmt" - "html/template" - "strings" - - 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/protocols/common/protocolstate" -) - -// Client is a kerberos client -type KerberosClient struct { - client *kclient.Client -} - -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(controller) { - // host is not valid according to network policy - return resp, protocolstate.ErrHostDenied.Msgf(controller) - } - - 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 - } - c.client = cl - rb, err := c.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(controller) { - // host is not valid according to network policy - return tgs, protocolstate.ErrHostDenied.Msgf(controller) - } - - 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 index 86cfe9408..e258b7fc4 100644 --- a/pkg/js/libs/kerberos/kerberosx.go +++ b/pkg/js/libs/kerberos/kerberosx.go @@ -2,19 +2,66 @@ package kerberos import ( "fmt" + "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" + ConversionUtil "github.com/projectdiscovery/utils/conversion" ) +// 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 + nj *utils.NucleiJS // helper functions/bindings + Krb5Config *kconfig.Config + Realm string + config Config } // Constructor for KerberosClient -// creates client object and can be created using new -// var client = new kerberos.Client(domain,controller); +// 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) func NewKerberosClient(call goja.ConstructorCall, runtime *goja.Runtime) *goja.Object { // setup nucleijs utils c := &Client{nj: utils.NewNucleiJS(runtime)} @@ -27,30 +74,150 @@ func NewKerberosClient(call goja.ConstructorCall, runtime *goja.Runtime) *goja.O // validate arguments c.nj.Require(domain != "", "domain cannot be empty") - c.nj.Require(controller != "", "controller cannot be empty") + + cfg := kconfig.New() + + if controller != "" { + 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) } -// EnumerateUserResponse is the response from EnumerateUsers -func (c *Client) EnumerateUser(username string) (EnumerateUserResponse, error) { - return EnumerateUserResponse{}, nil +// 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 } -type ServiceOptions struct { - Username string - Password string - Target string - SPN string +// SetConfig sets additional config for the kerberos client +// Signature: SetConfig(cfg) +// @param cfg: @Config +func (c *Client) SetConfig(cfg *Config) { + c.config = *cfg + fmt.Println(c.config) } -func (c *Client) GetServiceTicket(sv ServiceOptions) (TGS, error) { - fmt.Printf("get service ticket %v\n", sv) - return TGS{}, nil +// EnumerateUser and attempt to get AS-REP hash by disabling PA-FX-FAST +// Signature: EnumerateUser(username, {password}) +// @param username: string +// @param password: string (optional) +func (c *Client) EnumerateUser(username string, password string) (EnumerateUserResponse, error) { + c.nj.Require(c.Krb5Config != nil, "Kerberos client not initialized") + if password == "" { + 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 } -// prefer using string or hex over byte array in javascript modules -func (c *Client) Send(data string) (string, error) { - return "", 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 Target: string Target domain +// @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") + + // 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 } diff --git a/pkg/js/libs/kerberos/sendtokdc.go b/pkg/js/libs/kerberos/sendtokdc.go index d638e5206..e1bc27026 100644 --- a/pkg/js/libs/kerberos/sendtokdc.go +++ b/pkg/js/libs/kerberos/sendtokdc.go @@ -5,108 +5,121 @@ package kerberos // 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/iana/errorcode" "github.com/jcmturner/gokrb5/v8/messages" + "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate" ) -// SendToKDC performs network actions to send data to the KDC. -func (cl *KerberosClient) SendToKDC(b []byte, realm string) ([]byte, error) { - var rb []byte - if cl.client.Config.LibDefaults.UDPPreferenceLimit == 1 { - //1 means we should always use TCP - rb, errtcp := cl.sendKDCTCP(realm, b) - if errtcp != nil { - if e, ok := errtcp.(messages.KRBError); ok { - return rb, e - } - return rb, fmt.Errorf("communication error with KDC via TCP: %v", errtcp) - } - return rb, nil +// 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 len(b) <= cl.client.Config.LibDefaults.UDPPreferenceLimit { - //Try UDP first, TCP second - rb, errudp := cl.sendKDCUDP(realm, b) - if errudp != nil { - if e, ok := errudp.(messages.KRBError); ok && e.ErrorCode != errorcode.KRB_ERR_RESPONSE_TOO_BIG { - // Got a KRBError from KDC - // If this is not a KRB_ERR_RESPONSE_TOO_BIG we will return immediately otherwise will try TCP. - return rb, e - } - // Try TCP - r, errtcp := cl.sendKDCTCP(realm, b) - if errtcp != nil { - if e, ok := errtcp.(messages.KRBError); ok { - // Got a KRBError - return r, e - } - return r, fmt.Errorf("failed to communicate with KDC. Attempts made with UDP (%v) and then TCP (%v)", errudp, errtcp) - } - rb = r - } - return rb, nil + if kclient.config.timeout == 0 { + kclient.config.timeout = 5 // default timeout 5 seconds } - //Try TCP first, UDP second - rb, errtcp := cl.sendKDCTCP(realm, b) - if errtcp != nil { - if e, ok := errtcp.(messages.KRBError); ok { - // Got a KRBError from KDC so returning and not trying UDP. - return rb, e + 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 } - rb, errudp := cl.sendKDCUDP(realm, b) - if errudp != nil { - if e, ok := errudp.(messages.KRBError); ok { - // Got a KRBError - return rb, e - } - return rb, fmt.Errorf("failed to communicate with KDC. Attempts made with TCP (%v) and then UDP (%v)", errtcp, errudp) + // if it is krb error no need to do udp + if e, ok := err.(messages.KRBError); ok { + return string(response), e } } - return rb, nil + + // 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 } -// sendKDCUDP sends bytes to the KDC via UDP. -func (cl *KerberosClient) sendKDCUDP(realm string, b []byte) ([]byte, error) { - var r []byte - _, kdcs, err := cl.client.Config.GetKDCs(realm, false) - if err != nil { - return r, err - } - r, err = dialSendUDP(kdcs, b) - if err != nil { - return r, err - } - return checkForKRBError(r) -} +// 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") -// dialSendUDP establishes a UDP connection to a KDC. -func dialSendUDP(kdcs map[int]string, b []byte) ([]byte, error) { var errs []string for i := 1; i <= len(kdcs); i++ { - conn, err := net.DialTimeout("udp", kdcs[i], 5*time.Second) + 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 } - if err := conn.SetDeadline(time.Now().Add(5 * time.Second)); err != nil { - errs = append(errs, fmt.Sprintf("error setting deadline on connection to %s: %v", kdcs[i], err)) - continue - } - // conn is guaranteed to be a UDPConn - rb, err := sendUDP(conn.(*net.UDPConn), b) + 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 sneding to %s: %v", kdcs[i], err)) + errs = append(errs, fmt.Sprintf("error sending to %s: %v", kdcs[i], err)) continue } return rb, nil } - return nil, fmt.Errorf("error sending to a KDC: %s", strings.Join(errs, "; ")) + 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. @@ -129,44 +142,6 @@ func sendUDP(conn *net.UDPConn, b []byte) ([]byte, error) { return r, nil } -// sendKDCTCP sends bytes to the KDC via TCP. -func (cl *KerberosClient) sendKDCTCP(realm string, b []byte) ([]byte, error) { - var r []byte - _, kdcs, err := cl.client.Config.GetKDCs(realm, true) - if err != nil { - return r, err - } - r, err = dialSendTCP(kdcs, b) - if err != nil { - return r, err - } - return checkForKRBError(r) -} - -// dialKDCTCP establishes a TCP connection to a KDC. -func dialSendTCP(kdcs map[int]string, b []byte) ([]byte, error) { - var errs []string - for i := 1; i <= len(kdcs); i++ { - conn, err := net.DialTimeout("tcp", kdcs[i], 5*time.Second) - if err != nil { - errs = append(errs, fmt.Sprintf("error establishing connection to %s: %v", kdcs[i], err)) - continue - } - if err := conn.SetDeadline(time.Now().Add(5 * time.Second)); err != nil { - errs = append(errs, fmt.Sprintf("error setting deadline on connection to %s: %v", kdcs[i], err)) - continue - } - // conn is guaranteed to be a TCPConn - rb, err := sendTCP(conn.(*net.TCPConn), b) - if err != nil { - errs = append(errs, fmt.Sprintf("error sneding to %s: %v", kdcs[i], err)) - continue - } - return rb, nil - } - return nil, fmt.Errorf("error sending to a KDC: %s", strings.Join(errs, "; ")) -} - // sendTCP sends bytes to connection over TCP. func sendTCP(conn *net.TCPConn, b []byte) ([]byte, error) { defer conn.Close() @@ -199,11 +174,33 @@ func sendTCP(conn *net.TCPConn, b []byte) ([]byte, error) { return rb, nil } -// checkForKRBError checks if the response bytes from the KDC are a KRBError. -func checkForKRBError(b []byte) ([]byte, error) { +// 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 index 2b4d44ef5..d3d456081 100644 --- a/pkg/js/utils/nucleijs.go +++ b/pkg/js/utils/nucleijs.go @@ -3,6 +3,7 @@ package utils import ( "fmt" "reflect" + "strings" "sync" "github.com/dop251/goja" @@ -43,7 +44,7 @@ func (j *NucleiJS) runtime() *goja.Runtime { // see: https://arc.net/l/quote/wpenftpc for throwing docs -// ThrowError throws an error in goja runtime +// ThrowError throws an error in goja runtime if is not nil func (j *NucleiJS) ThrowError(err error) { if err == nil { return @@ -51,6 +52,17 @@ func (j *NucleiJS) ThrowError(err error) { 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...))) From 01487ba1b86318f9103aadd7f1aa24c801f3b6d2 Mon Sep 17 00:00:00 2001 From: Tarun Koyalwar Date: Mon, 5 Feb 2024 23:56:16 +0530 Subject: [PATCH 5/7] network policy check + ASREP method --- pkg/js/libs/kerberos/kerberosx.go | 104 +++++++++++++++++++++++++++--- 1 file changed, 95 insertions(+), 9 deletions(-) diff --git a/pkg/js/libs/kerberos/kerberosx.go b/pkg/js/libs/kerberos/kerberosx.go index e258b7fc4..54f555ac6 100644 --- a/pkg/js/libs/kerberos/kerberosx.go +++ b/pkg/js/libs/kerberos/kerberosx.go @@ -1,7 +1,6 @@ package kerberos import ( - "fmt" "strings" "github.com/dop251/goja" @@ -10,6 +9,7 @@ import ( "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" ) @@ -62,6 +62,8 @@ type Client struct { // 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)} @@ -78,6 +80,11 @@ func NewKerberosClient(call goja.ConstructorCall, runtime *goja.Runtime) *goja.O 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") @@ -124,21 +131,21 @@ func NewKerberosClientFromString(cfg string) (*Client, error) { // 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 - fmt.Println(c.config) } // EnumerateUser and attempt to get AS-REP hash by disabling PA-FX-FAST // Signature: EnumerateUser(username, {password}) // @param username: string -// @param password: string (optional) -func (c *Client) EnumerateUser(username string, password string) (EnumerateUserResponse, error) { +func (c *Client) EnumerateUser(username string) (EnumerateUserResponse, error) { c.nj.Require(c.Krb5Config != nil, "Kerberos client not initialized") - if password == "" { - password = "password" - - } + 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() @@ -185,7 +192,6 @@ func (c *Client) EnumerateUser(username string, password string) (EnumerateUserR // Signature: GetServiceTicket(User, Pass, Target, SPN) // @param User: string // @param Pass: string -// @param Target: string Target domain // @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") @@ -193,6 +199,31 @@ func (c *Client) GetServiceTicket(User, Pass, SPN string) (TGS, error) { 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 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") + } + } + } + // 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() @@ -221,3 +252,58 @@ func (c *Client) GetServiceTicket(User, Pass, SPN string) (TGS, error) { 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 +} From d6ee445feb22304dde61e07898e658dbf26883a4 Mon Sep 17 00:00:00 2001 From: Tarun Koyalwar Date: Mon, 5 Feb 2024 23:56:57 +0530 Subject: [PATCH 6/7] go mod tidy --- go.mod | 1 - go.sum | 2 -- 2 files changed, 3 deletions(-) diff --git a/go.mod b/go.mod index d5d218dc1..76a4f96f3 100644 --- a/go.mod +++ b/go.mod @@ -71,7 +71,6 @@ require ( github.com/go-sql-driver/mysql v1.6.0 github.com/h2non/filetype v1.1.3 github.com/hirochachacha/go-smb2 v1.1.0 - github.com/jcmturner/gokrb5 v8.4.4+incompatible github.com/labstack/echo/v4 v4.10.2 github.com/lib/pq v1.10.1 github.com/mholt/archiver v3.1.1+incompatible diff --git a/go.sum b/go.sum index b1f854120..19c2c5350 100644 --- a/go.sum +++ b/go.sum @@ -577,8 +577,6 @@ github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVET 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/gokrb5 v8.4.4+incompatible h1:aX4yX9Lwq0U7yurW6pzRH5JJYDwK0hWIPBTTWfWBOLQ= -github.com/jcmturner/gokrb5 v8.4.4+incompatible/go.mod h1:0Q5eFyVvYsEsZ8xl1A/jUqhXvxUp/X9ELrJm+zieq5E= 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= From 71154918b04e6b162bd67d76544a99a65690d3b1 Mon Sep 17 00:00:00 2001 From: Tarun Koyalwar Date: Tue, 6 Feb 2024 02:03:33 +0530 Subject: [PATCH 7/7] fix network policy error --- pkg/js/libs/kerberos/kerberosx.go | 108 +++++++++--------- .../common/protocolstate/headless.go | 19 +++ 2 files changed, 75 insertions(+), 52 deletions(-) diff --git a/pkg/js/libs/kerberos/kerberosx.go b/pkg/js/libs/kerberos/kerberosx.go index 54f555ac6..b9c95e06f 100644 --- a/pkg/js/libs/kerberos/kerberosx.go +++ b/pkg/js/libs/kerberos/kerberosx.go @@ -13,6 +13,10 @@ import ( 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"` @@ -204,12 +208,12 @@ func (c *Client) GetServiceTicket(User, Pass, SPN string) (TGS, error) { for _, r := range c.Krb5Config.Realms { for _, kdc := range r.KDC { if !protocolstate.IsHostAllowed(kdc) { - c.nj.Throw("KDC address blacklisted by network policy") + 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 blacklisted by network policy") + c.nj.Throw("Kpasswd address %v blacklisted by network policy", kpasswd) } } } @@ -219,7 +223,7 @@ func (c *Client) GetServiceTicket(User, Pass, SPN string) (TGS, error) { _, kdcs, _ := c.Krb5Config.GetKDCs(c.Realm, true) for _, v := range kdcs { if !protocolstate.IsHostAllowed(v) { - c.nj.Throw("KDC address blacklisted by network policy") + c.nj.Throw("KDC address %v blacklisted by network policy", v) } } } @@ -253,57 +257,57 @@ func (c *Client) GetServiceTicket(User, Pass, SPN string) (TGS, error) { 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") +// // 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") - } - } - } +// 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() +// // 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") +// // 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") +// // 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 -} +// // 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/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 }