Lesson 2 of 6 · 30 min read

Velociraptor VQL — Fleet Hunts, Custom Artifacts, and Live Response

Velociraptor transforms the hunter from a single-host forensic analyst into a fleet interrogator. The same question that would take days to answer manually — “Which of our 3,000 endpoints has a process connecting to an IP in AS12345?” — takes minutes when you can push a VQL query to the fleet.


VQL Fundamentals

Basic Structure

-- VQL syntax mirrors SQL with endpoint-native plugins
SELECT
  Pid,
  Ppid,
  Name,
  Exe,
  CommandLine,
  Username,
  CreateTime
FROM pslist()
WHERE Name =~ "svchost"  -- regex match
  AND Exe !~ "System32"  -- exe path doesn't contain System32

Key VQL plugins:

PluginReturnsHunt Use
pslist()Running processesProcess rarity, masquerading
glob(globs=...)File system pathsFile presence, timestomping
stat(filename=...)File metadataHash, timestamps, size
parse_mft()MFT recordsFile system timeline
netstat()Network connectionsProcess-to-IP mapping
reg_values(...)Registry valuesPersistence keys
wmi(query=...)WMI query resultsScheduled tasks, services
parse_pe(file=...)PE header infoImports, sections, signature

Enrichment Patterns

-- Hash every process executable and check against threat intel
SELECT
  Pid,
  Name,
  Exe,
  hash(path=Exe).SHA256 AS SHA256,
  -- Velociraptor can call external lookup (VirusTotal, etc.)
  lookupHashVT(hash=hash(path=Exe).SHA256) AS VTResult
FROM pslist()
WHERE NOT Exe =~ "System32|SysWOW64|Program Files"

Writing a Custom Artifact

Artifacts are YAML-defined, parameterized hunt units. Here’s a complete artifact for LOLBin execution via scheduled tasks:

name: Custom.Hunt.SuspiciousScheduledTasks
description: |
  Find scheduled tasks whose action binary is a known LOLBin or runs from 
  an unusual path (AppData, Temp, Downloads). Targets T1053.005 persistence 
  combined with LOLBin execution (T1218).
  
author: Threat Hunt Team

parameters:
  - name: LOLBinRegex
    default: "certutil|mshta|regsvr32|rundll32|wmic|bitsadmin|msiexec|cmstp"
    description: Regex of LOLBins to flag in task action
  - name: SuspiciousPathRegex
    default: "AppData|Temp|Downloads|Public|ProgramData"
    description: Regex of paths considered unusual for scheduled tasks

sources:
  - name: SuspiciousTasks
    query: |
      -- Query WMI for all scheduled tasks
      LET tasks = SELECT *
        FROM wmi(query="SELECT * FROM Win32_ScheduledJob",
                 namespace="ROOT\\CIMV2\\Security\\MicrosoftScheduledJob")
      
      -- Also check Task Scheduler COM interface (more complete)
      LET sched_tasks = SELECT
        TaskName,
        TaskPath,
        XML AS TaskXML,
        parse_xml(file=XML).Task.Actions.Exec.Command AS ActionBinary,
        parse_xml(file=XML).Task.Actions.Exec.Arguments AS ActionArgs,
        parse_xml(file=XML).Task.Triggers AS Triggers
      FROM glob(globs="C:/Windows/System32/Tasks/**")
      WHERE NOT IsDir
      
      SELECT
        TaskName,
        TaskPath,
        ActionBinary,
        ActionArgs,
        Triggers,
        hash(path=ActionBinary).SHA256 AS BinaryHash
      FROM sched_tasks
      WHERE ActionBinary =~ LOLBinRegex
         OR ActionBinary =~ SuspiciousPathRegex

Fleet Hunt: Rarity Analysis at Scale

The most impactful fleet hunt for unknown threats:

name: Custom.Hunt.ProcessRarity
description: |
  Fleet-wide process rarity — collect all running process executables and their 
  paths. Centrally aggregate to find binaries running on very few hosts.

sources:
  - name: ProcessInventory
    query: |
      SELECT
        Name,
        Exe,
        hash(path=Exe).MD5 AS MD5,
        parse_pe(file=Exe).VersionInformation.CompanyName AS Company,
        authenticode(filename=Exe).Trusted AS Trusted
      FROM pslist()
      WHERE Exe AND NOT Exe =~ "\\[System Process\\]"

After collection from 3,000 endpoints, aggregate in the Velociraptor server:

-- In the hunt results, post-collection analysis
SELECT Exe, Name, Company, Trusted, count() AS host_count
FROM hunt_results
GROUP BY Exe, Name
HAVING host_count <= 5  -- seen on 5 or fewer hosts
ORDER BY host_count

Each result with host_count = 1: “Why does only this host run this binary?” — that’s the hunt question.


Live Response: The Investigation Phase

Once a hunt narrows suspects to 3 hosts, switch to investigation mode for deep collection:

name: Custom.Investigate.FullEndpointTriage
description: Targeted collection from a confirmed suspicious host.

sources:
  - name: RunningProcesses
    query: |
      SELECT Pid, Ppid, Name, Exe, CommandLine, Username,
             hash(path=Exe).SHA256 AS Hash
      FROM pslist()

  - name: NetworkConnections
    query: |
      SELECT Pid, Family, Type, Status,
             Laddr.IP AS LocalIP, Laddr.Port AS LocalPort,
             Raddr.IP AS RemoteIP, Raddr.Port AS RemotePort,
             Name AS ProcessName
      FROM netstat()
      WHERE Status = "ESTABLISHED" AND Raddr.IP !~ "^(127\\.0\\.0\\.1|::1)"

  - name: PersistenceLocations
    query: |
      SELECT Key, Name, Data
      FROM read_reg_key(globs=[
        "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run\\**",
        "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\**",
        "HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run\\**"
      ])

  - name: RecentDownloads
    query: |
      SELECT OSPath, Size, Mtime, Atime, Ctime,
             hash(path=OSPath).SHA256 AS SHA256
      FROM glob(globs=[
        "C:/Users/*/Downloads/**",
        "C:/Users/*/AppData/Local/Temp/**"
      ])
      WHERE NOT IsDir
        AND Mtime > now() - 7 * 86400  -- last 7 days
      ORDER BY Mtime DESC