#!/usr/bin/env python3 """Deobfuscate common webshell obfuscation techniques. Handles PHP eval/base64/gzinflate chains, variable function calls, string construction, and multi-layer encoding. """ from __future__ import annotations import argparse import base64 import codecs import json import re import sys import zlib from datetime import datetime from pathlib import Path def decode_base64(data) -> dict: """Attempt base64 decode.""" try: # Try standard base64 decoded = base64.b64decode(data) return decoded.decode("utf-8", errors="ignore") except Exception: # Try URL-safe base64 try: decoded = base64.urlsafe_b64decode(data + "==") return decoded.decode("utf-8", errors="ignore") except Exception: return None def decode_gzinflate(data) -> dict: """Attempt gzinflate (raw DEFLATE decompress).""" try: if isinstance(data, str): data = data.encode("latin-1") return zlib.decompress(data, -15).decode("utf-8", errors="ignore") except Exception: return None def decode_gzuncompress(data) -> dict: """Attempt gzuncompress (zlib decompress).""" try: if isinstance(data, str): data = data.encode("latin-1") return zlib.decompress(data).decode("utf-8", errors="ignore") except Exception: return None def decode_rot13(data) -> dict: """Apply ROT13 decoding.""" return codecs.decode(data, "rot_13") def decode_hex_string(data) -> dict: """Decode hex-encoded string.""" try: # Remove common prefixes/formatting clean = re.sub(r'[\\x\s]', '', data) return bytes.fromhex(clean).decode("utf-8", errors="ignore") except Exception: return None def decode_chr_sequence(content) -> dict: """Decode PHP chr() sequences like chr(72).chr(101).chr(108).""" result = content # Replace chr(N) with actual characters def chr_replace(match) -> dict: try: return chr(int(match.group(1))) except (ValueError, OverflowError): return match.group(0) result = re.sub(r'chr\s*\(\s*(\d+)\s*\)', chr_replace, result, flags=re.IGNORECASE) # Clean up concatenation operators result = result.replace(".", "").replace(" . ", "") return result def decode_url_encoding(data) -> dict: """Decode URL-encoded strings.""" try: from urllib.parse import unquote return unquote(data) except Exception: return data def deobfuscate_php(content, max_layers=10) -> dict: """Recursively deobfuscate PHP code.""" layers = [] current = content iteration = 0 while iteration < max_layers: iteration += 1 decoded = None method = None # Try base64_decode extraction b64_match = re.search( r'base64_decode\s*\(\s*["\']([A-Za-z0-9+/=]+)["\']\s*\)', current ) if b64_match: decoded = decode_base64(b64_match.group(1)) method = "base64_decode" # Try eval(base64_decode(...)) patterns if not decoded: eval_b64 = re.search( r'eval\s*\(\s*base64_decode\s*\(\s*["\']([A-Za-z0-9+/=]+)["\']\s*\)\s*\)', current ) if eval_b64: decoded = decode_base64(eval_b64.group(1)) method = "eval(base64_decode())" # Try str_rot13 if not decoded: rot13_match = re.search( r'str_rot13\s*\(\s*["\']([^"\']+)["\']\s*\)', current ) if rot13_match: decoded = decode_rot13(rot13_match.group(1)) method = "str_rot13" # Try chr() sequences chr_match = re.search(r'(?:chr\s*\(\s*\d+\s*\)\s*\.?\s*){3,}', current, re.IGNORECASE) if chr_match and not decoded: decoded = decode_chr_sequence(chr_match.group()) method = "chr() sequence" # Try hex string decoding if not decoded: hex_match = re.search(r'(?:\\x[0-9a-fA-F]{2}){4,}', current) if hex_match: decoded = decode_hex_string(hex_match.group()) method = "hex string" # Try URL decoding if not decoded: url_match = re.search(r'(?:%[0-9a-fA-F]{2}){4,}', current) if url_match: decoded = decode_url_encoding(url_match.group()) method = "URL encoding" if decoded and decoded != current: layers.append({ "layer": iteration, "method": method, "content_preview": decoded[:500], "content_length": len(decoded), }) current = decoded else: break return { "original_length": len(content), "layers_decoded": len(layers), "layers": layers, "final_output": current, } def main() -> None: parser = argparse.ArgumentParser( description="Deobfuscate webshell code" ) parser.add_argument("--input", "--file", "-f", required=True, help="Webshell file to deobfuscate") parser.add_argument("--output", "-o", help="Output file") parser.add_argument("--format", choices=["json", "text"], default="text") parser.add_argument("--max-layers", type=int, default=10, help="Max deobfuscation layers") args = parser.parse_args() path = Path(args.file) if not path.exists(): print(f"[!] File not found: {args.file}", file=sys.stderr) sys.exit(1) content = path.read_text(encoding="utf-8", errors="ignore") result = deobfuscate_php(content, args.max_layers) result["file"] = str(args.file) result["timestamp"] = datetime.now().isoformat() if args.format == "json": output = json.dumps(result, indent=2) else: output = f"=== Webshell Deobfuscation ===\n" output += f"File: {args.file}\n" output += f"Layers decoded: {result['layers_decoded']}\n\n" for layer in result["layers"]: output += f"--- Layer {layer['layer']} ({layer['method']}) ---\n" output += f"{layer['content_preview']}\n\n" output += f"--- Final Output ---\n" output += result["final_output"][:2000] if args.output: Path(args.output).write_text(output) print(f"[+] Saved to {args.output}") else: print(output) if __name__ == "__main__": main()