102 lines
3.7 KiB
Python
Executable File
102 lines
3.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Inspect a Java/JAR sample statically: structure, manifest, obfuscation, capabilities.
|
|
|
|
Treats the JAR as a ZIP, inventories .class files and embedded payloads, reads the manifest entry
|
|
point, and scans class bytes for obfuscator fingerprints and dangerous API usage. Reads the archive
|
|
statically; never launches the JVM. Defangs URLs.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import sys
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
EMBED_SIGS = [(b"PK\x03\x04", "nested ZIP/JAR"), (b"MZ", "PE executable"),
|
|
(b"#!", "script"), (b"\x7fELF", "ELF")]
|
|
OBFUSCATORS = [b"allatori", b"proguard", b"zelix", b"klassmaster", b"stringer", b"dasho"]
|
|
CAPABILITY_APIS = [b"java/lang/Runtime", b"java/lang/ProcessBuilder", b"java/lang/reflect",
|
|
b"java/net/URLClassLoader", b"javax/crypto", b"java/net/Socket",
|
|
b"java/net/URL", b"java/awt/Robot", b"java/util/Base64"]
|
|
RE_URL = re.compile(rb"https?://[\x21-\x7e]{4,}")
|
|
|
|
|
|
def _defang(s: str) -> str:
|
|
return s.replace("http", "hxxp").replace(".", "[.]")
|
|
|
|
|
|
def inspect(path: str) -> dict:
|
|
data = Path(path).read_bytes()
|
|
result: dict = {"is_jar": False}
|
|
if not zipfile.is_zipfile(path):
|
|
return {"error": "not a ZIP/JAR archive"}
|
|
result["is_jar"] = True
|
|
classes = []
|
|
resources = []
|
|
manifest = ""
|
|
obf = set()
|
|
caps = set()
|
|
urls = set()
|
|
nested = []
|
|
with zipfile.ZipFile(path) as zf:
|
|
for name in zf.namelist():
|
|
if name.endswith(".class"):
|
|
classes.append(name)
|
|
try:
|
|
body = zf.read(name)
|
|
except Exception:
|
|
continue
|
|
for o in OBFUSCATORS:
|
|
if o in body.lower():
|
|
obf.add(o.decode())
|
|
for api in CAPABILITY_APIS:
|
|
if api in body:
|
|
caps.add(api.decode().replace("/", "."))
|
|
for u in RE_URL.findall(body):
|
|
urls.add(_defang(u.decode("latin-1", "ignore")))
|
|
elif name == "META-INF/MANIFEST.MF":
|
|
manifest = zf.read(name).decode("latin-1", "ignore")
|
|
else:
|
|
resources.append(name)
|
|
try:
|
|
head = zf.read(name)[:8]
|
|
except Exception:
|
|
head = b""
|
|
for sig, label in EMBED_SIGS:
|
|
if head.startswith(sig):
|
|
nested.append({"name": name, "type": label})
|
|
main_class = ""
|
|
for line in manifest.splitlines():
|
|
if line.lower().startswith(("main-class:", "premain-class:")):
|
|
main_class = line.split(":", 1)[1].strip()
|
|
break
|
|
# single-character class names suggest name obfuscation
|
|
short_names = sum(1 for c in classes if len(Path(c).stem) <= 2)
|
|
return {
|
|
"is_jar": True,
|
|
"class_count": len(classes),
|
|
"main_class": main_class,
|
|
"embedded_payloads": nested[:50],
|
|
"resources": resources[:50],
|
|
"obfuscators": sorted(obf),
|
|
"name_obfuscation_suspected": short_names > max(3, len(classes) // 5),
|
|
"capability_apis": sorted(caps),
|
|
"urls": sorted(urls)[:50],
|
|
}
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(description="Inspect a Java/JAR sample")
|
|
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
p = sub.add_parser("inspect")
|
|
p.add_argument("file")
|
|
args = parser.parse_args(argv)
|
|
print(json.dumps(inspect(args.file), indent=2))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|