File Transfers
Upload and download files with chunked transfer and integrity verification.
Overview
NexusC2 implements bidirectional file transfer between the server and agents using a chunked transfer protocol. This allows efficient transfer of large files while maintaining progress tracking and supporting resumption on network interruptions.
Transfer Types:
- Upload (Server → Agent): Push files to agent filesystem
- Download (Agent → Server): Pull files from agent filesystem
Key Features:
- Chunked transfer with configurable sizes
- In-memory and disk-based chunk assembly
- Progress tracking and status reporting
- UNC path support for Windows network shares
- Automatic cleanup of temporary files
Architecture
Upload Flow (Server → Agent)
flowchart TB
subgraph Client["GUI Client"]
USELECT[Select File + Destination]
end
subgraph Server["Server"]
UWS[WebSocket Service
Assemble in /app/uploads]
UAH[Agent Handler
Split to 512KB chunks]
end
subgraph Agent["Agent"]
URECV[Receive chunks via GET]
UMEM[Store in memory map]
UWRITE[Write to destination]
end
USELECT --> UWS
UWS -->|gRPC notify| UAH
UAH -->|Queue chunks| URECV
URECV --> UMEM --> UWRITE
UWRITE --> DEST[Destination File]
Download Flow (Agent → Server)
flowchart TB
subgraph Client["GUI Client"]
DCMD[Issue download command]
DNOTIFY[Receive notification]
end
subgraph Agent["Agent"]
DOPEN[Open file, calc chunks]
DREAD[Read 512KB chunk]
DCONT[Continue reading]
end
subgraph Server["Server"]
DAH[Agent Handler
Write .partN files]
DTRACK[Download Tracker
Assemble final file]
end
DCMD --> DOPEN --> DREAD
DREAD -->|POST chunk| DAH
DAH -->|download_continue| DCONT
DCONT -->|POST chunk| DAH
DAH --> DTRACK
DTRACK --> DNOTIFY
Upload (Server → Agent)
Chunk Configuration
| Property | Value |
|---|---|
| Chunk Size | 512 KB (524,288 bytes) |
| Encoding | Base64 |
| Storage | In-memory (agent), disk (server) |
Server-Side Processing
Step 1: GUI Client Upload
The GUI client sends file chunks to the WebSocket service:
{
"type": "file_upload",
"data": {
"upload_id": "uuid",
"agent_id": "target-agent-uuid",
"file_name": "payload.exe",
"remote_path": "C:\\Windows\\Temp\\payload.exe",
"chunk_num": 0,
"total_chunks": 10,
"chunk_data": "<base64 encoded data>",
"file_size": 5242880
}
}
Step 2: WebSocket Service Assembly
- Creates temp directory:
/app/temp/{upload_id}/ - Writes each chunk to:
chunk_0,chunk_1, etc. - When all chunks received, assembles to:
/app/uploads/{filename}_{timestamp}.ext - Notifies Agent Handler via gRPC
HandleUpload
Step 3: Agent Handler Processing
- Reads assembled file from
/app/uploads/ - Calculates total chunks (512KB each)
- Queues first chunk to agent’s command buffer
- Stores remaining chunks to disk:
/app/temp/{filename}/chunk_N - Saves metadata for chunk continuation
Metadata Structure:
{
"agent_id": "agent-uuid",
"command_db_id": 42,
"original_filename": "payload.exe",
"current_filename": "payload_20250115_103000.exe",
"remote_path": "C:\\Windows\\Temp\\payload.exe",
"total_chunks": 10,
"chunk_dir": "/app/temp/payload_20250115_103000.exe",
"current_chunk": 0
}
Agent-Side Processing
Chunk Reception
Chunks arrive in GET poll responses as commands:
{
"command_type": 16,
"command": "upload",
"filename": "payload_20250115_103000.exe",
"remote_path": "C:\\Windows\\Temp\\payload.exe",
"current_chunk": 0,
"total_chunks": 10,
"data": "<base64 encoded chunk>"
}
In-Memory Assembly
The agent maintains active uploads in memory:
type UploadInfo struct {
Chunks map[int][]byte // Chunk index → data
TotalChunks int
RemotePath string
Filename string
StartTime time.Time
LastUpdate time.Time
}
Processing Steps:
- Base64 decode chunk data
- Store in
Chunksmap by index - Update
LastUpdatetimestamp - If last chunk received:
- Create parent directories (with network share support)
- Write chunks in order to destination file
- Clear chunks from memory as written
- Clean up tracking state
Path Resolution
The agent supports multiple path formats:
| Format | Example | Handling |
|---|---|---|
| Absolute (Unix) | /etc/config | Used directly |
| Absolute (Windows) | C:\Windows\Temp | Used directly |
| UNC Path | \\server\share\file | Special handling for network auth |
| Relative | payload.exe | Joined with working directory |
UNC Path Handling:
// Normalize to backslashes for UNC
workingDir = strings.ReplaceAll(workingDir, "/", "\\")
if !strings.HasSuffix(workingDir, "\\") {
workingDir += "\\"
}
remotePath = workingDir + args[0]
Download (Agent → Server)
Chunk Configuration
| Property | Value |
|---|---|
| Chunk Size | 512 KB (524,288 bytes) |
| Encoding | Base64 |
| Buffer Pool | Reusable 512KB buffers |
Agent-Side Processing
Initial Command
When the agent receives a download command:
// Buffer pool for memory efficiency
var downloadBufferPool = sync.Pool{
New: func() interface{} {
buf := make([]byte, 512*1024) // 512KB
return &buf
},
}
Processing Steps:
- Resolve path (absolute, relative, or UNC)
- Open file, get size information
- Calculate total chunks
- Read first chunk using pooled buffer
- Base64 encode and return with metadata
- Register download with internal tracker
Chunk Continuation
When server requests next chunk via download_continue:
func GetNextFileChunk(filePath string, chunkNumber int, originalCmd Command) (*CommandResult, error) {
file, err := NetworkAwareOpenFile(filePath, os.O_RDONLY, 0)
// ...
// Seek to correct position
offset := int64(chunkNumber-1) * chunkSize
file.Seek(offset, 0)
// Read chunk using buffer pool
bufPtr := downloadBufferPool.Get().(*[]byte)
defer downloadBufferPool.Put(bufPtr)
chunk := *bufPtr
n, err := file.Read(chunk)
// Encode and return
encodedData := base64.StdEncoding.EncodeToString(chunk[:n])
// ...
}
Server-Side Processing
Download Tracker
The server tracks in-progress downloads:
type DownloadTracker struct {
mu sync.RWMutex
ongoing map[string]map[int]bool // filename → received chunks
tempPath string // /app/temp
destPath string // /app/downloads
manifestManager *ManifestManager
}
Chunk Storage
Each chunk is stored as a separate file:
/app/temp/
├── payload.exe.part1
├── payload.exe.part2
├── payload.exe.part3
...
File Assembly
When all chunks received:
func (dt *DownloadTracker) assembleFile(filename string, totalChunks int) error {
destPath := filepath.Join(dt.destPath, filename)
outFile, err := os.Create(destPath)
// Assemble chunks in order
for i := 1; i <= totalChunks; i++ {
chunkPath := filepath.Join(dt.tempPath, fmt.Sprintf("%s.part%d", filename, i))
chunkData, _ := os.ReadFile(chunkPath)
outFile.Write(chunkData)
os.Remove(chunkPath) // Clean up as we go
}
// Add to manifest
dt.manifestManager.AddDownload(filename)
return nil
}
Downloads Manifest
Completed downloads are tracked in /app/downloads/downloads.json:
{
"downloads": [
{
"filename": "secrets.txt",
"size": 1024,
"timestamp": "2025-01-15T10:30:00Z"
},
{
"filename": "config.ini",
"size": 2048,
"timestamp": "2025-01-15T11:00:00Z"
}
]
}
Progress Tracking
Upload Progress
The server tracks upload progress in real-time:
type ProgressStats struct {
Filename string
Current int64
Total int64
Percentage float64
Speed float64 // bytes per second
}
Logged Output:
[Progress] File: payload.exe, Current: 1048576/5242880 bytes (20.00%), Speed: 2.50 MB/s
Download Progress
Downloads report progress via command output:
S4:1/10 → "Progress: 1/10"
S4:2/10 → "Progress: 2/10"
...
S5:payload.exe → "File successfully written: payload.exe"
Error Handling
Upload Errors
| Error Code | Description |
|---|---|
| E1 | Missing arguments (no path specified) |
| E11 | Failed to create file or write data |
| E24 | Missing chunk data or incomplete upload |
Download Errors
| Error Code | Description |
|---|---|
| E1 | Missing arguments (no path specified) |
| E6 | Path is a directory (not a file) |
| E10 | Cannot read file (permissions, not found) |
| E24 | No data returned (empty chunk) |
Recovery Behavior
- Timeout: Stale uploads are cleaned up based on
LastUpdatetimestamp - Interruption: Partial downloads remain as
.partNfiles until retry - Memory Pressure: Agent clears chunks from memory as they’re written
Network Share Support
Windows UNC Paths
The agent supports Windows network shares via NetworkAwareOpenFile:
// Handles paths like \\server\share\path\file.txt
func NetworkAwareOpenFile(path string, flag int, perm os.FileMode) (*os.File, error)
// Creates directories on network shares
func NetworkAwareMkdirAll(path string, perm os.FileMode) error
Features:
- Automatic authentication with current user token
- Network-only impersonation support
- SMB share traversal
Data Flow Summary
Upload (Server → Agent)
1. GUI sends file chunks to WebSocket (100KB chunks)
2. WebSocket assembles file in /app/uploads
3. WebSocket notifies Agent Handler via gRPC
4. Agent Handler reads file, splits to 512KB chunks
5. Agent Handler queues first chunk to command buffer
6. Agent Handler stores remaining chunks on disk
7. Agent receives chunk in GET response
8. Agent stores chunk in memory map
9. Agent Handler queues next chunk (repeat 6-8)
10. Agent assembles all chunks to destination file
Download (Agent → Server)
1. GUI issues download command
2. Agent receives command in GET response
3. Agent opens file, reads first 512KB chunk
4. Agent returns chunk in POST results
5. Server stores chunk as .part1 file
6. Server queues download_continue command
7. Agent receives continue, reads next chunk
8. Agent returns chunk in POST results
9. Server stores chunk (repeat 6-8)
10. Server assembles all parts to final file
11. Server updates downloads.json manifest
12. GUI notified of completed download
Related Files
| Component | File Path |
|---|---|
| Agent Upload Handler | server/docker/payloads/Linux/action_upload.go |
| Agent Download Handler | server/docker/payloads/Linux/action_download.go |
| Server Upload Handler | server/internal/agent/server/upload.go |
| Server Download Tracker | server/internal/agent/listeners/handle_downloads.go |
| WebSocket Upload Handler | server/internal/websocket/handlers/upload.go |
| WebSocket File Operations | server/internal/websocket/handlers/file_operations.go |
| Server Upload Tracker | server/internal/agent/listeners/upload_tracker.go |
| Downloads Manifest | server/internal/agent/listeners/manifest.go |