diff --git a/client/command/help/long-help.go b/client/command/help/long-help.go index 89dc61de69..1a2b61a385 100644 --- a/client/command/help/long-help.go +++ b/client/command/help/long-help.go @@ -147,10 +147,10 @@ You must specificy at least one c2 endpoint when generating an implant, this can The command requires at least one use of --mtls, --wg, --http, or --dns, --named-pipe, or --tcp-pivot. The follow command is used to generate a sliver Windows executable (PE) file, that will connect back to the server using mutual-TLS: - generate --mtls foo.example.com + generate --mtls foo.example.com The follow command is used to generate a sliver Windows executable (PE) file, that will connect back to the server using Wireguard on UDP port 9090, -then connect to TCP port 1337 on the server's virtual tunnel interface to retrieve new wireguard keys, re-establish the wireguard connection using the new keys, +then connect to TCP port 1337 on the server's virtual tunnel interface to retrieve new wireguard keys, re-establish the wireguard connection using the new keys, then connect to TCP port 8888 on the server's virtual tunnel interface to establish c2 comms. generate --wg 3.3.3.3:9090 --key-exchange 1337 --tcp-comms 8888 @@ -163,28 +163,28 @@ Supported output formats are Windows PE, Windows DLL, Windows Shellcode, Mach-O, with the --os and --format flags. To output a 64bit Windows PE file (defaults to WinPE/64bit), either of the following command would be used: - generate --mtls foo.example.com + generate --mtls foo.example.com generate --os windows --arch 64bit --mtls foo.example.com A Windows DLL can be generated with the following command: generate --format shared --mtls foo.example.com To output a MacOS Mach-O executable file, the following command would be used - generate --os mac --mtls foo.example.com + generate --os mac --mtls foo.example.com To output a Linux ELF executable file, the following command would be used: - generate --os linux --mtls foo.example.com + generate --os linux --mtls foo.example.com [[.Bold]][[.Underline]]++ DNS Canaries ++[[.Normal]] -DNS canaries are unique per-binary domains that are deliberately NOT obfuscated during the compilation process. -This is done so that these unique domains show up if someone runs 'strings' on the binary, if they then attempt -to probe the endpoint or otherwise resolve the domain you'll be alerted that your implant has been discovered, +DNS canaries are unique per-binary domains that are deliberately NOT obfuscated during the compilation process. +This is done so that these unique domains show up if someone runs 'strings' on the binary, if they then attempt +to probe the endpoint or otherwise resolve the domain you'll be alerted that your implant has been discovered, and which implant file was discovered along with any affected sessions. [[.Bold]]Important:[[.Normal]] You must have a DNS listener/server running to detect the DNS queries (see the "dns" command). -Unique canary subdomains are automatically generated and inserted using the --canary flag. You can view previously generated +Unique canary subdomains are automatically generated and inserted using the --canary flag. You can view previously generated canaries and their status using the "canaries" command: generate --mtls foo.example.com --canary 1.foobar.com @@ -198,13 +198,17 @@ command. ` stageListenerHelp = `[[.Bold]]Command:[[.Normal]] stage-listener [[.Bold]]About:[[.Normal]] Starts a stager listener bound to a Sliver profile. -[[.Bold]]Examples:[[.Normal]] +[[.Bold]]Examples:[[.Normal]] The following command will start a TCP listener on 1.2.3.4:8080, and link the [[.Bold]]my-sliver-profile[[.Normal]] profile to it. When a stager calls back to this URL, a sliver corresponding to the said profile will be sent. stage-listener --url tcp://1.2.3.4:8080 --profile my-sliver-profile +You can also specify a network interface name instead of an IP address: + +stage-listener --url tcp://tun0:8080 --profile my-sliver-profile + To create a profile, use the [[.Bold]]profiles new[[.Normal]] command. A common scenario is to create a profile that generates a shellcode, which can act as a stage 2: profiles new --format shellcode --mtls 1.2.3.4 --skip-symbols windows-shellcode @@ -236,7 +240,7 @@ settings. Generated implants will still have per-binary certificates/obfuscation [[.Bold]]About:[[.Normal]] List processes on remote system.` pingHelp = `[[.Bold]]Command:[[.Normal]] ping -[[.Bold]]About:[[.Normal]] Ping session by name or the active session. This does NOT send an ICMP packet, it just sends a small +[[.Bold]]About:[[.Normal]] Ping session by name or the active session. This does NOT send an ICMP packet, it just sends a small C2 message round trip to ensure the remote implant is still responding to commands.` killHelp = `[[.Bold]]Command:[[.Normal]] kill @@ -281,7 +285,7 @@ On Windows, escaping is disabled. Instead, '\\' is treated as path separator. rmHelp = `[[.Bold]]Command:[[.Normal]] rm [remote path] [[.Bold]]About:[[.Normal]] Delete a remote file or directory.` - catHelp = `[[.Bold]]Command:[[.Normal]] cat + catHelp = `[[.Bold]]Command:[[.Normal]] cat [[.Bold]]About:[[.Normal]] Cat a remote file to stdout.` downloadHelp = `[[.Bold]]Command:[[.Normal]] download [remote src] @@ -306,10 +310,10 @@ Downloads can be filtered using the following patterns: If you need to match a special character (*, ?, '-', '[', ']', '\\'), place '\\' in front of it (example: \\?). On Windows, escaping is disabled. Instead, '\\' is treated as path separator.` - headHelp = `[[.Bold]]Command:[[.Normal]] head [--bytes/-b ] [--lines/-l ] + headHelp = `[[.Bold]]Command:[[.Normal]] head [--bytes/-b ] [--lines/-l ] [[.Bold]]About:[[.Normal]] Fetch the first number of bytes or lines from a remote file and display it to stdout.` - tailHelp = `[[.Bold]]Command:[[.Normal]] tail [--bytes/-b ] [--lines/-l ] + tailHelp = `[[.Bold]]Command:[[.Normal]] tail [--bytes/-b ] [--lines/-l ] [[.Bold]]About:[[.Normal]] Fetch the last number of bytes or lines from a remote file and display it to stdout.` uploadHelp = `[[.Bold]]Command:[[.Normal]] upload [local src] @@ -429,7 +433,7 @@ Parameters to the Linux and MacOS shared module are passed using the [[.Bold]]LD screenshotHelp = `[[.Bold]]Command:[[.Normal]] screenshot [[.Bold]]About:[[.Normal]] Take a screenshot from the remote implant. ` - loadAliasHelp = `[[.Bold]]Command:[[.Normal]] load-macro + loadAliasHelp = `[[.Bold]]Command:[[.Normal]] load-macro [[.Bold]]About:[[.Normal]] Load a Sliver macro to add new commands. Macros are using the [[.Bold]]sideload[[.Normal]] or [[.Bold]]spawndll[[.Normal]] commands under the hood, depending on the use case. For Linux and Mac OS, the [[.Bold]]sideload[[.Normal]] command will be used. On Windows, it will depend the macro file is a reflective DLL or not. @@ -643,9 +647,9 @@ ssh -l ubuntu ec2-instance ps aux lootHelp = `[[.Bold]]Command:[[.Normal]] loot [[.Bold]]About:[[.Normal]] Store and share loot between operators. -A piece of loot is a file, that can be one of two loot types: text or binary. +A piece of loot is a file, that can be one of two loot types: text or binary. -Sliver will attempt to detect the type of file automatically or you can specify a file type with +Sliver will attempt to detect the type of file automatically or you can specify a file type with --file-type. You can add local files as loot using the "local" sub-command, or you can add files from a session using the "remote" sub-command. @@ -686,10 +690,10 @@ using SliverPy (Python) or sliver-script (TypeScript/JavaScript). ) reactionSetHelp = fmt.Sprintf(`[[.Bold]]Command:[[.Normal]] reaction set -[[.Bold]]About:[[.Normal]] Set automated commands in reaction to event(s). +[[.Bold]]About:[[.Normal]] Set automated commands in reaction to event(s). -The built-in reactions do not support variables or logic, they simply allow you to -run verbatim commands when an event occurs. To implement complex event-based logic +The built-in reactions do not support variables or logic, they simply allow you to +run verbatim commands when an event occurs. To implement complex event-based logic we recommend using SliverPy (Python) or sliver-script (TypeScript/JavaScript). [[.Bold]]Examples:[[.Normal]] @@ -751,8 +755,8 @@ If no extension is specified, Sliver will enumerate all installed extensions, ex permissions and determine a valid target for injection. For Cursed Chrome to work properly the target extension must have either of these two sets of permissions: -1. "webRequest" "webRequestBlocking" "" -2. "webRequest" "webRequestBlocking" "http://*/*" "https://*/*" +1. "webRequest" "webRequestBlocking" "" +2. "webRequest" "webRequestBlocking" "http://*/*" "https://*/*" More information: https://github.com/mandatoryprogrammer/CursedChrome ` @@ -1303,7 +1307,7 @@ On Windows, escaping is disabled. Instead, '\\' is treated as path separator.` servicesHelp = `[[.Bold]]Command:[[.Normal]] services [-H ] [[.Bold]]About:[[.Normal]] Get information about services and control them (start, stop). - + To get information about services, you need to be an authenticated user on the system or domain. To control services, you need administrator or higher privileges.` ) diff --git a/client/command/jobs/stage.go b/client/command/jobs/stage.go index 1a0a7234cc..07bf87666f 100644 --- a/client/command/jobs/stage.go +++ b/client/command/jobs/stage.go @@ -23,6 +23,7 @@ import ( "compress/zlib" "context" "encoding/binary" + "net" "net/url" "strconv" "strings" @@ -56,6 +57,14 @@ func StageListenerCmd(cmd *cobra.Command, con *console.SliverClient, args []stri con.PrintErrorf("Listener-url format not supported") return } + + // Handle interface names in host + host := stagingURL.Hostname() + if net.ParseIP(host) == nil { + // If not an IP, assume it's an interface name + con.PrintInfof("Using interface %s for stager listener\n", host) + } + stagingPort, err := strconv.ParseUint(stagingURL.Port(), 10, 32) if err != nil { con.PrintErrorf("error parsing staging port: %v\n", err) diff --git a/server/c2/c2_test.go b/server/c2/c2_test.go index 52b5884845..7048d14f25 100644 --- a/server/c2/c2_test.go +++ b/server/c2/c2_test.go @@ -21,8 +21,10 @@ package c2 import ( "crypto/sha256" "encoding/hex" + "fmt" "os" "testing" + "time" implantCrypto "github.com/bishopfox/sliver/implant/sliver/cryptography" "github.com/bishopfox/sliver/server/certs" @@ -59,11 +61,12 @@ func setup() *models.ImplantConfig { digest.Write([]byte(peerAgeKeyPair.Public)) publicKeyDigest := hex.EncodeToString(digest.Sum(nil)) + // Use a unique name based on timestamp to avoid conflicts implantBuild := &models.ImplantBuild{ + Name: fmt.Sprintf("test-build-%d", time.Now().UnixNano()), PeerPublicKey: peerAgeKeyPair.Public, PeerPublicKeyDigest: publicKeyDigest, PeerPrivateKey: peerAgeKeyPair.Private, - AgeServerPublicKey: serverAgeKeyPair.Public, } err = db.Session().Create(implantBuild).Error diff --git a/server/c2/tcp-stager_test.go b/server/c2/tcp-stager_test.go new file mode 100644 index 0000000000..6f765e2a67 --- /dev/null +++ b/server/c2/tcp-stager_test.go @@ -0,0 +1,129 @@ +package c2 + +/* + Sliver Implant Framework + Copyright (C) 2021 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 . +*/ + +import ( + "bytes" + "fmt" + "io" + "net" + "testing" +) + +func TestTCPStager(t *testing.T) { + testData := []byte("test shellcode data") + + // Find an available port + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Failed to find available port: %v", err) + } + addr := listener.Addr().(*net.TCPAddr) + port := uint16(addr.Port) + listener.Close() + + // Test localhost + ln, err := StartTCPListener("127.0.0.1", port, testData) + if err != nil { + t.Fatalf("Failed to start TCP stager: %v", err) + } + defer ln.Close() + + conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) + if err != nil { + t.Fatalf("Failed to connect to stager: %v", err) + } + defer conn.Close() + + receivedData, err := io.ReadAll(conn) + if err != nil { + t.Fatalf("Failed to read from stager: %v", err) + } + + if !bytes.Equal(receivedData, testData) { + t.Fatalf("Received data does not match sent data") + } +} + +func TestTCPStagerInterface(t *testing.T) { + testData := []byte("test shellcode data") + + // Find an available port + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Failed to find available port: %v", err) + } + addr := listener.Addr().(*net.TCPAddr) + port := uint16(addr.Port) + listener.Close() + + // Test eth0 interface, this might fail if eth0 doesn't exist, we just want to verify the function accepts interface names + ln, err := StartTCPListener("eth0", port, testData) + if err != nil { + t.Logf("Note: eth0 test skipped (expected if eth0 doesn't exist): %v", err) + return + } + defer ln.Close() + + // Get eth0 IP + ifaces, err := net.Interfaces() + if err != nil { + t.Fatalf("Failed to get interfaces: %v", err) + } + + var eth0IP string + for _, iface := range ifaces { + if iface.Name == "eth0" { + addrs, err := iface.Addrs() + if err != nil { + t.Fatalf("Failed to get addresses for eth0: %v", err) + } + for _, addr := range addrs { + if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { + if ipnet.IP.To4() != nil { + eth0IP = ipnet.IP.String() + break + } + } + } + break + } + } + + if eth0IP == "" { + t.Logf("Note: Could not find IPv4 address for eth0") + return + } + + // Connect using eth0 IP + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", eth0IP, port)) + if err != nil { + t.Fatalf("Failed to connect to stager: %v", err) + } + defer conn.Close() + + receivedData, err := io.ReadAll(conn) + if err != nil { + t.Fatalf("Failed to read from stager: %v", err) + } + + if !bytes.Equal(receivedData, testData) { + t.Fatalf("Received data does not match sent data") + } +} \ No newline at end of file diff --git a/server/rpc/rpc-stager.go b/server/rpc/rpc-stager.go index f8d6e45fb6..bb04c64283 100644 --- a/server/rpc/rpc-stager.go +++ b/server/rpc/rpc-stager.go @@ -20,6 +20,7 @@ package rpc import ( "context" + "fmt" "net" "github.com/bishopfox/sliver/client/constants" @@ -31,9 +32,16 @@ import ( // StartTCPStagerListener starts a TCP stager listener func (rpc *Server) StartTCPStagerListener(ctx context.Context, req *clientpb.StagerListenerReq) (*clientpb.StagerListener, error) { host := req.GetHost() - if !checkInterface(req.GetHost()) { - host = "0.0.0.0" + + // If host is not an IP address, try to resolve it as an interface name + if net.ParseIP(host) == nil { + ifaceIP, err := getInterfaceIP(host) + if err != nil { + return nil, fmt.Errorf("failed to resolve interface %s: %v", host, err) + } + host = ifaceIP } + job, err := c2.StartTCPStagerListenerJob(host, uint16(req.GetPort()), req.ProfileName, req.GetData()) if err != nil { return nil, err @@ -52,27 +60,59 @@ func (rpc *Server) StartTCPStagerListener(ctx context.Context, req *clientpb.Sta return &clientpb.StagerListener{JobID: uint32(job.ID)}, nil } -// checkInterface verifies if an IP address -// is attached to an existing network interface -func checkInterface(a string) bool { - interfaces, err := net.Interfaces() +// checkInterface verifies if an IP address or interface name is attached to an existing network interface and returns the IP +func checkInterface(host string) bool { + // First check if it's an IP address + if net.ParseIP(host) != nil { + return true + } + + // If not an IP, try to resolve as interface name + ifaces, err := net.Interfaces() if err != nil { return false } - for _, i := range interfaces { - addresses, err := i.Addrs() - if err != nil { - return false - } - for _, netAddr := range addresses { - addr, err := net.ResolveTCPAddr("tcp", netAddr.String()) + + for _, iface := range ifaces { + if iface.Name == host { + addrs, err := iface.Addrs() if err != nil { - return false + continue } - if addr.IP.String() == a { - return true + for _, addr := range addrs { + switch v := addr.(type) { + case *net.IPNet: + if v.IP.To4() != nil { + return true + } + } } } } + return false } + +// getInterfaceIP returns the first IPv4 address of the specified interface +func getInterfaceIP(ifaceName string) (string, error) { + iface, err := net.InterfaceByName(ifaceName) + if err != nil { + return "", err + } + + addrs, err := iface.Addrs() + if err != nil { + return "", err + } + + for _, addr := range addrs { + switch v := addr.(type) { + case *net.IPNet: + if v.IP.To4() != nil { + return v.IP.String(), nil + } + } + } + + return "", fmt.Errorf("no IPv4 address found for interface %s", ifaceName) +} \ No newline at end of file