mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2025-12-17 23:35:27 +00:00
* introducing execution id * wip * . * adding separate execution context id * lint * vet * fixing pg dialers * test ignore * fixing loader FD limit * test * fd fix * wip: remove CloseProcesses() from dev merge * wip: fix merge issue * protocolstate: stop memguarding on last dialer delete * avoid data race in dialers.RawHTTPClient * use shared logger and avoid race conditions * use shared logger and avoid race conditions * go mod * patch executionId into compiled template cache * clean up comment in Parse * go mod update * bump echarts * address merge issues * fix use of gologger * switch cmd/nuclei to options.Logger * address merge issues with go.mod * go vet: address copy of lock with new Copy function * fixing tests * disable speed control * fix nil ExecuterOptions * removing deprecated code * fixing result print * default logger * cli default logger * filter warning from results * fix performance test * hardcoding path * disable upload * refactor(runner): uses `Warning` instead of `Print` for `pdcpUploadErrMsg` Signed-off-by: Dwi Siswanto <git@dw1.io> * Revert "disable upload" This reverts commit 114fbe6663361bf41cf8b2645fd2d57083d53682. * Revert "hardcoding path" This reverts commit cf12ca800e0a0e974bd9fd4826a24e51547f7c00. --------- Signed-off-by: Dwi Siswanto <git@dw1.io> Co-authored-by: Mzack9999 <mzack9999@protonmail.com> Co-authored-by: Dwi Siswanto <git@dw1.io> Co-authored-by: Dwi Siswanto <25837540+dwisiswant0@users.noreply.github.com>
360 lines
11 KiB
Go
360 lines
11 KiB
Go
package kerberos
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/Mzack9999/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"
|
|
)
|
|
|
|
type (
|
|
// EnumerateUserResponse is the response from EnumerateUser
|
|
EnumerateUserResponse struct {
|
|
Valid bool `json:"valid"`
|
|
ASREPHash string `json:"asrep_hash"`
|
|
Error string `json:"error"`
|
|
}
|
|
)
|
|
|
|
type (
|
|
// TGS is the response from GetServiceTicket
|
|
TGS struct {
|
|
Ticket messages.Ticket `json:"ticket"`
|
|
Hash string `json:"hash"`
|
|
ErrMsg string `json:"error"`
|
|
}
|
|
)
|
|
|
|
type (
|
|
// Config is extra configuration for the kerberos client
|
|
Config struct {
|
|
ip string
|
|
timeout int // in seconds
|
|
}
|
|
)
|
|
|
|
// SetIPAddress sets the IP address for the kerberos client
|
|
// @example
|
|
// ```javascript
|
|
// const kerberos = require('nuclei/kerberos');
|
|
// const cfg = new kerberos.Config();
|
|
// cfg.SetIPAddress('10.10.10.1');
|
|
// ```
|
|
func (c *Config) SetIPAddress(ip string) *Config {
|
|
c.ip = ip
|
|
return c
|
|
}
|
|
|
|
// SetTimeout sets the RW timeout for the kerberos client
|
|
// @example
|
|
// ```javascript
|
|
// const kerberos = require('nuclei/kerberos');
|
|
// const cfg = new kerberos.Config();
|
|
// cfg.SetTimeout(5);
|
|
// ```
|
|
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)
|
|
|
|
type (
|
|
// Known Issues:
|
|
// Hardcoded timeout in gokrb5 library
|
|
// TGT / Session Handling not exposed
|
|
// Client is kerberos client
|
|
// @example
|
|
// ```javascript
|
|
// const kerberos = require('nuclei/kerberos');
|
|
// // if controller is empty a dns lookup for default kdc server will be performed
|
|
// const client = new kerberos.Client('acme.com', 'kdc.acme.com');
|
|
// ```
|
|
Client struct {
|
|
nj *utils.NucleiJS // helper functions/bindings
|
|
Krb5Config *kconfig.Config
|
|
Realm string
|
|
config Config
|
|
}
|
|
)
|
|
|
|
// Constructor for Kerberos Client
|
|
// Constructor: constructor(public domain: string, public controller?: string)
|
|
// 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
|
|
executionId := c.nj.ExecutionId()
|
|
if !protocolstate.IsHostAllowed(executionId, 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
|
|
// @example
|
|
// ```javascript
|
|
// const kerberos = require('nuclei/kerberos');
|
|
// const client = kerberos.NewKerberosClientFromString(`
|
|
// [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
|
|
// Note: as of now ip and timeout overrides are only supported
|
|
// in EnumerateUser due to fastdialer but can be extended to other methods currently
|
|
// @example
|
|
// ```javascript
|
|
// const kerberos = require('nuclei/kerberos');
|
|
// const client = new kerberos.Client('acme.com', 'kdc.acme.com');
|
|
// const cfg = new kerberos.Config();
|
|
// cfg.SetIPAddress('192.168.100.22');
|
|
// cfg.SetTimeout(5);
|
|
// client.SetConfig(cfg);
|
|
// ```
|
|
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
|
|
// @example
|
|
// ```javascript
|
|
// const kerberos = require('nuclei/kerberos');
|
|
// const client = new kerberos.Client('acme.com', 'kdc.acme.com');
|
|
// const resp = client.EnumerateUser('pdtm');
|
|
// log(resp);
|
|
// ```
|
|
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 and SPN
|
|
// @example
|
|
// ```javascript
|
|
// const kerberos = require('nuclei/kerberos');
|
|
// const client = new kerberos.Client('acme.com', 'kdc.acme.com');
|
|
// const resp = client.GetServiceTicket('pdtm', 'password', 'HOST/CLIENT1');
|
|
// log(resp);
|
|
// ```
|
|
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")
|
|
|
|
executionId := c.nj.ExecutionId()
|
|
|
|
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(executionId, kdc) {
|
|
c.nj.Throw("KDC address %v blacklisted by network policy", kdc)
|
|
}
|
|
}
|
|
for _, kpasswd := range r.KPasswdServer {
|
|
if !protocolstate.IsHostAllowed(executionId, 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(executionId, 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
|
|
// }
|