#!/usr/bin/env python3 """Agent for hunting web shell activity on web servers via process tree and log analysis.""" import json import argparse import re from datetime import datetime WEB_SERVER_PROCESSES = [ "w3wp.exe", "httpd", "apache2", "nginx", "tomcat", "java", "php-cgi", "php-fpm", "node", "iisexpress", ] SHELL_SPAWNS = [ "cmd.exe", "powershell.exe", "pwsh.exe", "bash", "sh", "wscript.exe", "cscript.exe", "certutil.exe", "whoami.exe", "net.exe", "net1.exe", "ipconfig.exe", "systeminfo.exe", "tasklist.exe", "nslookup.exe", ] WEBSHELL_HTTP_PATTERNS = [ r"POST\s+.*\.(asp|aspx|php|jsp|jspx)\s+", r"cmd=", r"exec=", r"command=", r"shell=", r"c99shell", r"r57shell", r"b374k", r"weevely", r"china\s*chopper", r"antsword", ] def load_process_logs(log_path): """Load process creation logs (JSON lines).""" entries = [] with open(log_path) as f: for line in f: try: entries.append(json.loads(line)) except json.JSONDecodeError: continue return entries def detect_webserver_child_shells(process_logs): """Detect shell processes spawned by web server processes.""" findings = [] for entry in process_logs: parent = entry.get("ParentImage", entry.get("parent_process", "")).lower() child = entry.get("Image", entry.get("process_name", "")).lower() is_web_parent = any(ws in parent for ws in WEB_SERVER_PROCESSES) is_shell_child = any(sh in child for sh in SHELL_SPAWNS) if is_web_parent and is_shell_child: cmd = entry.get("CommandLine", entry.get("command_line", "")) findings.append({ "timestamp": entry.get("UtcTime", entry.get("timestamp", "")), "hostname": entry.get("Computer", entry.get("hostname", "")), "parent_process": parent, "child_process": child, "command_line": cmd[:500], "user": entry.get("User", ""), "severity": "CRITICAL", "technique": "T1505.003", }) return findings def analyze_web_access_logs(access_log_path): """Analyze web access logs for webshell indicators.""" findings = [] with open(access_log_path) as f: for i, line in enumerate(f, 1): for pattern in WEBSHELL_HTTP_PATTERNS: if re.search(pattern, line, re.IGNORECASE): ip_match = re.match(r"^(\S+)", line) findings.append({ "line_number": i, "source_ip": ip_match.group(1) if ip_match else "", "log_entry": line.strip()[:500], "pattern_matched": pattern, "severity": "HIGH", }) break return findings def detect_file_creation_in_webroot(file_events, webroot_paths=None): """Detect new script files created in web server directories.""" if webroot_paths is None: webroot_paths = [ "/var/www", "/opt/lampp/htdocs", "inetpub/wwwroot", "/usr/share/nginx/html", "/srv/www", ] script_extensions = [".php", ".asp", ".aspx", ".jsp", ".jspx", ".cgi", ".cfm"] findings = [] for event in file_events: filepath = event.get("TargetFilename", event.get("file_path", "")).lower() in_webroot = any(wr in filepath for wr in webroot_paths) is_script = any(filepath.endswith(ext) for ext in script_extensions) if in_webroot and is_script: findings.append({ "timestamp": event.get("UtcTime", event.get("timestamp", "")), "file_path": filepath, "process": event.get("Image", event.get("process_name", "")), "hostname": event.get("Computer", event.get("hostname", "")), "severity": "CRITICAL", "reason": "script_created_in_webroot", }) return findings def detect_post_exploitation(process_logs): """Detect reconnaissance commands typically run through webshells.""" recon_patterns = [ (r"whoami", "user_discovery"), (r"ipconfig|ifconfig", "network_config"), (r"net\s+(user|group|localgroup)", "account_enum"), (r"systeminfo", "system_info"), (r"tasklist|ps\s+aux", "process_enum"), (r"netstat\s+-an", "connection_enum"), (r"dir\s+/s|find\s+/|ls\s+-la", "file_enum"), ] findings = [] for entry in process_logs: parent = entry.get("ParentImage", entry.get("parent_process", "")).lower() if not any(ws in parent for ws in WEB_SERVER_PROCESSES): continue cmd = entry.get("CommandLine", entry.get("command_line", "")) for pattern, category in recon_patterns: if re.search(pattern, cmd, re.IGNORECASE): findings.append({ "timestamp": entry.get("UtcTime", entry.get("timestamp", "")), "command": cmd[:300], "category": category, "parent": parent, "severity": "HIGH", }) break return findings def main(): parser = argparse.ArgumentParser(description="Webshell Activity Hunter") parser.add_argument("--process-log", help="JSON lines process creation log") parser.add_argument("--access-log", help="Web server access log") parser.add_argument("--file-events", help="JSON lines file creation events") parser.add_argument("--output", default="webshell_hunt_report.json") args = parser.parse_args() report = {"generated_at": datetime.utcnow().isoformat(), "findings": {}} if args.process_log: logs = load_process_logs(args.process_log) shells = detect_webserver_child_shells(logs) report["findings"]["shell_spawns"] = shells print(f"[+] Web server shell spawns: {len(shells)}") recon = detect_post_exploitation(logs) report["findings"]["post_exploitation"] = recon print(f"[+] Post-exploitation commands: {len(recon)}") if args.access_log: hits = analyze_web_access_logs(args.access_log) report["findings"]["access_log_hits"] = hits print(f"[+] Access log webshell indicators: {len(hits)}") if args.file_events: events = load_process_logs(args.file_events) files = detect_file_creation_in_webroot(events) report["findings"]["webroot_files"] = files print(f"[+] Scripts created in webroot: {len(files)}") with open(args.output, "w") as f: json.dump(report, f, indent=2, default=str) print(f"[+] Report saved to {args.output}") if __name__ == "__main__": main()