Compare commits

...
Author SHA1 Message Date
Joe 62583cac8f Merge branch 'master' into fix/cert-revoke 2026-03-13 11:02:34 -07:00
moloch 95764945b8 Improve operator cert revocation 2026-03-13 10:59:00 -07:00
Joe 26abda1c15 Merge pull request #2216 from skoveit/fix/timeout-int64p-mismatch
fix: timeout flag ignored in aliases and extensions due to type mismatch
2026-03-13 07:38:21 -07:00
moloch 01dd3cac72 Go mod tidy 2026-03-13 07:02:06 -07:00
0xOral f8faa1f767 fix: normalize all command timeout flags to Int64P across client codebase 2026-03-13 02:49:01 +02:00
0xOral a9e48e84cd fix: extension timeout flag ignored due to IntP/Int64P mismatch 2026-03-13 02:30:55 +02:00
0xOral 9d6704d925 fix: alias -t flag always capped at 30s due to IntP/Int64P type mismatch (#2183) 2026-03-13 02:04:35 +02:00
Joe ce5157e2e9 Merge pull request #2213 from skoveit/fix/wininet-handle-leak
fix(wininet): Fix handle leak in WinINet driver
2026-03-11 08:29:42 -07:00
0xOral 7d19051df8 fix(wininet): close connection and request handles after each request 2026-03-11 01:32:27 +02:00
Joe a0d3bd579f Merge pull request #2211 from skoveit/feat/configurable-portfwd-keepalive
feat: make TCP keepalive period configurable for portfwd and rportfwd
2026-03-08 10:36:42 -07:00
0xOral 82c309632a default keepalive value 30s for both rportfwd and portfwd 2026-03-08 17:27:27 +02:00
0xOral 5c0b8ec460 disable keepalive with --keepalive -1 value 2026-03-08 17:25:06 +02:00
0xOral 7791a86eb4 configurable keepalive: portfwd --keepalive 2026-03-08 17:20:35 +02:00
Joe 7ac4db3fa0 Merge pull request #2207 from BishopFox/fix/beacon-getsystem
Fix for getsystem in beacon mode
2026-03-05 18:31:39 -07:00
moloch-- 512bc822ef Implement fix for getsystem in beacon mode 2026-03-05 08:22:42 -08:00
Joe f2bb9c3733 Merge pull request #2205 from BishopFox/feature/lazy-lg-msg
Lazy spool large messages to disk
2026-03-04 15:40:56 -07:00
moloch-- 0bd4f100fd Lazy spool large messages to disk 2026-03-04 14:21:07 -08:00
Joe 3230322fd6 Merge pull request #2201 from BishopFox/feature/exec-shellcode
Add support for wasm-donut options in windows `execute-shellcode`
2026-03-02 08:23:49 -07:00
44 changed files with 1637 additions and 614 deletions

No files matched your search

+1 -1
View File
@@ -250,7 +250,7 @@ func LoadAlias(manifestPath string, cmd *cobra.Command, con *console.SliverClien
f.StringP("process-arguments", "A", "", "arguments to pass to the hosting process")
f.Uint32P("ppid", "P", 0, "parent process ID to use when creating the hosting process (Windows only)")
f.BoolP("save", "s", false, "Save output to disk")
f.IntP("timeout", "t", defaultTimeout, "command timeout in seconds")
f.Int64P("timeout", "t", defaultTimeout, "command timeout in seconds")
addAliasCmd.Flags().AddFlagSet(f)
// Setup completions for alias arguments
+1 -1
View File
@@ -28,7 +28,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
},
}
flags.Bind("beacons", true, beaconsCmd, func(f *pflag.FlagSet) {
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
flags.Bind("beacons", false, beaconsCmd, func(f *pflag.FlagSet) {
f.StringP("kill", "k", "", "kill the designated beacon")
+1 -1
View File
@@ -377,7 +377,7 @@ func ExtensionRegisterCommand(extCmd *ExtCommand, cmd *cobra.Command, con *conso
// Flags
f := pflag.NewFlagSet(extCmd.CommandName, pflag.ContinueOnError)
f.BoolP("save", "s", false, "Save output to disk")
f.IntP("timeout", "t", defaultTimeout, "command timeout in seconds")
f.Int64P("timeout", "t", defaultTimeout, "command timeout in seconds")
extensionCmd.Flags().AddFlagSet(f)
extensionCmd.Flags().ParseErrorsWhitelist.UnknownFlags = true
+1 -1
View File
@@ -36,7 +36,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
GroupID: consts.PayloadsHelpGroup,
}
flags.Bind("generate", true, generateCmd, func(f *pflag.FlagSet) {
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
// Session flags and completions.
+2 -2
View File
@@ -43,12 +43,12 @@ func Commands(con *console.SliverClient) []*cobra.Command {
GroupID: consts.NetworkHelpGroup,
}
flags.Bind("jobs", true, jobsCmd, func(f *pflag.FlagSet) {
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
flags.Bind("jobs", false, jobsCmd, func(f *pflag.FlagSet) {
f.Int32P("kill", "k", -1, "kill a background job")
f.BoolP("kill-all", "K", false, "kill all jobs")
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
flags.BindFlagCompletions(jobsCmd, func(comp *carapace.ActionMap) {
(*comp)["kill"] = JobsIDCompleter(con)
+1 -1
View File
@@ -24,7 +24,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
GroupID: consts.SliverHelpGroup,
}
flags.Bind("loot", true, lootCmd, func(f *pflag.FlagSet) {
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
flags.Bind("loot", false, lootCmd, func(f *pflag.FlagSet) {
f.StringP("filter", "f", "", "filter based on loot type")
+1 -1
View File
@@ -81,7 +81,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
}
pivotsCmd.AddCommand(pivotDetailsCmd)
flags.Bind("", false, pivotDetailsCmd, func(f *pflag.FlagSet) {
f.IntP("id", "i", 0, "id of the pivot listener to get details for")
f.Uint32P("id", "i", 0, "id of the pivot listener to get details for")
})
flags.BindFlagCompletions(pivotDetailsCmd, func(comp *carapace.ActionMap) {
(*comp)["id"] = PivotIDCompleter(con)
+1
View File
@@ -39,6 +39,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
flags.Bind("", false, addCmd, func(f *pflag.FlagSet) {
f.StringP("remote", "r", "", "remote target host:port (e.g., 10.0.0.1:445)")
f.StringP("bind", "b", "127.0.0.1:8080", "bind port forward to interface")
f.Int32P("keepalive", "k", 0, "keepalive period in seconds (default 30s, -1 to disable)")
})
flags.BindFlagCompletions(addCmd, func(comp *carapace.ActionMap) {
(*comp)["bind"] = completers.ClientInterfacesCompleter()
+11 -1
View File
@@ -68,13 +68,23 @@ func PortfwdAddCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
bindAddr = fmt.Sprintf("127.0.0.1:%s", bindAddr)
}
keepAlive, _ := cmd.Flags().GetInt32("keepalive")
var keepAlivePeriod time.Duration
if keepAlive > 0 {
keepAlivePeriod = time.Duration(keepAlive) * time.Second
} else if keepAlive < 0 {
keepAlivePeriod = -1 * time.Second
} else {
keepAlivePeriod = 30 * time.Second
}
tcpProxy := &tcpproxy.Proxy{}
channelProxy := &core.ChannelProxy{
Rpc: con.Rpc,
Session: session,
RemoteAddr: remoteAddr,
BindAddr: bindAddr,
KeepAlivePeriod: 60 * time.Second,
KeepAlivePeriod: keepAlivePeriod,
DialTimeout: 30 * time.Second,
}
tcpProxy.AddRoute(bindAddr, channelProxy)
+1 -1
View File
@@ -74,7 +74,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
GetSystemCmd(cmd, con, args)
},
GroupID: consts.PrivilegesHelpGroup,
Annotations: flags.RestrictTargets(consts.WindowsCmdsFilter),
Annotations: flags.RestrictTargets(consts.WindowsCmdsFilter, consts.SessionCmdsFilter),
}
flags.Bind("", false, getSystemCmd, func(f *pflag.FlagSet) {
f.StringP("process", "p", "spoolsv.exe", "SYSTEM process to inject into")
+8
View File
@@ -37,6 +37,10 @@ func GetSystemCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
if session == nil && beacon == nil {
return
}
if session == nil {
con.PrintErrorf("Command is only supported for sessions.\n")
return
}
targetOS := getOS(session, beacon)
if targetOS != "windows" {
con.PrintErrorf("Command only supported on Windows.\n")
@@ -45,6 +49,10 @@ func GetSystemCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
process, _ := cmd.Flags().GetString("process")
config := con.GetActiveSessionConfig()
if config == nil {
con.PrintErrorf("Failed to derive active session config.\n")
return
}
/* If the HTTP C2 Config name is not defined, then put in the default value
This will have no effect on implants that do not use HTTP C2
+1 -1
View File
@@ -52,7 +52,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.BoolP("loot", "X", false, "save output as loot")
f.StringP("loot-name", "N", "", "name to assign when adding the memory dump to the loot store (optional)")
f.Int32P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
flags.BindFlagCompletions(procdumpCmd, func(comp *carapace.ActionMap) {
(*comp)["save"] = carapace.ActionFiles()
+2 -2
View File
@@ -55,7 +55,7 @@ func ProcdumpCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
return
}
timeout, _ := cmd.Flags().GetInt32("timeout")
timeout, _ := cmd.Flags().GetInt64("timeout")
if timeout < 1 {
con.PrintErrorf("Invalid timeout argument\n")
@@ -68,7 +68,7 @@ func ProcdumpCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
dump, err := con.Rpc.ProcessDump(context.Background(), &sliverpb.ProcessDumpReq{
Request: con.ActiveTarget.Request(cmd),
Pid: int32(pid),
Timeout: timeout,
Timeout: int32(timeout),
})
ctrl <- true
<-ctrl
+1 -1
View File
@@ -22,7 +22,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
Annotations: flags.RestrictTargets(consts.WindowsCmdsFilter),
}
flags.Bind("registry", true, registryCmd, func(f *pflag.FlagSet) {
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
registryReadCmd := &cobra.Command{
+1
View File
@@ -39,6 +39,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
flags.Bind("", false, rportfwdAddCmd, func(f *pflag.FlagSet) {
f.StringP("remote", "r", "", "remote address <ip>:<port> connection is forwarded to")
f.StringP("bind", "b", "", "bind address <ip>:<port> for implants to listen on")
f.Int32P("keepalive", "k", 0, "keepalive period in seconds (default 30s, -1 to disable)")
})
flags.BindFlagCompletions(rportfwdAddCmd, func(comp *carapace.ActionMap) {
(*comp)["remote"] = completers.ClientInterfacesCompleter()
+3
View File
@@ -53,10 +53,13 @@ func StartRportFwdListenerCmd(cmd *cobra.Command, con *console.SliverClient, arg
if portNumberOnlyRegexp.MatchString(forwardAddress) {
forwardAddress = fmt.Sprintf("127.0.0.1:%s", forwardAddress)
}
keepAlive, _ := cmd.Flags().GetInt32("keepalive")
rportfwdListener, err := con.Rpc.StartRportFwdListener(context.Background(), &sliverpb.RportFwdStartListenerReq{
Request: con.ActiveTarget.Request(cmd),
BindAddress: bindAddress,
ForwardAddress: forwardAddress,
KeepAlive: keepAlive,
})
if err != nil {
con.PrintWarnf("%s\n", err)
+1 -1
View File
@@ -27,7 +27,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
GroupID: consts.SliverHelpGroup,
}
flags.Bind("sessions", true, sessionsCmd, func(f *pflag.FlagSet) {
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
flags.Bind("sessions", false, sessionsCmd, func(f *pflag.FlagSet) {
f.StringP("interact", "i", "", "interact with a session")
+1 -1
View File
@@ -23,7 +23,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
Annotations: flags.RestrictTargets(consts.BeaconCmdsFilter),
}
flags.Bind("tasks", true, tasksCmd, func(f *pflag.FlagSet) {
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.BoolP("overflow", "O", false, "overflow terminal width (display truncated rows)")
f.IntP("skip-pages", "S", 0, "skip the first n page(s)")
f.StringP("filter", "f", "", "filter based on task type (case-insensitive prefix matching)")
+1 -1
View File
@@ -28,7 +28,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.StringP("save", "s", "", "save downloaded files to specific directory (default downloads dir)")
f.BoolP("insecure", "I", false, "skip tls certificate validation")
f.Bool("force", false, "skip version comparison and download the latest release")
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
flags.BindFlagCompletions(updateCmd, func(comp *carapace.ActionMap) {
(*comp)["proxy"] = completers.LocalProxyCompleter()
+1 -1
View File
@@ -60,7 +60,7 @@ const (
func UpdateCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
VerboseVersionsCmd(cmd, con, args)
timeoutF, _ := cmd.Flags().GetInt("timeout")
timeoutF, _ := cmd.Flags().GetInt64("timeout")
timeout := time.Duration(timeoutF) * time.Second
insecure, _ := cmd.Flags().GetBool("insecure")
+1
View File
@@ -216,6 +216,7 @@ func (p *ChannelProxy) dialImplant(ctx context.Context) (*TunnelIO, error) {
Port: p.Port(),
Protocol: sliverpb.PortFwdProtoTCP,
TunnelID: tunnel.ID,
KeepAlive: int32(p.KeepAlivePeriod.Seconds()),
})
if err != nil {
return nil, err
+17 -3
View File
@@ -20,10 +20,15 @@ set -u
set -o pipefail
SKIP_GENERATE=0
UNIT_ONLY=0
for arg in "$@"; do
if [ "$arg" = "--skip-generate" ]; then
SKIP_GENERATE=1
fi
if [ "$arg" = "--unit-only" ]; then
UNIT_ONLY=1
SKIP_GENERATE=1
fi
done
echo "----------------------------------------------------------------"
@@ -219,8 +224,13 @@ done
## Server c2 (unit + e2e)
run_test_cmd "./server/c2" go test -tags="server,$TAGS" ./server/c2 || exit 1
run_test_cmd "./server/c2 (e2e yamux)" go test -tags="server,$TAGS,sliver_e2e" ./server/c2 -run 'Test(MTLS|WG)Yamux_' -count=1 || exit 1
run_test_cmd "./server/c2 (e2e dns)" go test -tags="server,$TAGS,sliver_e2e" ./server/c2 -run 'TestDNS_' -count=1 || exit 1
if [ "$UNIT_ONLY" -eq 1 ]; then
echo
echo "Skipping ./server/c2 e2e tests (--unit-only)"
else
run_test_cmd "./server/c2 (e2e yamux)" go test -tags="server,$TAGS,sliver_e2e" ./server/c2 -run 'Test(MTLS|WG)Yamux_' -count=1 || exit 1
run_test_cmd "./server/c2 (e2e dns)" go test -tags="server,$TAGS,sliver_e2e" ./server/c2 -run 'TestDNS_' -count=1 || exit 1
fi
## Server generate
if [ "$SKIP_GENERATE" -eq 0 ]; then
@@ -235,5 +245,9 @@ if [ "$SKIP_GENERATE" -eq 0 ]; then
go test -timeout 6h -p "$GENERATE_GO_P" -parallel "$GENERATE_TEST_PARALLEL" -tags="server,$TAGS" ./server/generate || exit 1
else
echo
echo "Skipping ./server/generate tests (--skip-generate)"
if [ "$UNIT_ONLY" -eq 1 ]; then
echo "Skipping ./server/generate tests (--unit-only)"
else
echo "Skipping ./server/generate tests (--skip-generate)"
fi
fi
+1 -2
View File
@@ -211,7 +211,6 @@ require (
)
require (
charm.land/bubbles/v2 v2.0.0
charm.land/bubbletea/v2 v2.0.0
cloud.google.com/go/compute/metadata v0.9.0 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
@@ -231,7 +230,7 @@ require (
github.com/carapace-sh/carapace v1.11.0
github.com/carapace-sh/carapace-shlex v1.1.1 // indirect
github.com/catppuccin/go v0.3.0 // indirect
github.com/charmbracelet/colorprofile v0.4.2 // indirect
github.com/charmbracelet/colorprofile v0.4.2
github.com/charmbracelet/x/ansi v0.11.6
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
-4
View File
@@ -4,8 +4,6 @@ c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd h1:ZLsPO6WdZ5zatV4UfVpr7oAw
c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd/go.mod h1:SrHC2C7r5GkDk8R+NFVzYy/sdj0Ypg9htaPXQq5Cqeo=
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
charm.land/bubbletea/v2 v2.0.0 h1:p0d6CtWyJXJ9GfzMpUUqbP/XUUhhlk06+vCKWmox1wQ=
charm.land/bubbletea/v2 v2.0.0/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
charm.land/lipgloss/v2 v2.0.0 h1:sd8N/B3x892oiOjFfBQdXBQp3cAkvjGaU5TvVZC3ivo=
@@ -194,8 +192,6 @@ github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRb
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
+10 -1
View File
@@ -97,11 +97,20 @@ func rportFwdStartListenerHandler(envelope *pb.Envelope, connection *transports.
}
}
tcpProxy := &tcpproxy.Proxy{}
var keepAlivePeriod time.Duration
if req.KeepAlive > 0 {
keepAlivePeriod = time.Duration(req.KeepAlive) * time.Second
} else if req.KeepAlive < 0 {
keepAlivePeriod = -1 * time.Second
} else {
keepAlivePeriod = 30 * time.Second
}
channelProxy := &rportfwd.ChannelProxy{
Conn: connection,
RemoteAddr: req.ForwardAddress,
BindAddr: req.BindAddress,
KeepAlivePeriod: 1000 * time.Second,
KeepAlivePeriod: keepAlivePeriod,
DialTimeout: 30 * time.Second,
}
tcpProxy.AddRoute(req.BindAddress, channelProxy)
@@ -81,9 +81,16 @@ func PortfwdReqHandler(envelope *sliverpb.Envelope, connection *transports.Conne
// {{if .Config.Debug}}
log.Printf("[portfwd] Configuring keep alive")
// {{end}}
conn.SetKeepAlive(true)
// TODO: Make KeepAlive configurable
conn.SetKeepAlivePeriod(30 * time.Second)
if portfwdReq.KeepAlive < 0 {
conn.SetKeepAlive(false)
} else {
conn.SetKeepAlive(true)
if portfwdReq.KeepAlive > 0 {
conn.SetKeepAlivePeriod(time.Duration(portfwdReq.KeepAlive) * time.Second)
} else {
conn.SetKeepAlivePeriod(30 * time.Second)
}
}
}
// Add tunnel
+10 -3
View File
@@ -140,9 +140,16 @@ func (p *ChannelProxy) HandleConn(src net.Conn) {
// {{if .Config.Debug}}
log.Printf("[portfwd] Configuring keep alive")
// {{end}}
conn.SetKeepAlive(true)
// TODO: Make KeepAlive configurable
conn.SetKeepAlivePeriod(30 * time.Second)
if p.KeepAlivePeriod < 0 {
conn.SetKeepAlive(false)
} else {
conn.SetKeepAlive(true)
if p.KeepAlivePeriod > 0 {
conn.SetKeepAlivePeriod(p.KeepAlivePeriod)
} else {
conn.SetKeepAlivePeriod(30 * time.Second)
}
}
}
// Add tunnel
// {{if .Config.Debug}}
@@ -58,6 +58,7 @@ func (c *Client) Do(request *http.Request) (*http.Response, error) {
var buf []byte
var err error
var reqHandle uintptr
var connHandle uintptr
var resp *Response
var rawBody []byte
@@ -81,9 +82,17 @@ func (c *Client) Do(request *http.Request) (*http.Response, error) {
Body: rawBody,
}
if reqHandle, err = buildRequest(c.handle, req); err != nil {
if connHandle, reqHandle, err = buildRequest(c.handle, req); err != nil {
if reqHandle != 0 {
InternetCloseHandle(reqHandle)
}
if connHandle != 0 {
InternetCloseHandle(connHandle)
}
return nil, err
}
defer InternetCloseHandle(reqHandle)
defer InternetCloseHandle(connHandle)
if c.Timeout > 0 {
buf = make([]byte, 4)
@@ -232,3 +241,11 @@ func (jar *Jar) SetCookies(u *url.URL, cookies []*Cookie) {
func (jar *Jar) Cookies(u *url.URL) []*Cookie {
return jar.cookies
}
// Close will close the client handle
func (c *Client) Close() {
if c.handle != 0 {
InternetCloseHandle(c.handle)
c.handle = 0
}
}
@@ -17,7 +17,7 @@ func convertFail(str string, err error) error {
)
}
func buildRequest(sessionHndl uintptr, r *Request) (uintptr, error) {
func buildRequest(sessionHndl uintptr, r *Request) (uintptr, uintptr, error) {
var connHndl uintptr
var err error
var flags uintptr
@@ -29,7 +29,7 @@ func buildRequest(sessionHndl uintptr, r *Request) (uintptr, error) {
// Parse URL
if uri, err = url.Parse(r.URL); err != nil {
return 0, fmt.Errorf("failed to parse url %s: %w", r.URL, err)
return 0, 0, fmt.Errorf("failed to parse url %s: %w", r.URL, err)
}
passwd, _ = uri.User.Password()
@@ -37,7 +37,7 @@ func buildRequest(sessionHndl uintptr, r *Request) (uintptr, error) {
if uri.Port() != "" {
if port, err = strconv.ParseInt(uri.Port(), 10, 64); err != nil {
err = fmt.Errorf("port %s invalid: %w", uri.Port(), err)
return 0, err
return 0, 0, err
}
}
@@ -58,7 +58,7 @@ func buildRequest(sessionHndl uintptr, r *Request) (uintptr, error) {
0,
)
if err != nil {
return 0, fmt.Errorf("failed to create connection: %w", err)
return 0, 0, fmt.Errorf("failed to create connection: %w", err)
}
// Send query string too
@@ -82,10 +82,10 @@ func buildRequest(sessionHndl uintptr, r *Request) (uintptr, error) {
0,
)
if err != nil {
return 0, fmt.Errorf("failed to open request: %w", err)
return connHndl, 0, fmt.Errorf("failed to open request: %w", err)
}
return reqHndl, nil
return connHndl, reqHndl, nil
}
var cookies []*Cookie
@@ -213,6 +213,20 @@ func HTTPSendRequestW(
return nil
}
// InternetCloseHandle is from wininet.h
func InternetCloseHandle(hndl uintptr) error {
var err error
var proc string = "InternetCloseHandle"
var success uintptr
success, _, err = wininet.NewProc(proc).Call(hndl)
if success == 0 {
return fmt.Errorf("%s: %w", proc, err)
}
return nil
}
// InternetConnectW is from wininet.h
func InternetConnectW(
sessionHndl uintptr,
@@ -437,7 +451,7 @@ func InternetErrorDlg(
)
// we expect 12032 if user hits okay, otherwise we return the response code as an error
if uint32(success) != ERROR_INTERNET_FORCE_RETRY {
return success, fmt.Errorf("%s: %s", proc, success)
return success, fmt.Errorf("%s: %v", proc, success)
}
*lppvData = buf
File diff suppressed because it is too large. Load diff
+2
View File
@@ -883,6 +883,7 @@ message PortfwdReq {
int32 Protocol = 2;
string Host = 3;
int32 KeepAlive = 4;
uint64 TunnelID = 8 [jstype = JS_STRING]; // Bind to this tunnel
commonpb.Request Request = 9;
}
@@ -1185,6 +1186,7 @@ message RportFwdStartListenerReq {
uint32 BindPort = 2;
uint32 ForwardPort = 3;
string ForwardAddress = 4;
int32 KeepAlive = 5;
commonpb.Request Request = 9;
}
+69
View File
@@ -0,0 +1,69 @@
package c2
import (
"io"
"os"
)
const (
// socketEnvelopeDiskSpoolThreshold is the point where envelope payloads are
// streamed to disk first to avoid large attacker-triggered allocations.
socketEnvelopeDiskSpoolThreshold = 64 * 1024 * 1024
// socketEnvelopeReadChunkSize bounds peak in-memory usage while reading large
// envelopes from the network.
socketEnvelopeReadChunkSize = 1 * 1024 * 1024
)
func readSocketEnvelopeData(reader io.Reader, dataLength int, inMemoryLimit int) ([]byte, error) {
if dataLength <= inMemoryLimit {
dataBuf := make([]byte, dataLength)
if _, err := io.ReadFull(reader, dataBuf); err != nil {
return nil, err
}
return dataBuf, nil
}
tmpFile, err := os.CreateTemp("", "sliver-envelope-*")
if err != nil {
return nil, err
}
tmpPath := tmpFile.Name()
defer func() {
_ = tmpFile.Close()
_ = os.Remove(tmpPath)
}()
chunkSize := socketEnvelopeReadChunkSize
if dataLength < chunkSize {
chunkSize = dataLength
}
chunkBuf := make([]byte, chunkSize)
remaining := dataLength
for remaining > 0 {
nextRead := chunkSize
if remaining < nextRead {
nextRead = remaining
}
if _, err := io.ReadFull(reader, chunkBuf[:nextRead]); err != nil {
return nil, err
}
if _, err := tmpFile.Write(chunkBuf[:nextRead]); err != nil {
return nil, err
}
remaining -= nextRead
}
if _, err := tmpFile.Seek(0, io.SeekStart); err != nil {
return nil, err
}
dataBuf := make([]byte, dataLength)
if _, err := io.ReadFull(tmpFile, dataBuf); err != nil {
return nil, err
}
return dataBuf, nil
}
+135
View File
@@ -0,0 +1,135 @@
package c2
import (
"bytes"
"encoding/binary"
"errors"
"io"
"math"
"net"
"runtime"
"testing"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/bishopfox/sliver/util/minisign"
)
type readSizeRecorder struct {
reader io.Reader
maxRead int
}
func (r *readSizeRecorder) Read(p []byte) (int, error) {
if len(p) > r.maxRead {
r.maxRead = len(p)
}
return r.reader.Read(p)
}
func envelopeFrameWithDeclaredLength(dataLength uint32, payload []byte) []byte {
frame := make([]byte, minisign.RawSigSize+4+len(payload))
binary.LittleEndian.PutUint16(frame[:2], minisign.EdDSA)
binary.LittleEndian.PutUint32(frame[minisign.RawSigSize:minisign.RawSigSize+4], dataLength)
copy(frame[minisign.RawSigSize+4:], payload)
return frame
}
func measureEnvelopeReadAlloc(t *testing.T, readFunc func(net.Conn) (*sliverpb.Envelope, error), frame []byte) (uint64, error) {
t.Helper()
srv, cli := net.Pipe()
defer srv.Close()
defer cli.Close()
errCh := make(chan error, 1)
go func() {
_, err := readFunc(srv)
errCh <- err
}()
runtime.GC()
var before runtime.MemStats
runtime.ReadMemStats(&before)
go func() {
_, _ = cli.Write(frame)
_ = cli.Close()
}()
err := <-errCh
runtime.GC()
var after runtime.MemStats
runtime.ReadMemStats(&after)
if after.TotalAlloc < before.TotalAlloc {
return 0, err
}
return after.TotalAlloc - before.TotalAlloc, err
}
func assertLargeTruncatedFrameHasBoundedAlloc(t *testing.T, readFunc func(net.Conn) (*sliverpb.Envelope, error)) {
t.Helper()
declaredLength := uint32(socketEnvelopeDiskSpoolThreshold + (8 * 1024 * 1024))
frame := envelopeFrameWithDeclaredLength(declaredLength, []byte{0x01})
minAllocDelta := uint64(math.MaxUint64)
for i := 0; i < 3; i++ {
allocDelta, err := measureEnvelopeReadAlloc(t, readFunc, frame)
if err == nil {
t.Fatalf("expected read failure for truncated frame")
}
if !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrUnexpectedEOF) {
t.Fatalf("expected EOF-like error for truncated frame, got %v", err)
}
if allocDelta < minAllocDelta {
minAllocDelta = allocDelta
}
}
const maxAllowedAlloc = 24 * 1024 * 1024
if minAllocDelta > maxAllowedAlloc {
t.Fatalf("expected bounded allocation <= %d bytes, got %d", maxAllowedAlloc, minAllocDelta)
}
}
func TestReadSocketEnvelopeDataKeepsSmallPayloadsInMemory(t *testing.T) {
payload := bytes.Repeat([]byte{0x7a}, 4096)
recorder := &readSizeRecorder{reader: bytes.NewReader(payload)}
got, err := readSocketEnvelopeData(recorder, len(payload), 8192)
if err != nil {
t.Fatalf("readSocketEnvelopeData returned err: %v", err)
}
if !bytes.Equal(got, payload) {
t.Fatalf("unexpected payload contents")
}
if recorder.maxRead != len(payload) {
t.Fatalf("expected in-memory read size %d, got %d", len(payload), recorder.maxRead)
}
}
func TestReadSocketEnvelopeDataSpoolsLargePayloadsInChunks(t *testing.T) {
payload := bytes.Repeat([]byte{0x42}, (2*socketEnvelopeReadChunkSize)+123)
recorder := &readSizeRecorder{reader: bytes.NewReader(payload)}
got, err := readSocketEnvelopeData(recorder, len(payload), socketEnvelopeReadChunkSize/2)
if err != nil {
t.Fatalf("readSocketEnvelopeData returned err: %v", err)
}
if !bytes.Equal(got, payload) {
t.Fatalf("unexpected payload contents")
}
if recorder.maxRead > socketEnvelopeReadChunkSize {
t.Fatalf("expected max read chunk <= %d, got %d", socketEnvelopeReadChunkSize, recorder.maxRead)
}
}
func TestMTLSSocketReadEnvelopeLargeTruncatedMessageBoundsAllocation(t *testing.T) {
assertLargeTruncatedFrameHasBoundedAlloc(t, socketReadEnvelope)
}
func TestWGSocketReadEnvelopeLargeTruncatedMessageBoundsAllocation(t *testing.T) {
assertLargeTruncatedFrameHasBoundedAlloc(t, socketWGReadEnvelope)
}
+2 -4
View File
@@ -358,10 +358,8 @@ func socketReadEnvelope(connection net.Conn) (*sliverpb.Envelope, error) {
return nil, errors.New("[pivot] invalid data length")
}
dataBuf := make([]byte, dataLength)
n, err = io.ReadFull(connection, dataBuf)
if err != nil || n != dataLength {
dataBuf, err := readSocketEnvelopeData(connection, dataLength, socketEnvelopeDiskSpoolThreshold)
if err != nil {
mtlsLog.Errorf("Socket error (read data): %v", err)
return nil, err
}
+2 -5
View File
@@ -452,11 +452,8 @@ func socketWGReadEnvelope(connection net.Conn) (*sliverpb.Envelope, error) {
return nil, errors.New("[wireguard] zero data length")
}
dataBuf := make([]byte, dataLength)
n, err = io.ReadFull(connection, dataBuf)
if err != nil || n != dataLength {
dataBuf, err := readSocketEnvelopeData(connection, dataLength, socketEnvelopeDiskSpoolThreshold)
if err != nil {
wgLog.Errorf("Socket error (read data): %v", err)
return nil, err
}
+58
View File
@@ -21,6 +21,7 @@ package certs
import (
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"github.com/bishopfox/sliver/server/db"
@@ -35,6 +36,17 @@ const (
serverNamespace = "server" // Operator servers
)
var (
// ErrOperatorClientCertificateNotFound indicates that the presented operator
// client certificate is no longer trusted because it is not present in the
// certificate store.
ErrOperatorClientCertificateNotFound = errors.New("operator client certificate not found in database")
// ErrInvalidOperatorClientCertificate indicates that the presented
// certificate is not shaped like an operator client leaf certificate.
ErrInvalidOperatorClientCertificate = errors.New("invalid operator client certificate")
)
// OperatorClientGenerateCertificate - Generate a certificate signed with a given CA
func OperatorClientGenerateCertificate(operator string) ([]byte, []byte, error) {
cert, key := GenerateECCCertificate(OperatorCA, operator, false, true, true)
@@ -52,6 +64,52 @@ func OperatorClientRemoveCertificate(operator string) error {
return RemoveCertificate(OperatorCA, ECCKey, fmt.Sprintf("%s.%s", clientNamespace, operator))
}
// ValidateOperatorClientCertificate ensures that the presented operator client
// certificate is still present in the database. A valid chain alone is not
// enough; the exact leaf certificate must still exist in storage.
func ValidateOperatorClientCertificate(peerCertificates []*x509.Certificate) error {
if len(peerCertificates) == 0 || peerCertificates[0] == nil {
return ErrInvalidOperatorClientCertificate
}
leaf := peerCertificates[0]
if leaf.IsCA || leaf.Subject.CommonName == "" || !hasExtKeyUsage(leaf, x509.ExtKeyUsageClientAuth) {
return ErrInvalidOperatorClientCertificate
}
pemBytes := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: leaf.Raw,
})
if len(pemBytes) == 0 {
return ErrInvalidOperatorClientCertificate
}
record := &models.Certificate{}
result := db.Session().Select("id").Where(&models.Certificate{
CommonName: fmt.Sprintf("%s.%s", clientNamespace, leaf.Subject.CommonName),
CAType: OperatorCA,
KeyType: ECCKey,
CertificatePEM: string(pemBytes),
}).First(record)
if result.Error == nil {
return nil
}
if errors.Is(result.Error, db.ErrRecordNotFound) {
return ErrOperatorClientCertificateNotFound
}
return result.Error
}
func hasExtKeyUsage(cert *x509.Certificate, usage x509.ExtKeyUsage) bool {
for _, extKeyUsage := range cert.ExtKeyUsage {
if extKeyUsage == usage {
return true
}
}
return false
}
// OperatorServerGetCertificate - Helper function to fetch a server cert
func OperatorServerGetCertificate(hostname string) ([]byte, []byte, error) {
return GetECCCertificate(OperatorCA, fmt.Sprintf("%s.%s", serverNamespace, hostname))
+117
View File
@@ -0,0 +1,117 @@
package certs
import (
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"testing"
"time"
"github.com/bishopfox/sliver/server/db"
"github.com/bishopfox/sliver/server/db/models"
)
func TestValidateOperatorClientCertificateAdversarial(t *testing.T) {
SetupCAs()
t.Run("accepts stored operator client leaf", func(t *testing.T) {
operatorName := uniqueOperatorCertificateName(t, "stored")
certPEM, _, err := OperatorClientGenerateCertificate(operatorName)
if err != nil {
t.Fatalf("generate operator client certificate: %v", err)
}
err = ValidateOperatorClientCertificate([]*x509.Certificate{mustParseCertificate(t, certPEM)})
if err != nil {
t.Fatalf("expected stored operator client certificate to be accepted, got %v", err)
}
})
t.Run("rejects valid stored certificate hidden behind an unstored first certificate", func(t *testing.T) {
operatorName := uniqueOperatorCertificateName(t, "hidden")
storedCertPEM, _, err := OperatorClientGenerateCertificate(operatorName)
if err != nil {
t.Fatalf("generate stored operator certificate: %v", err)
}
rogueCertPEM, _ := GenerateECCCertificate(OperatorCA, operatorName, false, true, true)
err = ValidateOperatorClientCertificate([]*x509.Certificate{
mustParseCertificate(t, rogueCertPEM),
mustParseCertificate(t, storedCertPEM),
})
if !errors.Is(err, ErrOperatorClientCertificateNotFound) {
t.Fatalf("expected first unstored certificate to be rejected, got %v", err)
}
})
t.Run("rejects same-common-name different-leaf certificate", func(t *testing.T) {
operatorName := uniqueOperatorCertificateName(t, "same-cn")
_, _, err := OperatorClientGenerateCertificate(operatorName)
if err != nil {
t.Fatalf("generate stored operator certificate: %v", err)
}
rogueCertPEM, _ := GenerateECCCertificate(OperatorCA, operatorName, false, true, true)
err = ValidateOperatorClientCertificate([]*x509.Certificate{mustParseCertificate(t, rogueCertPEM)})
if !errors.Is(err, ErrOperatorClientCertificateNotFound) {
t.Fatalf("expected rotated or forged same-CN leaf to be rejected, got %v", err)
}
})
t.Run("rejects stored operator server certificate", func(t *testing.T) {
hostName := uniqueOperatorCertificateName(t, "server")
serverCertPEM, _, err := OperatorServerGenerateCertificate(hostName)
if err != nil {
t.Fatalf("generate operator server certificate: %v", err)
}
err = ValidateOperatorClientCertificate([]*x509.Certificate{mustParseCertificate(t, serverCertPEM)})
if !errors.Is(err, ErrInvalidOperatorClientCertificate) {
t.Fatalf("expected stored operator server certificate to be rejected, got %v", err)
}
})
t.Run("rejects operator CA certificate even if inserted into the certificates table", func(t *testing.T) {
caCertPEM, caKeyPEM, err := GetCertificateAuthorityPEM(OperatorCA)
if err != nil {
t.Fatalf("get operator CA: %v", err)
}
caCert := mustParseCertificate(t, caCertPEM)
record := &models.Certificate{
CommonName: fmt.Sprintf("%s.%s", clientNamespace, caCert.Subject.CommonName),
CAType: OperatorCA,
KeyType: ECCKey,
CertificatePEM: string(caCertPEM),
PrivateKeyPEM: string(caKeyPEM),
}
if err := db.Session().Create(record).Error; err != nil {
t.Fatalf("insert CA cert into certificates table: %v", err)
}
err = ValidateOperatorClientCertificate([]*x509.Certificate{caCert})
if !errors.Is(err, ErrInvalidOperatorClientCertificate) {
t.Fatalf("expected CA certificate to be rejected, got %v", err)
}
})
}
func mustParseCertificate(t *testing.T, certPEM []byte) *x509.Certificate {
t.Helper()
block, _ := pem.Decode(certPEM)
if block == nil {
t.Fatal("failed to decode certificate PEM")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
t.Fatalf("parse x509 certificate: %v", err)
}
return cert
}
func uniqueOperatorCertificateName(t *testing.T, prefix string) string {
t.Helper()
return fmt.Sprintf("%s-%d", prefix, time.Now().UnixNano())
}
+29 -11
View File
@@ -218,18 +218,9 @@ func kickOperatorCmd(cmd *cobra.Command, args []string) {
}
fmt.Printf(Info+"Removing auth token(s) for %s, please wait ... \n", operator)
err = db.Session().Where(&models.Operator{
Name: operator,
}).Delete(&models.Operator{}).Error
err = kickOperator(operator)
if err != nil {
fmt.Printf(Warn+"Failed to remove operator %s: %v\n", operator, err)
return
}
transport.ClearTokenCache()
fmt.Printf(Info+"Removing client certificate(s) for %s, please wait ... \n", operator)
err = certs.OperatorClientRemoveCertificate(operator)
if err != nil {
fmt.Printf(Warn+"Failed to remove the operator certificate: %v \n", err)
fmt.Printf(Warn+"Failed to kick operator %s: %v\n", operator, err)
return
}
fmt.Printf(Info+"Operator %s has been kicked out.\n", operator)
@@ -242,6 +233,33 @@ func shouldPromptKickOperator(cmd *cobra.Command, args []string) bool {
return cmd.Flags().NFlag() == 0
}
func removeOperator(operator string) error {
err := db.Session().Where(&models.Operator{
Name: operator,
}).Delete(&models.Operator{}).Error
if err != nil {
return err
}
transport.ClearTokenCache()
return nil
}
func revokeOperatorClientCertificate(operator string) error {
return certs.OperatorClientRemoveCertificate(operator)
}
func closeOperatorStreams(operator string) {
transport.CloseOperatorStreams(operator)
}
func kickOperator(operator string) error {
if err := removeOperator(operator); err != nil {
return err
}
defer closeOperatorStreams(operator)
return revokeOperatorClientCertificate(operator)
}
func operatorNames() ([]string, error) {
operators, err := db.OperatorAll()
if err != nil {
+211
View File
@@ -0,0 +1,211 @@
package console
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"testing"
"time"
clientassets "github.com/bishopfox/sliver/client/assets"
clienttransport "github.com/bishopfox/sliver/client/transport"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/protobuf/rpcpb"
"github.com/bishopfox/sliver/server/certs"
"github.com/bishopfox/sliver/server/core"
servertransport "github.com/bishopfox/sliver/server/transport"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/status"
"google.golang.org/grpc/test/bufconn"
)
func TestKickOperatorClosesActiveEventStreams(t *testing.T) {
certs.SetupCAs()
listener := bufconn.Listen(2 * 1024 * 1024)
grpcServer, err := servertransport.StartMtlsClientServer(listener)
if err != nil {
t.Fatalf("start mTLS client server: %v", err)
}
defer grpcServer.Stop()
defer listener.Close()
operatorName := uniqueKickOperatorName(t)
t.Cleanup(func() {
_ = removeOperator(operatorName)
_ = revokeOperatorClientCertificate(operatorName)
closeOperatorStreams(operatorName)
})
config := mustNewOperatorAssetsConfig(t, operatorName)
rpcClient, conn, err := mustMTLSBufconnClient(t, listener, config)
if err != nil {
t.Fatalf("connect operator client: %v", err)
}
defer conn.Close()
stream, err := rpcClient.Events(context.Background(), &commonpb.Empty{})
if err != nil {
t.Fatalf("start events stream: %v", err)
}
waitForCondition(t, 3*time.Second, func() bool {
return operatorActive(operatorName)
}, "operator to appear in the active client registry")
recvErr := make(chan error, 1)
go func() {
_, err := stream.Recv()
recvErr <- err
}()
if err := kickOperator(operatorName); err != nil {
t.Fatalf("kick operator: %v", err)
}
select {
case err := <-recvErr:
if err == nil {
t.Fatal("expected kicked operator event stream to close")
}
if errors.Is(err, io.EOF) {
break
}
code := status.Code(err)
if code != codes.Canceled && code != codes.Unavailable && code != codes.Unknown {
t.Fatalf("expected stream cancellation after kick, got %v", err)
}
case <-time.After(3 * time.Second):
t.Fatal("timed out waiting for kicked operator event stream to close")
}
waitForCondition(t, 3*time.Second, func() bool {
return !operatorActive(operatorName)
}, "operator to leave the active client registry")
_, err = rpcClient.GetVersion(context.Background(), &commonpb.Empty{})
if err == nil {
t.Fatal("expected kicked operator to be denied on subsequent RPCs")
}
code := status.Code(err)
if code != codes.Unauthenticated && code != codes.Unavailable {
t.Fatalf("expected unauthenticated or unavailable after kick, got %v", err)
}
exists, err := operatorExists(operatorName)
if err != nil {
t.Fatalf("lookup operator after kick: %v", err)
}
if exists {
t.Fatal("expected operator record to be removed after kick")
}
_, _, err = certs.OperatorClientGetCertificate(operatorName)
if !errors.Is(err, certs.ErrCertDoesNotExist) {
t.Fatalf("expected operator certificate to be removed after kick, got %v", err)
}
}
func mustNewOperatorAssetsConfig(t *testing.T, operatorName string) *clientassets.ClientConfig {
t.Helper()
configJSON, err := NewOperatorConfig(operatorName, "bufnet", 31337, []string{"all"})
if err != nil {
t.Fatalf("generate operator config: %v", err)
}
config := &clientassets.ClientConfig{}
if err := json.Unmarshal(configJSON, config); err != nil {
t.Fatalf("parse operator config: %v", err)
}
return config
}
func mustMTLSBufconnClient(t *testing.T, listener *bufconn.Listener, config *clientassets.ClientConfig) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
t.Helper()
dialCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
conn, err := grpc.DialContext(dialCtx, "bufnet",
grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
return listener.Dial()
}),
grpc.WithTransportCredentials(credentials.NewTLS(mustClientTLSConfig(t, config))),
grpc.WithPerRPCCredentials(staticTokenAuth(config.Token)),
grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(clienttransport.ClientMaxReceiveMessageSize)),
grpc.WithBlock(),
)
if err != nil {
return nil, nil, err
}
return rpcpb.NewSliverRPCClient(conn), conn, nil
}
func mustClientTLSConfig(t *testing.T, config *clientassets.ClientConfig) *tls.Config {
t.Helper()
clientCert, err := tls.X509KeyPair([]byte(config.Certificate), []byte(config.PrivateKey))
if err != nil {
t.Fatalf("parse client certificate: %v", err)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM([]byte(config.CACertificate))
return &tls.Config{
Certificates: []tls.Certificate{clientCert},
RootCAs: caCertPool,
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS13,
VerifyPeerCertificate: func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
return clienttransport.RootOnlyVerifyCertificate(config.CACertificate, rawCerts)
},
}
}
func operatorActive(name string) bool {
for _, active := range core.Clients.ActiveOperators() {
if active == name {
return true
}
}
return false
}
func waitForCondition(t *testing.T, timeout time.Duration, predicate func() bool, description string) {
t.Helper()
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
if predicate() {
return
}
time.Sleep(25 * time.Millisecond)
}
t.Fatalf("timed out waiting for %s", description)
}
func uniqueKickOperatorName(t *testing.T) string {
t.Helper()
return fmt.Sprintf("kick-operator-%d", time.Now().UnixNano())
}
type staticTokenAuth string
func (t staticTokenAuth) GetRequestMetadata(context.Context, ...string) (map[string]string, error) {
return map[string]string{
"Authorization": "Bearer " + string(t),
}, nil
}
func (staticTokenAuth) RequireTransportSecurity() bool {
return true
}
+2
View File
@@ -74,6 +74,7 @@ func initMiddleware(enableAuth bool) []grpc.ServerOption {
grpc.ChainStreamInterceptor(
grpc_auth.StreamServerInterceptor(tokenAuthFunc),
permissionsStreamServerInterceptor(),
trackOperatorStreamInterceptor(),
grpc_tags.StreamServerInterceptor(grpc_tags.WithFieldExtractor(grpc_tags.CodeGenRequestFieldExtractor)),
grpc_logrus.StreamServerInterceptor(logrusEntry, logrusOpts...),
grpc_logrus.PayloadStreamServerInterceptor(logrusEntry, deciderStream),
@@ -90,6 +91,7 @@ func initMiddleware(enableAuth bool) []grpc.ServerOption {
),
grpc.ChainStreamInterceptor(
grpc_auth.StreamServerInterceptor(serverAuthFunc),
trackOperatorStreamInterceptor(),
grpc_tags.StreamServerInterceptor(grpc_tags.WithFieldExtractor(grpc_tags.CodeGenRequestFieldExtractor)),
grpc_logrus.StreamServerInterceptor(logrusEntry, logrusOpts...),
grpc_logrus.PayloadStreamServerInterceptor(logrusEntry, deciderStream),
+28 -5
View File
@@ -21,6 +21,7 @@ package transport
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"net"
"runtime/debug"
@@ -50,15 +51,34 @@ var (
// StartMtlsClientListener - Start a mutual TLS listener
func StartMtlsClientListener(host string, port uint16) (*grpc.Server, net.Listener, error) {
mtlsLog.Infof("Starting gRPC/mtls listener on %s:%d", host, port)
tlsConfig := getOperatorServerTLSConfig("multiplayer")
creds := credentials.NewTLS(tlsConfig)
ln, err := net.Listen("tcp", fmt.Sprintf("%s:%d", host, port))
if err != nil {
mtlsLog.Error(err)
return nil, nil, err
}
grpcServer, err := StartMtlsClientServer(ln)
if err != nil {
ln.Close()
return nil, nil, err
}
return grpcServer, ln, nil
}
// StartMtlsClientServer serves the authenticated multiplayer gRPC server on an
// existing listener. This is primarily useful for tests that need the full mTLS
// + auth stack without opening a real TCP socket.
func StartMtlsClientServer(ln net.Listener) (*grpc.Server, error) {
if ln == nil {
return nil, errors.New("listener is required")
}
tlsConfig := getOperatorServerTLSConfig("multiplayer")
if tlsConfig == nil {
return nil, errors.New("failed to create operator TLS config")
}
creds := credentials.NewTLS(tlsConfig)
options := []grpc.ServerOption{
grpc.Creds(creds),
grpc.MaxRecvMsgSize(ServerMaxMessageSize),
@@ -81,7 +101,7 @@ func StartMtlsClientListener(host string, port uint16) (*grpc.Server, net.Listen
panicked = false
}
}()
return grpcServer, ln, nil
return grpcServer, nil
}
// getOperatorServerTLSConfig - Generate the TLS configuration, we do now allow the end user
@@ -115,6 +135,9 @@ func getOperatorServerTLSConfig(host string) *tls.Config {
ClientCAs: caCertPool,
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS13,
VerifyConnection: func(state tls.ConnectionState) error {
return certs.ValidateOperatorClientCertificate(state.PeerCertificates)
},
}
return tlsConfig
+153
View File
@@ -0,0 +1,153 @@
package transport
import (
"crypto/tls"
"errors"
"fmt"
"net"
"strings"
"testing"
"time"
"github.com/bishopfox/sliver/server/certs"
)
func TestOperatorClientCertificateValidation(t *testing.T) {
certs.SetupCAs()
t.Run("accepts stored operator certificate", func(t *testing.T) {
operatorName := uniqueOperatorName(t, "stored")
certPEM, keyPEM, err := certs.OperatorClientGenerateCertificate(operatorName)
if err != nil {
t.Fatalf("generate operator certificate: %v", err)
}
err = performMutualTLSHandshake(getOperatorServerTLSConfig("multiplayer"), newClientTLSConfig(t, certPEM, keyPEM))
if err != nil {
t.Fatalf("expected stored operator certificate to be accepted, got %v", err)
}
})
t.Run("rejects deleted operator certificate", func(t *testing.T) {
operatorName := uniqueOperatorName(t, "deleted")
certPEM, keyPEM, err := certs.OperatorClientGenerateCertificate(operatorName)
if err != nil {
t.Fatalf("generate operator certificate: %v", err)
}
if err := certs.OperatorClientRemoveCertificate(operatorName); err != nil {
t.Fatalf("remove operator certificate: %v", err)
}
err = performMutualTLSHandshake(getOperatorServerTLSConfig("multiplayer"), newClientTLSConfig(t, certPEM, keyPEM))
if err == nil {
t.Fatal("expected deleted operator certificate to be rejected")
}
if !strings.Contains(err.Error(), certs.ErrOperatorClientCertificateNotFound.Error()) {
t.Fatalf("expected database rejection error, got %v", err)
}
})
t.Run("rejects rotated-out certificate but accepts current one", func(t *testing.T) {
operatorName := uniqueOperatorName(t, "rotated")
oldCertPEM, oldKeyPEM, err := certs.OperatorClientGenerateCertificate(operatorName)
if err != nil {
t.Fatalf("generate original operator certificate: %v", err)
}
if err := certs.OperatorClientRemoveCertificate(operatorName); err != nil {
t.Fatalf("remove original operator certificate: %v", err)
}
newCertPEM, newKeyPEM, err := certs.OperatorClientGenerateCertificate(operatorName)
if err != nil {
t.Fatalf("generate replacement operator certificate: %v", err)
}
err = performMutualTLSHandshake(getOperatorServerTLSConfig("multiplayer"), newClientTLSConfig(t, oldCertPEM, oldKeyPEM))
if err == nil {
t.Fatal("expected rotated-out operator certificate to be rejected")
}
if !strings.Contains(err.Error(), certs.ErrOperatorClientCertificateNotFound.Error()) {
t.Fatalf("expected rotated certificate to fail database validation, got %v", err)
}
err = performMutualTLSHandshake(getOperatorServerTLSConfig("multiplayer"), newClientTLSConfig(t, newCertPEM, newKeyPEM))
if err != nil {
t.Fatalf("expected replacement operator certificate to be accepted, got %v", err)
}
})
t.Run("rejects certificate from another authority", func(t *testing.T) {
certPEM, keyPEM := certs.GenerateECCCertificate(certs.HTTPSCA, uniqueOperatorName(t, "wrong-ca"), false, true, false)
err := performMutualTLSHandshake(getOperatorServerTLSConfig("multiplayer"), newClientTLSConfig(t, certPEM, keyPEM))
if err == nil {
t.Fatal("expected certificate from another authority to be rejected")
}
if strings.Contains(err.Error(), certs.ErrOperatorClientCertificateNotFound.Error()) {
t.Fatalf("expected trust-chain rejection before database lookup, got %v", err)
}
})
}
func newClientTLSConfig(t *testing.T, certPEM []byte, keyPEM []byte) *tls.Config {
t.Helper()
clientCert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
t.Fatalf("parse client certificate: %v", err)
}
return &tls.Config{
Certificates: []tls.Certificate{clientCert},
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS13,
}
}
func performMutualTLSHandshake(serverConfig *tls.Config, clientConfig *tls.Config) error {
serverConn, clientConn := net.Pipe()
defer serverConn.Close()
defer clientConn.Close()
deadline := time.Now().Add(2 * time.Second)
_ = serverConn.SetDeadline(deadline)
_ = clientConn.SetDeadline(deadline)
serverTLS := tls.Server(serverConn, serverConfig)
clientTLS := tls.Client(clientConn, clientConfig)
defer serverTLS.Close()
defer clientTLS.Close()
type handshakeResult struct {
side string
err error
}
results := make(chan handshakeResult, 2)
go func() {
results <- handshakeResult{side: "server", err: serverTLS.Handshake()}
}()
go func() {
results <- handshakeResult{side: "client", err: clientTLS.Handshake()}
}()
var errs []string
for i := 0; i < 2; i++ {
result := <-results
if result.err == nil {
continue
}
if errors.Is(result.err, net.ErrClosed) {
continue
}
errs = append(errs, fmt.Sprintf("%s: %v", result.side, result.err))
}
if len(errs) == 0 {
return nil
}
return errors.New(strings.Join(errs, "; "))
}
func uniqueOperatorName(t *testing.T, prefix string) string {
t.Helper()
return fmt.Sprintf("%s-%d", prefix, time.Now().UnixNano())
}
+130
View File
@@ -0,0 +1,130 @@
package transport
import (
"context"
"strings"
"sync"
"sync/atomic"
"github.com/bishopfox/sliver/server/db/models"
"google.golang.org/grpc"
)
type trackedServerStream struct {
grpc.ServerStream
ctx context.Context
}
func (s *trackedServerStream) Context() context.Context {
return s.ctx
}
type operatorStreamRegistry struct {
mu sync.Mutex
nextID atomic.Uint64
operators map[string]map[uint64]context.CancelFunc
}
var activeOperatorStreams = &operatorStreamRegistry{
operators: map[string]map[uint64]context.CancelFunc{},
}
func (r *operatorStreamRegistry) register(operator string, cancel context.CancelFunc) uint64 {
if cancel == nil {
return 0
}
operator = strings.TrimSpace(operator)
if operator == "" {
return 0
}
streamID := r.nextID.Add(1)
r.mu.Lock()
defer r.mu.Unlock()
streams := r.operators[operator]
if streams == nil {
streams = map[uint64]context.CancelFunc{}
r.operators[operator] = streams
}
streams[streamID] = cancel
return streamID
}
func (r *operatorStreamRegistry) unregister(operator string, streamID uint64) {
if streamID == 0 {
return
}
operator = strings.TrimSpace(operator)
if operator == "" {
return
}
r.mu.Lock()
defer r.mu.Unlock()
streams := r.operators[operator]
if streams == nil {
return
}
delete(streams, streamID)
if len(streams) == 0 {
delete(r.operators, operator)
}
}
func (r *operatorStreamRegistry) close(operator string) int {
operator = strings.TrimSpace(operator)
if operator == "" {
return 0
}
r.mu.Lock()
streams := r.operators[operator]
if len(streams) == 0 {
r.mu.Unlock()
return 0
}
cancels := make([]context.CancelFunc, 0, len(streams))
for streamID, cancel := range streams {
cancels = append(cancels, cancel)
delete(streams, streamID)
}
delete(r.operators, operator)
r.mu.Unlock()
for _, cancel := range cancels {
cancel()
}
return len(cancels)
}
// CloseOperatorStreams cancels all tracked operator-owned gRPC streams.
func CloseOperatorStreams(operator string) int {
return activeOperatorStreams.close(operator)
}
func trackOperatorStreamInterceptor() grpc.StreamServerInterceptor {
return func(srv interface{}, ss grpc.ServerStream, _ *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
operator, ok := ss.Context().Value(Operator).(*models.Operator)
if !ok || operator == nil || strings.TrimSpace(operator.Name) == "" {
return handler(srv, ss)
}
ctx, cancel := context.WithCancel(ss.Context())
streamID := activeOperatorStreams.register(operator.Name, cancel)
defer func() {
activeOperatorStreams.unregister(operator.Name, streamID)
cancel()
}()
return handler(srv, &trackedServerStream{
ServerStream: ss,
ctx: ctx,
})
}
}