TerminalFix Asynchronous Shell and Reverse Tunnel
An intruder has established long-term C2 presence using a PowerShell file-watch loop for asynchronous command execution and a Python-based reverse tunnel for persistent network-level proxying.
Based on research by Microsoft 2026-09-20 9 steps · 3 queries T1059.001 T1090.003 T1572
Brief
The TerminalFix Persistence Strategy
Microsoft recently published a detailed analysis of the TerminalFix campaign at https://www.microsoft.com/en-us/security/blog/2026/08/28/terminalfix-campaign-deploys-reverse-tunnel-through-multistage-intrusion/. The adversary uses a multi-stage strategy to maintain command-and-control access after a social engineering compromise. This hunt focuses on the specific implants used to sustain this persistent connection.
Identifying the Network Lead
The hunt begins by identifying the network lead in hb_dns_activity. We look for resolutions of gitnow.dev and other known campaign infrastructure. These lookups serve as the primary indicator that a host is attempting to establish or maintain its reverse tunnel connection. Identifying these lookups early allows an analyst to scope the potential compromise to a specific set of endpoints before beginning more resource-intensive queries.
Searching for the Asynchronous Shell
After identifying a lead, we pivot to hb_script_activity to find the command loop. The adversary uses the PowerShell FileSystemWatcher class to monitor a specific text file for updates. When the file changes, the script reads the content and passes it to Invoke-Expression. This method allows the attacker to execute arbitrary commands without spawning new, suspicious processes for every action. The hunt looks for this specific combination of monitoring and execution logic within captured script blocks. This behavioral focus makes the hunt more resilient than simple filename detections.
Detecting the Reverse Tunnel
In parallel, we search hb_process_activity for the reverse tunnel client. The intruder typically runs a Python script named client.py using the pythonw.exe binary. Running under pythonw.exe allows the process to remain hidden from the taskbar and terminal. We baseline these processes across the environment to identify rare instances running from unusual directories like ProgramData. This step uses frequency analysis to separate legitimate developer tools from malicious implants.
Triage and Correlation
The final phase correlates these signals. A single DNS lookup or a legitimate Python script might be benign, but the co-occurrence of these three indicators on a single host strongly suggests an active intrusion. This is why this logic is a hunt rather than a single detection: we aggregate the network leads, the PowerShell command loop signatures, and the rare Python process metadata to produce a high-confidence verdict. By weighing evidence from three different surfaces, the hunt confirms the presence of an active command-and-control channel.
Blind Spots and Limitations
This hunt has two primary blind spots. First, it requires PowerShell Script Block Logging to be enabled. Without EID 4104 data, the file-watch script remains invisible to the hb_script_activity surface. Second, the Python tunnel may be ephemeral. If the attacker only establishes the connection during specific windows, periodic process snapshots might miss the active tunnel.
Execution
This hunt is an open hunt.md playbook. You can import it into Huntbase or any runtime that supports the hunt.md format. The playbook automates the correlation between network, script, and process data to produce an actionable host verdict. It ensures that responders have the full context of the intrusion before initiating containment.
In this series
Steps
-
DNS lookups for TerminalFix C2 domains
Query · detection candidateIdentify hosts attempting to resolve the known C2 infrastructure used for reverse tunneling.
reads hb_dns_activitysqlSELECT device_hostname, query_hostname, COUNT(*) AS lookup_count, MIN(time) AS first_seen, MAX(time) AS last_seen FROM hb_dns_activity WHERE instr(',' || '{{c2_domains}}' || ',', ',' || LOWER(query_hostname) || ',') > 0 AND ('{{scope_hosts}}' = '' OR instr(',' || '{{scope_hosts}}' || ',', ',' || device_hostname || ',') > 0) AND time >= datetime('now', '-{{lookback_days}} days') GROUP BY device_hostname, query_hostnameWhat a hit looks like. Specific hostnames resolving gitnow.dev. This indicates the reverse tunnel is likely active on those hosts.
-
PowerShell file-watch command loop
Query · triageLocate the script blocks responsible for monitoring a file and executing its contents, which forms the attacker's shell.
reads hb_script_activitysqlSELECT device_hostname, script_path, script_content, time FROM hb_script_activity WHERE (LOWER(script_content) LIKE '%filesystemwatcher%' AND LOWER(script_content) LIKE '%invoke-expression%' AND (LOWER(script_content) LIKE '%set-content%' OR LOWER(script_content) LIKE '%out-file%')) AND ('{{scope_hosts}}' = '' OR instr(',' || '{{scope_hosts}}' || ',', ',' || device_hostname || ',') > 0) AND time >= datetime('now', '-{{lookback_days}} days')What a hit looks like. Script blocks showing FileSystemWatcher being initialized on a text file followed by Invoke-Expression (IEX).
-
Python reverse tunnel processes
Query · baselineFind the specific Python runtime instances used to maintain the reverse WebSocket tunnel.
reads hb_process_activitysqlSELECT device_hostname, process_name, process_cmd_line, user_name, time FROM hb_process_activity WHERE (LOWER(process_name) LIKE '%pythonw.exe%' AND LOWER(process_cmd_line) LIKE '%client.py%') AND ('{{scope_hosts}}' = '' OR instr(',' || '{{scope_hosts}}' || ',', ',' || device_hostname || ',') > 0) AND time >= datetime('now', '-{{lookback_days}} days')What a hit looks like. A pythonw.exe process running client.py, often from a hidden or non-standard directory like ProgramData.
-
Triage C2 evidence
Agent triageWeigh the co-occurrence of DNS resolves, the async shell script, and the Python process per host.
-
Route on verdict
DecisionRoute to containment if the triage agent confirms malicious C2 activity.
-
Isolate host
Response actionSever the attacker's network-level access by isolating the compromised endpoint.
-
Analyst review
Analyst taskFinal manual confirmation and review of the findings.
-
Close out
Analyst taskHunt completion for negative results.
Coverage
Scenario coverage
| Stage | Covered | How, or why not |
|---|---|---|
| Asynchronous File-Watch Command Loop T1059.001 |
Yes | powershell-async-shell |
| Reverse WebSocket Tunneling T1572 · T1090.003 |
Yes | dns-c2-lead, python-tunnel-implant |
| Social Engineering via Fake CAPTCHA T1204.001 |
Out of scope | Belongs to another part of the 'TerminalFix campaign deploys a reverse tunnel through multistage intrusion' series. |
| Malicious PowerShell Launcher T1059.001 · T1105 |
Out of scope | Belongs to another part of the 'TerminalFix campaign deploys a reverse tunnel through multistage intrusion' series. |
| DLL Sideloading via LockScreenContentServer T1574.001 |
Out of scope | Belongs to another part of the 'TerminalFix campaign deploys a reverse tunnel through multistage intrusion' series. |
| Steganographic Payload Extraction T1027.003 |
Out of scope | Belongs to another part of the 'TerminalFix campaign deploys a reverse tunnel through multistage intrusion' series. |
| Redundant Persistence T1547.001 · T1053.005 |
Out of scope | Belongs to another part of the 'TerminalFix campaign deploys a reverse tunnel through multistage intrusion' series. |
| Extensive Domain Discovery T1018 · T1087.002 · T1482 |
Out of scope | Belongs to another part of the 'TerminalFix campaign deploys a reverse tunnel through multistage intrusion' series. |
Blind spots
- Needs PowerShell Script Block Logging (EID 4104). An attacker can maintain a stealthy shell that leaves no process command-line artifacts. It would answer whether the file-watch command loop is running on hosts where script logging is disabled.
- Needs high-frequency hb_process_activity snapshots. Short-lived proxy connections used for targeted data exfiltration might be missed. It would answer whether the Python tunnel was established and torn down between collection intervals.
Parameters & data
Parameters
| Parameter | Type | Default | What it is |
|---|---|---|---|
c2_domains | list[domain] | gitnow.dev | C2 domains observed in the TerminalFix campaign. |
lookback_days | number | 14 | Days of history to examine. |
scope_hosts | list[host] | — | Optional list of hostnames to narrow the search. |
Telemetry
| Source | Category | Telemetry |
|---|---|---|
| Endpoint telemetry (hb_ surfaces) | endpoint | endpoint |
Source
---
analysis: A single rule might catch the DNS lookup, but this hunt correlates the network
lead with behavioral signals from PowerShell script blocks and Python process activity,
baseline counts the rarity of the tunnel script, and weighs all three pieces of
evidence to confirm active C2.
blind_spots:
- id: no-script-logging
question: whether the file-watch command loop is running on hosts where script logging
is disabled
requires: PowerShell Script Block Logging (EID 4104)
risk: An attacker can maintain a stealthy shell that leaves no process command-line
artifacts.
stage: command-and-control-asynchronous-shell
- id: ephemeral-tunnel-processes
question: whether the Python tunnel was established and torn down between collection
intervals
requires: high-frequency hb_process_activity snapshots
risk: Short-lived proxy connections used for targeted data exfiltration might be
missed.
stage: command-and-control-reverse-tunnel
coverage:
- stage: command-and-control-asynchronous-shell
status: covered
steps:
- powershell-async-shell
- stage: command-and-control-reverse-tunnel
status: covered
steps:
- dns-c2-lead
- python-tunnel-implant
- reason: Belongs to another part of the 'TerminalFix campaign deploys a reverse tunnel
through multistage intrusion' series.
stage: initial-access-social-engineering
status: out_of_scope
- reason: Belongs to another part of the 'TerminalFix campaign deploys a reverse tunnel
through multistage intrusion' series.
stage: execution-powershell-launcher
status: out_of_scope
- reason: Belongs to another part of the 'TerminalFix campaign deploys a reverse tunnel
through multistage intrusion' series.
stage: defense-evasion-dll-sideloading
status: out_of_scope
- reason: Belongs to another part of the 'TerminalFix campaign deploys a reverse tunnel
through multistage intrusion' series.
stage: defense-evasion-steganography
status: out_of_scope
- reason: Belongs to another part of the 'TerminalFix campaign deploys a reverse tunnel
through multistage intrusion' series.
stage: persistence-mechanisms
status: out_of_scope
- reason: Belongs to another part of the 'TerminalFix campaign deploys a reverse tunnel
through multistage intrusion' series.
stage: discovery-domain-reconnaissance
status: out_of_scope
guardrails:
claims: no_unsupported
evidence: citation_required
missing_data: not_benign
telemetry: untrusted
hunt:
applicability: campaign-specific
handoff: promote-to-detection
justification: Reverse tunnels provide persistent, bypass-capable access to the
internal network. Identifying these implants is critical for preventing lateral
movement and data exfiltration after an initial social engineering compromise.
methodology: model-assisted
trigger: intel-report
hypothesis: An intruder has established long-term C2 presence using a PowerShell file-watch
loop for asynchronous command execution and a Python-based reverse tunnel for persistent
network-level proxying.
labels:
- hunt
- attack.t1059.001
- attack.t1572
- attack.t1090.003
name: TerminalFix Asynchronous Shell and Reverse Tunnel
parameters:
c2_domains:
default:
- gitnow.dev
description: C2 domains observed in the TerminalFix campaign.
from:
kind: article
observed: '2026-08-28'
ref: msrc-blog-terminalfix
type: list[domain]
lookback_days:
default: '14'
description: Days of history to examine.
type: number
scope_hosts:
default: []
description: Optional list of hostnames to narrow the search.
type: list[host]
provenance:
authors:
- name: Huntbase hunt generation
org: huntbase.io
generated:
by: huntbase-hunt-generation
from: https://www.microsoft.com/en-us/security/blog/2026/08/28/terminalfix-campaign-deploys-reverse-tunnel-through-multistage-intrusion/
gates:
- dry-run
- lint
- critic
model: hb_google/gemini-3-flash-preview
rationale: Target systems with recent suspicious software installations or those that
triggered earlier ClickFix-related alerts. Focus on workstations where Windows Terminal
or PowerShell is frequently used by non-admins.
references:
- name: 'MSRC Blog: TerminalFix campaign deploys a reverse tunnel through multistage
intrusion'
url: https://www.microsoft.com/en-us/security/blog/2026/08/28/terminalfix-campaign-deploys-reverse-tunnel-through-multistage-intrusion/
related:
- hunt: terminalfix-initial-persistence
reason: This hunt focuses on the C2 stage; persistence mechanisms via DLL sideloading
and scheduled tasks are handled in the preceding hunt.
relation: out-of-scope-alternative
- hunt: terminalfix-clickfix-delivery-reconnaissance
relation: follows
scenario:
stages:
- name: Social Engineering via Fake CAPTCHA
observables:
- Cloudflare Turnstile verification overlay
- Verification command copied to clipboard
- Instructions to open Windows Terminal or PowerShell
- Fake Cloudflare-themed terminal output messages
slug: initial-access-social-engineering
tactic: initial-access
techniques:
- T1204.001
- name: Malicious PowerShell Launcher
observables:
- C:\ProgramData\f47f2a8c21c9df4e
- 1.bat
- ZIP archive download with custom User-Agent
- 'I am not a robot - Cloudflare ID: f47f2a8c21c9df4e'
slug: execution-powershell-launcher
tactic: execution
techniques:
- T1059.001
- T1105
- name: DLL Sideloading via LockScreenContentServer
observables:
- LockScreenContentServer.exe
- dui70.dll (unsigned, forged timestamp 2104)
- LockScreenContentServer.exe loading dui70.dll from ProgramData
slug: defense-evasion-dll-sideloading
tactic: defense-evasion
techniques:
- T1574.001
- name: Steganographic Payload Extraction
observables:
- p1.png
- p2.png
- p3.png
- gitnow.dev
- Extract-RawFileFromImage PowerShell function
- Reassembling DLL fragments from PNG pixel data
slug: defense-evasion-steganography
tactic: defense-evasion
techniques:
- T1027.003
- name: Redundant Persistence
observables:
- LockScreenContentServer_MuODG5yBM
- 'Registry Run Key: HKCU\Software\Microsoft\Windows\CurrentVersion\Run'
- Scheduled Task running every 60 minutes
- attrib +h +s folder hiding on C:\ProgramData subfolders
slug: persistence-mechanisms
tactic: persistence
techniques:
- T1547.001
- T1053.005
- name: Extensive Domain Discovery
observables:
- nltest /domain_trusts
- net group "domain admins" /domain
- get-aduser
- get-adcomputer
- Ping sweeps of dc, db, backup, gateway, mail servers
slug: discovery-domain-reconnaissance
tactic: discovery
techniques:
- T1018
- T1087.002
- T1482
- name: Asynchronous File-Watch Command Loop
observables:
- PowerShell file-watch loop monitoring text files
- Invoke-Expression (IEX) on watched file content
- Command output written to disk files
slug: command-and-control-asynchronous-shell
tactic: command-and-control
techniques:
- T1059.001
- name: Reverse WebSocket Tunneling
observables:
- pythonw.exe
- client.py
- gitnow.dev:443
- Reverse WebSocket tunnel providing SOCKS proxy access
slug: command-and-control-reverse-tunnel
tactic: command-and-control
techniques:
- T1572
- T1090.003
summary: The TerminalFix campaign employs fake Cloudflare CAPTCHA prompts to trick
users into executing malicious PowerShell commands that initiate a multi-stage
infection. The attack leverages DLL sideloading and steganography to deploy a
persistent Python-based reverse tunnel, enabling attackers to conduct extensive
Active Directory reconnaissance and maintain encrypted SOCKS-style proxy access
to the victim's internal network.
series:
index: 3
slug: terminalfix-campaign-deploys-a-reverse-tunnel-through-multistage-intrusion
title: TerminalFix campaign deploys a reverse tunnel through multistage intrusion
total: 3
severity: high
targets:
analyst:
name: Tier-2 analyst
role: analyst
endpoint:
category: endpoint
name: Endpoint telemetry (hb_ surfaces)
telemetry:
- endpoint
hunter:
agent: true
name: Hunt agent
tlp: clear
type: investigation
---
# TerminalFix Asynchronous Shell and Reverse Tunnel
The TerminalFix campaign establishes persistent control by deploying an asynchronous command shell and a reverse WebSocket tunnel. The command shell monitors a local text file using PowerShell's FileSystemWatcher and executes content via Invoke-Expression, while the reverse tunnel, typically a Python script named client.py running under pythonw.exe, connects to attacker infrastructure to provide proxy access. This hunt identifies the network lead for the C2 domain, then fanned-out searches for both the shell and the tunnel implant to settle on a per-host verdict.
## dns-c2-lead
<!-- DNS lookups for TerminalFix C2 domains -->
Identify hosts attempting to resolve the known C2 infrastructure used for reverse tunneling.
```sqlite target=endpoint role=detection-candidate params=(lookback_days=lookback_days, c2_domains=c2_domains, scope_hosts=scope_hosts)
~~~yaml
expected: Specific hostnames resolving gitnow.dev. This indicates the reverse tunnel
is likely active on those hosts.
reads:
- device_hostname
- query_hostname
- time
silence: not_evidence_of_absence
source: hb_dns_activity
verified: dry-run
verified_at: '2026-09-20'
~~~
SELECT device_hostname, query_hostname, COUNT(*) AS lookup_count, MIN(time) AS first_seen, MAX(time) AS last_seen FROM hb_dns_activity WHERE instr(',' || '{{c2_domains}}' || ',', ',' || LOWER(query_hostname) || ',') > 0 AND ('{{scope_hosts}}' = '' OR instr(',' || '{{scope_hosts}}' || ',', ',' || device_hostname || ',') > 0) AND time >= datetime('now', '-{{lookback_days}} days') GROUP BY device_hostname, query_hostname
```
## c2-parallel-checks
<!-- Parallel evidence gathering -->
parallel:
- → powershell-async-shell
- → python-tunnel-implant
join: → triage-c2-evidence
## powershell-async-shell
<!-- PowerShell file-watch command loop -->
Locate the script blocks responsible for monitoring a file and executing its contents, which forms the attacker's shell.
```sqlite target=endpoint role=triage params=(lookback_days=lookback_days, scope_hosts=scope_hosts)
~~~yaml
expected: Script blocks showing FileSystemWatcher being initialized on a text file
followed by Invoke-Expression (IEX).
reads:
- device_hostname
- script_content
- script_path
- time
silence: not_evidence_of_absence
source: hb_script_activity
verified: dry-run
verified_at: '2026-09-20'
~~~
SELECT device_hostname, script_path, script_content, time FROM hb_script_activity WHERE (LOWER(script_content) LIKE '%filesystemwatcher%' AND LOWER(script_content) LIKE '%invoke-expression%' AND (LOWER(script_content) LIKE '%set-content%' OR LOWER(script_content) LIKE '%out-file%')) AND ('{{scope_hosts}}' = '' OR instr(',' || '{{scope_hosts}}' || ',', ',' || device_hostname || ',') > 0) AND time >= datetime('now', '-{{lookback_days}} days')
```
## python-tunnel-implant
<!-- Python reverse tunnel processes -->
Find the specific Python runtime instances used to maintain the reverse WebSocket tunnel.
```sqlite target=endpoint role=baseline params=(lookback_days=lookback_days, scope_hosts=scope_hosts)
~~~yaml
baseline:
compare: first_seen
window: '{{lookback_days}}d'
expected: A pythonw.exe process running client.py, often from a hidden or non-standard
directory like ProgramData.
prevalence:
by: device_hostname
key:
- process_cmd_line
rare_below: 3
reads:
- device_hostname
- process_cmd_line
- process_name
- time
- user_name
silence: not_evidence_of_absence
source: hb_process_activity
verified: dry-run
verified_at: '2026-09-20'
~~~
SELECT device_hostname, process_name, process_cmd_line, user_name, time FROM hb_process_activity WHERE (LOWER(process_name) LIKE '%pythonw.exe%' AND LOWER(process_cmd_line) LIKE '%client.py%') AND ('{{scope_hosts}}' = '' OR instr(',' || '{{scope_hosts}}' || ',', ',' || device_hostname || ',') > 0) AND time >= datetime('now', '-{{lookback_days}} days')
```
## triage-c2-evidence
<!-- Triage C2 evidence -->
```agent target=hunter
cite: required
context:
- dns-c2-lead
- powershell-async-shell
- python-tunnel-implant
max_iterations: 6
objective: Determine if any host shows confirmed TerminalFix command-and-control activity
by combining the DNS, script activity, and process evidence.
success_criteria: A verdict of malicious, suspicious, or benign per host, citing specific
rows from all input steps.
tools:
- endpoint
```
## route-on-verdict
<!-- Route on verdict -->
if~: "The triage verdict is malicious for at least one host, indicating an active reverse tunnel or command loop." (confidence: high, judge=hunter)
then: → isolate-host
indeterminate: → analyst-review
unavailable: → analyst-review (blind_spot: no-script-logging)
else: → close-out
## isolate-host
<!-- Isolate host -->
```action target=endpoint
~~~yaml
approval: required
~~~
Isolate the host and revoke any active sessions for users logged into this host. Collect the client.py file and any monitored text files for forensic analysis.
```
→ analyst-review
## analyst-review
<!-- Analyst review -->
```manual target=analyst
Review the Python process and PowerShell script activity to confirm the C2 nature; search for secondary implants or lateral movement from this host. Examine the text file content being watched for evidence of past commands.
```
→ end
## close-out
<!-- Close out -->
```manual target=analyst
Record that no active command loops or reverse tunnels were found on the scoped hosts. Note any legitimate Python or FileSystemWatcher usage discovered for future tuning.
```
→ end
Run it
Take this hunt into your environment.
Open it in Huntbase to run every step against your own connections, with Scout weighing the evidence and your analysts in command. Or take the open hunt.md file anywhere that reads the format.
Machine-drafted by huntbase-hunt-generation using hb_google/gemini-3-flash-preview, gated by dry-run, lint, critic, then reviewed by a person.