Skip to content

Commit

Permalink
Merge pull request #1407 from BishopFox/heads-and-tails
Browse files Browse the repository at this point in the history
Head and Tail commands
  • Loading branch information
moloch-- authored Oct 31, 2023
2 parents e4ade4e + d0574ff commit 13fb854
Show file tree
Hide file tree
Showing 9 changed files with 1,378 additions and 1,066 deletions.
5 changes: 3 additions & 2 deletions client/command/filesystem/cat.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,9 @@ func CatCmd(cmd *cobra.Command, con *console.SliverConsoleClient, args []string)
ctrl := make(chan bool)
con.SpinUntil(fmt.Sprintf("Downloading %s ...", filePath), ctrl)
download, err := con.Rpc.Download(context.Background(), &sliverpb.DownloadReq{
Request: con.ActiveTarget.Request(cmd),
Path: filePath,
Request: con.ActiveTarget.Request(cmd),
RestrictedToFile: true,
Path: filePath,
})
ctrl <- true
<-ctrl
Expand Down
14 changes: 10 additions & 4 deletions client/command/filesystem/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,10 @@ func DownloadCmd(cmd *cobra.Command, con *console.SliverConsoleClient, args []st
ctrl := make(chan bool)
con.SpinUntil(fmt.Sprintf("Downloading %s ...", remotePath), ctrl)
download, err := con.Rpc.Download(context.Background(), &sliverpb.DownloadReq{
Request: con.ActiveTarget.Request(cmd),
Path: remotePath,
Recurse: recurse,
Request: con.ActiveTarget.Request(cmd),
Path: remotePath,
Recurse: recurse,
RestrictedToFile: false,
})
ctrl <- true
<-ctrl
Expand Down Expand Up @@ -119,7 +120,12 @@ func HandleDownloadResponse(download *sliverpb.Download, cmd *cobra.Command, arg
}

remotePath := args[0]
localPath := args[1]
var localPath string
if len(args) == 1 {
localPath = "."
} else {
localPath = args[1]
}
saveLoot, _ := cmd.Flags().GetBool("loot")

if download.ReadFiles == 0 {
Expand Down
127 changes: 127 additions & 0 deletions client/command/filesystem/head.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package filesystem

/*
Sliver Implant Framework
Copyright (C) 2023 Bishop Fox
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

import (
"context"
"fmt"

"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/spf13/cobra"
"google.golang.org/protobuf/proto"
)

func HeadCmd(cmd *cobra.Command, con *console.SliverConsoleClient, args []string, head bool) {
session, beacon := con.ActiveTarget.GetInteractive()
if session == nil && beacon == nil {
return
}

var filePath string
var fetchBytes bool
var fetchSize int64
var operationName string
var download *sliverpb.Download
var err error

if len(args) > 0 {
filePath = args[0]
}
if filePath == "" {
con.PrintErrorf("Missing parameter: file name\n")
return
}

if cmd.Flags().Changed("bytes") {
fetchBytes = true
fetchSize, _ = cmd.Flags().GetInt64("bytes")
if fetchSize < 0 {
// Cannot fetch a negative number of bytes
con.PrintErrorf("The number of bytes specified must be positive.")
return
}
if fetchSize == 1 {
operationName = "byte"
} else {
operationName = "bytes"
}
} else if cmd.Flags().Changed("lines") {
fetchBytes = false
fetchSize, _ = cmd.Flags().GetInt64("lines")
if fetchSize < 0 {
// Cannot fetch a negative number of lines
con.PrintErrorf("The number of lines specified must be positive.")
return
}
if fetchSize == 1 {
operationName = "line"
} else {
operationName = "lines"
}
} else {
con.PrintErrorf("A number of bytes or a number of lines must be specified.")
return
}

ctrl := make(chan bool)
con.SpinUntil(fmt.Sprintf("Retrieving %d %s from %s ...", fetchSize, operationName, filePath), ctrl)

// A tail is a negative head
if !head {
fetchSize *= -1
}

if fetchBytes {
download, err = con.Rpc.Download(context.Background(), &sliverpb.DownloadReq{
Request: con.ActiveTarget.Request(cmd),
Path: filePath,
MaxBytes: fetchSize,
RestrictedToFile: true,
})
} else {
download, err = con.Rpc.Download(context.Background(), &sliverpb.DownloadReq{
Request: con.ActiveTarget.Request(cmd),
Path: filePath,
MaxLines: fetchSize,
RestrictedToFile: true,
})
}

ctrl <- true
<-ctrl
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
if download.Response != nil && download.Response.Async {
con.AddBeaconCallback(download.Response.TaskID, func(task *clientpb.BeaconTask) {
err = proto.Unmarshal(task.Response, download)
if err != nil {
con.PrintErrorf("Failed to decode response %s\n", err)
return
}
PrintCat(download, cmd, con)
})
con.PrintAsyncResponse(download.Response)
} else {
PrintCat(download, cmd, con)
}
}
8 changes: 8 additions & 0 deletions client/command/help/long-help.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ var (
consts.CatStr: catHelp,
consts.DownloadStr: downloadHelp,
consts.GrepStr: grepHelp,
consts.HeadStr: headHelp,
consts.TailStr: tailHelp,
consts.UploadStr: uploadHelp,
consts.MkdirStr: mkdirHelp,
consts.RmStr: rmHelp,
Expand Down Expand Up @@ -322,6 +324,12 @@ Downloads can be filtered using the following patterns:
If you need to match a special character (*, ?, '-', '[', ']', '\\'), place '\\' in front of it (example: \\?).
On Windows, escaping is disabled. Instead, '\\' is treated as path separator.`

headHelp = `[[.Bold]]Command:[[.Normal]] head [--bytes/-b <number of bytes>] [--lines/-l <number of lines>] <remote path>
[[.Bold]]About:[[.Normal]] Fetch the first number of bytes or lines from a remote file and display it to stdout.`

tailHelp = `[[.Bold]]Command:[[.Normal]] tail [--bytes/-b <number of bytes>] [--lines/-l <number of lines>] <remote path>
[[.Bold]]About:[[.Normal]] Fetch the last number of bytes or lines from a remote file and display it to stdout.`

uploadHelp = `[[.Bold]]Command:[[.Normal]] upload [local src] <remote dst>
[[.Bold]]About:[[.Normal]] Upload a file to the remote system.`

Expand Down
56 changes: 56 additions & 0 deletions client/command/sliver.go
Original file line number Diff line number Diff line change
Expand Up @@ -968,6 +968,62 @@ func SliverCommands(con *client.SliverConsoleClient) console.Commands {
carapace.ActionValues().Usage("remote path / file to search in"),
)

headCmd := &cobra.Command{
Use: consts.HeadStr,
Short: "Grab the first number of bytes or lines from a file",
Long: help.GetHelpFor([]string{consts.HeadStr}),
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
/*
The last argument tells head if the user requested the head or tail of the file
True means head, false means tail
*/
filesystem.HeadCmd(cmd, con, args, true)
},
GroupID: consts.FilesystemHelpGroup,
}
sliver.AddCommand(headCmd)
Flags("", false, headCmd, func(f *pflag.FlagSet) {
f.BoolP("colorize-output", "c", false, "colorize output")
f.BoolP("hex", "x", false, "display as a hex dump")
f.BoolP("loot", "X", false, "save output as loot")
f.StringP("name", "n", "", "name to assign loot (optional)")
f.StringP("type", "T", "", "force a specific loot type (file/cred) if looting (optional)")
f.StringP("file-type", "F", "", "force a specific file type (binary/text) if looting (optional)")
f.Int64P("timeout", "t", defaultTimeout, "grpc timeout in seconds")
f.Int64P("bytes", "b", 0, "Grab the first number of bytes from the file")
f.Int64P("lines", "l", 0, "Grab the first number of lines from the file")
})
carapace.Gen(headCmd).PositionalCompletion(carapace.ActionValues().Usage("path to the file to print"))

tailCmd := &cobra.Command{
Use: consts.TailStr,
Short: "Grab the last number of bytes or lines from a file",
Long: help.GetHelpFor([]string{consts.TailStr}),
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
/*
The last argument tells head if the user requested the head or tail of the file
True means head, false means tail
*/
filesystem.HeadCmd(cmd, con, args, false)
},
GroupID: consts.FilesystemHelpGroup,
}
sliver.AddCommand(tailCmd)
Flags("", false, tailCmd, func(f *pflag.FlagSet) {
f.BoolP("colorize-output", "c", false, "colorize output")
f.BoolP("hex", "x", false, "display as a hex dump")
f.BoolP("loot", "X", false, "save output as loot")
f.StringP("name", "n", "", "name to assign loot (optional)")
f.StringP("type", "T", "", "force a specific loot type (file/cred) if looting (optional)")
f.StringP("file-type", "F", "", "force a specific file type (binary/text) if looting (optional)")
f.Int64P("timeout", "t", defaultTimeout, "grpc timeout in seconds")
f.Int64P("bytes", "b", 0, "Grab the last number of bytes from the file")
f.Int64P("lines", "l", 0, "Grab the last number of lines from the file")
})
carapace.Gen(tailCmd).PositionalCompletion(carapace.ActionValues().Usage("path to the file to print"))

uploadCmd := &cobra.Command{
Use: consts.UploadStr,
Short: "Upload a file",
Expand Down
2 changes: 2 additions & 0 deletions client/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ const (
PwdStr = "pwd"
CatStr = "cat"
DownloadStr = "download"
HeadStr = "head"
TailStr = "tail"
GrepStr = "grep"
UploadStr = "upload"
IfconfigStr = "ifconfig"
Expand Down
84 changes: 81 additions & 3 deletions implant/sliver/handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -448,19 +448,79 @@ func pwdHandler(data []byte, resp RPCResponse) {
resp(data, err)
}

func prepareDownload(path string, filter string, recurse bool) ([]byte, bool, int, int, error) {
func prepareDownload(path string, filter string, recurse bool, maxBytes int64, maxLines int64) ([]byte, bool, int, int, error) {
/*
Combine the path and filter to see if the user wants
to download a single file
*/
var rawData []byte
var err error

fileInfo, err := os.Stat(path + filter)

if err == nil && !fileInfo.IsDir() {
// Then this is a single file
rawData, err := os.ReadFile(path + filter)
fileHandle, err := os.Open(path + filter)
if err != nil {
// Then we could not read the file
return nil, false, 0, 1, err
}
defer fileHandle.Close()

if maxBytes != 0 {
var readFirst bool = maxBytes > 0
if readFirst {
rawData = make([]byte, maxBytes)
_, err = fileHandle.Read(rawData)
} else {
rawData = make([]byte, maxBytes*-1)
var bytesToRead int64 = 0
if fileInfo.Size()+maxBytes < 0 {
bytesToRead = 0
} else {
bytesToRead = fileInfo.Size() + maxBytes
}
_, err = fileHandle.ReadAt(rawData, bytesToRead)
}

} else if maxLines != 0 {
var linesRead int64 = 0
var lines []string
var readFirst bool = true

if maxLines < 0 {
maxLines *= -1
readFirst = false
}

fileScanner := bufio.NewScanner(fileHandle)
for fileScanner.Scan() {
lines = append(lines, fileScanner.Text())
linesRead += 1
if linesRead == maxLines && readFirst {
break
}
}
err = fileScanner.Err()
if err == nil {
if readFirst {
rawData = []byte(strings.Join(lines, "\n"))
} else {
linePosition := int64(len(lines)) - maxLines
if linePosition < 0 {
linePosition = 0
}
rawData = []byte(strings.Join(lines[linePosition:], "\n"))
}
}
} else {
// Read the entire file
rawData = make([]byte, fileInfo.Size())
_, err = fileHandle.Read(rawData)
}
if err != nil && err != io.EOF {
// Then we could not read the file
return nil, false, 0, 1, err
} else {
return rawData, false, 1, 0, nil
}
Expand Down Expand Up @@ -498,11 +558,29 @@ func downloadHandler(data []byte, resp RPCResponse) {
if pathIsDirectory(target) {
// Even if the implant is running on Windows, Go can deal with "/" as a path separator
target += "/"
if downloadReq.RestrictedToFile {
/*
The user has asked to perform a download operation that should only be allowed on
files, and this is a directory. We should let them know.
*/
err = fmt.Errorf("cannot complete command because target %s is a directory", target)
// {{if .Config.Debug}}
log.Printf("error completing download command: %v", err)
// {{end}}
download = &sliverpb.Download{Path: target, Exists: false, ReadFiles: 0, UnreadableFiles: 0}
download.Response = &commonpb.Response{
Err: fmt.Sprintf("%v", err),
}

data, _ = proto.Marshal(download)
resp(data, err)
return
}
}

path, filter := determineDirPathFilter(target)

rawData, isDir, readFiles, unreadableFiles, err := prepareDownload(path, filter, downloadReq.Recurse)
rawData, isDir, readFiles, unreadableFiles, err := prepareDownload(path, filter, downloadReq.Recurse, downloadReq.MaxBytes, downloadReq.MaxLines)

if err != nil {
if isDir {
Expand Down
Loading

0 comments on commit 13fb854

Please sign in to comment.