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.
410 lines
10 KiB
Go
410 lines
10 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 (
|
|
"encoding/binary"
|
|
"flag"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/mandiant/gopacket/internal/build"
|
|
"github.com/mandiant/gopacket/pkg/dcerpc"
|
|
"github.com/mandiant/gopacket/pkg/dcerpc/epmapper"
|
|
"github.com/mandiant/gopacket/pkg/flags"
|
|
"github.com/mandiant/gopacket/pkg/transport"
|
|
)
|
|
|
|
// MGMT interface UUID: afa8bd80-7d8a-11c9-bef4-08002b102989
|
|
var mgmtUUID = dcerpc.MustParseUUID("afa8bd80-7d8a-11c9-bef4-08002b102989")
|
|
|
|
const (
|
|
mgmtMajorVersion = 1
|
|
mgmtMinorVersion = 0
|
|
opInqIfIDs = 0
|
|
)
|
|
|
|
var (
|
|
bruteUUIDs = flag.Bool("brute-uuids", false, "Bruteforce UUIDs even if MGMT interface works")
|
|
bruteOpnums = flag.Bool("brute-opnums", false, "Bruteforce opnums for each discovered UUID")
|
|
bruteVers = flag.Bool("brute-versions", false, "Bruteforce versions for each discovered UUID")
|
|
opnumMax = flag.Int("opnum-max", 64, "Max opnum to try when bruteforcing opnums")
|
|
versionMax = flag.Int("version-max", 64, "Max major version to try when bruteforcing versions")
|
|
singleUUID = flag.String("uuid", "", "Test a single UUID instead of all known UUIDs")
|
|
debug = flag.Bool("debug", false, "Turn DEBUG output ON")
|
|
)
|
|
|
|
type stringBinding struct {
|
|
protocol string
|
|
host string
|
|
endpoint string
|
|
}
|
|
|
|
func parseStringBinding(s string) (stringBinding, error) {
|
|
// Format: protocol:host[endpoint]
|
|
// e.g. ncacn_ip_tcp:192.168.0.1[135]
|
|
colonIdx := strings.Index(s, ":")
|
|
if colonIdx == -1 {
|
|
return stringBinding{}, fmt.Errorf("invalid string binding %q: missing ':'", s)
|
|
}
|
|
|
|
proto := s[:colonIdx]
|
|
rest := s[colonIdx+1:]
|
|
|
|
var host, endpoint string
|
|
bracketIdx := strings.Index(rest, "[")
|
|
if bracketIdx == -1 {
|
|
host = rest
|
|
} else {
|
|
host = rest[:bracketIdx]
|
|
endBracket := strings.Index(rest, "]")
|
|
if endBracket == -1 {
|
|
return stringBinding{}, fmt.Errorf("invalid string binding %q: missing ']'", s)
|
|
}
|
|
endpoint = rest[bracketIdx+1 : endBracket]
|
|
}
|
|
|
|
return stringBinding{protocol: proto, host: host, endpoint: endpoint}, nil
|
|
}
|
|
|
|
type ifaceResult struct {
|
|
uuid string
|
|
version string
|
|
}
|
|
|
|
func main() {
|
|
flag.Usage = func() {
|
|
fmt.Fprintln(os.Stderr, flags.Banner())
|
|
fmt.Fprintf(os.Stderr, `
|
|
Scans for listening MSRPC interfaces. Tries the MGMT interface first,
|
|
falls back to UUID bruteforce if MGMT is not available.
|
|
|
|
Usage: %s [options] string_binding
|
|
|
|
String Binding Format:
|
|
ncacn_ip_tcp:host[port] TCP transport
|
|
ncacn_np:host[\pipe\name] Named pipe transport (SMB)
|
|
|
|
Options:
|
|
`, os.Args[0])
|
|
flag.PrintDefaults()
|
|
fmt.Fprintf(os.Stderr, `
|
|
Examples:
|
|
%s 'ncacn_ip_tcp:192.168.1.10[135]'
|
|
%s -brute-uuids 'ncacn_ip_tcp:192.168.1.10[135]'
|
|
%s -uuid 12345778-1234-abcd-ef00-0123456789ac 'ncacn_ip_tcp:192.168.1.10[135]'
|
|
`, os.Args[0], os.Args[0], os.Args[0])
|
|
}
|
|
|
|
configureProxy := flags.RegisterProxyFlag()
|
|
flag.Parse()
|
|
configureProxy()
|
|
|
|
if flag.NArg() < 1 {
|
|
flag.Usage()
|
|
os.Exit(1)
|
|
}
|
|
|
|
fmt.Println(flags.Banner())
|
|
fmt.Println()
|
|
|
|
if *debug {
|
|
build.Debug = true
|
|
}
|
|
|
|
binding, err := parseStringBinding(flag.Arg(0))
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "[-] %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if binding.protocol != "ncacn_ip_tcp" {
|
|
fmt.Fprintf(os.Stderr, "[-] Only ncacn_ip_tcp transport is currently supported\n")
|
|
os.Exit(1)
|
|
}
|
|
|
|
if binding.endpoint == "" {
|
|
binding.endpoint = "135"
|
|
}
|
|
|
|
addr := net.JoinHostPort(binding.host, binding.endpoint)
|
|
fmt.Printf("[*] Trying to connect to %s...\n", addr)
|
|
|
|
mgmtWorked := false
|
|
if !*bruteUUIDs {
|
|
mgmtWorked = tryMGMT(binding.host, addr)
|
|
}
|
|
|
|
if !mgmtWorked || *bruteUUIDs {
|
|
if *bruteUUIDs && mgmtWorked {
|
|
fmt.Println()
|
|
fmt.Println("[*] Bruteforcing UUIDs as requested...")
|
|
} else if !mgmtWorked {
|
|
fmt.Println("[*] MGMT interface not available, falling back to UUID bruteforce...")
|
|
}
|
|
bruteforceUUIDs(addr)
|
|
}
|
|
}
|
|
|
|
func tryMGMT(host, addr string) bool {
|
|
conn, err := transport.Dial("tcp", addr)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "[-] Failed to connect to %s: %v\n", addr, err)
|
|
return false
|
|
}
|
|
defer conn.Close()
|
|
conn.SetDeadline(time.Now().Add(30 * time.Second))
|
|
|
|
transport := dcerpc.NewTCPTransport(conn)
|
|
client := dcerpc.NewClientTCP(transport)
|
|
|
|
// Try binding to MGMT interface
|
|
err = client.Bind(mgmtUUID, mgmtMajorVersion, mgmtMinorVersion)
|
|
if err != nil {
|
|
if build.Debug {
|
|
fmt.Printf("[D] MGMT bind failed: %v\n", err)
|
|
}
|
|
return false
|
|
}
|
|
|
|
fmt.Println("[*] Bound to MGMT interface, querying remote interface list...")
|
|
fmt.Println()
|
|
|
|
// Call inq_if_ids (opnum 0, empty stub)
|
|
resp, err := client.Call(opInqIfIDs, []byte{})
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "[-] inq_if_ids call failed: %v\n", err)
|
|
return false
|
|
}
|
|
|
|
ifaces := parseIfIDVector(resp)
|
|
|
|
// Add the MGMT interface itself (it won't list itself)
|
|
ifaces = append(ifaces, ifaceResult{
|
|
uuid: "AFA8BD80-7D8A-11C9-BEF4-08002B102989",
|
|
version: "v1.0",
|
|
})
|
|
|
|
// Sort by UUID to match Impacket output
|
|
sort.Slice(ifaces, func(i, j int) bool {
|
|
return ifaces[i].uuid < ifaces[j].uuid
|
|
})
|
|
|
|
for _, iface := range ifaces {
|
|
printIface(iface)
|
|
if *bruteOpnums {
|
|
bruteforceOpnums(addr, iface)
|
|
}
|
|
}
|
|
|
|
fmt.Printf("[*] Received %d interfaces from MGMT.\n", len(ifaces))
|
|
return true
|
|
}
|
|
|
|
func parseIfIDVector(data []byte) []ifaceResult {
|
|
if len(data) < 8 {
|
|
return nil
|
|
}
|
|
|
|
// NDR response for inq_if_ids:
|
|
// [referent_id:4][count:4][max_count:4]
|
|
// then count * [referent_id:4] (pointer array)
|
|
// then count * [uuid:16 + ver_major:2 + ver_minor:2] (deferred data)
|
|
// then [status:4]
|
|
|
|
offset := 0
|
|
|
|
// Read referent ID for the if_id_vector pointer
|
|
if offset+4 > len(data) {
|
|
return nil
|
|
}
|
|
offset += 4 // skip referent ID
|
|
|
|
// Read count
|
|
if offset+4 > len(data) {
|
|
return nil
|
|
}
|
|
count := binary.LittleEndian.Uint32(data[offset:])
|
|
offset += 4
|
|
|
|
if count == 0 || count > 1000 {
|
|
return nil
|
|
}
|
|
|
|
// Read max_count (conformant array)
|
|
if offset+4 > len(data) {
|
|
return nil
|
|
}
|
|
offset += 4 // skip max_count
|
|
|
|
// Read referent IDs for each pointer in the array
|
|
if offset+int(count)*4 > len(data) {
|
|
return nil
|
|
}
|
|
offset += int(count) * 4 // skip referent IDs
|
|
|
|
// Now read the deferred data: each entry is uuid(16) + ver_major(2) + ver_minor(2) = 20 bytes
|
|
var results []ifaceResult
|
|
for i := 0; i < int(count); i++ {
|
|
if offset+20 > len(data) {
|
|
break
|
|
}
|
|
|
|
var uuid [16]byte
|
|
copy(uuid[:], data[offset:offset+16])
|
|
offset += 16
|
|
|
|
major := binary.LittleEndian.Uint16(data[offset:])
|
|
offset += 2
|
|
minor := binary.LittleEndian.Uint16(data[offset:])
|
|
offset += 2
|
|
|
|
uuidStr := strings.ToUpper(dcerpc.FormatUUID(uuid))
|
|
results = append(results, ifaceResult{
|
|
uuid: uuidStr,
|
|
version: fmt.Sprintf("v%d.%d", major, minor),
|
|
})
|
|
}
|
|
|
|
return results
|
|
}
|
|
|
|
func printIface(iface ifaceResult) {
|
|
protocol := epmapper.LookupProtocol(iface.uuid)
|
|
provider := epmapper.LookupProvider(iface.uuid)
|
|
fmt.Printf("Protocol: %s\n", protocol)
|
|
fmt.Printf("Provider: %s\n", provider)
|
|
fmt.Printf("UUID : %s %s\n", iface.uuid, iface.version)
|
|
fmt.Println()
|
|
}
|
|
|
|
func bruteforceUUIDs(addr string) {
|
|
var uuids []string
|
|
if *singleUUID != "" {
|
|
uuids = []string{strings.ToUpper(*singleUUID)}
|
|
} else {
|
|
uuids = epmapper.KnownUUIDs()
|
|
}
|
|
|
|
found := 0
|
|
for _, uuidStr := range uuids {
|
|
if *bruteVers {
|
|
for ver := 0; ver <= *versionMax; ver++ {
|
|
if tryBindUUID(addr, uuidStr, uint16(ver), 0) {
|
|
iface := ifaceResult{
|
|
uuid: uuidStr,
|
|
version: fmt.Sprintf("v%d.0", ver),
|
|
}
|
|
printIface(iface)
|
|
found++
|
|
if *bruteOpnums {
|
|
bruteforceOpnums(addr, iface)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Try version 1.0 by default (most common)
|
|
if tryBindUUID(addr, uuidStr, 1, 0) {
|
|
iface := ifaceResult{
|
|
uuid: uuidStr,
|
|
version: "v1.0",
|
|
}
|
|
printIface(iface)
|
|
found++
|
|
if *bruteOpnums {
|
|
bruteforceOpnums(addr, iface)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fmt.Printf("[*] Found %d UUID(s) via bruteforce.\n", found)
|
|
}
|
|
|
|
func tryBindUUID(addr, uuidStr string, major, minor uint16) bool {
|
|
uuid, err := dcerpc.ParseUUID(uuidStr)
|
|
if err != nil {
|
|
if build.Debug {
|
|
fmt.Printf("[D] Invalid UUID %s: %v\n", uuidStr, err)
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Each attempt uses a fresh TCP connection (matching Impacket behavior)
|
|
conn, err := transport.DialTimeout("tcp", addr, 5)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
defer conn.Close()
|
|
conn.SetDeadline(time.Now().Add(10 * time.Second))
|
|
|
|
transport := dcerpc.NewTCPTransport(conn)
|
|
client := dcerpc.NewClientTCP(transport)
|
|
|
|
err = client.Bind(uuid, major, minor)
|
|
return err == nil
|
|
}
|
|
|
|
func bruteforceOpnums(addr string, iface ifaceResult) {
|
|
uuid, err := dcerpc.ParseUUID(iface.uuid)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Parse version
|
|
var major, minor uint16
|
|
fmt.Sscanf(iface.version, "v%d.%d", &major, &minor)
|
|
|
|
for opnum := 0; opnum <= *opnumMax; opnum++ {
|
|
// Each opnum test needs a fresh connection + bind
|
|
conn, err := transport.DialTimeout("tcp", addr, 5)
|
|
if err != nil {
|
|
break
|
|
}
|
|
conn.SetDeadline(time.Now().Add(10 * time.Second))
|
|
|
|
transport := dcerpc.NewTCPTransport(conn)
|
|
client := dcerpc.NewClientTCP(transport)
|
|
|
|
err = client.Bind(uuid, major, minor)
|
|
if err != nil {
|
|
conn.Close()
|
|
break
|
|
}
|
|
|
|
// Try calling the opnum with empty stub data
|
|
_, err = client.Call(uint16(opnum), []byte{})
|
|
conn.Close()
|
|
|
|
if err != nil {
|
|
// Check if it's an RPC Fault with nca_s_op_rng_error (0x1c010002)
|
|
// That means the opnum doesn't exist
|
|
if strings.Contains(err.Error(), "0x1c010002") {
|
|
// No more opnums
|
|
fmt.Printf("[*] UUID %s %s: opnums 0-%d accessible\n", iface.uuid, iface.version, opnum-1)
|
|
return
|
|
}
|
|
// Other fault = opnum exists but call failed (expected with empty stub)
|
|
fmt.Printf(" [*] Opnum %d: exists (fault: %v)\n", opnum, err)
|
|
} else {
|
|
fmt.Printf(" [*] Opnum %d: success\n", opnum)
|
|
}
|
|
}
|
|
}
|