Files
mandiant-gopacket/pkg/smb/errors_test.go
T
Jacob Paullus dfe4d2ea7b exec-tools: fix output polling for long-running commands
Two compounding bugs in wmiexec / atexec / smbexec / dcomexec made
the output-retrieval loop return early on any command that didn't
finish within the first 100ms poll, leaking the temp file in ADMIN$
/ C$. Issue #22 reports the symptom for wmiexec (systeminfo, ping);
the same broken pattern existed in all four tools.

1. Wrong call site. The polling loop treated a sharing-violation
   on smbClient.Cat as the signal "command still running." Cat
   opens with FILE_SHARE_READ-compatible access — it succeeds
   against the writer on the first poll and returns the file as
   it currently exists (typically empty). The conflict actually
   lives on Rm: deleting the file requires DELETE access, which
   conflicts with the writer's share mode. Moved the sharing-
   violation check from Cat to Rm. Matches Impacket's wmiexec.py.

2. Dead string match. The check was
   strings.Contains(err.Error(), "STATUS_SHARING_VIOLATION"). The
   underlying smb2 library's NtStatus.Error() returns only the
   human-readable description ("A file cannot be opened because
   the share access flags are incompatible.") — the literal
   constant name never appears in err.Error(), so the check could
   never fire. Same issue for STATUS_OBJECT_NAME_NOT_FOUND in the
   not-found branch (further muddled by smb2/conn.go translating
   that NTSTATUS to os.ErrNotExist before wrapping).

Added typed helpers in pkg/smb (IsSharingViolation, IsNotFound)
that unwrap through *os.PathError and match on
*smb2.ResponseError.Code plus the os.ErrNotExist sentinel.
NTSTATUS values are hardcoded since the smb2/internal/erref
package can't be imported from outside the smb2 tree.

The dcomexec "broken / connection reset / use of closed" branch
stays string-matched — those errors come from net, not smb2.

Thanks to @aimogging in #22 for the diagnosis and proof-of-concept
in #29; this change applies the same Cat-vs-Rm insight across all
four exec tools and replaces the err.Error() substring matching
with typed-error helpers so future smb2 library changes don't
silently break the check again.

Verified against GOAD winterfell: wmiexec whoami / systeminfo /
ping -n 4 1.1.1.1 all return full output, exit 0, no ADMIN$
residue, both directly from the operator box and over SOCKS5H
proxy.
2026-05-12 01:30:10 -05:00

64 lines
2.1 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
package smb
import (
"errors"
"fmt"
"os"
"testing"
"github.com/mandiant/gopacket/pkg/third_party/smb2"
)
func TestIsSharingViolation(t *testing.T) {
tests := []struct {
name string
err error
want bool
}{
{"nil", nil, false},
{"plain error", errors.New("boom"), false},
{"raw response error", &smb2.ResponseError{Code: ntStatusSharingViolation}, true},
{"wrapped in PathError", &os.PathError{Op: "remove", Path: "x", Err: &smb2.ResponseError{Code: ntStatusSharingViolation}}, true},
{"different NTSTATUS", &os.PathError{Op: "remove", Path: "x", Err: &smb2.ResponseError{Code: ntStatusObjectNameNotFound}}, false},
{"fmt-wrapped", fmt.Errorf("retrieval: %w", &smb2.ResponseError{Code: ntStatusSharingViolation}), true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := IsSharingViolation(tc.err); got != tc.want {
t.Errorf("IsSharingViolation(%v) = %v, want %v", tc.err, got, tc.want)
}
})
}
}
func TestIsNotFound(t *testing.T) {
tests := []struct {
name string
err error
want bool
}{
{"nil", nil, false},
{"name not found", &os.PathError{Op: "open", Path: "x", Err: &smb2.ResponseError{Code: ntStatusObjectNameNotFound}}, true},
{"path not found", &os.PathError{Op: "open", Path: "x", Err: &smb2.ResponseError{Code: ntStatusObjectPathNotFound}}, true},
{"os.ErrNotExist sentinel (smb2 translates name_not_found here)", &os.PathError{Op: "open", Path: "__output", Err: os.ErrNotExist}, true},
{"bare os.ErrNotExist", os.ErrNotExist, true},
{"sharing violation", &os.PathError{Op: "remove", Path: "x", Err: &smb2.ResponseError{Code: ntStatusSharingViolation}}, false},
{"unrelated error", errors.New("no such file"), false},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := IsNotFound(tc.err); got != tc.want {
t.Errorf("IsNotFound(%v) = %v, want %v", tc.err, got, tc.want)
}
})
}
}