nuclei/pkg/js/libs/kerberos/kerberos.go
Valerio Casalino 70452efec1
add method GetServiceTicket to the kerberos module (#4422)
* add method GetServiceTicket to the kerberos module

* add target username to service ticket

* destroy kerberos client when function returns
2024-01-12 14:24:39 +05:30

194 lines
4.9 KiB
Go

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
}