// question

Windows process creation logs show a PowerShell-driven file transfer to an FTP service. Which executable was leveraged to perform this exfiltration?

Reference KQL answer and model-by-model comparison.

b854eb97-bf9b-45ab-a1b5-b94e4880c56b
question id
16/23
models correct
1
valid answer
70%
success rate
// reference answer

Human-validated correct answer for this question.

rclone.exe
// model comparison
ModelAnswerKQL queryStatusTriesTimeCost
gemini-2.5-flash-preview-04-17
rclone.exe
DeviceProcessEvents
| where InitiatingProcessFileName =~ "powershell.exe"
| where ProcessCommandLine has "ftp"
| distinct FileName
pass17.19s0.0026
gpt-35-turbo
rclone.exe
DeviceProcessEvents
| where ProcessCommandLine contains 'powershell' and ProcessCommandLine contains 'ftp'
| project DeviceName, ProcessId, ProcessCommandLine
pass12.31s0.0024
gpt-4-turbo-2024-04-09
rclone.exe
DeviceProcessEvents
| where InitiatingProcessFileName == 'powershell.exe' and ProcessCommandLine has 'ftp'
| project DeviceName, InitiatingProcessFileName, FileName, ProcessCommandLine, TimeGenerated
pass19.39s0.0541
gpt-4.1
rclone.exe
DeviceProcessEvents
| where InitiatingProcessFileName =~ "powershell.exe" or InitiatingProcessCommandLine has "powershell"
| where ProcessCommandLine has_any ("ftp", "put", "ftp:")
| where FileName !~ "powershell.exe"
| project TimeGenerated, DeviceName, AccountName, ParentProcess=InitiatingProcessFileName, ParentCommand=InitiatingProcessCommandLine, ExfilExecutable=FileName, ExfilCommand=ProcessCommandLine
| summarize ExfilCount=count() by ExfilExecutable, ExfilCommand
| sort by ExfilCount desc
pass211.56s0.0223
gpt-4.1-finetuned
no answer
no query generatedfail50.00s0.0445
gpt-4.1-mini
rclone.exe
DeviceProcessEvents
| where InitiatingProcessFileName == "powershell.exe"
| where ProcessCommandLine has_any ("ftp", "-ftp", "ftp://", "ftp.")
| project TimeGenerated, DeviceName, AccountName, InitiatingProcessFileName, ProcessFileName = FileName, ProcessCommandLine
| distinct ProcessFileName, ProcessCommandLine
pass16.22s0.0023
gpt-4.1-nano
rclone.exe
DeviceProcessEvents
| where ProcessCommandLine contains 'powershell' and ProcessCommandLine contains 'ftp'
| project InitiatingProcessFileName, ProcessCommandLine, InitiatingProcessCreationTime, DeviceName
pass11.84s0.0005
gpt-4o
rclone.exe
let PowerShell_FTP_Events = DeviceProcessEvents
| where InitiatingProcessFileName =~ 'powershell.exe' and InitiatingProcessCommandLine contains 'ftp'
| project Timestamp, InitiatingProcessCommandLine, InitiatingProcessFileName, DeviceName, AccountName;
PowerShell_FTP_Events
pass323.44s0.0350
gpt-5-high
rclone.exe
let ps_bins = dynamic(["powershell.exe","pwsh.exe","powershell_ise.exe"]);
let ftp_tools = dynamic(["ftp.exe","winscp.exe","winscp.com","psftp.exe","curl.exe","wget.exe","bitsadmin.exe","certutil.exe","tftp.exe","rclone.exe"]);
let ftp_markers = dynamic(["ftp://","ftps://","WebRequestMethods+Ftp","FtpWebRequest"," -Method Ftp"," -Method ftp","Invoke-WebRequest","Start-BitsTransfer"]);
DeviceProcessEvents
| where InitiatingProcessFileName in (ps_bins)
| where FileName in (ftp_tools)
    or ProcessCommandLine has_any (ftp_markers)
    or InitiatingProcessCommandLine has_any (ftp_markers)
| summarize FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated), EventCount=count(), sample_child_cmd=any(ProcessCommandLine), sample_parent_cmd=any(InitiatingProcessCommandLine)
    by Executable=tolower(FileName)
| order by EventCount desc, LastSeen desc
pass2177.34s0.0970
gpt-5-mini-high
no answer
no query generatedfail50.00s0.0257
gpt-5-mini-low
rclone.exe
// Detect PowerShell-driven FTP exfiltration and identify the executable used for exfiltration
let ftp_regex = "(?i)(ftp://|\\bftp\\b|ftps://|\\bSTOR\\b|\\bPORT\\b|Invoke-WebRequest|Invoke-RestMethod|WebClient|UploadFile\\(|System\\.Net\\.FtpWebRequest|ftp\\.exe|\\bcurl\\b|\\bwget\\b|\\bbitsadmin\\b|\\bcertutil\\b)";

// 1) PowerShell processes that reference FTP in their commandline
let ps_direct = DeviceProcessEvents
| where FileName has_cs "powershell" or ProcessCommandLine has_cs "powershell" or ProcessCommandLine has_cs "pwsh"
| where tostring(ProcessCommandLine) matches regex ftp_regex
| project TimeGenerated, DeviceName, AccountName, ExfiltratingExecutable = tolower(FileName), ProcessId, ProcessCommandLine, InitiatingProcessFileName, InitiatingProcessCommandLine, InitiatingProcessId;

// 2) Child processes spawned by PowerShell that look like FTP/upload tools
let child_procs = DeviceProcessEvents
| where isnotempty(InitiatingProcessFileName) and (tolower(InitiatingProcessFileName) has "powershell" or tolower(InitiatingProcessCommandLine) has "powershell" or tolower(InitiatingProcessCommandLine) has "pwsh")
| where tolower(FileName) has_any ("ftp","curl","wget","bitsadmin","certutil","ftp.exe") or tostring(ProcessCommandLine) matches regex ftp_regex
| project TimeGenerated, DeviceName, AccountName, ExfiltratingExecutable = tolower(FileName), ProcessId, ProcessCommandLine, InitiatingProcessFileName, InitiatingProcessId;

// 3) Network events likely indicating FTP (port 21 or ftp:// URLs)
let network_confirm = DeviceNetworkEvents
| where RemotePort == 21 or tostring(RemoteUrl) has "ftp://" or tostring(RemoteUrl) has ":21"
| project NetTime = TimeGenerated, NetDeviceName = DeviceName, NetInitiatingProcessId = InitiatingProcessId, NetInitiatingProcessFileName = InitiatingProcessFileName, RemoteIP = RemoteIP, RemotePort = RemotePort, RemoteUrl = RemoteUrl;

// Combine detections and validate with network events. Since join with OR is not allowed, perform two joins and union results.
(ps_direct
| union child_procs)
| as detections
| join kind=leftouter (network_confirm) on $left.ProcessId == $right.NetInitiatingProcessId
| project TimeGenerated, DeviceName, AccountName, ExfiltratingExecutable, ProcessId, ProcessCommandLine, InitiatingProcessFileName, NetInitiatingProcessFileName, RemoteIP, RemotePort, RemoteUrl, NetTime
| union (
    (ps_direct
    | union child_procs)
    | join kind=leftouter (network_confirm) on $left.InitiatingProcessId == $right.NetInitiatingProcessId
    | project TimeGenerated, DeviceName, AccountName, ExfiltratingExecutable, ProcessId, ProcessCommandLine, InitiatingProcessFileName, NetInitiatingProcessFileName, RemoteIP, RemotePort, RemoteUrl, NetTime
)
| summarize Attempts = count(), ExampleCommands = make_set(ProcessCommandLine, 5), Initiators = make_set(InitiatingProcessFileName, 5), RemoteEndpoints = make_set(coalesce(tostring(RemoteUrl), strcat(RemoteIP, ":", tostring(RemotePort))), 10) by ExfiltratingExecutable
| order by Attempts desc
| take 50
pass481.69s0.0198
gpt-5-mini-medium
rclone.exe
let ftp_ports = dynamic([21, 990, 2121, 8021]);
// Candidate PowerShell process creations invoking FTP-related commands
DeviceProcessEvents
| where FileName =~ 'powershell.exe' or FileName =~ 'pwsh.exe'
| where ProcessCommandLine has_any ('ftp','WebClient','FtpWebRequest','-Uri','-OutFile','Invoke-WebRequest','Start-BitsTransfer')
| project TimeGenerated, DeviceName, ProcessId, FileName, ProcessCommandLine, InitiatingProcessFileName, InitiatingProcessCommandLine
| join kind=leftouter (
    // Network events from same process or initiated by same process
    DeviceNetworkEvents
    | where RemotePort in (21, 990, 2121, 8021) or RemoteUrl has 'ftp' or RemoteUrl has 'ftps'
    | project NetworkTime=TimeGenerated, DeviceName, InitiatingProcessId, InitiatingProcessFileName, InitiatingProcessCommandLine, RemoteIP, RemotePort, RemoteUrl
) on DeviceName
| where isnotempty(ProcessId) and (ProcessId == InitiatingProcessId or InitiatingProcessFileName has 'powershell' or InitiatingProcessCommandLine has 'ftp')
| summarize NetworkEvents = make_set(pack('NetworkTime',NetworkTime,'RemoteIP',RemoteIP,'RemotePort',RemotePort,'RemoteUrl',RemoteUrl), 50), min(TimeGenerated), max(TimeGenerated) by DeviceName, ProcessId, FileName, ProcessCommandLine
| extend StartTime = todatetime(min_TimeGenerated), EndTime = todatetime(max_TimeGenerated)
| project StartTime, EndTime, DeviceName, ExfiltratingExecutable = FileName, ProcessId, ProcessCommandLine, NetworkEvents
| order by StartTime desc
| take 50
pass115.12s0.0046
gpt-5-nano-high
no answer
no query generatedfail50.00s0.0115
gpt-5-nano-medium
no answer
no query generatedfail50.00s0.0087
grok-3-beta
no answer
no query generatedfail50.00s0.0923
grok-3-mini-beta
rclone.exe
DeviceProcessEvents
| where InitiatingProcessFileName =~ 'powershell.exe'
| where ProcessCommandLine has "ftp" or ProcessCommandLine has "21"
| distinct FileName
pass226.93s0.0029
o1-high
rclone.exe
DeviceProcessEvents
| where ActionType == "ProcessCreated"
| where InitiatingProcessFileName =~ "powershell.exe"
| where ProcessCommandLine has_any ("ftp", "ftp://", "FtpWebRequest")
| project TimeGenerated, DeviceName, FileName, FolderPath, ProcessCommandLine
| order by TimeGenerated desc
pass125.88s0.1952
o1-low
rclone.exe
DeviceProcessEvents
| where ActionType == "ProcessCreated"
| where InitiatingProcessFileName =~ "powershell.exe"
| where ProcessCommandLine has "ftp"
| project TimeGenerated, DeviceName, InitiatingProcessFileName, ProcessCommandLine, FileName, FolderPath
| order by TimeGenerated desc
pass120.68s0.1803
o3-high
rclone.exe
// -----------------------------------------------------------------------------
//  What executable did PowerShell use to transfer data to an FTP service?
// -----------------------------------------------------------------------------
// Query logic
//   1. Look for processes that were spawned by PowerShell and whose command line
//      shows typical FTP keywords ( ftp / ftps / tftp ).
//   2. Look for network connections to an FTP service (TCP/21 or ftp:// URL)
//      where the *connecting* process – or its parent – is PowerShell.
//   3. Union both evidence sets and summarise so we can clearly see which
//      executable(s) were leveraged for the exfiltration, on which host and
//      when it was first & last observed.
//
// NOTE  –  The time-picker in the hunting portal controls the global time range.
//          If you want a fixed window instead, uncomment the first two lines
//          and adjust the value in the *ago()* operator.
// -----------------------------------------------------------------------------
// let TimeWindow = 7d;                   // <- uncomment / change if needed
// | where TimeGenerated >= ago(TimeWindow)

// ---------------------------------------
// 1.  Process-creation evidence
// ---------------------------------------
let procEvidence =
    DeviceProcessEvents
    // | where TimeGenerated >= ago(TimeWindow)   //   (handled by portal)
    | where ActionType == "ProcessCreated"
    | where InitiatingProcessFileName =~ "powershell.exe"
    | where FileName != "powershell.exe"                    // exclude the parent itself
    | where ProcessCommandLine has_any ("ftp ", "ftp://", "ftps://", "tftp ")
    | project DeviceId,
              DeviceName,
              TimeGenerated,
              ExfilExecutable = FileName,
              ExfilCommand   = ProcessCommandLine;

// ---------------------------------------
// 2.  Network evidence (FTP traffic)
// ---------------------------------------
let netEvidence =
    DeviceNetworkEvents
    // | where TimeGenerated >= ago(TimeWindow)
    | where RemotePort == 21 or RemoteUrl startswith "ftp://" or RemoteUrl startswith_cs "ftp."
    | where InitiatingProcessParentFileName =~ "powershell.exe"   // PowerShell launched it
           or InitiatingProcessFileName   =~ "powershell.exe"     // or is PowerShell itself
    | project DeviceId,
              DeviceName,
              TimeGenerated,
              ExfilExecutable = InitiatingProcessFileName,
              ExfilCommand   = InitiatingProcessCommandLine;

// ---------------------------------------
// 3.  Merge & answer
// ---------------------------------------
procEvidence
| union netEvidence
| summarize FirstSeen = min(TimeGenerated),
            LastSeen  = max(TimeGenerated),
            ExecCount = count()
            by DeviceName, ExfilExecutable, ExfilCommand
| sort by ExecCount desc, FirstSeen asc
| project Host              = DeviceName,
          Executable        = ExfilExecutable,
          ExampleCmdLine    = ExfilCommand,
          Executions        = ExecCount,
          FirstSeen,
          LastSeen
pass4151.84s0.0962
o3-mini-high
no answer
no query generatedfail50.00s0.0515
o3-mini-low
rclone.exe
union withsource=TableName DeviceProcessEvents, DeviceEvents
| extend ParentProc = tolower(InitiatingProcessFileName), ChildProc = tolower(FileName)
| where ParentProc == "powershell.exe"
| where (tostring(ProcessCommandLine) contains "ftp" or tostring(InitiatingProcessCommandLine) contains "ftp")
| where ChildProc != "powershell.exe"
| project TimeGenerated, DeviceName, ProcessId, FileName, InitiatingProcessFileName, ProcessCommandLine, InitiatingProcessCommandLine
| sort by TimeGenerated asc
pass5149.76s0.0520
o4-mini-high
rclone.exe
DeviceProcessEvents
| where InitiatingProcessFileName in ("powershell.exe", "pwsh.exe")
| where ProcessCommandLine has "ftp"
| project TimeGenerated, DeviceName, InitiatingProcessCommandLine, ExfiltrationProcess = FileName, ProcessCommandLine
| summarize Count = count() by ExfiltrationProcess, ProcessCommandLine
| order by Count desc
pass381.96s0.0248
o4-mini-low
no answer
no query generatedfail50.00s0.0413