Last active
November 26, 2025 13:06
-
-
Save stepanogil/f6cb9c6c73ef9e1becd73488e20ed002 to your computer and use it in GitHub Desktop.
Shai Hulud Detection Script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| ################################################################# | |
| # Shai-Hulud 2.0 Detection Script | |
| # Checks for infection indicators and compromised packages | |
| # Downloads latest IoC list from Wiz Security Research | |
| ################################################################# | |
| RED='\033[0;31m' | |
| YELLOW='\033[1;33m' | |
| GREEN='\033[0;32m' | |
| BLUE='\033[0;34m' | |
| NC='\033[0m' # No Color | |
| INFECTED=0 | |
| IOC_URL="https://raw.githubusercontent.com/wiz-sec-public/wiz-research-iocs/main/reports/shai-hulud-2-packages.csv" | |
| IOC_FILE="/tmp/shai-hulud-2-packages.csv" | |
| echo -e "${BLUE}================================================================${NC}" | |
| echo -e "${BLUE} Shai-Hulud 2.0 Detection Script${NC}" | |
| echo -e "${BLUE}================================================================${NC}" | |
| echo "" | |
| # Function to print findings | |
| print_finding() { | |
| local severity=$1 | |
| local message=$2 | |
| if [ "$severity" == "CRITICAL" ]; then | |
| echo -e "${RED}[CRITICAL] $message${NC}" | |
| INFECTED=1 | |
| elif [ "$severity" == "WARNING" ]; then | |
| echo -e "${YELLOW}[WARNING] $message${NC}" | |
| elif [ "$severity" == "INFO" ]; then | |
| echo -e "${GREEN}[INFO] $message${NC}" | |
| fi | |
| } | |
| # Download latest IoC list | |
| echo -e "${BLUE}[*] Downloading latest compromised package list from Wiz Security...${NC}" | |
| if command -v curl &> /dev/null; then | |
| curl -s "$IOC_URL" -o "$IOC_FILE" 2>/dev/null | |
| if [ $? -eq 0 ] && [ -s "$IOC_FILE" ]; then | |
| print_finding "INFO" "Successfully downloaded IoC list ($(wc -l < "$IOC_FILE") packages)" | |
| else | |
| print_finding "WARNING" "Failed to download IoC list, will use built-in package list" | |
| IOC_FILE="" | |
| fi | |
| elif command -v wget &> /dev/null; then | |
| wget -q "$IOC_URL" -O "$IOC_FILE" 2>/dev/null | |
| if [ $? -eq 0 ] && [ -s "$IOC_FILE" ]; then | |
| print_finding "INFO" "Successfully downloaded IoC list ($(wc -l < "$IOC_FILE") packages)" | |
| else | |
| print_finding "WARNING" "Failed to download IoC list, will use built-in package list" | |
| IOC_FILE="" | |
| fi | |
| else | |
| print_finding "WARNING" "curl/wget not found, will use built-in package list" | |
| IOC_FILE="" | |
| fi | |
| echo "" | |
| ################################################################# | |
| # CHECK 1: Malicious payload files | |
| ################################################################# | |
| echo -e "${BLUE}[1] Checking for malicious payload files...${NC}" | |
| PAYLOAD_FILES=$(find . -type f \( \ | |
| -name "setup_bun.js" -o \ | |
| -name "bun_environment.js" -o \ | |
| -name "cloud.json" -o \ | |
| -name "contents.json" -o \ | |
| -name "environment.json" -o \ | |
| -name "truffleSecrets.json" -o \ | |
| -name "actionsSecrets.json" \ | |
| \) 2>/dev/null) | |
| if [ -n "$PAYLOAD_FILES" ]; then | |
| print_finding "CRITICAL" "Found malicious payload files:" | |
| echo "$PAYLOAD_FILES" | |
| else | |
| print_finding "INFO" "No malicious payload files found" | |
| fi | |
| echo "" | |
| ################################################################# | |
| # CHECK 2: Suspicious large JavaScript files (payload is 10MB+) | |
| ################################################################# | |
| echo -e "${BLUE}[2] Checking for suspiciously large JavaScript files (>5MB)...${NC}" | |
| LARGE_JS=$(find . -name "*.js" -size +5M -exec ls -lh {} \; 2>/dev/null) | |
| if [ -n "$LARGE_JS" ]; then | |
| print_finding "WARNING" "Found large JavaScript files (bun_environment.js is ~10MB):" | |
| echo "$LARGE_JS" | |
| else | |
| print_finding "INFO" "No suspiciously large JS files found" | |
| fi | |
| echo "" | |
| ################################################################# | |
| # CHECK 3: Malicious GitHub workflows | |
| ################################################################# | |
| echo -e "${BLUE}[3] Checking for malicious GitHub workflows...${NC}" | |
| if [ -d ".github/workflows" ]; then | |
| # Check for discussion.yaml | |
| if [ -f ".github/workflows/discussion.yaml" ]; then | |
| print_finding "CRITICAL" "Found backdoor workflow: .github/workflows/discussion.yaml" | |
| fi | |
| # Check for formatter_* workflows | |
| FORMATTER_WORKFLOWS=$(find .github/workflows -name "formatter_*.yml" -o -name "formatter_*.yaml" 2>/dev/null) | |
| if [ -n "$FORMATTER_WORKFLOWS" ]; then | |
| print_finding "CRITICAL" "Found exfiltration workflows:" | |
| echo "$FORMATTER_WORKFLOWS" | |
| fi | |
| # Check for self-hosted runner usage | |
| SELF_HOSTED=$(grep -r "runs-on: self-hosted" .github/workflows/ 2>/dev/null) | |
| if [ -n "$SELF_HOSTED" ]; then | |
| print_finding "WARNING" "Found self-hosted runner references:" | |
| echo "$SELF_HOSTED" | |
| fi | |
| # Check for secret exfiltration patterns | |
| SECRET_EXFIL=$(grep -r "toJSON(secrets)" .github/workflows/ 2>/dev/null) | |
| if [ -n "$SECRET_EXFIL" ]; then | |
| print_finding "CRITICAL" "Found secret exfiltration pattern:" | |
| echo "$SECRET_EXFIL" | |
| fi | |
| # Check for SHA1HULUD or RUNNER_TRACKING_ID | |
| HULUD_REF=$(grep -r "SHA1HULUD\|RUNNER_TRACKING_ID" .github/ 2>/dev/null) | |
| if [ -n "$HULUD_REF" ]; then | |
| print_finding "CRITICAL" "Found Shai-Hulud references in workflows:" | |
| echo "$HULUD_REF" | |
| fi | |
| if [ -z "$FORMATTER_WORKFLOWS" ] && [ -z "$SELF_HOSTED" ] && [ -z "$SECRET_EXFIL" ] && [ -z "$HULUD_REF" ] && [ ! -f ".github/workflows/discussion.yaml" ]; then | |
| print_finding "INFO" "No malicious workflows detected" | |
| fi | |
| else | |
| print_finding "INFO" "No .github/workflows directory found" | |
| fi | |
| echo "" | |
| ################################################################# | |
| # CHECK 4: Preinstall scripts in package.json | |
| ################################################################# | |
| echo -e "${BLUE}[4] Checking package.json files for preinstall scripts...${NC}" | |
| PREINSTALL_SCRIPTS=$(find . -name "package.json" -exec sh -c ' | |
| if grep -q "preinstall" "$1" 2>/dev/null; then | |
| echo "$1" | |
| fi | |
| ' _ {} \; 2>/dev/null) | |
| if [ -n "$PREINSTALL_SCRIPTS" ]; then | |
| print_finding "WARNING" "Found package.json files with preinstall scripts:" | |
| echo "$PREINSTALL_SCRIPTS" | |
| echo "" | |
| echo "Reviewing preinstall content:" | |
| for pkg in $PREINSTALL_SCRIPTS; do | |
| echo "--- $pkg ---" | |
| grep -A 3 "preinstall" "$pkg" 2>/dev/null | |
| done | |
| else | |
| print_finding "INFO" "No preinstall scripts found in package.json files" | |
| fi | |
| echo "" | |
| ################################################################# | |
| # CHECK 5: Compromised package versions | |
| ################################################################# | |
| echo -e "${BLUE}[5] Checking for compromised package versions...${NC}" | |
| check_package_file() { | |
| local lockfile=$1 | |
| local found_packages="" | |
| if [ ! -f "$lockfile" ]; then | |
| return | |
| fi | |
| if [ -n "$IOC_FILE" ] && [ -s "$IOC_FILE" ]; then | |
| # Use downloaded IoC list | |
| while IFS=, read -r package version; do | |
| # Skip header and empty lines | |
| if [ "$package" == "package" ] || [ -z "$package" ]; then | |
| continue | |
| fi | |
| # Clean up package and version (remove quotes and whitespace) | |
| package=$(echo "$package" | tr -d '"' | xargs) | |
| version=$(echo "$version" | tr -d '"' | xargs) | |
| if grep -q "$package.*$version" "$lockfile" 2>/dev/null; then | |
| found_packages="${found_packages}\n - $package@$version" | |
| fi | |
| done < "$IOC_FILE" | |
| else | |
| print_finding "WARNING" "No IoC list available, skipping package version check for $lockfile" | |
| fi | |
| if [ -n "$found_packages" ]; then | |
| print_finding "CRITICAL" "Found compromised packages in $lockfile:" | |
| echo -e "$found_packages" | |
| fi | |
| } | |
| # Check all lock files | |
| for lockfile in package-lock.json yarn.lock pnpm-lock.yaml; do | |
| if [ -f "$lockfile" ]; then | |
| check_package_file "$lockfile" | |
| fi | |
| done | |
| # Also check node_modules if present | |
| if [ -d "node_modules" ]; then | |
| echo "" | |
| echo "Scanning node_modules directory..." | |
| SUSPICIOUS_MODULES=$(find node_modules -type f \( -name "setup_bun.js" -o -name "bun_environment.js" \) 2>/dev/null) | |
| if [ -n "$SUSPICIOUS_MODULES" ]; then | |
| print_finding "CRITICAL" "Found malicious files in node_modules:" | |
| echo "$SUSPICIOUS_MODULES" | |
| fi | |
| fi | |
| if [ ! -f "package-lock.json" ] && [ ! -f "yarn.lock" ] && [ ! -f "pnpm-lock.yaml" ]; then | |
| print_finding "INFO" "No lock files found in current directory" | |
| fi | |
| echo "" | |
| ################################################################# | |
| # CHECK 6: Git history for unauthorized commits | |
| ################################################################# | |
| echo -e "${BLUE}[6] Checking git history for suspicious commits...${NC}" | |
| if [ -d ".git" ]; then | |
| HULUD_COMMITS=$(git log --all --oneline --grep="discussion\|formatter\|hulud\|SHA1" 2>/dev/null) | |
| if [ -n "$HULUD_COMMITS" ]; then | |
| print_finding "WARNING" "Found potentially suspicious commits:" | |
| echo "$HULUD_COMMITS" | |
| else | |
| print_finding "INFO" "No suspicious commits found in git history" | |
| fi | |
| else | |
| print_finding "INFO" "Not a git repository" | |
| fi | |
| echo "" | |
| ################################################################# | |
| # CHECK 7: DNS hijacking indicators | |
| ################################################################# | |
| echo -e "${BLUE}[7] Checking for DNS hijacking indicators...${NC}" | |
| if [ -f "/etc/hosts" ]; then | |
| SUSPICIOUS_HOSTS=$(grep -E "webhook\.site|suspicious" /etc/hosts 2>/dev/null) | |
| if [ -n "$SUSPICIOUS_HOSTS" ]; then | |
| print_finding "WARNING" "Found suspicious entries in /etc/hosts:" | |
| echo "$SUSPICIOUS_HOSTS" | |
| else | |
| print_finding "INFO" "No suspicious DNS entries found" | |
| fi | |
| else | |
| print_finding "INFO" "Cannot check /etc/hosts (not accessible)" | |
| fi | |
| echo "" | |
| ################################################################# | |
| # CHECK 8: Home directory destruction indicators | |
| ################################################################# | |
| echo -e "${BLUE}[8] Checking for signs of destructive payload...${NC}" | |
| # Check for recently deleted files (if available) | |
| if command -v journalctl &> /dev/null; then | |
| DELETION_LOGS=$(journalctl -S today | grep -i "delete\|remove\|rm -rf" 2>/dev/null | grep -i "home" | head -10) | |
| if [ -n "$DELETION_LOGS" ]; then | |
| print_finding "WARNING" "Found recent deletion activity in system logs" | |
| fi | |
| fi | |
| print_finding "INFO" "Manual review of recent file deletions recommended" | |
| echo "" | |
| ################################################################# | |
| # SUMMARY | |
| ################################################################# | |
| echo -e "${BLUE}================================================================${NC}" | |
| echo -e "${BLUE} SUMMARY${NC}" | |
| echo -e "${BLUE}================================================================${NC}" | |
| if [ $INFECTED -eq 1 ]; then | |
| echo -e "${RED}⚠️ CRITICAL: INFECTION DETECTED${NC}" | |
| echo "" | |
| echo "IMMEDIATE ACTIONS REQUIRED:" | |
| echo "1. Disconnect from network immediately" | |
| echo "2. DO NOT run 'npm install' or any package manager commands" | |
| echo "3. Rotate ALL credentials (GitHub PATs, npm tokens, cloud keys, SSH keys)" | |
| echo "4. Check GitHub for unauthorized repositories (search for 'Sha1-Hulud: The Second Coming')" | |
| echo "5. Review GitHub Actions runners for 'SHA1HULUD' runner" | |
| echo "6. Clear npm cache: npm cache clean --force" | |
| echo "7. Delete node_modules: rm -rf node_modules" | |
| echo "8. Restore from clean backup (pre-November 21, 2025)" | |
| echo "9. Scan all systems that ran npm install recently" | |
| echo "10. Report to your security team immediately" | |
| else | |
| echo -e "${GREEN}✓ No definitive infection indicators found${NC}" | |
| echo "" | |
| echo "RECOMMENDED PRECAUTIONS:" | |
| echo "1. Review package.json for any unexpected dependencies" | |
| echo "2. Check GitHub account for any new repositories" | |
| echo "3. Verify no unexpected GitHub Actions runners registered" | |
| echo "4. Monitor for unusual network activity" | |
| echo "5. Keep dependencies updated from trusted sources" | |
| fi | |
| echo "" | |
| echo -e "${BLUE}================================================================${NC}" | |
| echo "For more information: https://www.wiz.io/blog/shai-hulud-2-0-ongoing-supply-chain-attack" | |
| echo -e "${BLUE}================================================================${NC}" | |
| # Cleanup | |
| if [ -f "$IOC_FILE" ]; then | |
| rm -f "$IOC_FILE" | |
| fi | |
| exit $INFECTED |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment