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:
mukul975
2026-03-11 00:41:37 +01:00
parent 546f1ae6ef
commit aea97ff9ff
20 changed files with 1857 additions and 0 deletions
@@ -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"]
```
@@ -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()