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

PropertyValue
Chunk Size512 KB (524,288 bytes)
EncodingBase64
StorageIn-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

  1. Creates temp directory: /app/temp/{upload_id}/
  2. Writes each chunk to: chunk_0, chunk_1, etc.
  3. When all chunks received, assembles to: /app/uploads/{filename}_{timestamp}.ext
  4. Notifies Agent Handler via gRPC HandleUpload

Step 3: Agent Handler Processing

  1. Reads assembled file from /app/uploads/
  2. Calculates total chunks (512KB each)
  3. Queues first chunk to agent’s command buffer
  4. Stores remaining chunks to disk: /app/temp/{filename}/chunk_N
  5. 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:

  1. Base64 decode chunk data
  2. Store in Chunks map by index
  3. Update LastUpdate timestamp
  4. 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:

FormatExampleHandling
Absolute (Unix)/etc/configUsed directly
Absolute (Windows)C:\Windows\TempUsed directly
UNC Path\\server\share\fileSpecial handling for network auth
Relativepayload.exeJoined 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

PropertyValue
Chunk Size512 KB (524,288 bytes)
EncodingBase64
Buffer PoolReusable 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:

  1. Resolve path (absolute, relative, or UNC)
  2. Open file, get size information
  3. Calculate total chunks
  4. Read first chunk using pooled buffer
  5. Base64 encode and return with metadata
  6. 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 CodeDescription
E1Missing arguments (no path specified)
E11Failed to create file or write data
E24Missing chunk data or incomplete upload

Download Errors

Error CodeDescription
E1Missing arguments (no path specified)
E6Path is a directory (not a file)
E10Cannot read file (permissions, not found)
E24No data returned (empty chunk)

Recovery Behavior

  • Timeout: Stale uploads are cleaned up based on LastUpdate timestamp
  • Interruption: Partial downloads remain as .partN files 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

ComponentFile Path
Agent Upload Handlerserver/docker/payloads/Linux/action_upload.go
Agent Download Handlerserver/docker/payloads/Linux/action_download.go
Server Upload Handlerserver/internal/agent/server/upload.go
Server Download Trackerserver/internal/agent/listeners/handle_downloads.go
WebSocket Upload Handlerserver/internal/websocket/handlers/upload.go
WebSocket File Operationsserver/internal/websocket/handlers/file_operations.go
Server Upload Trackerserver/internal/agent/listeners/upload_tracker.go
Downloads Manifestserver/internal/agent/listeners/manifest.go
to navigate to select ESC to close
Powered by Pagefind