threat hunting

Hunting For Living Off The Cloud Techniques

Hunt for adversary abuse of legitimate cloud services for C2, data staging, and exfiltration including abuse of Azure, AWS, GCP services, and SaaS platforms.

c2cloud-abuselotcmitre-attackproactive-detectionsaasthreat-hunting
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

When to Use

  • When proactively hunting for indicators of hunting for living off the cloud techniques in the environment
  • After threat intelligence indicates active campaigns using these techniques
  • During incident response to scope compromise related to these techniques
  • When EDR or SIEM alerts trigger on related indicators
  • During periodic security assessments and purple team exercises

Prerequisites

  • EDR platform with process and network telemetry (CrowdStrike, MDE, SentinelOne)
  • SIEM with relevant log data ingested (Splunk, Elastic, Sentinel)
  • Sysmon deployed with comprehensive configuration
  • Windows Security Event Log forwarding enabled
  • Threat intelligence feeds for IOC correlation

Workflow

  1. Formulate Hypothesis: Define a testable hypothesis based on threat intelligence or ATT&CK gap analysis.
  2. Identify Data Sources: Determine which logs and telemetry are needed to validate or refute the hypothesis.
  3. Execute Queries: Run detection queries against SIEM and EDR platforms to collect relevant events.
  4. Analyze Results: Examine query results for anomalies, correlating across multiple data sources.
  5. Validate Findings: Distinguish true positives from false positives through contextual analysis.
  6. Correlate Activity: Link findings to broader attack chains and threat actor TTPs.
  7. Document and Report: Record findings, update detection rules, and recommend response actions.

Key Concepts

Concept Description
T1102 Web Service
T1567 Exfiltration Over Web Service
T1537 Transfer Data to Cloud Account

Tools & Systems

Tool Purpose
CrowdStrike Falcon EDR telemetry and threat detection
Microsoft Defender for Endpoint Advanced hunting with KQL
Splunk Enterprise SIEM log analysis with SPL queries
Elastic Security Detection rules and investigation timeline
Sysmon Detailed Windows event monitoring
Velociraptor Endpoint artifact collection and hunting
Sigma Rules Cross-platform detection rule format

Common Scenarios

  1. Scenario 1: C2 over Discord webhooks for command delivery
  2. Scenario 2: Data exfiltration to Telegram bot API
  3. Scenario 3: Malware using Azure Functions for dynamic C2
  4. Scenario 4: Staging stolen data on Google Docs or Notion pages

Output Format

Hunt ID: TH-HUNTIN-[DATE]-[SEQ]
Technique: T1102
Host: [Hostname]
User: [Account context]
Evidence: [Log entries, process trees, network data]
Risk Level: [Critical/High/Medium/Low]
Confidence: [High/Medium/Low]
Recommended Action: [Containment, investigation, monitoring]
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 3

api-reference.md2.1 KB

API Reference — Hunting for Living-off-the-Cloud Techniques

Libraries Used

  • elasticsearch (elasticsearch-py): Query Elastic SIEM for cloud abuse indicators
  • re: Pattern matching against cloud C2 domain patterns in DNS logs

CLI Interface

python agent.py hunt --es-host <url> --index <pattern> [--api-key <key>] [--hours <n>]
python agent.py dns --log-file <path>

Core Functions

hunt_lotc_elastic(es_host, es_index, api_key=None, hours=24)

Executes five pre-built hunting queries against Elasticsearch to detect cloud service abuse.

Parameters:

Name Type Description
es_host str Elasticsearch host URL (e.g., https://es:9200)
es_index str Index pattern (default: logs-*)
api_key str Optional API key for authentication
hours int Lookback window in hours

Returns: dict with hunts list (each with name, description, hits, events) and total_hits.

analyze_dns_logs(log_file)

Scans DNS query log files for connections to known cloud services used for C2, staging, and exfiltration.

Parameters:

Name Type Description
log_file str Path to DNS query log file

Returns: dict with total_matches, findings list, and cloud_services_detected.

Hunting Queries

Query Name MITRE Technique Description
azure_storage_exfil T1567.002 Large uploads to Azure Blob Storage
aws_s3_staging T1537 Unusual S3 bucket creation or large PutObject
saas_c2_channel T1102 Outbound connections to SaaS APIs (Telegram, Slack, Discord)
cloud_function_invoke T1584.007 Cloud function invocation via LOLBins
github_raw_download T1105 Payload downloads from raw GitHub content

Elasticsearch API Calls

  • Elasticsearch(hosts=[url], api_key=key) — Initialize client
  • es.search(index=pattern, body=query) — Execute search query
  • Response: resp["hits"]["total"]["value"], resp["hits"]["hits"][]._source

Dependencies

pip install elasticsearch>=8.0
standards.md1.5 KB

Standards and References - Hunting For Living Off The Cloud Techniques

MITRE ATT&CK Mappings

Technique Name Description
T1102 Web Service See attack.mitre.org/techniques/T1102
T1567 Exfiltration Over Web Service See attack.mitre.org/techniques/T1567
T1537 Transfer Data to Cloud Account See attack.mitre.org/techniques/T1537

Detection Data Sources

Source Event ID Purpose
Sysmon 1 Process creation with command line
Sysmon 3 Network connection initiated
Sysmon 7 Image loaded (DLL)
Sysmon 10 Process access (LSASS)
Sysmon 11 File creation
Sysmon 12/13 Registry create/set
Sysmon 22 DNS query
Sysmon 25 Process tampering
Windows Security 4624 Successful logon
Windows Security 4625 Failed logon
Windows Security 4648 Explicit credential logon
Windows Security 4672 Special privileges assigned
Windows Security 4688 Process creation
Windows Security 4697 Service installed
Windows Security 4698 Scheduled task created
Windows Security 4769 Kerberos TGS requested
Windows Security 5140 Network share accessed

References

workflows.md2.9 KB

Detailed Hunting Workflow - Hunting For Living Off The Cloud Techniques

Phase 1: Data Collection and Querying

Splunk SPL Query

index=proxy
| where match(dest, "(?i)(pastebin|discord|telegram|notion|trello|slack|github\.io|workers\.dev|azurewebsites\.net|firebaseio)")
| where method IN ("POST", "PUT")
| stats sum(bytes_out) as uploaded count by src_ip dest user
| where count > 20 OR uploaded > 10485760

KQL Query (Microsoft Defender for Endpoint)

DeviceNetworkEvents
| where RemoteUrl has_any ("pastebin.com","discord.com","api.telegram.org","notion.so","trello.com")
| summarize Count=count(), BytesOut=sum(SentBytes) by DeviceName, RemoteUrl
| where Count > 20

Phase 2: Baseline and Anomaly Detection

Step 2.1 - Establish Normal Behavior Baseline

  • Collect 30 days of historical data for the targeted technique
  • Document expected patterns, frequencies, and legitimate use cases
  • Identify known false positive sources and document exceptions
  • Build statistical baseline (mean, standard deviation) for key metrics

Step 2.2 - Identify Anomalies

  • Compare current activity against the 30-day baseline
  • Flag events exceeding 3 standard deviations from normal
  • Prioritize anomalies by risk score and potential business impact
  • Cross-reference with threat intelligence for known IOCs

Phase 3: Investigation and Correlation

Step 3.1 - Deep Dive Analysis

  • For each anomaly, collect full process tree context
  • Correlate with network activity, file operations, and authentication events
  • Check binary signatures, file hashes, and certificate validity
  • Review user account context and access patterns

Step 3.2 - Attack Chain Reconstruction

  • Map findings to MITRE ATT&CK kill chain stages
  • Identify initial access vector if applicable
  • Trace lateral movement and privilege escalation paths
  • Determine data access and potential exfiltration

Phase 4: Validation and Response

Step 4.1 - True/False Positive Determination

  • Verify findings with system owners and IT operations
  • Check change management records for authorized activities
  • Validate user context (authorized actions vs. compromised account)
  • Document determination rationale for each finding

Step 4.2 - Response Actions

  • For confirmed threats: initiate incident response procedures
  • For detection gaps: create or update detection rules
  • For false positives: tune existing rules and update exclusions
  • Update threat hunting playbook with lessons learned

Phase 5: Documentation and Reporting

Step 5.1 - Hunt Report

  • Summarize hypothesis, methodology, and findings
  • Include all queries executed and their results
  • Document IOCs discovered and detection rules created
  • Provide recommendations for security improvements

Step 5.2 - Knowledge Base Update

  • Add findings to threat intelligence platform
  • Update MITRE ATT&CK coverage heatmap
  • Share detection rules via Sigma format
  • Schedule follow-up hunts for related techniques

Scripts 2

agent.py6.0 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Agent for hunting living-off-the-cloud (LOTC) techniques using cloud service logs."""

import json
import argparse
import re
from datetime import datetime

try:
    from elasticsearch import Elasticsearch
except ImportError:
    Elasticsearch = None

CLOUD_C2_DOMAINS = [
    "*.blob.core.windows.net", "*.s3.amazonaws.com", "*.storage.googleapis.com",
    "*.azurewebsites.net", "*.cloudfront.net", "*.execute-api.amazonaws.com",
    "*.cloudfunctions.net", "*.run.app", "*.appspot.com",
    "pastebin.com", "raw.githubusercontent.com", "gist.githubusercontent.com",
    "discord.com/api/webhooks", "hooks.slack.com", "api.telegram.org",
    "notion.so", "docs.google.com", "drive.google.com",
    "*.firebaseio.com", "*.azureedge.net", "*.ngrok.io",
]

SUSPICIOUS_PATTERNS = {
    "azure_storage_exfil": {
        "description": "Large uploads to Azure Blob Storage",
        "query": {"bool": {"must": [
            {"match": {"event.action": "PutBlob"}},
            {"range": {"http.request.bytes": {"gte": 10485760}}}
        ]}},
    },
    "aws_s3_staging": {
        "description": "Unusual S3 bucket creation or large PutObject",
        "query": {"bool": {"must": [
            {"terms": {"event.action": ["CreateBucket", "PutObject"]}},
            {"range": {"@timestamp": {"gte": "now-24h"}}}
        ]}},
    },
    "saas_c2_channel": {
        "description": "Outbound connections to SaaS APIs used for C2",
        "query": {"bool": {"must": [
            {"terms": {"dns.question.name": [
                "api.telegram.org", "discord.com", "hooks.slack.com",
                "pastebin.com", "notion.so"
            ]}},
            {"match": {"process.name": {"query": "powershell.exe cmd.exe rundll32.exe", "operator": "or"}}}
        ]}},
    },
    "cloud_function_invoke": {
        "description": "Suspicious invocation of cloud functions for payload delivery",
        "query": {"bool": {"must": [
            {"regexp": {"url.domain": ".*\\.(cloudfunctions\\.net|execute-api\\.amazonaws\\.com|azurewebsites\\.net)"}},
            {"terms": {"process.name": ["certutil.exe", "bitsadmin.exe", "curl.exe", "wget.exe"]}}
        ]}},
    },
    "github_raw_download": {
        "description": "Downloads from raw GitHub content indicating payload staging",
        "query": {"bool": {"must": [
            {"wildcard": {"url.domain": "*githubusercontent.com"}},
            {"terms": {"process.name": ["powershell.exe", "cmd.exe", "wscript.exe", "cscript.exe"]}}
        ]}},
    },
}


def hunt_lotc_elastic(es_host, es_index, api_key=None, hours=24):
    """Run LOTC hunting queries against Elasticsearch/Elastic SIEM."""
    if Elasticsearch is None:
        return {"error": "elasticsearch-py not installed"}
    kwargs = {"hosts": [es_host]}
    if api_key:
        kwargs["api_key"] = api_key
    es = Elasticsearch(**kwargs)
    results = {"timestamp": datetime.utcnow().isoformat(), "hunts": [], "total_hits": 0}
    for hunt_name, hunt_def in SUSPICIOUS_PATTERNS.items():
        body = {"query": hunt_def["query"], "size": 100, "sort": [{"@timestamp": "desc"}]}
        resp = es.search(index=es_index, body=body)
        hits = resp["hits"]["total"]["value"]
        events = []
        for hit in resp["hits"]["hits"]:
            src = hit["_source"]
            events.append({
                "timestamp": src.get("@timestamp"),
                "host": src.get("host", {}).get("name"),
                "process": src.get("process", {}).get("name"),
                "command_line": src.get("process", {}).get("command_line"),
                "destination": src.get("url", {}).get("domain") or src.get("dns", {}).get("question", {}).get("name"),
                "user": src.get("user", {}).get("name"),
            })
        results["hunts"].append({
            "name": hunt_name,
            "description": hunt_def["description"],
            "hits": hits,
            "events": events,
        })
        results["total_hits"] += hits
    return results


def analyze_dns_logs(log_file):
    """Analyze DNS query logs for cloud C2 domain patterns."""
    findings = []
    cloud_regex = re.compile(
        r"(blob\.core\.windows\.net|s3\.amazonaws\.com|storage\.googleapis\.com|"
        r"cloudfunctions\.net|execute-api\.amazonaws\.com|azurewebsites\.net|"
        r"ngrok\.io|firebaseio\.com|pastebin\.com|githubusercontent\.com|"
        r"api\.telegram\.org|discord\.com|hooks\.slack\.com)", re.I
    )
    with open(log_file, "r") as f:
        for line_num, line in enumerate(f, 1):
            match = cloud_regex.search(line)
            if match:
                findings.append({
                    "line": line_num,
                    "matched_domain": match.group(0),
                    "raw": line.strip()[:200],
                })
    return {
        "file": str(log_file),
        "total_matches": len(findings),
        "findings": findings[:500],
        "cloud_services_detected": list(set(f["matched_domain"] for f in findings)),
    }


def main():
    parser = argparse.ArgumentParser(description="Hunt for Living-off-the-Cloud (LOTC) techniques")
    sub = parser.add_subparsers(dest="command")

    hunt = sub.add_parser("hunt", help="Run LOTC hunts against Elasticsearch")
    hunt.add_argument("--es-host", required=True, help="Elasticsearch host URL")
    hunt.add_argument("--index", default="logs-*", help="Index pattern")
    hunt.add_argument("--api-key", help="Elasticsearch API key")
    hunt.add_argument("--hours", type=int, default=24, help="Lookback hours")

    dns = sub.add_parser("dns", help="Analyze DNS logs for cloud C2 domains")
    dns.add_argument("--log-file", required=True, help="Path to DNS query log file")

    args = parser.parse_args()
    if args.command == "hunt":
        result = hunt_lotc_elastic(args.es_host, args.index, args.api_key, args.hours)
    elif args.command == "dns":
        result = analyze_dns_logs(args.log_file)
    else:
        parser.print_help()
        return
    print(json.dumps(result, indent=2, default=str))


if __name__ == "__main__":
    main()
process.py3.6 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Living off the Cloud Detection - Analyzes logs for T1102 indicators."""

import json, csv, argparse, datetime, re
from collections import defaultdict
from pathlib import Path

DETECTION_PATTERNS = [
    r'pastebin',
    r'discord.*webhook',
    r'telegram.*api',
    r'notion\\.so',
    r'trello',
    r'workers\\.dev',
]

def parse_logs(path):
    p = Path(path)
    if p.suffix == ".json":
        with open(p, encoding="utf-8") as f:
            data = json.load(f)
            return data if isinstance(data, list) else data.get("events", [])
    elif p.suffix == ".csv":
        with open(p, encoding="utf-8-sig") as f:
            return [dict(r) for r in csv.DictReader(f)]
    return []

def analyze_event(event):
    cmd = event.get("CommandLine", event.get("command_line", event.get("ProcessCommandLine", "")))
    content = event.get("Task_Content", event.get("Parameters", event.get("RawEventData", "")))
    search_text = f"{cmd} {content}"
    risk = 0
    indicators = []
    for pattern in DETECTION_PATTERNS:
        if re.search(pattern, search_text, re.IGNORECASE):
            risk += 25
            indicators.append(f"Pattern match: {pattern}")
    if not indicators:
        return None
    risk = min(risk, 100)
    return {
        "technique": "T1102",
        "command_line": cmd[:500] if cmd else content[:500],
        "hostname": event.get("Computer", event.get("DeviceName", event.get("hostname", "unknown"))),
        "user": event.get("User", event.get("AccountName", event.get("UserId", "unknown"))),
        "timestamp": event.get("_time", event.get("timestamp", event.get("UtcTime", event.get("Timestamp", "")))),
        "risk_score": risk,
        "risk_level": "CRITICAL" if risk >= 75 else "HIGH" if risk >= 50 else "MEDIUM" if risk >= 25 else "LOW",
        "indicators": indicators,
    }

def run_hunt(input_path, output_dir):
    print(f"[*] Living off the Cloud Hunt - {datetime.datetime.now().isoformat()}")
    events = parse_logs(input_path)
    findings = [f for f in (analyze_event(e) for e in events) if f]
    Path(output_dir).mkdir(parents=True, exist_ok=True)
    slug = "hunting_for_living_o"
    with open(Path(output_dir) / f"{slug}_findings.json", "w", encoding="utf-8") as f:
        json.dump({"hunt_id": f"TH-{datetime.date.today()}", "total_events": len(events), "findings": findings}, f, indent=2)
    with open(Path(output_dir) / "hunt_report.md", "w", encoding="utf-8") as f:
        f.write(f"# Living off the Cloud Hunt Report\n\n")
        f.write(f"**Date**: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
        f.write(f"**Findings**: {len(findings)}\n\n")
        for finding in sorted(findings, key=lambda x: x["risk_score"], reverse=True)[:20]:
            f.write(f"### [{finding['risk_level']}] {finding['technique']}\n")
            f.write(f"- **Host**: {finding['hostname']}\n")
            f.write(f"- **Indicators**: {', '.join(finding['indicators'])}\n\n")
    print(f"[+] {len(findings)} findings written to {output_dir}")

def main():
    p = argparse.ArgumentParser(description="Living off the Cloud Detection")
    sp = p.add_subparsers(dest="cmd")
    h = sp.add_parser("hunt"); h.add_argument("--input", "-i", required=True); h.add_argument("--output", "-o", default="./hunting_for_liv_output")
    sp.add_parser("queries")
    args = p.parse_args()
    if args.cmd == "hunt": run_hunt(args.input, args.output)
    elif args.cmd == "queries":
        print("=== Detection Queries ===")
        print("See references/workflows.md for platform-specific queries")
    else: p.print_help()

if __name__ == "__main__": main()

Assets 1

template.mdtext/markdown · 2.6 KB
Keep exploring