Skip to content

Instantly share code, notes, and snippets.

@emilyploszaj
Last active June 7, 2023 19:59
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save emilyploszaj/a9693c4f3de5ec9fbc255c51ff3ca47e to your computer and use it in GitHub Desktop.
Save emilyploszaj/a9693c4f3de5ec9fbc255c51ff3ca47e to your computer and use it in GitHub Desktop.
Very Naive CF Malware Scanner
#! /bin/bash
echo "This check is not perfect, and can trigger both false positives AND false negatives, don't assume malware based on results"
echo ""
HIGHLIGHTED='\033[0;31m'
RESET='\033[0m'
INPUT=./*.jar
for file in $INPUT
do
unzip $file -d classes > /dev/null
if grep -e "ClassLoader" -r classes > /dev/null; then
echo -e "[!] WARNING ${HIGHLIGHTED}$file${RESET} contains a class loader and may be Fractureiser, please decompile and manually check!"
echo " Check the following classes:"
grep -e "ClassLoader" -r classes
fi
rm -rf classes
done
@SylvKT
Copy link

SylvKT commented Jun 7, 2023

https://pastebin.com/T6aQ7C2E for more accurate scanning (use both scripts + verify in decompiler like Recaf)

@Iridium-IO
Copy link

Here's an even more naive conversion of your shell script to Powershell so that it can be run on Windows PCs as well (very slow):

$INPUT = Get-ChildItem -Path .\*.jar

Write-Host "This check is not perfect, and can trigger both false positives AND false negatives, don't assume malware based on results"
Write-Host ""

foreach ($file in $INPUT) {
    Expand-Archive -Path $file.FullName -DestinationPath classes | Out-Null
    if (Select-String -Pattern "ClassLoader" -Path "classes\*" -Quiet) {
        Write-Host "[!] WARNING $($file.Name) contains a class loader and may be Fractureiser, please decompile and manually check!"
        Write-Host "    Check the following classes:"
        Select-String -Pattern "ClassLoader" -Path "classes\*"
    }
    Remove-Item -Path "classes" -Recurse -Force
}

@Iridium-IO
Copy link

@SylvKT I tried migrating your script essentially verbatim to Powershell to check on Windows; I've never used escaped \u characters before but I'm pretty sure this still works identically, but Expand-Archive is quite slow:

#Migrated essentially identically from SYLV256's bash code here: https://pastebin.com/T6aQ7C2E

Clear-Host

$version = "v1.0.1"

# Sanity checks

if (-not $args)
{
    Write-Host "Usage: ./bettercheck.ps1 <filename>"
    exit 1
}

$filename = $args[0]

if (-not (Test-Path -Path $filename))
{
    Write-Host "File $filename does not exist!"
    exit 1
}


Write-Host "Check for Fractureiser in CurseForge Mods: $version"
Write-Host "================================================="
Write-Host "This will take a while!"
Write-Host ""

# Match this (the IP)
$sequence = "\u38\u54\u59\u04\u10\u35\u54\u59\u05\u10\u2E\u54\u59\u06\u10\u32\u54\u59\u07\u10\u31\u54\u59\u08\u10\u37\u54\u59\u10\u06\u10\u2E\u54\u59\u10\u07\u10\u31\u54\u59\u10\u08\u10\u34\u54\u59\u10\u09\u10\u34\u54\u59\u10\u0A\u10\u2E\u54\u59\u10\u0B\u10\u31\u54\u59\u10\u0C\u10\u33\u54\u59\u10\u0D\u10\u30\u54\uB7"
# This has null bytes in it, so we slice it off and ignore it
# Rest_of_sequence_ignore_this="\u00\u5D\u11\u1F\u90\uBB\u00\u5A\u59\u06\uBC\u08\u59\u03\u10\u2F\u54\u59\u04\u10\u64\u54\u59\u05\u10\u6C"
# Base64 IP
$sequence2 = "\u68\u54\u59\u04\u10\u74\u54\u59\u05\u10\u74\u54\u59\u06\u10\u70\u54\u59\u07\u10\u3a\u54\u59\u08\u10\u2f\u54\u59\u10\u06\u10\u2f\u54\u59\u10\u07\u10\u66\u54\u59\u10\u08\u10\u69\u54\u59\u10\u09\u10\u6c\u54\u59\u10\u0a\u10\u65\u54\u59\u10\u0b\u10\u73\u54\u59\u10\u0c\u10\u2e\u54\u59\u10\u0a\u10\u73\u54\u59\u10\u0e\u10\u6b\u54\u59\u10\u0f\u10\u79\u54\u59\u10\u10\u10\u72\u54\u59\u10\u11\u10\u61\u54\u59\u10\u12\u10\u67\u54\u59\u10\u13\u10\u65\u54\u59\u10\u14\u10\u2e\u54\u59\u10\u15\u10\u64"
# Something? idk what this is but it's present in the Bukkit ones
$sequence3 = "\u2d\u54\u59\u04\u10\u6a\u54\u59\u05\u10\u61\u54\u59\u06\u10\u72"

function chk_file($file) {
    $fileName = Split-Path -Path $file -Leaf
    Write-Host "Checking $fileName"
    $unzipped = "$file.unzipped"
    Remove-Item -Path ".\$unzipped" -Recurse -Force -ErrorAction SilentlyContinue
    New-Item -ItemType Directory -Path $unzipped | Out-Null
    Expand-Archive -Path $file -DestinationPath $unzipped > $null

    # regex search everything
    $isInfected = $false

    $classFiles = Get-ChildItem -Path $unzipped -Filter "*.class" -Recurse

    foreach ($classFile in $classFiles) {
        $content = Get-Content -Path $classFile.FullName -Raw
        if ($content -match [regex]::Escape($sequence) -or
            $content -match [regex]::Escape($sequence2) -or
            $content -match [regex]::Escape($sequence3))
        {
            $isInfected = $true
            break
        }
    }

    if ($isInfected) {
        Write-Host "$fileName is infected!"
        Remove-Item -Path $unzipped -Recurse -Force
        return 1
    }

    Remove-Item -Path $unzipped -Recurse -Force
}

foreach ($entry in Get-ChildItem -Path $args[0]) {
    if ($entry.Extension -eq ".jar") {
        chk_file $entry.FullName
    }
    if ($entry.FullName -eq $args[0]) {
        chk_file $args[0]
    }
}

@SylvKT
Copy link

SylvKT commented Jun 7, 2023 via email

@LoneDev6
Copy link

LoneDev6 commented Jun 7, 2023

This has a lot of false positives because for example snakeyaml have a class called CustomClassLoaderConstructor which is not malicious code.
This lib is used by a lot of mods and plugins so I think this ClassLoader check is just pointless.

The only reliable check is the fingerprinting: https://pastebin.com/T6aQ7C2E

@KaiserCalm
Copy link

@SylvKT I edited your version to loop over all of the files in the directory:

#!/bin/bash
 
version=v1.0.1
 
# sanity checks
 
#if [ -z "$1" ]
#then
#    echo "Usage: ./chk_cf_oopsie.sh <filename>"
#    exit 1
#fi
# 
#if [ ! -f "$1" ] && [[ ! -d "$1" ]]
#then
#    echo "File $1 does not exist!"
#    exit 1
#fi
 
if ! command -v unzip &> /dev/null
then
    echo "Please install 'unzip' via your favorite package manager!"
fi
 
 
echo "Check CurseForge Oopsies $version"
 
# match this (the IP)
sequence="\u38\u54\u59\u04\u10\u35\u54\u59\u05\u10\u2E\u54\u59\u06\u10\u32\u54\u59\u07\u10\u31\u54\u59\u08\u10\u37\u54\u59\u10\u06\u10\u2E\u54\u59\u10\u07\u10\u31\u54\u59\u10\u08\u10\u34\u54\u59\u10\u09\u10\u34\u54\u59\u10\u0A\u10\u2E\u54\u59\u10\u0B\u10\u31\u54\u59\u10\u0C\u10\u33\u54\u59\u10\u0D\u10\u30\u54\uB7"
# this has null bytes in it so we slice it off and ignore it
#rest_of_sequence_ignore_this="\u00\u5D\u11\u1F\u90\uBB\u00\u5A\u59\u06\uBC\u08\u59\u03\u10\u2F\u54\u59\u04\u10\u64\u54\u59\u05\u10\u6C"
# base64 IP
sequence2="\u68\u54\u59\u04\u10\u74\u54\u59\u05\u10\u74\u54\u59\u06\u10\u70\u54\u59\u07\u10\u3a\u54\u59\u08\u10\u2f\u54\u59\u10\u06\u10\u2f\u54\u59\u10\u07\u10\u66\u54\u59\u10\u08\u10\u69\u54\u59\u10\u09\u10\u6c\u54\u59\u10\u0a\u10\u65\u54\u59\u10\u0b\u10\u73\u54\u59\u10\u0c\u10\u2e\u54\u59\u10\u0a\u10\u73\u54\u59\u10\u0e\u10\u6b\u54\u59\u10\u0f\u10\u79\u54\u59\u10\u10\u10\u72\u54\u59\u10\u11\u10\u61\u54\u59\u10\u12\u10\u67\u54\u59\u10\u13\u10\u65\u54\u59\u10\u14\u10\u2e\u54\u59\u10\u15\u10\u64"
# something? idk what this is but it's present in the Bukkit ones
sequence3="\u2d\u54\u59\u04\u10\u6a\u54\u59\u05\u10\u61\u54\u59\u06\u10\u72"
 
INPUT=./*.jar 

for file in $INPUT
do
	chk_file() {
		unzipped="$file.unzipped"
		rm -rf "./$unzipped"
		mkdir $unzipped
		unzip $file -d $unzipped > /dev/null
	 
		# grep entire thing
		if grep -q -r --include "*.class" "$(printf %b "$sequence")" $unzipped || grep -q -r --include "*.class" "$(printf %b "$sequence2")" $unzipped || grep -q -r --include "*.class" -- "$(printf %b "$sequence3")" $unzipped; then
		    echo "INFECTED: $file"
		    rm -rf $unzipped
		    return 1
		fi
		rm -rf $unzipped
	}
	 
	for entry in "$file"/*
	do
		if [[ $entry = *.jar ]]; then
		    chk_file $entry
		fi
		if [[ $entry = "$file/*" ]]; then
		    chk_file $file
		fi
	done
done

echo "Done."

@Trial97
Copy link

Trial97 commented Jun 7, 2023

From this script: https://pastebin.com/T6aQ7C2E
Added some " to accept jars with spaces

#!/usr/bin/bash
# set -ex
version='v1.0.1'

# sanity checks
if [ -z "$1" ]; then
    echo "Usage: ./chk_cf_oopsie.sh <filename>"
    exit 1
fi

if [ ! -f "$1" ] && [[ ! -d "$1" ]]; then
    echo "File $1 does not exist!"
    exit 1
fi

if ! command -v unzip &>/dev/null; then
    echo "Please install 'unzip' via your favorite package manager!"
fi

echo "Check CurseForge Oopsies $version"

# match this (the IP)
sequence="\u38\u54\u59\u04\u10\u35\u54\u59\u05\u10\u2E\u54\u59\u06\u10\u32\u54\u59\u07\u10\u31\u54\u59\u08\u10\u37\u54\u59\u10\u06\u10\u2E\u54\u59\u10\u07\u10\u31\u54\u59\u10\u08\u10\u34\u54\u59\u10\u09\u10\u34\u54\u59\u10\u0A\u10\u2E\u54\u59\u10\u0B\u10\u31\u54\u59\u10\u0C\u10\u33\u54\u59\u10\u0D\u10\u30\u54\uB7"
# this has null bytes in it so we slice it off and ignore it
#rest_of_sequence_ignore_this="\u00\u5D\u11\u1F\u90\uBB\u00\u5A\u59\u06\uBC\u08\u59\u03\u10\u2F\u54\u59\u04\u10\u64\u54\u59\u05\u10\u6C"
# base64 IP
sequence2="\u68\u54\u59\u04\u10\u74\u54\u59\u05\u10\u74\u54\u59\u06\u10\u70\u54\u59\u07\u10\u3a\u54\u59\u08\u10\u2f\u54\u59\u10\u06\u10\u2f\u54\u59\u10\u07\u10\u66\u54\u59\u10\u08\u10\u69\u54\u59\u10\u09\u10\u6c\u54\u59\u10\u0a\u10\u65\u54\u59\u10\u0b\u10\u73\u54\u59\u10\u0c\u10\u2e\u54\u59\u10\u0a\u10\u73\u54\u59\u10\u0e\u10\u6b\u54\u59\u10\u0f\u10\u79\u54\u59\u10\u10\u10\u72\u54\u59\u10\u11\u10\u61\u54\u59\u10\u12\u10\u67\u54\u59\u10\u13\u10\u65\u54\u59\u10\u14\u10\u2e\u54\u59\u10\u15\u10\u64"
# something? idk what this is but it's present in the Bukkit ones
sequence3="\u2d\u54\u59\u04\u10\u6a\u54\u59\u05\u10\u61\u54\u59\u06\u10\u72"

chk_file() {
    unzipped="$1.unzipped"
    rm -rf "./$unzipped"
    mkdir "$unzipped"
    unzip "$1" -d "$unzipped" >/dev/null

    # grep entire thing
    if grep -q -r --include "*.class" "$(printf %b "$sequence")" "$unzipped" || grep -q -r --include "*.class" "$(printf %b "$sequence2")" "$unzipped" || grep -q -r --include "*.class" -- "$(printf %b "$sequence3")" "$unzipped"; then
        echo "$1 is infected!"
        rm -rf "$unzipped"
        return 1
    fi
    rm -rf "$unzipped"
}

for entry in "$1"/*; do
    if [[ $entry = *.jar ]]; then
        chk_file "$entry"
    fi
    if [[ $entry = "$1/*" ]]; then
        chk_file "$1"
    fi
done

@AllenSeitz
Copy link

AllenSeitz commented Jun 7, 2023

@SylvKT I tried migrating your script essentially verbatim to Powershell to check on Windows; I've never used escaped \u characters before but I'm pretty sure this still works identically, but Expand-Archive is quite slow:

I tried running this and Expand-Archive does not support .jar files on my system, only .zip files.

@TigerWalts
Copy link

I implemented the fingerprint method in Python 3.

  • Accepts multiple files/folders as arguments
  • Sub-folders are checked
  • Checks in memory, no unzipping to the file system
  • Multiple processes

Notes:

  • If you pass a path containing spaces (even in quotes) it gets truncated and thus ignored as non-existent - Does not affect folder traversal
  • Uses a process pool the size of half your CPU count. Change CPUS as you see fit. File IO is the biggest impact in speed
  • Good news - I have scanned over 25000 files and not had a hit
  • Bad news - I have scanned over 25000 files and not had a hit, does it work?
    • I don't have a known infected file to test against
import itertools
from pathlib import Path
import sys
from typing import Dict, List, Set, Tuple
from zipfile import ZipFile
from multiprocessing import cpu_count
from multiprocessing.pool import Pool

PatternId = str
ZipFilePath = str
ClassFilePath = str
CheckResult = Tuple[ZipFilePath, ClassFilePath, PatternId]
CheckResults = List[CheckResult]
BytePatterns = Dict[PatternId, bytes]

PATTERNS: BytePatterns = {
    # short enough that it is likely to be found somewhere
    #"TEST PATTERN": b"\x38\x54",
    "fractureiser CnC IP": b"\x38\x54\x59\x04\x10\x35\x54\x59\x05\x10\x2E\x54\x59\x06\x10\x32\x54\x59\x07\x10\x31\x54\x59\x08\x10\x37\x54\x59\x10\x06\x10\x2E\x54\x59\x10\x07\x10\x31\x54\x59\x10\x08\x10\x34\x54\x59\x10\x09\x10\x34\x54\x59\x10\x0A\x10\x2E\x54\x59\x10\x0B\x10\x31\x54\x59\x10\x0C\x10\x33\x54\x59\x10\x0D\x10\x30\x54\xB7",
    # this has null bytes in it so we slice it off and ignore it
    #rest_of_sequence_ignore_this="\x00\x5D\x11\x1F\x90\xBB\x00\x5A\x59\x06\xBC\x08\x59\x03\x10\x2F\x54\x59\x04\x10\x64\x54\x59\x05\x10\x6C"
    "fractureiser CnC IP Base64": b"\x68\x54\x59\x04\x10\x74\x54\x59\x05\x10\x74\x54\x59\x06\x10\x70\x54\x59\x07\x10\x3a\x54\x59\x08\x10\x2f\x54\x59\x10\x06\x10\x2f\x54\x59\x10\x07\x10\x66\x54\x59\x10\x08\x10\x69\x54\x59\x10\x09\x10\x6c\x54\x59\x10\x0a\x10\x65\x54\x59\x10\x0b\x10\x73\x54\x59\x10\x0c\x10\x2e\x54\x59\x10\x0a\x10\x73\x54\x59\x10\x0e\x10\x6b\x54\x59\x10\x0f\x10\x79\x54\x59\x10\x10\x10\x72\x54\x59\x10\x11\x10\x61\x54\x59\x10\x12\x10\x67\x54\x59\x10\x13\x10\x65\x54\x59\x10\x14\x10\x2e\x54\x59\x10\x15\x10\x64",
    # something? idk what this is but it's present in the Bukkit ones
    "fractureiser sequence in Bukkit plugins": b"\x2d\x54\x59\x04\x10\x6a\x54\x59\x05\x10\x61\x54\x59\x06\x10\x72"
}

CPUS = cpu_count() // 2 or 1

def check_zip_file(zipfilepath: ZipFilePath) -> CheckResults:
    results: CheckResults = []
    with ZipFile(zipfilepath) as zf:
        for member_info in zf.infolist():
            classfilepath = member_info.filename
            if Path(classfilepath).suffix == '.class':
                results += scan_member_file(zf, classfilepath)
    return results

def check_zip_file_wrapper(x: Path):
    return check_zip_file(x.absolute())

def scan_member_file(zipfile: ZipFile, classfilepath: ClassFilePath) -> CheckResults:
    results: CheckResults = []
    with zipfile.open(classfilepath) as cf:
        data = cf.read()
        for patternId, pattern in PATTERNS.items():
            if pattern in data:
                results.append((zipfile.filename, classfilepath, patternId))
    return results

def main():
    if len(sys.argv) < 2:
        print("Pass a list of files or folders")
        sys.exit(1)
    paths: Set[Path] = set()
    for filepath in sys.argv[1:]:
        path = Path(filepath)
        if not path.exists():
            continue
        if path.is_dir():
            paths.update(set(path.glob('**/*.jar')))
        elif path.suffix == '.jar':
            paths.add(path)
    print(f"Found {len(paths)} jar files, checking...")

    results = list(itertools.chain.from_iterable(Pool(CPUS).map(check_zip_file_wrapper, paths)))

    [
        print(f"{x[0]} : Found '{x[2]}' in '{x[1]}'")
        for x in results
    ]

if __name__ == "__main__":
    main()

@Kraugel13
Copy link

how do I run all this? VS code? windows power shell? install as a notepad file and run it?

@SylvKT
Copy link

SylvKT commented Jun 7, 2023 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment