mirror of
https://github.com/mandiant/gopacket
synced 2026-06-21 13:57:02 +00:00
smbclient: add list_snapshots command
Fixes #8. smbclient's interactive shell now supports list_snapshots, which enumerates VSS shadow copies on the currently selected share via FSCTL_SRV_ENUMERATE_SNAPSHOTS. Matches Impacket smbclient.py's list_snapshots output format. The response follows MS-SMB2 2.2.32 and requires a two-call size probe: the first ioctl uses a 16-byte output buffer to read the SRV_SNAPSHOT_ARRAY header (SnapShotArraySize tells us how large the second buffer needs to be), then a second ioctl asks for the full payload and parses the UTF-16LE NUL-separated @GMT-... tokens. Changes: - pkg/third_party/smb2/client.go: add an exported Ioctl method on *File as a thin wrapper over the internal ioctl helper, so FSCTL operations beyond FSCTL_PIPE_TRANSCEIVE don't need their own dedicated method in the vendored library. - pkg/smb/client.go: EnumerateSnapshots() method on *Client. Handles the size-probe dance, parses the UTF-16LE token list via pkg/utf16le, returns []string. Empty slice when no snapshots exist. - tools/smbclient/main.go: wire list_snapshots into the shell's command switch and help text. Unblocks the VSS-based NTDS extraction path: use c$ list_snapshots # get @GMT-YYYY.MM.DD-HH.MM.SS get @GMT-...\Windows\NTDS\ntds.dit secretsdump -ntds ntds.dit -system SYSTEM
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
package smb
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -31,6 +32,7 @@ import (
|
||||
"github.com/mandiant/gopacket/pkg/session"
|
||||
"github.com/mandiant/gopacket/pkg/third_party/smb2"
|
||||
"github.com/mandiant/gopacket/pkg/transport"
|
||||
"github.com/mandiant/gopacket/pkg/utf16le"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
@@ -408,3 +410,76 @@ func (c *Client) Mget(pattern string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// fsctlSrvEnumerateSnapshots is FSCTL_SRV_ENUMERATE_SNAPSHOTS per MS-FSCC
|
||||
// 2.3.23 / MS-SMB2 2.2.31. Enumerates VSS shadow copies on the share.
|
||||
const fsctlSrvEnumerateSnapshots = 0x00144064
|
||||
|
||||
// EnumerateSnapshots returns the VSS shadow-copy tokens available on the
|
||||
// currently selected share, formatted as `@GMT-YYYY.MM.DD-HH.MM.SS`. Requires
|
||||
// a share to be selected via UseShare. Returns an empty slice if no snapshots
|
||||
// exist. Implements the two-call size-probe pattern from MS-SMB2 2.2.32.
|
||||
func (c *Client) EnumerateSnapshots() ([]string, error) {
|
||||
if c.currentShare == nil {
|
||||
return nil, fmt.Errorf("no share selected")
|
||||
}
|
||||
|
||||
f, err := c.currentShare.Open(".")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open share root: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// First call with just enough buffer for the 12-byte SRV_SNAPSHOT_ARRAY
|
||||
// header. The server returns SnapShotArraySize so we know how large the
|
||||
// second call's buffer needs to be.
|
||||
hdr, err := f.Ioctl(fsctlSrvEnumerateSnapshots, nil, 16)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ioctl (probe): %v", err)
|
||||
}
|
||||
if len(hdr) < 12 {
|
||||
return nil, fmt.Errorf("snapshot probe response too short: %d bytes", len(hdr))
|
||||
}
|
||||
arraySize := binary.LittleEndian.Uint32(hdr[8:12])
|
||||
if arraySize == 0 {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
// Second call, request header + full payload.
|
||||
resp, err := f.Ioctl(fsctlSrvEnumerateSnapshots, nil, 12+int(arraySize))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ioctl (full): %v", err)
|
||||
}
|
||||
if len(resp) < 12 {
|
||||
return nil, fmt.Errorf("snapshot response too short: %d bytes", len(resp))
|
||||
}
|
||||
numReturned := binary.LittleEndian.Uint32(resp[4:8])
|
||||
arraySize = binary.LittleEndian.Uint32(resp[8:12])
|
||||
if uint32(len(resp)) < 12+arraySize {
|
||||
return nil, fmt.Errorf("snapshot payload truncated: got %d, want %d", len(resp), 12+arraySize)
|
||||
}
|
||||
data := resp[12 : 12+arraySize]
|
||||
|
||||
// The payload is a sequence of UTF-16LE NUL-terminated strings followed by
|
||||
// a final UTF-16LE NUL terminator. Walk two bytes at a time looking for
|
||||
// UTF-16 NULs.
|
||||
var out []string
|
||||
for i := 0; i+1 < len(data); {
|
||||
end := i
|
||||
for end+1 < len(data) {
|
||||
if data[end] == 0 && data[end+1] == 0 {
|
||||
break
|
||||
}
|
||||
end += 2
|
||||
}
|
||||
if end == i {
|
||||
break // list terminator
|
||||
}
|
||||
out = append(out, utf16le.DecodeToString(data[i:end]))
|
||||
i = end + 2
|
||||
if uint32(len(out)) >= numReturned {
|
||||
break
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
Vendored
+21
@@ -2061,6 +2061,27 @@ func (f *File) Transact(input []byte, maxOutput int) ([]byte, error) {
|
||||
return f.ioctl(req)
|
||||
}
|
||||
|
||||
// Ioctl sends an arbitrary FSCTL against the open file handle. Thin public
|
||||
// wrapper over the internal ioctl method, used for operations that don't have
|
||||
// a dedicated helper (e.g. FSCTL_SRV_ENUMERATE_SNAPSHOTS). The input slice may
|
||||
// be nil for control codes that take no payload.
|
||||
func (f *File) Ioctl(ctlCode uint32, input []byte, maxOutput int) ([]byte, error) {
|
||||
f.m.Lock()
|
||||
defer f.m.Unlock()
|
||||
|
||||
req := &IoctlRequest{
|
||||
CtlCode: ctlCode,
|
||||
OutputOffset: 0,
|
||||
OutputCount: 0,
|
||||
MaxInputResponse: 0,
|
||||
MaxOutputResponse: uint32(maxOutput),
|
||||
Flags: SMB2_0_IOCTL_IS_FSCTL,
|
||||
Input: RawBytes(input),
|
||||
}
|
||||
|
||||
return f.ioctl(req)
|
||||
}
|
||||
|
||||
func (f *File) readdir(pattern string) (fi []os.FileInfo, err error) {
|
||||
req := &QueryDirectoryRequest{
|
||||
FileInfoClass: FileDirectoryInformation,
|
||||
|
||||
@@ -349,6 +349,19 @@ func shell(client *smb.Client, hostname string, inputFile string) {
|
||||
fmt.Printf("[+] %s\n", inf)
|
||||
}
|
||||
p.Close()
|
||||
case "list_snapshots":
|
||||
snaps, err := client.EnumerateSnapshots()
|
||||
if err != nil {
|
||||
fmt.Printf("[-] Error: %v\n", err)
|
||||
continue
|
||||
}
|
||||
if len(snaps) == 0 {
|
||||
fmt.Println("No snapshots available on this share.")
|
||||
continue
|
||||
}
|
||||
for _, s := range snaps {
|
||||
fmt.Println(s)
|
||||
}
|
||||
case "help":
|
||||
fmt.Print(`
|
||||
logoff - logs off
|
||||
@@ -368,6 +381,7 @@ func shell(client *smb.Client, hostname string, inputFile string) {
|
||||
mget {mask} - downloads all files from the current directory matching the provided mask
|
||||
cat {filename} - reads the filename from the current path
|
||||
info - returns NetrServerInfo main results
|
||||
list_snapshots - lists VSS shadow copies available on the current share
|
||||
close - closes the current SMB Session
|
||||
exit - terminates the session
|
||||
`)
|
||||
|
||||
Reference in New Issue
Block a user