-
-
Save emilyploszaj/a9693c4f3de5ec9fbc255c51ff3ca47e to your computer and use it in GitHub Desktop.
#! /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 |
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
}
@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]
}
}
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
@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."
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
@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.
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()
how do I run all this? VS code? windows power shell? install as a notepad file and run it?
https://pastebin.com/T6aQ7C2E for more accurate scanning (use both scripts + verify in decompiler like Recaf)