Created
March 25, 2026 10:43
-
-
Save junaire/c5f8a6c7d965fc6072c9827f6a2f1cc8 to your computer and use it in GitHub Desktop.
Apifox Self-Check for Malicious Code
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
| #!/usr/bin/env python3 | |
| import datetime | |
| import hashlib | |
| import os | |
| import struct | |
| import sys | |
| import zlib | |
| from pathlib import Path | |
| TARGET_URL = "https://cdn.apifox.com/www/assets/js/apifox-app-event-tracking.min.js" | |
| TARGET_BYTES = TARGET_URL.encode("utf-8") | |
| SUSPICIOUS_PATTERNS = [ | |
| "REMOTE_JS_URL", | |
| "rsaDecrypt", | |
| "loadAndExecute", | |
| "scheduleNext", | |
| "eval(", | |
| ] | |
| def unique(items): | |
| seen = set() | |
| result = [] | |
| for item in items: | |
| if not item: | |
| continue | |
| if item in seen: | |
| continue | |
| seen.add(item) | |
| result.append(item) | |
| return result | |
| def unique_real_paths(items): | |
| seen = set() | |
| result = [] | |
| for item in items: | |
| try: | |
| stat = os.stat(item) | |
| key = (stat.st_dev, stat.st_ino) | |
| except Exception: | |
| key = str(Path(item).resolve()) | |
| if key in seen: | |
| continue | |
| seen.add(key) | |
| result.append(str(Path(item).resolve())) | |
| return result | |
| def candidate_roots(): | |
| home = Path.home() | |
| if sys.platform == "darwin": | |
| return unique( | |
| [ | |
| str(home / "Library" / "Application Support" / "apifox" / "Cache" / "Cache_Data"), | |
| str(home / "Library" / "Application Support" / "Apifox" / "Cache" / "Cache_Data"), | |
| ] | |
| ) | |
| if os.name == "nt": | |
| appdata = os.environ.get("APPDATA") | |
| local_appdata = os.environ.get("LOCALAPPDATA") | |
| return unique( | |
| [ | |
| appdata and str(Path(appdata) / "apifox" / "Cache" / "Cache_Data"), | |
| appdata and str(Path(appdata) / "Apifox" / "Cache" / "Cache_Data"), | |
| local_appdata and str(Path(local_appdata) / "apifox" / "Cache" / "Cache_Data"), | |
| local_appdata and str(Path(local_appdata) / "Apifox" / "Cache" / "Cache_Data"), | |
| ] | |
| ) | |
| return unique( | |
| [ | |
| str(home / ".config" / "apifox" / "Cache" / "Cache_Data"), | |
| str(home / ".config" / "Apifox" / "Cache" / "Cache_Data"), | |
| str(home / ".cache" / "apifox" / "Cache" / "Cache_Data"), | |
| str(home / ".cache" / "Apifox" / "Cache" / "Cache_Data"), | |
| ] | |
| ) | |
| def walk_files(root): | |
| for base, _, files in os.walk(root): | |
| for name in files: | |
| yield os.path.join(base, name) | |
| def read_null_terminated(data, start): | |
| pos = start | |
| size = len(data) | |
| while pos < size and data[pos] != 0: | |
| pos += 1 | |
| return pos + 1 | |
| def try_inflate_gzip_member(data, gzip_offset): | |
| try: | |
| if data[gzip_offset : gzip_offset + 3] != b"\x1f\x8b\x08": | |
| return None | |
| pos = gzip_offset | |
| flg = data[pos + 3] | |
| pos += 10 | |
| if flg & 0x04: | |
| if pos + 2 > len(data): | |
| return None | |
| xlen = struct.unpack_from("<H", data, pos)[0] | |
| pos += 2 + xlen | |
| if flg & 0x08: | |
| pos = read_null_terminated(data, pos) | |
| if flg & 0x10: | |
| pos = read_null_terminated(data, pos) | |
| if flg & 0x02: | |
| pos += 2 | |
| if pos >= len(data): | |
| return None | |
| return zlib.decompress(data[pos:], -zlib.MAX_WBITS) | |
| except Exception: | |
| return None | |
| def extract_payload(data, url_offset): | |
| gzip_offset = data.find(b"\x1f\x8b\x08", url_offset) | |
| if gzip_offset != -1: | |
| inflated = try_inflate_gzip_member(data, gzip_offset) | |
| if inflated: | |
| return "gzip", inflated | |
| for marker in (b"/*!", b"!function", b"(function", b"(()=>"): | |
| marker_offset = data.find(marker, url_offset) | |
| if marker_offset != -1: | |
| return "plain", data[marker_offset:] | |
| return "unknown", b"" | |
| def sha256_hex(data): | |
| return hashlib.sha256(data).hexdigest() | |
| def main(): | |
| roots = candidate_roots() | |
| existing_roots = unique_real_paths([root for root in roots if os.path.exists(root)]) | |
| print(f"目标 URL: {TARGET_URL}") | |
| if existing_roots: | |
| print(f"检查目录: {' | '.join(existing_roots)}") | |
| else: | |
| print("检查目录: (未找到)") | |
| print("结论: 未找到 Apifox 缓存目录 (NO_APIFOX_CACHE_FOUND)") | |
| print( | |
| "说明: 这不代表机器安全,可能是没有安装 Apifox 桌面端,或者缓存已经被清理。" | |
| ) | |
| return 1 | |
| findings = [] | |
| for root in existing_roots: | |
| for file_path in walk_files(root): | |
| try: | |
| data = Path(file_path).read_bytes() | |
| except Exception: | |
| continue | |
| url_offset = data.find(TARGET_BYTES) | |
| if url_offset == -1: | |
| continue | |
| extracted_as, payload = extract_payload(data, url_offset) | |
| matched = [pattern for pattern in SUSPICIOUS_PATTERNS if pattern.encode("utf-8") in payload] | |
| try: | |
| modified = os.stat(file_path).st_mtime | |
| modified_iso = datetime.datetime.fromtimestamp( | |
| modified, tz=datetime.timezone.utc | |
| ).isoformat().replace("+00:00", "Z") | |
| except Exception: | |
| modified_iso = "(unknown)" | |
| findings.append( | |
| { | |
| "file_path": file_path, | |
| "cache_size": len(data), | |
| "payload_size": len(payload), | |
| "payload_sha256": sha256_hex(payload) if payload else None, | |
| "extracted_as": extracted_as, | |
| "matched": matched, | |
| "modified": modified_iso, | |
| } | |
| ) | |
| if not findings: | |
| print("结论: 缓存中未发现证据 (NO_EVIDENCE_IN_CACHE)") | |
| print("未在本机缓存中找到该可疑 Apifox 脚本 URL 的历史响应。") | |
| print( | |
| "说明: 这可能是假阴性,例如用户未触发相关页面、缓存路径不同,或缓存已经被清空。" | |
| ) | |
| return 2 | |
| confirmed = [item for item in findings if len(item["matched"]) >= 2] | |
| if confirmed: | |
| print("结论: 很可能已中招 (LIKELY_HIT)") | |
| print("原因: 本机缓存中的脚本包含已知恶意执行链特征。") | |
| else: | |
| print("结论: 命中过该 URL,但未发现已知恶意链 (URL_CACHED_BUT_MALICIOUS_CHAIN_NOT_FOUND)") | |
| print( | |
| "原因: 本机缓存中过该可疑 URL,但脚本中未发现目前已知的恶意模式。" | |
| ) | |
| for item in findings: | |
| print("---") | |
| print(f"文件: {item['file_path']}") | |
| print(f"修改时间: {item['modified']}") | |
| print(f"缓存文件大小: {item['cache_size']} 字节") | |
| print(f"解包后脚本大小: {item['payload_size']} 字节") | |
| print(f"提取方式: {item['extracted_as']}") | |
| if item["payload_sha256"]: | |
| print(f"脚本 SHA-256: {item['payload_sha256']}") | |
| if item["matched"]: | |
| print(f"命中特征: {', '.join(item['matched'])}") | |
| else: | |
| print("命中特征: (无)") | |
| if confirmed: | |
| print("---") | |
| print("处置建议: 按已中招机器处理,并轮换这台机器上使用过的 token、会话和密钥。") | |
| return 10 | |
| return 0 | |
| if __name__ == "__main__": | |
| sys.exit(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment