mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-10 21:24:56 +03:00
Add 5 new cybersecurity skills: AFL++ fuzzing, Sysmon scheduled task detection, ModSecurity WAF logging, MITRE Navigator TTP analysis, PowerShell script block hunting
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
name: analyzing-threat-actor-ttps-with-mitre-navigator
|
||||
description: >
|
||||
Map advanced persistent threat (APT) group tactics, techniques, and procedures (TTPs) to
|
||||
the MITRE ATT&CK framework using the ATT&CK Navigator and attackcti Python library. The
|
||||
analyst queries STIX/TAXII data for group-technique associations, generates Navigator layer
|
||||
files for visualization, and compares defensive coverage against adversary profiles.
|
||||
Activates for requests involving APT TTP mapping, ATT&CK Navigator layers, threat actor
|
||||
profiling, or MITRE technique coverage analysis.
|
||||
domain: cybersecurity
|
||||
subdomain: threat-intelligence
|
||||
tags: [mitre-attack, navigator, threat-intelligence, apt, ttp-mapping, stix, attackcti]
|
||||
version: "1.0"
|
||||
author: mahipal
|
||||
license: Apache-2.0
|
||||
---
|
||||
# Analyzing Threat Actor TTPs with MITRE Navigator
|
||||
|
||||
## Overview
|
||||
|
||||
The MITRE ATT&CK Navigator is a web application for annotating and visualizing ATT&CK matrices.
|
||||
Combined with the attackcti Python library (which queries ATT&CK STIX data via TAXII), analysts
|
||||
can programmatically generate Navigator layer files mapping specific threat group TTPs, compare
|
||||
multiple groups, and assess detection coverage gaps against known adversaries.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python 3.8+ with attackcti and stix2 libraries installed
|
||||
- MITRE ATT&CK Navigator (web UI or local instance)
|
||||
- Understanding of STIX 2.1 objects and relationships
|
||||
|
||||
## Steps
|
||||
|
||||
1. Query ATT&CK STIX data for target threat group using attackcti
|
||||
2. Extract techniques associated with the group via STIX relationships
|
||||
3. Generate ATT&CK Navigator layer JSON with technique annotations
|
||||
4. Overlay detection coverage to identify gaps
|
||||
5. Export layer for team review and defensive planning
|
||||
|
||||
## Expected Output
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "APT29 TTPs",
|
||||
"domain": "enterprise-attack",
|
||||
"techniques": [
|
||||
{"techniqueID": "T1566.001", "score": 1, "comment": "Spearphishing Attachment"},
|
||||
{"techniqueID": "T1059.001", "score": 1, "comment": "PowerShell"}
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,84 @@
|
||||
# Analyzing Threat Actor TTPs with MITRE Navigator — API Reference
|
||||
|
||||
## attackcti Python Library
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `attack_client()` | Initialize STIX/TAXII client for ATT&CK data |
|
||||
| `client.get_groups()` | Retrieve all threat groups from ATT&CK |
|
||||
| `client.get_techniques()` | Retrieve all techniques from ATT&CK |
|
||||
| `client.get_techniques_used_by_group(group)` | Get techniques linked to a specific group |
|
||||
| `client.get_software()` | Retrieve all software/tools from ATT&CK |
|
||||
| `client.get_software_used_by_group(group)` | Get software used by a specific group |
|
||||
| `client.get_mitigations()` | Retrieve all mitigations from ATT&CK |
|
||||
| `client.get_data_sources()` | Retrieve all data sources from ATT&CK |
|
||||
|
||||
## STIX 2.1 Group Object Fields
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `id` | STIX object ID (e.g., `intrusion-set--abc123`) |
|
||||
| `name` | Group name (e.g., APT29) |
|
||||
| `aliases` | Alternative names for the group |
|
||||
| `description` | Group description and background |
|
||||
| `external_references` | List of references including ATT&CK ID |
|
||||
| `created` | Object creation timestamp |
|
||||
| `modified` | Last modification timestamp |
|
||||
|
||||
## STIX 2.1 Technique Object Fields
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `name` | Technique name (e.g., Spearphishing Attachment) |
|
||||
| `external_references[].external_id` | ATT&CK technique ID (e.g., T1566.001) |
|
||||
| `x_mitre_platforms` | Target platforms (Windows, Linux, macOS) |
|
||||
| `kill_chain_phases` | Associated tactics in the kill chain |
|
||||
| `x_mitre_detection` | Detection guidance for the technique |
|
||||
| `x_mitre_is_subtechnique` | Whether this is a sub-technique |
|
||||
|
||||
## ATT&CK Navigator Layer JSON Schema
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `name` | string | Layer display name |
|
||||
| `versions.attack` | string | ATT&CK version (e.g., "15") |
|
||||
| `versions.navigator` | string | Navigator version (e.g., "5.0") |
|
||||
| `versions.layer` | string | Layer format version (e.g., "4.5") |
|
||||
| `domain` | string | `enterprise-attack`, `mobile-attack`, or `ics-attack` |
|
||||
| `techniques[].techniqueID` | string | ATT&CK technique ID |
|
||||
| `techniques[].score` | integer | Numeric score for coloring (0-100) |
|
||||
| `techniques[].color` | string | Hex color override (e.g., `#ff6666`) |
|
||||
| `techniques[].comment` | string | Annotation text for the technique |
|
||||
| `techniques[].enabled` | boolean | Whether technique cell is enabled |
|
||||
| `gradient.colors` | array | Color gradient from min to max score |
|
||||
| `gradient.minValue` | integer | Minimum score value |
|
||||
| `gradient.maxValue` | integer | Maximum score value |
|
||||
| `filters.platforms` | array | Platforms to display in the matrix |
|
||||
| `legendItems[].label` | string | Legend entry label |
|
||||
| `legendItems[].color` | string | Legend entry color |
|
||||
|
||||
## CLI Usage
|
||||
|
||||
```bash
|
||||
# List all ATT&CK threat groups
|
||||
python agent.py --list-groups
|
||||
|
||||
# Analyze a specific group
|
||||
python agent.py --group "APT29"
|
||||
|
||||
# Generate Navigator layer file
|
||||
python agent.py --group "APT29" --layer-output apt29_layer.json
|
||||
|
||||
# Compare multiple groups
|
||||
python agent.py --compare "APT29" "APT28" "Lazarus Group"
|
||||
|
||||
# Save full report as JSON
|
||||
python agent.py --group "APT29" --layer-output apt29.json --output report.json
|
||||
```
|
||||
|
||||
## External References
|
||||
|
||||
- [ATT&CK Navigator GitHub](https://github.com/mitre-attack/attack-navigator)
|
||||
- [attackcti Documentation](https://attackcti.readthedocs.io/)
|
||||
- [MITRE ATT&CK Groups](https://attack.mitre.org/groups/)
|
||||
- [STIX 2.1 Specification](https://docs.oasis-open.org/cti/stix/v2.1/stix-v2.1.html)
|
||||
@@ -0,0 +1,216 @@
|
||||
#!/usr/bin/env python3
|
||||
"""MITRE ATT&CK Navigator layer generation and threat actor TTP mapping agent."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
from attackcti import attack_client
|
||||
except ImportError:
|
||||
print("Install: pip install attackcti")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def get_attack_client():
|
||||
"""Initialize ATT&CK STIX/TAXII client."""
|
||||
return attack_client()
|
||||
|
||||
|
||||
def list_threat_groups(client):
|
||||
"""List all threat groups in ATT&CK."""
|
||||
groups = client.get_groups()
|
||||
results = []
|
||||
for g in groups:
|
||||
aliases = g.get("aliases", [])
|
||||
results.append({
|
||||
"name": g.get("name", ""),
|
||||
"id": g.get("external_references", [{}])[0].get("external_id", "")
|
||||
if g.get("external_references") else "",
|
||||
"aliases": aliases,
|
||||
"description": g.get("description", "")[:200],
|
||||
})
|
||||
return sorted(results, key=lambda x: x["name"])
|
||||
|
||||
|
||||
def get_group_techniques(client, group_name):
|
||||
"""Get all techniques used by a specific threat group."""
|
||||
groups = client.get_groups()
|
||||
target_group = None
|
||||
for g in groups:
|
||||
if g.get("name", "").lower() == group_name.lower():
|
||||
target_group = g
|
||||
break
|
||||
aliases = [a.lower() for a in g.get("aliases", [])]
|
||||
if group_name.lower() in aliases:
|
||||
target_group = g
|
||||
break
|
||||
|
||||
if not target_group:
|
||||
return {"error": f"Group '{group_name}' not found"}
|
||||
|
||||
group_stix_id = target_group["id"]
|
||||
techniques = client.get_techniques_used_by_group(target_group)
|
||||
|
||||
results = []
|
||||
for tech in techniques:
|
||||
ext_refs = tech.get("external_references", [])
|
||||
tech_id = ""
|
||||
url = ""
|
||||
for ref in ext_refs:
|
||||
if ref.get("source_name") == "mitre-attack":
|
||||
tech_id = ref.get("external_id", "")
|
||||
url = ref.get("url", "")
|
||||
break
|
||||
results.append({
|
||||
"technique_id": tech_id,
|
||||
"name": tech.get("name", ""),
|
||||
"description": tech.get("description", "")[:150],
|
||||
"url": url,
|
||||
"platforms": tech.get("x_mitre_platforms", []),
|
||||
})
|
||||
|
||||
return {
|
||||
"group_name": target_group.get("name", ""),
|
||||
"group_id": target_group.get("external_references", [{}])[0].get("external_id", ""),
|
||||
"technique_count": len(results),
|
||||
"techniques": results,
|
||||
}
|
||||
|
||||
|
||||
def generate_navigator_layer(group_data, color="#ff6666"):
|
||||
"""Generate ATT&CK Navigator layer JSON from group technique data."""
|
||||
techniques = []
|
||||
for tech in group_data.get("techniques", []):
|
||||
tid = tech.get("technique_id", "")
|
||||
if not tid:
|
||||
continue
|
||||
techniques.append({
|
||||
"techniqueID": tid,
|
||||
"score": 1,
|
||||
"color": color,
|
||||
"comment": tech.get("name", ""),
|
||||
"enabled": True,
|
||||
})
|
||||
|
||||
layer = {
|
||||
"name": f"{group_data.get('group_name', 'Unknown')} TTPs",
|
||||
"versions": {
|
||||
"attack": "15",
|
||||
"navigator": "5.0",
|
||||
"layer": "4.5",
|
||||
},
|
||||
"domain": "enterprise-attack",
|
||||
"description": f"Techniques used by {group_data.get('group_name', '')} "
|
||||
f"({group_data.get('group_id', '')})",
|
||||
"filters": {"platforms": ["Windows", "Linux", "macOS", "Cloud"]},
|
||||
"sorting": 0,
|
||||
"layout": {"layout": "side", "showID": True, "showName": True},
|
||||
"hideDisabled": False,
|
||||
"techniques": techniques,
|
||||
"gradient": {
|
||||
"colors": ["#ffffff", color],
|
||||
"minValue": 0,
|
||||
"maxValue": 1,
|
||||
},
|
||||
"legendItems": [
|
||||
{"label": f"{group_data.get('group_name', '')} techniques", "color": color},
|
||||
],
|
||||
"metadata": [],
|
||||
"showTacticRowBackground": True,
|
||||
"tacticRowBackground": "#dddddd",
|
||||
}
|
||||
return layer
|
||||
|
||||
|
||||
def compare_groups(client, group_names):
|
||||
"""Compare techniques across multiple threat groups."""
|
||||
all_techniques = {}
|
||||
group_techs = {}
|
||||
for name in group_names:
|
||||
data = get_group_techniques(client, name)
|
||||
if "error" in data:
|
||||
continue
|
||||
techs = {t["technique_id"] for t in data.get("techniques", [])}
|
||||
group_techs[data.get("group_name", name)] = techs
|
||||
for t in data.get("techniques", []):
|
||||
all_techniques[t["technique_id"]] = t["name"]
|
||||
|
||||
shared = set.intersection(*group_techs.values()) if group_techs else set()
|
||||
unique_per_group = {}
|
||||
for name, techs in group_techs.items():
|
||||
unique_per_group[name] = techs - set.union(*(v for k, v in group_techs.items() if k != name))
|
||||
|
||||
return {
|
||||
"groups_compared": list(group_techs.keys()),
|
||||
"total_unique_techniques": len(set.union(*group_techs.values())) if group_techs else 0,
|
||||
"shared_techniques": [{"id": t, "name": all_techniques.get(t, "")} for t in shared],
|
||||
"shared_count": len(shared),
|
||||
"unique_per_group": {k: len(v) for k, v in unique_per_group.items()},
|
||||
}
|
||||
|
||||
|
||||
def run_audit(args):
|
||||
"""Execute threat actor TTP mapping audit."""
|
||||
print(f"\n{'='*60}")
|
||||
print(f" MITRE ATT&CK THREAT ACTOR TTP ANALYSIS")
|
||||
print(f" Generated: {datetime.utcnow().isoformat()} UTC")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
client = get_attack_client()
|
||||
report = {}
|
||||
|
||||
if args.list_groups:
|
||||
groups = list_threat_groups(client)
|
||||
report["groups"] = groups
|
||||
print(f"--- THREAT GROUPS ({len(groups)}) ---")
|
||||
for g in groups[:30]:
|
||||
print(f" {g['id']}: {g['name']}")
|
||||
|
||||
if args.group:
|
||||
data = get_group_techniques(client, args.group)
|
||||
report["group_techniques"] = data
|
||||
print(f"--- {data.get('group_name','')} ({data.get('group_id','')}) ---")
|
||||
print(f" Techniques: {data.get('technique_count', 0)}")
|
||||
for t in data.get("techniques", [])[:20]:
|
||||
print(f" {t['technique_id']}: {t['name']}")
|
||||
|
||||
if args.layer_output:
|
||||
layer = generate_navigator_layer(data)
|
||||
with open(args.layer_output, "w") as f:
|
||||
json.dump(layer, f, indent=2)
|
||||
report["layer_file"] = args.layer_output
|
||||
print(f"\n Navigator layer saved to {args.layer_output}")
|
||||
|
||||
if args.compare:
|
||||
comparison = compare_groups(client, args.compare)
|
||||
report["comparison"] = comparison
|
||||
print(f"\n--- GROUP COMPARISON ---")
|
||||
print(f" Groups: {comparison['groups_compared']}")
|
||||
print(f" Total unique techniques: {comparison['total_unique_techniques']}")
|
||||
print(f" Shared: {comparison['shared_count']}")
|
||||
for t in comparison["shared_techniques"][:10]:
|
||||
print(f" {t['id']}: {t['name']}")
|
||||
|
||||
return report
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="MITRE ATT&CK TTP Mapping Agent")
|
||||
parser.add_argument("--group", help="Threat group name to analyze (e.g., APT29)")
|
||||
parser.add_argument("--list-groups", action="store_true", help="List all ATT&CK groups")
|
||||
parser.add_argument("--compare", nargs="+", help="Compare multiple groups")
|
||||
parser.add_argument("--layer-output", help="Save Navigator layer JSON to file")
|
||||
parser.add_argument("--output", help="Save report to JSON file")
|
||||
args = parser.parse_args()
|
||||
|
||||
report = run_audit(args)
|
||||
if args.output:
|
||||
with open(args.output, "w") as f:
|
||||
json.dump(report, f, indent=2, default=str)
|
||||
print(f"\n[+] Report saved to {args.output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
name: detecting-malicious-scheduled-tasks-with-sysmon
|
||||
description: >
|
||||
Detect malicious scheduled task creation and modification using Sysmon Event IDs 1 (Process
|
||||
Create for schtasks.exe), 11 (File Create for task XML), and Windows Security Event 4698/4702.
|
||||
The analyst correlates task creation with suspicious parent processes, public directory paths,
|
||||
and encoded command arguments to identify persistence and lateral movement via scheduled tasks.
|
||||
Activates for requests involving scheduled task detection, Sysmon persistence hunting, or
|
||||
T1053.005 Scheduled Task/Job analysis.
|
||||
domain: cybersecurity
|
||||
subdomain: threat-hunting
|
||||
tags: [sysmon, scheduled-tasks, persistence, detection, threat-hunting, windows-security]
|
||||
version: "1.0"
|
||||
author: mahipal
|
||||
license: Apache-2.0
|
||||
---
|
||||
# Detecting Malicious Scheduled Tasks with Sysmon
|
||||
|
||||
## Overview
|
||||
|
||||
Adversaries abuse Windows Task Scheduler (schtasks.exe, at.exe) for persistence (T1053.005)
|
||||
and lateral movement. Sysmon Event ID 1 captures schtasks.exe process creation with full
|
||||
command-line arguments, while Event ID 11 captures task XML files written to
|
||||
C:\Windows\System32\Tasks\. Windows Security Event 4698 logs task registration details.
|
||||
This skill covers building detection rules that correlate these events to identify
|
||||
malicious scheduled tasks created from suspicious paths, with encoded payloads, or
|
||||
targeting remote systems.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Sysmon installed with a detection-focused configuration (e.g., SwiftOnSecurity or Olaf Hartong)
|
||||
- Windows Event Log forwarding to SIEM (Splunk, Elastic, or Sentinel)
|
||||
- PowerShell ScriptBlock Logging enabled (Event 4104)
|
||||
|
||||
## Steps
|
||||
|
||||
1. Configure Sysmon to log Event IDs 1, 11, 12, 13 with task-related filters
|
||||
2. Build detection rules for schtasks.exe /create with suspicious arguments
|
||||
3. Correlate Event 4698 (task registered) with Sysmon Event 1 (process create)
|
||||
4. Hunt for tasks executing from public directories or with encoded commands
|
||||
5. Alert on remote task creation (schtasks /s) for lateral movement detection
|
||||
|
||||
## Expected Output
|
||||
|
||||
```
|
||||
[CRITICAL] Suspicious Scheduled Task Detected
|
||||
Task: \Microsoft\Windows\UpdateCheck
|
||||
Command: powershell.exe -enc SQBuAHYAbwBrAGUALQBXAGUAYgBSAGU...
|
||||
Created By: DOMAIN\compromised_user
|
||||
Parent Process: cmd.exe (PID 4532)
|
||||
Source: \\192.168.1.50 (remote creation)
|
||||
MITRE: T1053.005 - Scheduled Task/Job
|
||||
```
|
||||
@@ -0,0 +1,64 @@
|
||||
# Detecting Malicious Scheduled Tasks with Sysmon — API Reference
|
||||
|
||||
## Relevant Event IDs
|
||||
|
||||
| Event ID | Source | Description |
|
||||
|----------|--------|-------------|
|
||||
| 1 | Sysmon | Process Create — captures schtasks.exe with full command line |
|
||||
| 11 | Sysmon | File Create — task XML written to System32\Tasks |
|
||||
| 12/13 | Sysmon | Registry Create/Set — task registry modifications |
|
||||
| 4698 | Security | Scheduled task registered (includes task XML content) |
|
||||
| 4702 | Security | Scheduled task updated |
|
||||
| 4699 | Security | Scheduled task deleted |
|
||||
|
||||
## schtasks.exe Suspicious Flags
|
||||
|
||||
| Flag | Description | Detection Value |
|
||||
|------|-------------|----------------|
|
||||
| `/create` | Create new task | Baseline detection |
|
||||
| `/s <host>` | Remote system target | Lateral movement indicator |
|
||||
| `/ru SYSTEM` | Run as SYSTEM | Privilege escalation |
|
||||
| `/sc onstart` | Run at system boot | Persistence |
|
||||
| `/tr "powershell -enc"` | Encoded PowerShell payload | Obfuscation |
|
||||
| `/tn \Microsoft\Windows\*` | Masquerade as Microsoft task | Evasion |
|
||||
|
||||
## Splunk Detection Queries
|
||||
|
||||
```spl
|
||||
index=sysmon EventCode=1 Image="*\\schtasks.exe" CommandLine="*/create*"
|
||||
| eval suspicious=if(match(CommandLine,"(?i)(\\\\users\\\\public|\\\\temp\\\\|\\-enc)"),"YES","NO")
|
||||
| where suspicious="YES"
|
||||
```
|
||||
|
||||
```spl
|
||||
index=wineventlog EventCode=4698
|
||||
| spath input=TaskContent
|
||||
| search Command="*powershell*" OR Command="*cmd.exe*"
|
||||
```
|
||||
|
||||
## Sysmon Configuration (Task Monitoring)
|
||||
|
||||
```xml
|
||||
<RuleGroup groupRelation="or">
|
||||
<ProcessCreate onmatch="include">
|
||||
<Image condition="end with">schtasks.exe</Image>
|
||||
<Image condition="end with">at.exe</Image>
|
||||
</ProcessCreate>
|
||||
<FileCreate onmatch="include">
|
||||
<TargetFilename condition="contains">\Windows\System32\Tasks\</TargetFilename>
|
||||
</FileCreate>
|
||||
</RuleGroup>
|
||||
```
|
||||
|
||||
## MITRE ATT&CK
|
||||
|
||||
| Technique | ID | Description |
|
||||
|-----------|----|-------------|
|
||||
| Scheduled Task/Job | T1053.005 | Create/modify scheduled tasks for persistence |
|
||||
| Lateral Movement | T1021 | Remote task creation via schtasks /s |
|
||||
|
||||
## External References
|
||||
|
||||
- [Sysmon Configuration Guide](https://github.com/SwiftOnSecurity/sysmon-config)
|
||||
- [Splunk Scheduled Task Detection](https://research.splunk.com/endpoint/7feb7972-7ac3-11eb-bac8-acde48001122/)
|
||||
- [Red Canary: Scheduled Task](https://redcanary.com/threat-detection-report/techniques/scheduled-task/)
|
||||
@@ -0,0 +1,217 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Sysmon scheduled task detection agent for hunting malicious persistence."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import argparse
|
||||
import re
|
||||
import base64
|
||||
import xml.etree.ElementTree as ET
|
||||
from datetime import datetime
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
SUSPICIOUS_PATHS = [
|
||||
r"\\users\\public\\", r"\\programdata\\", r"\\windows\\temp\\",
|
||||
r"\\appdata\\local\\temp\\", r"\\downloads\\", r"\\desktop\\",
|
||||
r"c:\\temp\\", r"\\recycle",
|
||||
]
|
||||
|
||||
SUSPICIOUS_COMMANDS = [
|
||||
r"powershell.*-enc", r"powershell.*-e\s+", r"powershell.*downloadstring",
|
||||
r"powershell.*iex", r"powershell.*invoke-expression",
|
||||
r"cmd.*/c\s+", r"mshta\s+", r"certutil.*-urlcache",
|
||||
r"bitsadmin.*/transfer", r"regsvr32.*/s.*/u",
|
||||
r"rundll32.*javascript", r"wscript.*\.vbs",
|
||||
]
|
||||
|
||||
|
||||
def parse_evtx_xml(xml_path):
|
||||
"""Parse exported Windows Event Log XML for Sysmon and Security events."""
|
||||
events = []
|
||||
try:
|
||||
tree = ET.parse(xml_path)
|
||||
root = tree.getroot()
|
||||
ns = {"e": "http://schemas.microsoft.com/win/2004/08/events/event"}
|
||||
for event_el in root.findall(".//e:Event", ns):
|
||||
system = event_el.find("e:System", ns)
|
||||
event_data = event_el.find("e:EventData", ns)
|
||||
if system is None:
|
||||
continue
|
||||
event_id = int(system.findtext("e:EventID", "0", ns))
|
||||
data = {}
|
||||
if event_data is not None:
|
||||
for d in event_data.findall("e:Data", ns):
|
||||
name = d.get("Name", "")
|
||||
data[name] = d.text or ""
|
||||
events.append({
|
||||
"event_id": event_id,
|
||||
"timestamp": system.findtext("e:TimeCreated/@SystemTime", "", ns)
|
||||
or system.find("e:TimeCreated", ns).get("SystemTime", "") if system.find("e:TimeCreated", ns) is not None else "",
|
||||
"computer": system.findtext("e:Computer", "", ns),
|
||||
"data": data,
|
||||
})
|
||||
except ET.ParseError as e:
|
||||
return [{"error": f"XML parse error: {e}"}]
|
||||
return events
|
||||
|
||||
|
||||
def detect_schtasks_creation(events):
|
||||
"""Detect suspicious schtasks.exe process creation (Sysmon Event 1)."""
|
||||
findings = []
|
||||
for evt in events:
|
||||
if evt["event_id"] != 1:
|
||||
continue
|
||||
image = evt["data"].get("Image", "").lower()
|
||||
cmdline = evt["data"].get("CommandLine", "")
|
||||
parent = evt["data"].get("ParentImage", "")
|
||||
|
||||
if "schtasks" not in image and "at.exe" not in image:
|
||||
continue
|
||||
if "/create" not in cmdline.lower() and "/change" not in cmdline.lower():
|
||||
continue
|
||||
|
||||
severity = "MEDIUM"
|
||||
reasons = []
|
||||
|
||||
for pattern in SUSPICIOUS_PATHS:
|
||||
if re.search(pattern, cmdline, re.IGNORECASE):
|
||||
severity = "HIGH"
|
||||
reasons.append(f"Task executes from suspicious path: {pattern}")
|
||||
|
||||
for pattern in SUSPICIOUS_COMMANDS:
|
||||
if re.search(pattern, cmdline, re.IGNORECASE):
|
||||
severity = "CRITICAL"
|
||||
reasons.append(f"Suspicious command pattern: {pattern}")
|
||||
|
||||
if "/s " in cmdline.lower() or "/s\t" in cmdline:
|
||||
severity = "CRITICAL"
|
||||
reasons.append("Remote task creation detected (lateral movement)")
|
||||
|
||||
if "-enc" in cmdline.lower() or "-e " in cmdline.lower():
|
||||
encoded = re.search(r'-[eE](?:nc)?\s+([A-Za-z0-9+/=]{20,})', cmdline)
|
||||
if encoded:
|
||||
try:
|
||||
decoded = base64.b64decode(encoded.group(1)).decode("utf-16-le", errors="replace")
|
||||
reasons.append(f"Decoded command: {decoded[:150]}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not reasons:
|
||||
reasons.append("Scheduled task creation detected")
|
||||
|
||||
findings.append({
|
||||
"timestamp": evt["timestamp"],
|
||||
"computer": evt["computer"],
|
||||
"image": image,
|
||||
"command_line": cmdline[:300],
|
||||
"parent_process": parent,
|
||||
"user": evt["data"].get("User", ""),
|
||||
"severity": severity,
|
||||
"reasons": reasons,
|
||||
"mitre": "T1053.005",
|
||||
})
|
||||
return findings
|
||||
|
||||
|
||||
def detect_task_file_creation(events):
|
||||
"""Detect task XML file creation in System32\\Tasks (Sysmon Event 11)."""
|
||||
findings = []
|
||||
for evt in events:
|
||||
if evt["event_id"] != 11:
|
||||
continue
|
||||
target = evt["data"].get("TargetFilename", "")
|
||||
if "\\windows\\system32\\tasks\\" not in target.lower():
|
||||
continue
|
||||
process = evt["data"].get("Image", "")
|
||||
findings.append({
|
||||
"timestamp": evt["timestamp"],
|
||||
"task_file": target,
|
||||
"created_by": process,
|
||||
"severity": "MEDIUM",
|
||||
"detail": "New scheduled task XML file created",
|
||||
})
|
||||
return findings
|
||||
|
||||
|
||||
def detect_event_4698(events):
|
||||
"""Detect Security Event 4698 — scheduled task registered."""
|
||||
findings = []
|
||||
for evt in events:
|
||||
if evt["event_id"] != 4698:
|
||||
continue
|
||||
task_name = evt["data"].get("TaskName", "")
|
||||
task_content = evt["data"].get("TaskContent", "")
|
||||
user = evt["data"].get("SubjectUserName", "")
|
||||
severity = "MEDIUM"
|
||||
reasons = []
|
||||
|
||||
for pattern in SUSPICIOUS_COMMANDS:
|
||||
if re.search(pattern, task_content, re.IGNORECASE):
|
||||
severity = "CRITICAL"
|
||||
reasons.append(f"Task content contains: {pattern}")
|
||||
|
||||
findings.append({
|
||||
"timestamp": evt["timestamp"],
|
||||
"task_name": task_name,
|
||||
"registered_by": user,
|
||||
"severity": severity,
|
||||
"reasons": reasons or ["New task registered"],
|
||||
"task_content_preview": task_content[:200],
|
||||
})
|
||||
return findings
|
||||
|
||||
|
||||
def run_audit(args):
|
||||
"""Execute scheduled task detection audit."""
|
||||
print(f"\n{'='*60}")
|
||||
print(f" MALICIOUS SCHEDULED TASK DETECTION")
|
||||
print(f" Generated: {datetime.utcnow().isoformat()} UTC")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
report = {}
|
||||
|
||||
if args.evtx_xml:
|
||||
events = parse_evtx_xml(args.evtx_xml)
|
||||
report["total_events"] = len(events)
|
||||
print(f"Parsed {len(events)} events from {args.evtx_xml}\n")
|
||||
|
||||
schtask_findings = detect_schtasks_creation(events)
|
||||
report["schtasks_findings"] = schtask_findings
|
||||
print(f"--- SCHTASKS CREATION (Event 1) — {len(schtask_findings)} findings ---")
|
||||
for f in schtask_findings[:15]:
|
||||
print(f" [{f['severity']}] {f['computer']}: {f['command_line'][:80]}")
|
||||
for r in f["reasons"]:
|
||||
print(f" -> {r[:100]}")
|
||||
|
||||
file_findings = detect_task_file_creation(events)
|
||||
report["task_file_findings"] = file_findings
|
||||
print(f"\n--- TASK FILE CREATION (Event 11) — {len(file_findings)} findings ---")
|
||||
for f in file_findings[:10]:
|
||||
print(f" [{f['severity']}] {f['task_file']}")
|
||||
|
||||
reg_findings = detect_event_4698(events)
|
||||
report["event_4698_findings"] = reg_findings
|
||||
print(f"\n--- TASK REGISTRATION (Event 4698) — {len(reg_findings)} findings ---")
|
||||
for f in reg_findings[:10]:
|
||||
print(f" [{f['severity']}] {f['task_name']} by {f['registered_by']}")
|
||||
|
||||
return report
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Sysmon Scheduled Task Detection Agent")
|
||||
parser.add_argument("--evtx-xml", required=True,
|
||||
help="Exported event log XML file to analyze")
|
||||
parser.add_argument("--output", help="Save report to JSON file")
|
||||
args = parser.parse_args()
|
||||
|
||||
report = run_audit(args)
|
||||
if args.output:
|
||||
with open(args.output, "w") as f:
|
||||
json.dump(report, f, indent=2, default=str)
|
||||
print(f"\n[+] Report saved to {args.output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
name: hunting-for-anomalous-powershell-execution
|
||||
description: >
|
||||
Hunt for malicious PowerShell activity by analyzing Script Block Logging (Event 4104),
|
||||
Module Logging (Event 4103), and process creation events. The analyst parses Windows
|
||||
Event Log EVTX files to detect obfuscated commands, AMSI bypass attempts, encoded
|
||||
payloads, credential dumping keywords, and suspicious download cradles. Activates for
|
||||
requests involving PowerShell threat hunting, script block analysis, encoded command
|
||||
detection, or AMSI bypass identification.
|
||||
domain: cybersecurity
|
||||
subdomain: threat-hunting
|
||||
tags: [powershell, script-block-logging, event-4104, amsi, threat-hunting, evtx, obfuscation]
|
||||
version: "1.0"
|
||||
author: mahipal
|
||||
license: MIT
|
||||
---
|
||||
# Hunting for Anomalous PowerShell Execution
|
||||
|
||||
## Overview
|
||||
|
||||
PowerShell Script Block Logging (Event ID 4104) records the full deobfuscated script text
|
||||
executed on a Windows endpoint, making it the primary data source for hunting malicious
|
||||
PowerShell. Combined with Module Logging (4103) and process creation events, analysts can
|
||||
detect encoded commands, AMSI bypass patterns, download cradles, credential theft tools,
|
||||
and fileless attack techniques even when the attacker uses obfuscation layers.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Windows Event Log exports (.evtx) from Microsoft-Windows-PowerShell/Operational
|
||||
- Python 3.8+ with python-evtx and lxml libraries
|
||||
- Script Block Logging enabled via Group Policy
|
||||
- Understanding of common PowerShell attack techniques
|
||||
|
||||
## Steps
|
||||
|
||||
1. Parse EVTX files extracting Event 4104 script block text and metadata
|
||||
2. Reassemble multi-part script blocks using ScriptBlock ID correlation
|
||||
3. Scan script text for AMSI bypass indicators and obfuscation patterns
|
||||
4. Detect encoded command execution and base64 payloads
|
||||
5. Identify download cradles, credential dumping, and lateral movement commands
|
||||
6. Score and prioritize findings by threat severity
|
||||
|
||||
## Expected Output
|
||||
|
||||
```json
|
||||
{
|
||||
"total_events": 1247,
|
||||
"suspicious_events": 23,
|
||||
"amsi_bypass_attempts": 2,
|
||||
"encoded_commands": 8,
|
||||
"download_cradles": 5,
|
||||
"credential_access": 3
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,106 @@
|
||||
# Hunting for Anomalous PowerShell Execution — API Reference
|
||||
|
||||
## Windows Event Log IDs
|
||||
|
||||
| Event ID | Log Source | Description |
|
||||
|----------|-----------|-------------|
|
||||
| 4104 | Microsoft-Windows-PowerShell/Operational | Script Block Logging — full deobfuscated script text |
|
||||
| 4103 | Microsoft-Windows-PowerShell/Operational | Module Logging — pipeline execution details |
|
||||
| 4688 | Security | Process Creation with command line auditing |
|
||||
| 800 | Windows PowerShell | Pipeline execution (classic log) |
|
||||
|
||||
## Event 4104 XML Fields
|
||||
|
||||
| Field | Path | Description |
|
||||
|-------|------|-------------|
|
||||
| ScriptBlockText | EventData/Data[@Name='ScriptBlockText'] | Full script block content |
|
||||
| ScriptBlockId | EventData/Data[@Name='ScriptBlockId'] | GUID linking multi-part blocks |
|
||||
| MessageNumber | EventData/Data[@Name='MessageNumber'] | Part number for split blocks |
|
||||
| MessageTotal | EventData/Data[@Name='MessageTotal'] | Total parts in split block |
|
||||
| Path | EventData/Data[@Name='Path'] | Script file path (if applicable) |
|
||||
|
||||
## AMSI Bypass Indicators
|
||||
|
||||
| Indicator | Context |
|
||||
|-----------|---------|
|
||||
| `System.Management.Automation.AmsiUtils` | Reflection access to AMSI internals |
|
||||
| `amsiInitFailed` | Setting AMSI init flag to bypass scanning |
|
||||
| `AmsiScanBuffer` | Patching the scan buffer function |
|
||||
| `amsi.dll` | Direct DLL manipulation |
|
||||
| `VirtualProtect` | Memory protection change for AMSI patching |
|
||||
| `Marshal::Copy` | Overwriting AMSI function bytes in memory |
|
||||
|
||||
## Suspicious PowerShell Keywords
|
||||
|
||||
| Keyword | Category |
|
||||
|---------|----------|
|
||||
| `Invoke-Mimikatz` | Credential Dumping |
|
||||
| `Invoke-Kerberoast` | Credential Access |
|
||||
| `Invoke-ShellCode` | Code Injection |
|
||||
| `Invoke-ReflectivePEInjection` | Process Injection |
|
||||
| `PowerView` | Active Directory Enumeration |
|
||||
| `SharpHound` / `BloodHound` | AD Attack Path Mapping |
|
||||
| `Rubeus` | Kerberos Ticket Manipulation |
|
||||
| `Out-Minidump` | LSASS Memory Dumping |
|
||||
|
||||
## Download Cradle Patterns
|
||||
|
||||
| Pattern | Example |
|
||||
|---------|---------|
|
||||
| `Net.WebClient` | `(New-Object Net.WebClient).DownloadString(...)` |
|
||||
| `Invoke-WebRequest` | `IWR -Uri http://... -OutFile ...` |
|
||||
| `DownloadString` | `$wc.DownloadString('http://...')` |
|
||||
| `Start-BitsTransfer` | `Start-BitsTransfer -Source http://...` |
|
||||
| `Invoke-RestMethod` | `IRM http://... \| IEX` |
|
||||
|
||||
## Obfuscation Indicators
|
||||
|
||||
| Pattern | Description |
|
||||
|---------|-------------|
|
||||
| `-EncodedCommand` / `-enc` | Base64-encoded PowerShell command |
|
||||
| `IEX` / `Invoke-Expression` | Dynamic execution of string content |
|
||||
| `[Convert]::FromBase64String` | Base64 decoding in script |
|
||||
| `-join [char[]]` | Character array concatenation obfuscation |
|
||||
| `.Replace()` chaining | String substitution for keyword evasion |
|
||||
|
||||
## python-evtx Library Usage
|
||||
|
||||
```python
|
||||
import Evtx.Evtx as evtx
|
||||
from lxml import etree
|
||||
|
||||
with evtx.Evtx("PowerShell-Operational.evtx") as log:
|
||||
for record in log.records():
|
||||
xml = record.xml()
|
||||
root = etree.fromstring(xml.encode("utf-8"))
|
||||
# Extract EventID, EventData fields
|
||||
```
|
||||
|
||||
## CLI Usage
|
||||
|
||||
```bash
|
||||
# Hunt for suspicious PowerShell in EVTX file
|
||||
python agent.py --evtx /path/to/PowerShell-Operational.evtx
|
||||
|
||||
# Limit events parsed
|
||||
python agent.py --evtx logs.evtx --max-events 5000
|
||||
|
||||
# Save report to JSON
|
||||
python agent.py --evtx logs.evtx --output hunt_report.json
|
||||
```
|
||||
|
||||
## Group Policy Settings for Script Block Logging
|
||||
|
||||
```
|
||||
Computer Configuration > Administrative Templates > Windows Components
|
||||
> Windows PowerShell > Turn on PowerShell Script Block Logging
|
||||
-> Enabled
|
||||
-> Log script block invocation start / stop events: Checked
|
||||
```
|
||||
|
||||
## External References
|
||||
|
||||
- [Splunk: Hunting for Malicious PowerShell using Script Block Logging](https://www.splunk.com/en_us/blog/security/hunting-for-malicious-powershell-using-script-block-logging.html)
|
||||
- [block-parser: PowerShell Script Block Log Parser](https://github.com/matthewdunwoody/block-parser)
|
||||
- [Windows Forensic Artifacts: EVTX 4104](https://github.com/Psmths/windows-forensic-artifacts/blob/main/execution/evtx-4104-script-block-logging.md)
|
||||
- [Elastic: AMSI Bypass via PowerShell Detection Rule](https://www.elastic.co/docs/reference/security/prebuilt-rules/rules/windows/defense_evasion_amsi_bypass_powershell)
|
||||
@@ -0,0 +1,247 @@
|
||||
#!/usr/bin/env python3
|
||||
"""PowerShell Script Block Logging threat hunting agent."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import argparse
|
||||
import base64
|
||||
import re
|
||||
from datetime import datetime
|
||||
from collections import defaultdict
|
||||
|
||||
try:
|
||||
import Evtx.Evtx as evtx
|
||||
from lxml import etree
|
||||
except ImportError:
|
||||
print("Install: pip install python-evtx lxml")
|
||||
sys.exit(1)
|
||||
|
||||
NS = {"e": "http://schemas.microsoft.com/win/2004/08/events/event"}
|
||||
|
||||
AMSI_INDICATORS = [
|
||||
"amsiutils", "amsiinitfailed", "amsicontext", "amsisession",
|
||||
"amsiinitialize", "amsi.dll", "amsiScanBuffer",
|
||||
"System.Management.Automation.AmsiUtils",
|
||||
]
|
||||
|
||||
SUSPICIOUS_KEYWORDS = [
|
||||
"Invoke-Mimikatz", "Invoke-Kerberoast", "Invoke-ShellCode",
|
||||
"Invoke-ReflectivePEInjection", "Invoke-TokenManipulation",
|
||||
"Get-GPPPassword", "Get-Keystrokes", "Get-TimedScreenshot",
|
||||
"Out-Minidump", "Invoke-NinjaCopy", "Invoke-CredentialInjection",
|
||||
"Invoke-DllInjection", "Invoke-WMICommand", "PowerSploit",
|
||||
"Empire", "BloodHound", "Rubeus", "SharpHound",
|
||||
"Invoke-PSInject", "Invoke-RunAs", "PowerView",
|
||||
]
|
||||
|
||||
DOWNLOAD_PATTERNS = [
|
||||
r"Net\.WebClient", r"Invoke-WebRequest", r"wget\s", r"curl\s",
|
||||
r"DownloadString", r"DownloadFile", r"DownloadData",
|
||||
r"Start-BitsTransfer", r"Invoke-RestMethod",
|
||||
r"New-Object\s+IO\.MemoryStream",
|
||||
]
|
||||
|
||||
OBFUSCATION_PATTERNS = [
|
||||
r"-[Ee]nc(?:oded)?[Cc]ommand",
|
||||
r"\-e\s+[A-Za-z0-9+/=]{20,}",
|
||||
r"IEX\s*\(",
|
||||
r"Invoke-Expression",
|
||||
r"\[Convert\]::FromBase64String",
|
||||
r"\[System\.Text\.Encoding\]::",
|
||||
r"\.Replace\(['\"][^'\"]+['\"],\s*['\"][^'\"]+['\"]\)",
|
||||
r"-join\s*\[char\[\]\]",
|
||||
r"\$env:comspec",
|
||||
]
|
||||
|
||||
|
||||
def parse_evtx_4104(evtx_path, max_events=10000):
|
||||
"""Parse Event 4104 script block logging entries from EVTX."""
|
||||
events = []
|
||||
count = 0
|
||||
with evtx.Evtx(evtx_path) as log:
|
||||
for record in log.records():
|
||||
if count >= max_events:
|
||||
break
|
||||
xml = record.xml()
|
||||
root = etree.fromstring(xml.encode("utf-8"))
|
||||
event_id_el = root.find(".//e:System/e:EventID", NS)
|
||||
if event_id_el is None or event_id_el.text != "4104":
|
||||
continue
|
||||
count += 1
|
||||
time_el = root.find(".//e:System/e:TimeCreated", NS)
|
||||
timestamp = time_el.get("SystemTime", "") if time_el is not None else ""
|
||||
data = {}
|
||||
for el in root.findall(".//e:EventData/e:Data", NS):
|
||||
name = el.get("Name", "")
|
||||
data[name] = el.text or ""
|
||||
events.append({
|
||||
"timestamp": timestamp,
|
||||
"script_block_id": data.get("ScriptBlockId", ""),
|
||||
"script_block_text": data.get("ScriptBlockText", ""),
|
||||
"message_number": data.get("MessageNumber", "1"),
|
||||
"message_total": data.get("MessageTotal", "1"),
|
||||
"path": data.get("Path", ""),
|
||||
})
|
||||
return events
|
||||
|
||||
|
||||
def reassemble_script_blocks(events):
|
||||
"""Reassemble multi-part script blocks by ScriptBlockId."""
|
||||
blocks = defaultdict(list)
|
||||
for ev in events:
|
||||
sb_id = ev.get("script_block_id", "")
|
||||
if sb_id:
|
||||
blocks[sb_id].append(ev)
|
||||
assembled = []
|
||||
for sb_id, parts in blocks.items():
|
||||
parts.sort(key=lambda x: int(x.get("message_number", "1")))
|
||||
full_text = "".join(p.get("script_block_text", "") for p in parts)
|
||||
assembled.append({
|
||||
"script_block_id": sb_id,
|
||||
"timestamp": parts[0].get("timestamp", ""),
|
||||
"path": parts[0].get("path", ""),
|
||||
"parts": len(parts),
|
||||
"full_text": full_text,
|
||||
})
|
||||
return assembled
|
||||
|
||||
|
||||
def detect_amsi_bypass(script_text):
|
||||
"""Check script text for AMSI bypass indicators."""
|
||||
findings = []
|
||||
lower = script_text.lower()
|
||||
for indicator in AMSI_INDICATORS:
|
||||
if indicator.lower() in lower:
|
||||
findings.append({"type": "amsi_bypass", "indicator": indicator})
|
||||
return findings
|
||||
|
||||
|
||||
def detect_suspicious_keywords(script_text):
|
||||
"""Check for known offensive tool keywords."""
|
||||
findings = []
|
||||
for kw in SUSPICIOUS_KEYWORDS:
|
||||
if kw.lower() in script_text.lower():
|
||||
findings.append({"type": "credential_or_offensive_tool", "keyword": kw})
|
||||
return findings
|
||||
|
||||
|
||||
def detect_download_cradles(script_text):
|
||||
"""Detect download cradle patterns in script text."""
|
||||
findings = []
|
||||
for pattern in DOWNLOAD_PATTERNS:
|
||||
if re.search(pattern, script_text, re.IGNORECASE):
|
||||
findings.append({"type": "download_cradle", "pattern": pattern})
|
||||
return findings
|
||||
|
||||
|
||||
def detect_obfuscation(script_text):
|
||||
"""Detect obfuscation and encoded command patterns."""
|
||||
findings = []
|
||||
for pattern in OBFUSCATION_PATTERNS:
|
||||
if re.search(pattern, script_text, re.IGNORECASE):
|
||||
findings.append({"type": "obfuscation", "pattern": pattern})
|
||||
b64_match = re.search(r"[A-Za-z0-9+/=]{40,}", script_text)
|
||||
if b64_match:
|
||||
try:
|
||||
decoded = base64.b64decode(b64_match.group()).decode("utf-16-le", errors="ignore")
|
||||
if any(c.isalpha() for c in decoded[:20]):
|
||||
findings.append({
|
||||
"type": "encoded_payload",
|
||||
"decoded_preview": decoded[:200],
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
return findings
|
||||
|
||||
|
||||
def hunt_scripts(assembled_blocks):
|
||||
"""Run all detection checks on assembled script blocks."""
|
||||
results = []
|
||||
for block in assembled_blocks:
|
||||
text = block.get("full_text", "")
|
||||
if not text.strip():
|
||||
continue
|
||||
findings = []
|
||||
findings.extend(detect_amsi_bypass(text))
|
||||
findings.extend(detect_suspicious_keywords(text))
|
||||
findings.extend(detect_download_cradles(text))
|
||||
findings.extend(detect_obfuscation(text))
|
||||
if findings:
|
||||
results.append({
|
||||
"script_block_id": block["script_block_id"],
|
||||
"timestamp": block["timestamp"],
|
||||
"path": block["path"],
|
||||
"text_preview": text[:300],
|
||||
"findings": findings,
|
||||
"severity": "high" if any(
|
||||
f["type"] in ("amsi_bypass", "credential_or_offensive_tool")
|
||||
for f in findings
|
||||
) else "medium",
|
||||
})
|
||||
return results
|
||||
|
||||
|
||||
def run_audit(args):
|
||||
"""Execute PowerShell script block hunting."""
|
||||
print(f"\n{'='*60}")
|
||||
print(f" POWERSHELL SCRIPT BLOCK HUNTING")
|
||||
print(f" Generated: {datetime.utcnow().isoformat()} UTC")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
report = {}
|
||||
events = parse_evtx_4104(args.evtx, args.max_events)
|
||||
report["total_4104_events"] = len(events)
|
||||
print(f"Parsed {len(events)} Event 4104 records\n")
|
||||
|
||||
blocks = reassemble_script_blocks(events)
|
||||
report["unique_script_blocks"] = len(blocks)
|
||||
print(f"Reassembled {len(blocks)} unique script blocks\n")
|
||||
|
||||
results = hunt_scripts(blocks)
|
||||
report["suspicious_blocks"] = len(results)
|
||||
report["findings"] = results
|
||||
|
||||
amsi = sum(1 for r in results if any(f["type"] == "amsi_bypass" for f in r["findings"]))
|
||||
cred = sum(1 for r in results if any(f["type"] == "credential_or_offensive_tool" for f in r["findings"]))
|
||||
dl = sum(1 for r in results if any(f["type"] == "download_cradle" for f in r["findings"]))
|
||||
obf = sum(1 for r in results if any(f["type"] == "obfuscation" for f in r["findings"]))
|
||||
report["summary"] = {
|
||||
"amsi_bypass_attempts": amsi,
|
||||
"credential_access": cred,
|
||||
"download_cradles": dl,
|
||||
"obfuscation_detected": obf,
|
||||
}
|
||||
|
||||
print(f"--- HUNT RESULTS ---")
|
||||
print(f" AMSI bypass attempts: {amsi}")
|
||||
print(f" Credential/offensive tools: {cred}")
|
||||
print(f" Download cradles: {dl}")
|
||||
print(f" Obfuscation detected: {obf}")
|
||||
print(f"\n--- HIGH SEVERITY ---")
|
||||
for r in results[:15]:
|
||||
if r["severity"] == "high":
|
||||
print(f" [{r['timestamp']}] {r['script_block_id']}")
|
||||
for f in r["findings"]:
|
||||
print(f" {f['type']}: {f.get('keyword', f.get('indicator', ''))}")
|
||||
|
||||
return report
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="PowerShell Script Block Hunting Agent")
|
||||
parser.add_argument("--evtx", required=True,
|
||||
help="Path to PowerShell Operational .evtx file")
|
||||
parser.add_argument("--max-events", type=int, default=10000,
|
||||
help="Max events to parse (default: 10000)")
|
||||
parser.add_argument("--output", help="Save report to JSON file")
|
||||
args = parser.parse_args()
|
||||
|
||||
report = run_audit(args)
|
||||
if args.output:
|
||||
with open(args.output, "w") as f:
|
||||
json.dump(report, f, indent=2, default=str)
|
||||
print(f"\n[+] Report saved to {args.output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
name: implementing-web-application-logging-with-modsecurity
|
||||
description: >
|
||||
Configure ModSecurity WAF with OWASP Core Rule Set (CRS) for web application logging,
|
||||
tune rules to reduce false positives, analyze audit logs for attack detection, and
|
||||
implement custom SecRules for application-specific threats. The analyst configures
|
||||
SecRuleEngine, SecAuditEngine, and CRS paranoia levels to balance security coverage
|
||||
with operational stability. Activates for requests involving WAF configuration,
|
||||
ModSecurity rule tuning, web application audit logging, or CRS deployment.
|
||||
domain: cybersecurity
|
||||
subdomain: web-application-security
|
||||
tags: [modsecurity, waf, crs, owasp, web-security, audit-logging, rule-tuning]
|
||||
version: "1.0"
|
||||
author: mahipal
|
||||
license: Apache-2.0
|
||||
---
|
||||
# Implementing Web Application Logging with ModSecurity
|
||||
|
||||
## Overview
|
||||
|
||||
ModSecurity is an open-source WAF engine that works with Apache, Nginx, and IIS. The OWASP
|
||||
Core Rule Set (CRS) provides generic attack detection rules covering SQL injection, XSS,
|
||||
RCE, LFI, and other OWASP Top 10 attacks. ModSecurity logs full request/response data in
|
||||
audit logs for forensic analysis and generates alerts that feed into SIEM platforms.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Web server (Apache 2.4+ or Nginx) with ModSecurity v3 module
|
||||
- OWASP CRS v4.x installed
|
||||
- Log aggregation infrastructure (ELK, Splunk, or Wazuh)
|
||||
|
||||
## Steps
|
||||
|
||||
1. Install ModSecurity and configure SecRuleEngine in DetectionOnly mode
|
||||
2. Deploy OWASP CRS v4 and set paranoia level (PL1-PL4)
|
||||
3. Configure SecAuditEngine for relevant-only logging
|
||||
4. Tune false positives with SecRuleRemoveById and rule exclusions
|
||||
5. Switch to blocking mode (SecRuleEngine On) after tuning period
|
||||
6. Forward audit logs to SIEM for correlation and alerting
|
||||
|
||||
## Expected Output
|
||||
|
||||
```
|
||||
ModSecurity: Warning. Pattern match "(?:union\s+select)" [file "/etc/modsecurity/crs/rules/REQUEST-942-APPLICATION-ATTACK-SQLI.conf"] [line "45"] [id "942100"] [msg "SQL Injection Attack Detected via libinjection"] [severity "CRITICAL"]
|
||||
```
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
# ModSecurity WAF Logging — API Reference
|
||||
|
||||
## Key ModSecurity Directives
|
||||
|
||||
| Directive | Description |
|
||||
|-----------|-------------|
|
||||
| `SecRuleEngine On/Off/DetectionOnly` | Enable/disable rule engine |
|
||||
| `SecAuditEngine On/Off/RelevantOnly` | Configure audit logging scope |
|
||||
| `SecAuditLog /path/to/modsec_audit.log` | Audit log file path |
|
||||
| `SecAuditLogParts ABCDEFHZ` | Audit log sections to include |
|
||||
| `SecRequestBodyAccess On` | Inspect request bodies |
|
||||
| `SecResponseBodyAccess On` | Inspect response bodies |
|
||||
| `SecRuleRemoveById <id>` | Disable specific rule by ID |
|
||||
| `SecRuleUpdateTargetById <id> "!ARGS:param"` | Exclude parameter from rule |
|
||||
|
||||
## Audit Log Sections
|
||||
|
||||
| Section | Contents |
|
||||
|---------|----------|
|
||||
| A | Audit log header (timestamp, transaction ID) |
|
||||
| B | Request headers |
|
||||
| C | Request body |
|
||||
| E | Response body |
|
||||
| F | Response headers |
|
||||
| H | Audit log trailer (rule matches, scores) |
|
||||
| Z | End of entry marker |
|
||||
|
||||
## OWASP CRS Rule ID Ranges
|
||||
|
||||
| Range | Category |
|
||||
|-------|----------|
|
||||
| 911xxx | Method Enforcement |
|
||||
| 920xxx | Protocol Enforcement |
|
||||
| 930xxx | Local File Inclusion |
|
||||
| 932xxx | Remote Code Execution |
|
||||
| 941xxx | Cross-Site Scripting (XSS) |
|
||||
| 942xxx | SQL Injection |
|
||||
| 944xxx | Java/Spring Attack |
|
||||
| 949xxx | Inbound Anomaly Score Blocking |
|
||||
|
||||
## CRS Paranoia Levels
|
||||
|
||||
| Level | Description |
|
||||
|-------|-------------|
|
||||
| PL1 | Default — low false positives, covers common attacks |
|
||||
| PL2 | Moderate — adds more patterns, some tuning needed |
|
||||
| PL3 | High — aggressive detection, significant tuning needed |
|
||||
| PL4 | Extreme — maximum coverage, heavy tuning required |
|
||||
|
||||
## Configuration Example
|
||||
|
||||
```apache
|
||||
SecRuleEngine DetectionOnly
|
||||
SecAuditEngine RelevantOnly
|
||||
SecAuditLogRelevantStatus "^(?:5|4(?!04))"
|
||||
SecAuditLogParts ABCDEFHZ
|
||||
SecAuditLogType Serial
|
||||
SecAuditLog /var/log/modsec_audit.log
|
||||
```
|
||||
|
||||
## External References
|
||||
|
||||
- [ModSecurity v3 Reference Manual](https://github.com/owasp-modsecurity/ModSecurity/wiki/Reference-Manual-(v3.x))
|
||||
- [OWASP CRS Documentation](https://coreruleset.org/docs/)
|
||||
- [CRS Tuning Guide](https://coreruleset.org/docs/concepts/false_positives_tuning/)
|
||||
@@ -0,0 +1,242 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ModSecurity WAF audit log analysis and rule tuning agent."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import argparse
|
||||
import re
|
||||
from datetime import datetime
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
SECTION_PATTERN = re.compile(r'^--([a-f0-9]+)-([A-Z])--$')
|
||||
|
||||
CRS_CATEGORIES = {
|
||||
"911": "Method Enforcement",
|
||||
"913": "Scanner Detection",
|
||||
"920": "Protocol Enforcement",
|
||||
"921": "Protocol Attack",
|
||||
"930": "Local File Inclusion",
|
||||
"931": "Remote File Inclusion",
|
||||
"932": "Remote Code Execution",
|
||||
"933": "PHP Injection",
|
||||
"934": "Node.js Injection",
|
||||
"941": "XSS Attack",
|
||||
"942": "SQL Injection",
|
||||
"943": "Session Fixation",
|
||||
"944": "Java Attack",
|
||||
"949": "Inbound Blocking",
|
||||
"959": "Outbound Blocking",
|
||||
}
|
||||
|
||||
|
||||
def parse_audit_log(log_path, max_entries=5000):
|
||||
"""Parse ModSecurity serial audit log format."""
|
||||
entries = []
|
||||
current = {}
|
||||
current_section = None
|
||||
|
||||
with open(log_path, "r", errors="replace") as f:
|
||||
for line in f:
|
||||
match = SECTION_PATTERN.match(line.strip())
|
||||
if match:
|
||||
tx_id = match.group(1)
|
||||
section = match.group(2)
|
||||
if section == "A":
|
||||
if current and current.get("tx_id"):
|
||||
entries.append(current)
|
||||
if len(entries) >= max_entries:
|
||||
break
|
||||
current = {"tx_id": tx_id, "sections": {}}
|
||||
current_section = section
|
||||
current["sections"][section] = ""
|
||||
elif current_section and current_section in current.get("sections", {}):
|
||||
current["sections"][current_section] += line
|
||||
|
||||
if current and current.get("tx_id"):
|
||||
entries.append(current)
|
||||
|
||||
parsed = []
|
||||
for entry in entries:
|
||||
record = {"tx_id": entry["tx_id"]}
|
||||
section_a = entry["sections"].get("A", "")
|
||||
if section_a:
|
||||
parts = section_a.strip().split()
|
||||
if len(parts) >= 3:
|
||||
record["timestamp"] = parts[0] if parts else ""
|
||||
record["client_ip"] = parts[1] if len(parts) > 1 else ""
|
||||
|
||||
section_b = entry["sections"].get("B", "")
|
||||
if section_b:
|
||||
first_line = section_b.strip().split("\n")[0]
|
||||
req_parts = first_line.split()
|
||||
if len(req_parts) >= 2:
|
||||
record["method"] = req_parts[0]
|
||||
record["uri"] = req_parts[1]
|
||||
|
||||
section_h = entry["sections"].get("H", "")
|
||||
record["rules_matched"] = []
|
||||
for rule_match in re.finditer(
|
||||
r'\[id "(\d+)"\].*?\[msg "([^"]+)"\].*?\[severity "([^"]+)"\]',
|
||||
section_h
|
||||
):
|
||||
record["rules_matched"].append({
|
||||
"rule_id": rule_match.group(1),
|
||||
"message": rule_match.group(2),
|
||||
"severity": rule_match.group(3),
|
||||
})
|
||||
|
||||
anomaly = re.search(r'Inbound Anomaly Score.*?(\d+)', section_h)
|
||||
if anomaly:
|
||||
record["anomaly_score"] = int(anomaly.group(1))
|
||||
|
||||
parsed.append(record)
|
||||
return parsed
|
||||
|
||||
|
||||
def analyze_rule_frequency(entries):
|
||||
"""Analyze which rules fire most frequently for tuning."""
|
||||
rule_counts = defaultdict(int)
|
||||
rule_msgs = {}
|
||||
for entry in entries:
|
||||
for rule in entry.get("rules_matched", []):
|
||||
rid = rule["rule_id"]
|
||||
rule_counts[rid] += 1
|
||||
rule_msgs[rid] = rule["message"]
|
||||
|
||||
sorted_rules = sorted(rule_counts.items(), key=lambda x: x[1], reverse=True)
|
||||
results = []
|
||||
for rid, count in sorted_rules:
|
||||
category = CRS_CATEGORIES.get(rid[:3], "Other")
|
||||
results.append({
|
||||
"rule_id": rid,
|
||||
"count": count,
|
||||
"message": rule_msgs.get(rid, ""),
|
||||
"category": category,
|
||||
})
|
||||
return results
|
||||
|
||||
|
||||
def identify_false_positive_candidates(entries, threshold=50):
|
||||
"""Identify rules that may be false positives based on frequency and pattern."""
|
||||
rule_ips = defaultdict(set)
|
||||
rule_uris = defaultdict(set)
|
||||
rule_counts = defaultdict(int)
|
||||
|
||||
for entry in entries:
|
||||
for rule in entry.get("rules_matched", []):
|
||||
rid = rule["rule_id"]
|
||||
rule_counts[rid] += 1
|
||||
rule_ips[rid].add(entry.get("client_ip", ""))
|
||||
rule_uris[rid].add(entry.get("uri", ""))
|
||||
|
||||
candidates = []
|
||||
for rid, count in rule_counts.items():
|
||||
if count >= threshold and len(rule_ips[rid]) > 10:
|
||||
candidates.append({
|
||||
"rule_id": rid,
|
||||
"hit_count": count,
|
||||
"unique_ips": len(rule_ips[rid]),
|
||||
"unique_uris": len(rule_uris[rid]),
|
||||
"recommendation": f"SecRuleRemoveById {rid}",
|
||||
"reason": "High frequency across many IPs — likely false positive",
|
||||
})
|
||||
return candidates
|
||||
|
||||
|
||||
def generate_exclusion_rules(candidates):
|
||||
"""Generate ModSecurity rule exclusion configuration."""
|
||||
lines = ["# Auto-generated false positive exclusions"]
|
||||
for c in candidates:
|
||||
lines.append(f"# Rule {c['rule_id']}: {c['hit_count']} hits, "
|
||||
f"{c['unique_ips']} unique IPs")
|
||||
lines.append(f"SecRuleRemoveById {c['rule_id']}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def analyze_attack_summary(entries):
|
||||
"""Summarize detected attacks by category and severity."""
|
||||
category_counts = defaultdict(int)
|
||||
severity_counts = defaultdict(int)
|
||||
top_attackers = defaultdict(int)
|
||||
|
||||
for entry in entries:
|
||||
for rule in entry.get("rules_matched", []):
|
||||
cat = CRS_CATEGORIES.get(rule["rule_id"][:3], "Other")
|
||||
category_counts[cat] += 1
|
||||
severity_counts[rule["severity"]] += 1
|
||||
if entry.get("anomaly_score", 0) >= 5:
|
||||
top_attackers[entry.get("client_ip", "")] += 1
|
||||
|
||||
return {
|
||||
"by_category": dict(sorted(category_counts.items(), key=lambda x: x[1], reverse=True)),
|
||||
"by_severity": dict(severity_counts),
|
||||
"top_attackers": dict(sorted(top_attackers.items(), key=lambda x: x[1], reverse=True)[:20]),
|
||||
}
|
||||
|
||||
|
||||
def run_audit(args):
|
||||
"""Execute ModSecurity audit log analysis."""
|
||||
print(f"\n{'='*60}")
|
||||
print(f" MODSECURITY AUDIT LOG ANALYSIS")
|
||||
print(f" Generated: {datetime.utcnow().isoformat()} UTC")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
report = {}
|
||||
|
||||
entries = parse_audit_log(args.audit_log, args.max_entries)
|
||||
report["total_entries"] = len(entries)
|
||||
print(f"Parsed {len(entries)} audit log entries\n")
|
||||
|
||||
attack_summary = analyze_attack_summary(entries)
|
||||
report["attack_summary"] = attack_summary
|
||||
print(f"--- ATTACK SUMMARY ---")
|
||||
for cat, count in list(attack_summary["by_category"].items())[:10]:
|
||||
print(f" {cat}: {count}")
|
||||
print(f"\n Severity: {attack_summary['by_severity']}")
|
||||
print(f"\n--- TOP ATTACKERS ---")
|
||||
for ip, count in list(attack_summary["top_attackers"].items())[:10]:
|
||||
print(f" {ip}: {count} alerts")
|
||||
|
||||
rule_freq = analyze_rule_frequency(entries)
|
||||
report["rule_frequency"] = rule_freq[:20]
|
||||
print(f"\n--- TOP FIRING RULES ---")
|
||||
for r in rule_freq[:15]:
|
||||
print(f" [{r['rule_id']}] {r['count']}x — {r['message'][:60]}")
|
||||
|
||||
if args.tune:
|
||||
fp_candidates = identify_false_positive_candidates(entries, args.fp_threshold)
|
||||
report["false_positive_candidates"] = fp_candidates
|
||||
print(f"\n--- FALSE POSITIVE CANDIDATES ({len(fp_candidates)}) ---")
|
||||
for c in fp_candidates[:10]:
|
||||
print(f" Rule {c['rule_id']}: {c['hit_count']} hits, "
|
||||
f"{c['unique_ips']} IPs — {c['reason']}")
|
||||
if fp_candidates:
|
||||
exclusions = generate_exclusion_rules(fp_candidates)
|
||||
report["exclusion_config"] = exclusions
|
||||
|
||||
return report
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="ModSecurity Audit Log Agent")
|
||||
parser.add_argument("--audit-log", required=True,
|
||||
help="Path to ModSecurity audit log file")
|
||||
parser.add_argument("--max-entries", type=int, default=5000,
|
||||
help="Max log entries to parse (default: 5000)")
|
||||
parser.add_argument("--tune", action="store_true",
|
||||
help="Identify false positive candidates for tuning")
|
||||
parser.add_argument("--fp-threshold", type=int, default=50,
|
||||
help="Minimum hits for false positive candidate (default: 50)")
|
||||
parser.add_argument("--output", help="Save report to JSON file")
|
||||
args = parser.parse_args()
|
||||
|
||||
report = run_audit(args)
|
||||
if args.output:
|
||||
with open(args.output, "w") as f:
|
||||
json.dump(report, f, indent=2, default=str)
|
||||
print(f"\n[+] Report saved to {args.output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
name: performing-fuzzing-with-aflplusplus
|
||||
description: >
|
||||
Perform coverage-guided fuzzing of compiled binaries using AFL++ (American Fuzzy Lop Plus Plus)
|
||||
to discover memory corruption, crashes, and security vulnerabilities. The tester instruments
|
||||
target binaries with afl-cc/afl-clang-fast, manages input corpora with afl-cmin and afl-tmin,
|
||||
runs parallel fuzzing campaigns with afl-fuzz, and triages crashes using CASR or GDB scripts.
|
||||
Activates for requests involving binary fuzzing, crash discovery, coverage-guided testing, or
|
||||
AFL++ fuzzing campaigns.
|
||||
domain: cybersecurity
|
||||
subdomain: application-security
|
||||
tags: [fuzzing, aflplusplus, coverage-guided, crash-triage, binary-analysis, security-testing]
|
||||
version: "1.0"
|
||||
author: mahipal
|
||||
license: Apache-2.0
|
||||
---
|
||||
# Performing Fuzzing with AFL++
|
||||
|
||||
## Overview
|
||||
|
||||
AFL++ is a community-maintained fork of American Fuzzy Lop (AFL) that provides coverage-guided
|
||||
fuzzing for compiled binaries. It instruments targets at compile time or via QEMU/Unicorn mode
|
||||
for binary-only fuzzing, then mutates input corpora to discover new code paths. AFL++ includes
|
||||
advanced scheduling (MOpt, rare), custom mutators, CMPLOG for input-to-state comparison solving,
|
||||
and persistent mode for high-throughput fuzzing.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- AFL++ installed (`apt install afl++` or build from source)
|
||||
- Target binary source code (for compile-time instrumentation) or QEMU mode for binary-only
|
||||
- Initial seed corpus of valid inputs for the target format
|
||||
- Linux system with /proc/sys/kernel/core_pattern configured
|
||||
|
||||
## Steps
|
||||
|
||||
1. Instrument the target binary with `afl-cc` or `afl-clang-fast`
|
||||
2. Prepare seed corpus directory with minimal valid inputs
|
||||
3. Minimize corpus with `afl-cmin` to remove redundant seeds
|
||||
4. Run `afl-fuzz` with appropriate flags (-i input -o output)
|
||||
5. Monitor fuzzing progress via afl-whatsup and UI stats
|
||||
6. Triage crashes with `afl-tmin` minimization and CASR/GDB analysis
|
||||
7. Report unique crashes with reproduction steps
|
||||
|
||||
## Expected Output
|
||||
|
||||
```
|
||||
+++ Findings +++
|
||||
unique crashes: 12
|
||||
unique hangs: 3
|
||||
last crash: 00:02:15 ago
|
||||
+++ Coverage +++
|
||||
map density: 4.23% / 8.41%
|
||||
paths found: 1847
|
||||
exec speed: 2145/sec
|
||||
```
|
||||
@@ -0,0 +1,64 @@
|
||||
# AFL++ Fuzzing — API Reference
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
apt install afl++ # Ubuntu/Debian
|
||||
# Or build from source:
|
||||
git clone https://github.com/AFLplusplus/AFLplusplus && cd AFLplusplus && make all
|
||||
```
|
||||
|
||||
## AFL++ CLI Tools
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `afl-cc` / `afl-clang-fast` | Compile-time instrumentation compiler wrapper |
|
||||
| `afl-fuzz` | Main fuzzer — coverage-guided mutation engine |
|
||||
| `afl-cmin` | Corpus minimization — remove redundant seeds |
|
||||
| `afl-tmin` | Test case minimization — shrink individual inputs |
|
||||
| `afl-whatsup` | Multi-instance campaign status summary |
|
||||
| `afl-plot` | Generate fuzzing progress plots |
|
||||
| `afl-showmap` | Display coverage map for a single input |
|
||||
|
||||
## afl-fuzz Key Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-i <dir>` | Input seed corpus directory |
|
||||
| `-o <dir>` | Output directory for findings |
|
||||
| `-m <MB>` | Memory limit (use `none` for ASAN) |
|
||||
| `-t <ms>` | Execution timeout per test case |
|
||||
| `-x <dict>` | Optional fuzzing dictionary |
|
||||
| `-p <sched>` | Power schedule: fast, coe, explore, rare, mmopt |
|
||||
| `-l <level>` | CMPLOG instrumentation level (2=transforms, 3=all) |
|
||||
| `-c <bin>` | CMPLOG binary for input-to-state |
|
||||
| `-M <name>` | Main fuzzer instance (parallel mode) |
|
||||
| `-S <name>` | Secondary fuzzer instance (parallel mode) |
|
||||
| `-Q` | QEMU mode (binary-only fuzzing) |
|
||||
| `-U` | Unicorn mode |
|
||||
|
||||
## fuzzer_stats File Fields
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `execs_done` | Total executions completed |
|
||||
| `execs_per_sec` | Current execution speed |
|
||||
| `corpus_count` | Total paths in corpus |
|
||||
| `saved_crashes` | Unique crashes discovered |
|
||||
| `saved_hangs` | Unique hangs discovered |
|
||||
| `stability` | Execution stability percentage |
|
||||
| `bitmap_cvg` | Code coverage bitmap density |
|
||||
|
||||
## Crash Triage Tools
|
||||
|
||||
| Tool | Purpose |
|
||||
|------|---------|
|
||||
| `casr-afl` | CASR crash severity analysis for AFL++ |
|
||||
| `afl-tmin` | Minimize crash inputs |
|
||||
| `gdb --batch -ex run` | Reproduce crash under debugger |
|
||||
|
||||
## External References
|
||||
|
||||
- [AFL++ Documentation](https://aflplus.plus/docs/)
|
||||
- [AFL++ GitHub](https://github.com/AFLplusplus/AFLplusplus)
|
||||
- [CASR Crash Triage](https://github.com/ispras/casr)
|
||||
@@ -0,0 +1,189 @@
|
||||
#!/usr/bin/env python3
|
||||
# For authorized testing only
|
||||
"""AFL++ fuzzing campaign management and crash triage agent."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
import glob
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def instrument_target(source_path, output_path, compiler="afl-clang-fast",
|
||||
sanitizer=None):
|
||||
"""Compile target with AFL++ instrumentation."""
|
||||
cmd = [compiler, "-o", output_path, source_path]
|
||||
if sanitizer == "asan":
|
||||
cmd.insert(1, "-fsanitize=address")
|
||||
elif sanitizer == "ubsan":
|
||||
cmd.insert(1, "-fsanitize=undefined")
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
||||
return {
|
||||
"compiler": compiler,
|
||||
"source": source_path,
|
||||
"output": output_path,
|
||||
"sanitizer": sanitizer,
|
||||
"success": result.returncode == 0,
|
||||
"stderr": result.stderr[:500] if result.stderr else "",
|
||||
}
|
||||
|
||||
|
||||
def minimize_corpus(afl_cmin_path, target_binary, input_dir, output_dir):
|
||||
"""Minimize seed corpus using afl-cmin."""
|
||||
cmd = [afl_cmin_path or "afl-cmin", "-i", input_dir, "-o", output_dir,
|
||||
"--", target_binary]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
|
||||
before = len(os.listdir(input_dir)) if os.path.isdir(input_dir) else 0
|
||||
after = len(os.listdir(output_dir)) if os.path.isdir(output_dir) else 0
|
||||
return {
|
||||
"before": before,
|
||||
"after": after,
|
||||
"reduction_pct": round((1 - after / max(before, 1)) * 100, 1),
|
||||
"success": result.returncode == 0,
|
||||
}
|
||||
|
||||
|
||||
def parse_fuzzer_stats(output_dir):
|
||||
"""Parse afl-fuzz fuzzer_stats file for campaign metrics."""
|
||||
stats_path = os.path.join(output_dir, "default", "fuzzer_stats")
|
||||
if not os.path.exists(stats_path):
|
||||
stats_path = os.path.join(output_dir, "fuzzer_stats")
|
||||
if not os.path.exists(stats_path):
|
||||
return {"error": f"fuzzer_stats not found in {output_dir}"}
|
||||
|
||||
stats = {}
|
||||
with open(stats_path, "r") as f:
|
||||
for line in f:
|
||||
if ":" in line:
|
||||
key, val = line.split(":", 1)
|
||||
stats[key.strip()] = val.strip()
|
||||
|
||||
return {
|
||||
"start_time": stats.get("start_time", ""),
|
||||
"last_update": stats.get("last_update", ""),
|
||||
"execs_done": int(stats.get("execs_done", 0)),
|
||||
"execs_per_sec": float(stats.get("execs_per_sec", 0)),
|
||||
"paths_total": int(stats.get("corpus_count", stats.get("paths_total", 0))),
|
||||
"paths_found": int(stats.get("paths_found", 0)),
|
||||
"unique_crashes": int(stats.get("saved_crashes", stats.get("unique_crashes", 0))),
|
||||
"unique_hangs": int(stats.get("saved_hangs", stats.get("unique_hangs", 0))),
|
||||
"stability": stats.get("stability", ""),
|
||||
"bitmap_cvg": stats.get("bitmap_cvg", ""),
|
||||
"command_line": stats.get("command_line", ""),
|
||||
}
|
||||
|
||||
|
||||
def triage_crashes(output_dir):
|
||||
"""Enumerate and classify crash files from AFL++ output."""
|
||||
crash_dirs = [
|
||||
os.path.join(output_dir, "default", "crashes"),
|
||||
os.path.join(output_dir, "crashes"),
|
||||
]
|
||||
crash_dir = None
|
||||
for d in crash_dirs:
|
||||
if os.path.isdir(d):
|
||||
crash_dir = d
|
||||
break
|
||||
if not crash_dir:
|
||||
return {"crashes": [], "total": 0}
|
||||
|
||||
crashes = []
|
||||
for filename in sorted(os.listdir(crash_dir)):
|
||||
if filename.startswith("README") or filename == ".state":
|
||||
continue
|
||||
filepath = os.path.join(crash_dir, filename)
|
||||
size = os.path.getsize(filepath)
|
||||
sig_parts = filename.split(",")
|
||||
signal = ""
|
||||
for part in sig_parts:
|
||||
if part.startswith("sig:"):
|
||||
signal = part.split(":")[1]
|
||||
crashes.append({
|
||||
"filename": filename,
|
||||
"size_bytes": size,
|
||||
"signal": signal,
|
||||
"path": filepath,
|
||||
})
|
||||
return {"crashes": crashes, "total": len(crashes)}
|
||||
|
||||
|
||||
def minimize_crash(afl_tmin_path, target_binary, crash_file, output_file):
|
||||
"""Minimize a crash test case with afl-tmin."""
|
||||
cmd = [afl_tmin_path or "afl-tmin", "-i", crash_file, "-o", output_file,
|
||||
"--", target_binary]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
||||
orig_size = os.path.getsize(crash_file) if os.path.exists(crash_file) else 0
|
||||
min_size = os.path.getsize(output_file) if os.path.exists(output_file) else 0
|
||||
return {
|
||||
"original_size": orig_size,
|
||||
"minimized_size": min_size,
|
||||
"reduction_pct": round((1 - min_size / max(orig_size, 1)) * 100, 1),
|
||||
"success": result.returncode == 0,
|
||||
}
|
||||
|
||||
|
||||
def run_whatsup(output_dir):
|
||||
"""Run afl-whatsup to get multi-instance campaign summary."""
|
||||
cmd = ["afl-whatsup", "-s", output_dir]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
||||
return {"output": result.stdout[:2000] if result.stdout else result.stderr[:500]}
|
||||
|
||||
|
||||
def run_audit(args):
|
||||
"""Execute AFL++ fuzzing campaign audit."""
|
||||
print(f"\n{'='*60}")
|
||||
print(f" AFL++ FUZZING CAMPAIGN AUDIT")
|
||||
print(f" Generated: {datetime.utcnow().isoformat()} UTC")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
report = {}
|
||||
|
||||
if args.output_dir:
|
||||
stats = parse_fuzzer_stats(args.output_dir)
|
||||
report["fuzzer_stats"] = stats
|
||||
print(f"--- FUZZER STATS ---")
|
||||
print(f" Executions: {stats.get('execs_done', 0):,}")
|
||||
print(f" Exec/sec: {stats.get('execs_per_sec', 0)}")
|
||||
print(f" Paths: {stats.get('paths_total', 0)}")
|
||||
print(f" Crashes: {stats.get('unique_crashes', 0)}")
|
||||
print(f" Hangs: {stats.get('unique_hangs', 0)}")
|
||||
print(f" Stability: {stats.get('stability', '')}")
|
||||
print(f" Coverage: {stats.get('bitmap_cvg', '')}")
|
||||
|
||||
crash_data = triage_crashes(args.output_dir)
|
||||
report["crash_triage"] = crash_data
|
||||
print(f"\n--- CRASH TRIAGE ({crash_data['total']} crashes) ---")
|
||||
for c in crash_data["crashes"][:20]:
|
||||
print(f" {c['filename']} ({c['size_bytes']}B) signal={c['signal']}")
|
||||
|
||||
if args.instrument_src and args.instrument_out:
|
||||
inst = instrument_target(args.instrument_src, args.instrument_out,
|
||||
sanitizer=args.sanitizer)
|
||||
report["instrumentation"] = inst
|
||||
print(f"\n--- INSTRUMENTATION ---")
|
||||
print(f" {'SUCCESS' if inst['success'] else 'FAILED'}: {inst['source']}")
|
||||
|
||||
return report
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="AFL++ Fuzzing Campaign Agent")
|
||||
parser.add_argument("--output-dir", help="AFL++ output directory to analyze")
|
||||
parser.add_argument("--instrument-src", help="Source file to instrument")
|
||||
parser.add_argument("--instrument-out", help="Output path for instrumented binary")
|
||||
parser.add_argument("--sanitizer", choices=["asan", "ubsan"],
|
||||
help="Address or undefined behavior sanitizer")
|
||||
parser.add_argument("--output", help="Save report to JSON file")
|
||||
args = parser.parse_args()
|
||||
|
||||
report = run_audit(args)
|
||||
if args.output:
|
||||
with open(args.output, "w") as f:
|
||||
json.dump(report, f, indent=2, default=str)
|
||||
print(f"\n[+] Report saved to {args.output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user