Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ Designed to showcase **evidence-backed security engineering skills** through rep
| PT-2026-011 | T1218.010 | Regsvr32 proxy execution | Sigma + Splunk evidence | **Live validated** |
| PT-2026-012 | T1569.002 | Service-launched command execution | Sigma + Splunk evidence | **Live validated** |
| PT-2026-013 | T1546.003 | Permanent WMI event subscription creation | Sigma + Splunk evidence | **Live validated** |
| PT-2026-014 | T1105 | PowerShell web ingress transfer | Sigma + Splunk evidence | **Live validated** |
| PT-2026-015 | T1218.005 | Mshta child-process proxy execution | Sigma + Splunk + Defender evidence | **Live validated with prevention control** |

Detection Cycle 1 also adds three completed threat hunts and a loopback-only benign HTA campaign that correlated T1105 to T1218.005 in three seconds. See [Detection Cycle 1](docs/current-state/DETECTION_CYCLE_1.md).

**Meaning of statuses in this repo**
- **Live validated**: replayed in the Mayuri lab with positive/negative evidence and cleanup confirmation.
Expand Down
15 changes: 15 additions & 0 deletions automation/execution/campaign_2026_001_benign_hta_chain.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
$ErrorActionPreference='Stop'
$root='C:\Windows\Temp\dc1-campaign'
New-Item -ItemType Directory $root -Force|Out-Null
$source=Join-Path $root 'source.hta';$staged=Join-Path $root 'staged.hta';$marker=Join-Path $root 'campaign-marker.txt'
$pidFile=Join-Path $root 'process-ids.txt';if(Test-Path $pidFile){throw 'prior process record exists; run cleanup before reuse'};Remove-Item $source,$staged,$marker,(Join-Path $root 'result.json') -Force -ErrorAction SilentlyContinue
$body=@'
<html><head><hta:application showintaskbar="no" windowstate="minimize" /><script language="VBScript">
Set s=CreateObject("WScript.Shell")
s.Run "powershell.exe -NoProfile -NonInteractive -Command Set-Content -LiteralPath C:\Windows\Temp\dc1-campaign\campaign-marker.txt -Value benign-campaign",0,True
window.close
</script></head></html>
'@
Set-Content $source $body -Encoding Ascii
$server=Start-Job -ScriptBlock {param($file)$l=[Net.HttpListener]::new();$l.Prefixes.Add('http://127.0.0.1:18769/');$l.Start();try{$c=$l.GetContext();$b=[IO.File]::ReadAllBytes($file);$c.Response.StatusCode=200;$c.Response.ContentType='text/html';$c.Response.ContentLength64=$b.Length;$c.Response.OutputStream.Write($b,0,$b.Length);$c.Response.OutputStream.Close()}finally{$l.Stop();$l.Close()}} -ArgumentList $source
try{Start-Sleep 2;Invoke-WebRequest -Uri 'http://127.0.0.1:18769/stage.hta' -OutFile $staged -UseBasicParsing;if(-not(Wait-Job $server -Timeout 30)){throw 'server timeout'};$sourceHash=(Get-FileHash $source).Hash;$stagedHash=(Get-FileHash $staged).Hash;if($sourceHash -ne $stagedHash){throw 'staged HTA hash mismatch'};$p=Start-Process mshta.exe -ArgumentList $staged -PassThru;[string]$p.Id|Set-Content $pidFile;if(-not$p.WaitForExit(30000)){try{$p.Kill()}catch{};throw 'mshta timeout'};$deadline=(Get-Date).AddSeconds(10);do{Start-Sleep -Milliseconds 250}until((Test-Path $marker)-or(Get-Date)-gt$deadline);if(-not(Test-Path $marker)){throw 'campaign marker missing'};[pscustomobject]@{SourceHash=$sourceHash;StagedHash=$stagedHash;MarkerHash=(Get-FileHash $marker).Hash;Completed=$true}|ConvertTo-Json -Compress|Set-Content (Join-Path $root 'result.json')}finally{Get-Job|Stop-Job -ErrorAction SilentlyContinue;Get-Job|Remove-Job -Force -ErrorAction SilentlyContinue}
6 changes: 6 additions & 0 deletions automation/execution/campaign_2026_001_cleanup.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
$root='C:\Windows\Temp\dc1-campaign'
$ids=@(Get-Content (Join-Path $root 'process-ids.txt') -ErrorAction SilentlyContinue|Where-Object {$_ -match '^\d+$'}|ForEach-Object {[int]$_})
@(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue|Where-Object {$ids -contains [int]$_.ParentProcessId}|ForEach-Object {[int]$_.ProcessId})+$ids|Sort-Object -Unique|ForEach-Object {Stop-Process -Id $_ -Force -ErrorAction SilentlyContinue}
Remove-Item $root -Recurse -Force -ErrorAction SilentlyContinue
$remaining=@(Get-ChildItem $root -Recurse -ErrorAction SilentlyContinue)
[pscustomobject]@{RecordedProcessIds=$ids;Remaining=@($remaining|ForEach-Object FullName);Clean=($remaining.Count -eq 0)}|ConvertTo-Json -Compress
4 changes: 4 additions & 0 deletions automation/execution/pt_2026_014_cleanup.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
$root='C:\Windows\Temp\dc1-t1105'
Remove-Item $root -Recurse -Force -ErrorAction SilentlyContinue
$remaining=@(Get-ChildItem $root -Recurse -ErrorAction SilentlyContinue)
[pscustomobject]@{Remaining=@($remaining|ForEach-Object FullName);Clean=($remaining.Count -eq 0)}|ConvertTo-Json -Compress
1 change: 1 addition & 0 deletions automation/execution/pt_2026_014_negative_discovery.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Get-Command Invoke-WebRequest | Select-Object Name,CommandType,Version | ConvertTo-Json -Compress
1 change: 1 addition & 0 deletions automation/execution/pt_2026_014_negative_iwr_head.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
try{Invoke-WebRequest -Uri 'http://127.0.0.1:9/' -Method Head -TimeoutSec 1 -ErrorAction Stop}catch{Write-Output 'expected bounded local refusal'}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
$client=New-Object System.Net.WebClient;[pscustomobject]@{Type=$client.GetType().FullName;NoTransfer=$true}|ConvertTo-Json -Compress;$client.Dispose()
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
$ErrorActionPreference='Stop';Set-ExecutionPolicy -Scope Process Bypass -Force;Import-Module Invoke-AtomicRedTeam -Force
$guid='42dc4460-9aa6-45d3-b1a6-3955d34e1fe8';$root='C:\Windows\Temp\dc1-t1105';New-Item -ItemType Directory $root -Force|Out-Null;$source=Join-Path $root 'source.txt';$dest=Join-Path $root 'atomic-transfer.txt';Set-Content $source 'benign Detection Cycle 1 transfer fixture' -Encoding Ascii
$server=Start-Job -ScriptBlock {param($file)$l=[Net.HttpListener]::new();$l.Prefixes.Add('http://127.0.0.1:18765/');$l.Start();try{$c=$l.GetContext();$b=[IO.File]::ReadAllBytes($file);$c.Response.ContentLength64=$b.Length;$c.Response.OutputStream.Write($b,0,$b.Length);$c.Response.Close()}finally{$l.Stop();$l.Close()}} -ArgumentList $source
try{Start-Sleep 2;Invoke-AtomicTest T1105 -TestGuids $guid -PathToAtomicsFolder 'C:\Tools\AtomicRedTeam\atomics' -InputArgs @{remote_file='http://127.0.0.1:18765/source.txt';destination_path=$dest} -Confirm:$false;if(-not(Wait-Job $server -Timeout 30)){throw 'server timeout'};if(-not(Test-Path $dest)){throw 'destination missing'};$sourceHash=(Get-FileHash $source).Hash;$destinationHash=(Get-FileHash $dest).Hash;if($sourceHash -ne $destinationHash){throw 'transfer hash mismatch'};[pscustomobject]@{AtomicGuid=$guid;SourceHash=$sourceHash;DestinationHash=$destinationHash;Completed=$true}|ConvertTo-Json -Compress|Set-Content (Join-Path $root 'positive.json')}finally{Get-Job|Stop-Job -ErrorAction SilentlyContinue;Get-Job|Remove-Job -Force -ErrorAction SilentlyContinue}
3 changes: 3 additions & 0 deletions automation/execution/pt_2026_014_positive_variant_iwr.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
$ErrorActionPreference='Stop';$root='C:\Windows\Temp\dc1-t1105';New-Item -ItemType Directory $root -Force|Out-Null;$source=Join-Path $root 'variant-source.txt';$dest=Join-Path $root 'variant-transfer.txt';Set-Content $source 'benign modified transfer fixture' -Encoding Ascii
$server=Start-Job -ScriptBlock {param($file)$l=[Net.HttpListener]::new();$l.Prefixes.Add('http://127.0.0.1:18766/');$l.Start();try{$c=$l.GetContext();$b=[IO.File]::ReadAllBytes($file);$c.Response.ContentLength64=$b.Length;$c.Response.OutputStream.Write($b,0,$b.Length);$c.Response.Close()}finally{$l.Stop();$l.Close()}} -ArgumentList $source
try{Start-Sleep 2;Invoke-WebRequest -Uri 'http://127.0.0.1:18766/variant.txt' -OutFile $dest -UseBasicParsing;if(-not(Wait-Job $server -Timeout 30)){throw 'server timeout'};if(-not(Test-Path $dest)){throw 'destination missing'};$sourceHash=(Get-FileHash $source).Hash;$destinationHash=(Get-FileHash $dest).Hash;if($sourceHash -ne $destinationHash){throw 'transfer hash mismatch'};[pscustomobject]@{SourceHash=$sourceHash;DestinationHash=$destinationHash;Completed=$true}|ConvertTo-Json -Compress|Set-Content (Join-Path $root 'variant.json')}finally{Get-Job|Stop-Job -ErrorAction SilentlyContinue;Get-Job|Remove-Job -Force -ErrorAction SilentlyContinue}
1 change: 1 addition & 0 deletions automation/execution/pt_2026_015_cleanup.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
$root='C:\Windows\Temp\dc1-t1218-005';$ids=@(Get-ChildItem (Join-Path $root 'process-ids-*.txt') -ErrorAction SilentlyContinue|Get-Content -ErrorAction SilentlyContinue|Where-Object {$_ -match '^\d+$'}|ForEach-Object {[int]$_});@(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue|Where-Object {$ids -contains [int]$_.ParentProcessId}|ForEach-Object {[int]$_.ProcessId})+$ids|Sort-Object -Unique|ForEach-Object {Stop-Process -Id $_ -Force -ErrorAction SilentlyContinue};Remove-Item $root -Recurse -Force -ErrorAction SilentlyContinue;$remaining=@(Get-ChildItem $root -Recurse -ErrorAction SilentlyContinue);[pscustomobject]@{RecordedProcessIds=$ids;Remaining=@($remaining|ForEach-Object FullName);Clean=($remaining.Count -eq 0)}|ConvertTo-Json -Compress

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Verify process termination before reporting clean

If Stop-Process fails—for example because a recorded process cannot be terminated—the error is suppressed and Clean is still calculated solely from files remaining beneath $root. Once the directory is removed, this reports Clean=true even while the process remains alive, undermining the scenario's required cleanup confirmation; re-query the recorded process IDs and include them in the clean predicate.

Useful? React with 👍 / 👎.

1 change: 1 addition & 0 deletions automation/execution/pt_2026_015_negative_benign_hta.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
$root='C:\Windows\Temp\dc1-t1218-005-negative';New-Item -ItemType Directory $root -Force|Out-Null;$hta=Join-Path $root 'display-only.hta';Set-Content $hta '<html><script language="VBScript">window.close</script></html>' -Encoding Ascii;$p=Start-Process mshta.exe -ArgumentList $hta -PassThru;if(-not $p.WaitForExit(15000)){try{$p.Kill()}catch{}};Remove-Item $root -Recurse -Force
1 change: 1 addition & 0 deletions automation/execution/pt_2026_015_negative_discovery.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Get-Command mshta.exe | Select-Object Name,Source,Version | ConvertTo-Json -Compress
1 change: 1 addition & 0 deletions automation/execution/pt_2026_015_negative_signature.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Get-AuthenticodeSignature $env:WINDIR\System32\mshta.exe | Select-Object Status,StatusMessage | ConvertTo-Json -Compress
11 changes: 11 additions & 0 deletions automation/execution/pt_2026_015_positive_atomic_mshta.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
$ErrorActionPreference='Continue'
$guid='8707a805-2b76-4f32-b1c0-14e558205772'
$atomicYaml='C:\Tools\AtomicRedTeam\atomics\T1218.005\T1218.005.yaml'
if(-not (Select-String -LiteralPath $atomicYaml -SimpleMatch $guid -Quiet)){throw 'Pinned Atomic UUID not present in staged definition'}
$root='C:\Windows\Temp\dc1-t1218-005';New-Item -ItemType Directory $root -Force|Out-Null
$start=(Get-Date).ToUniversalTime();$arg='about:<hta:application><script language="VBScript">Close(Execute("CreateObject(""Wscript.Shell"").Run%20""powershell.exe%20-nop%20-Command%20Write-Host%20Detection%20Cycle%201;Start-Sleep%20-Seconds%201"""))</script>'
try{$p=Start-Process "$env:WINDIR\System32\mshta.exe" -ArgumentList ('"{0}"' -f $arg) -PassThru -ErrorAction Stop;[string]$p.Id|Set-Content (Join-Path $root 'process-ids-atomic.txt');if(-not$p.WaitForExit(20000)){try{$p.Kill()}catch{};throw 'timeout'};$started=$true;$exit=$p.ExitCode}catch{$started=$false;$exit=$null;$launchError=$_.Exception.Message}
Start-Sleep 2
$defender=@(Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-Windows Defender/Operational';StartTime=$start} -ErrorAction SilentlyContinue|Where-Object {$_.Id -in 1116,1117 -and $_.Message -match 'mshta'})
$result=[pscustomobject]@{AtomicGuid=$guid;ProcessStarted=$started;ExitCode=$exit;PreventiveEvents=$defender.Count;Outcome=$(if(-not$started -and $defender.Count){'prevented'}elseif($started){'executed'}else{'failed-unconfirmed'});Completed=$true}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject unconfirmed Atomic outcomes

When Start-Process fails for a reason other than confirmed Defender prevention—such as a missing executable or unavailable event log—the catch sets $started false and the result becomes failed-unconfirmed, but this line still records Completed=true and the script exits successfully. An automation runner can therefore accept a failed positive validation as completed; return a nonzero status unless execution or prevention was actually confirmed.

Useful? React with 👍 / 👎.

$result|ConvertTo-Json -Compress|Set-Content (Join-Path $root 'positive.json');$result|ConvertTo-Json -Compress
10 changes: 10 additions & 0 deletions automation/execution/pt_2026_015_positive_variant_mshta.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
$ErrorActionPreference='Stop';$root='C:\Windows\Temp\dc1-t1218-005';New-Item -ItemType Directory $root -Force|Out-Null;$marker=Join-Path $root 'variant-marker.txt';$hta=Join-Path $root 'variant.hta'
$body=@'
<html><head><hta:application showintaskbar="no" windowstate="minimize" /><script language="VBScript">
Set s=CreateObject("WScript.Shell")
s.Run "cmd.exe /c echo benign-mshta-variant> C:\Windows\Temp\dc1-t1218-005\variant-marker.txt",0,True
window.close
</script></head></html>
'@
Remove-Item $marker,$hta,(Join-Path $root 'process-ids-variant.txt') -Force -ErrorAction SilentlyContinue;Set-Content $hta $body -Encoding Ascii; $p=Start-Process mshta.exe -ArgumentList $hta -PassThru;[string]$p.Id|Set-Content (Join-Path $root 'process-ids-variant.txt'); if(-not $p.WaitForExit(30000)){try{$p.Kill()}catch{};throw 'mshta timeout'};if(-not(Test-Path $marker)){throw 'marker missing'}
[pscustomobject]@{MarkerHash=(Get-FileHash $marker).Hash;Completed=$true}|ConvertTo-Json -Compress|Set-Content (Join-Path $root 'variant.json')
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
any where process.parent.executable:"*\\mshta.exe" and (process.executable like~ ("*\\cmd.exe", "*\\powershell.exe", "*\\pwsh.exe", "*\\wscript.exe", "*\\cscript.exe"))
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
any where (powershell.file.script_block_text:"*WebClient*" and powershell.file.script_block_text:"*DownloadFile*") or (powershell.file.script_block_text:"*Invoke-WebRequest*" and powershell.file.script_block_text:"*OutFile*")
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
search index=main source="WinEventLog:Microsoft-Windows-Sysmon/Operational" _raw="*<EventID>1</EventID>*" (_raw="*<Data Name='ParentImage'>*\mshta.exe*</Data>*") AND ((_raw="*<Data Name='Image'>*\cmd.exe*</Data>*" OR _raw="*<Data Name='Image'>*\powershell.exe*</Data>*" OR _raw="*<Data Name='Image'>*\pwsh.exe*</Data>*" OR _raw="*<Data Name='Image'>*\wscript.exe*</Data>*" OR _raw="*<Data Name='Image'>*\cscript.exe*</Data>*"))
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
search index=main source="WinEventLog:Microsoft-Windows-PowerShell/Operational" _raw="*<EventID>4104</EventID>*" ((_raw="*<Data Name='ScriptBlockText'>*WebClient*" AND _raw="*<Data Name='ScriptBlockText'>*DownloadFile*")) OR ((_raw="*<Data Name='ScriptBlockText'>*Invoke-WebRequest*" AND _raw="*<Data Name='ScriptBlockText'>*OutFile*"))
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ParentImage="*\\mshta.exe" Image IN ("*\\cmd.exe", "*\\powershell.exe", "*\\pwsh.exe", "*\\wscript.exe", "*\\cscript.exe")
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Channel IN ("Microsoft-Windows-PowerShell/Operational", "PowerShellCore/Operational") EventID=4104 (ScriptBlockText="*WebClient*" ScriptBlockText="*DownloadFile*") OR (ScriptBlockText="*Invoke-WebRequest*" ScriptBlockText="*OutFile*")
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
title: Suspicious Child Process Spawned By Mshta
id: 8b36d6e2-16e6-4ae2-b86a-d396aac273c1
status: test
description: Detects script interpreters or command shells launched directly by mshta.exe.
references:
- https://attack.mitre.org/techniques/T1218/005/
- https://github.com/redcanaryco/atomic-red-team/blob/master/atomics/T1218.005/T1218.005.yaml
author: mell0wx
logsource:
product: windows
category: process_creation
detection:
selection_parent:
ParentImage|endswith: '\mshta.exe'
selection_child:
Image|endswith:
- '\cmd.exe'
- '\powershell.exe'
- '\pwsh.exe'
- '\wscript.exe'
- '\cscript.exe'
condition: selection_parent and selection_child
falsepositives:
- Legacy enterprise HTML applications that intentionally launch approved child processes
- Controlled security validation activity
date: 2026-08-03
level: high
tags:
- attack.t1218.005
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
title: Suspicious PowerShell Web Download to File
id: 4f18fcd8-8a39-4aa2-a71d-9b247d3ea475
status: test
description: Detects PowerShell web retrieval that writes remote content to a local file.
references:
- https://attack.mitre.org/techniques/T1105/
- https://github.com/redcanaryco/atomic-red-team/blob/master/atomics/T1105/T1105.yaml
author: mell0wx
logsource:
product: windows
category: ps_script
detection:
selection_webclient:
ScriptBlockText|contains|all:
- 'WebClient'
- 'DownloadFile'
selection_iwr:
ScriptBlockText|contains|all:
- 'Invoke-WebRequest'
- 'OutFile'
condition: selection_webclient or selection_iwr
falsepositives:
- Approved software deployment or administration scripts that retrieve files
- Controlled security validation activity
date: 2026-08-03
level: medium
tags:
- attack.t1105
66 changes: 66 additions & 0 deletions detections/validation/live/VAL-2026-014-PT-2026-014.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{
"scenario_id": "PT-2026-014",
"validation_run_id": "VAL-2026-014",
"technique_id": "T1105",
"atomic_test_guid": "42dc4460-9aa6-45d3-b1a6-3955d34e1fe8",
"atomic_test_name": "Windows - PowerShell Download",
"validation_date_utc": "2026-08-03",
"target": "approved-windows-victim",
"rollback_snapshot": "private-rollback-reference",
"scope": {
"loopback_only": true,
"benign_content": true,
"external_c2": false,
"credential_access": false,
"lateral_movement": false
},
"preflight": {
"authorization_confirmed": true,
"rollback_ready": true,
"endpoint_sensors_healthy": true,
"siem_current": true,
"domain_healthy": true,
"opencti_semantic_enrichment_healthy": true
},
"original": {
"start_time_utc": "2026-08-03T14:34:32.288656Z",
"end_time_utc": "2026-08-03T14:34:43.741449Z",
"source_destination_hash_equal": true,
"detection_times_utc": [
"2026-08-03T14:34:40Z"
],
"detection_fired": true,
"detection_latency_seconds": 7.711344
},
"variant": {
"start_time_utc": "2026-08-03T14:35:39.497829Z",
"end_time_utc": "2026-08-03T14:35:44.348059Z",
"source_destination_hash_equal": true,
"detection_times_utc": [
"2026-08-03T14:35:40Z"
],
"detection_fired": true,
"detection_latency_seconds": 0.502171
},
"negatives": [
{
"name": "WebClient object construction without transfer",
"detection_fired": false
},
{
"name": "bounded Invoke-WebRequest HEAD request without OutFile",
"detection_fired": false
},
{
"name": "Invoke-WebRequest command discovery",
"detection_fired": false
}
],
"cleanup": {
"exec_output": "PowerShell transfer artifacts, loopback server jobs, and completion records removed; first and idempotent second cleanup passes verified zero residue.",
"first_pass_clean": true,
"second_pass_clean": true,
"remaining": []
},
"status": "validated"
}
Loading