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

тема

Получаем десятки тысяч доменов в минуту на шести строчках кода

ocean · · 0 ответов · 342 просмотров

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

 Эксклюзивно для форума LIMB.cc

Решить проблему получения крупных диапазонов доменных имен можно разными способами.

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

Раздачи списков доменных имен - еще один чудесный способ. В них отсутствие игрушечного Т3 тира порой компенсируется аномально огромным количеством рпанелей. Впрочем, каждому свое.

Каталоги сайтов a1webdirectory, blogarama, sitepromotiondirectory, curlie - легко спарсить. Например, парсер под viesearch:

Спойлер: code
C-like:
package main

import (
    "bufio"
    "fmt"
    "io"
    "net/http"
    "net/url"
    "os"
    "regexp"
    "strings"
    "time"
)

func main() {
    const delay = 10 * time.Second

    client := &http.Client{
        CheckRedirect: func(req *http.Request, via []*http.Request) error {
            if len(via) > 0 {
                fmt.Printf("🔁 Редирект с %s на %s\n", via[len(via)-1].URL, req.URL)
            }
            return nil
        },
    }

    file, err := os.Open("links.txt")
    if err != nil {
        fmt.Printf("❌ Ошибка при открытии links.txt: %v\n", err)
        return
    }
    defer file.Close()

    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        baseURL := strings.TrimSpace(scanner.Text())
        if baseURL == "" {
            continue
        }

        parsed, err := url.Parse(baseURL)
        if err != nil {
            fmt.Printf("⚠️  Невалидный URL %s: %v\n", baseURL, err)
            continue
        }

        segments := strings.Split(strings.Trim(parsed.Path, "/"), "/")
        category := segments[len(segments)-1]
        if category == "" && len(segments) > 1 {
            category = segments[len(segments)-2]
        }

        filename := category + ".txt"
        output, err := os.Create(filename)
        if err != nil {
            fmt.Printf("❌ Не удалось создать файл %s: %v\n", filename, err)
            continue
        }
        defer output.Close()

        fmt.Printf("📂 Обработка категории: %s\n", category)

        page := 1
        for {
            pageURL := fmt.Sprintf("%s?p=%d", baseURL, page)
            fmt.Printf("🌐 Загружаем: %s\n", pageURL)

            req, err := http.NewRequest("GET", pageURL, nil)
            if err != nil {
                fmt.Printf("⚠️  Ошибка создания запроса: %v\n", err)
                break
            }

            resp, err := client.Do(req)
            if err != nil {
                fmt.Printf("⚠️  Ошибка при выполнении запроса: %v\n", err)
                break
            }

            bodyBytes, err := io.ReadAll(resp.Body)
            resp.Body.Close()
            if err != nil {
                fmt.Printf("⚠️  Ошибка чтения тела: %v\n", err)
                break
            }

            body := string(bodyBytes)

            if strings.Contains(body, "&s=n") {
                fmt.Println("🚧 Достигнут конец категории (найдено '&s=n')")
                break
            }

            re := regexp.MustCompile(`<div\s+class="vie_flex_url">.*?<a[^>]+href="(https?://[^"]+)"`)
            matches := re.FindAllStringSubmatch(body, -1)

            if len(matches) == 0 {
                fmt.Println("⚠️  Нет <div class=\"vie_flex_url\"> на странице.")
            }

            for _, m := range matches {
                link := m[1]
                fmt.Fprintf(output, "%s\n", link)
                fmt.Printf("✅ Найдена ссылка: %s\n", link)
            }

            page++
            fmt.Printf("⏳ Пауза %v...\n", delay)
            time.Sleep(delay)
        }
        fmt.Printf("✅ Завершено: %s\n\n", filename)
    }

    if err := scanner.Err(); err != nil {
        fmt.Printf("❌ Ошибка при чтении links.txt: %v\n", err)
    }
}


Минус в том, что это всегда ограниченное статическое количество доменов, обычно до нескольких сотен тысяч. Оставлю одну мысль, ее суть в рекурсивном парсинге сайтов, где будут ссылки на другие сайты, например, википедии.

DNS-протокол отвечает за преобразование доменных имен в IP-адреса, представляет собой специальную программу, запущенную на специальных серверах. И даже не одну, а целый кластер. Он как бы знает домены своей зоны, но отдать ее не может. Операция называется AXFR, и на нее, насколько я понимаю, существует некий запрет.

А можно ли получить домены через сертификаты?
Все началось с сайта crt.sh, который используют для получения поддоменов. По сути, он берет из своей таблицы все сертификаты, которые были выписаны домену,
и предоставляет их тому, кто запрашивает. Но можно ли составить запрос так, чтобы он вернул все сертификаты, выданные центром сертификации, например lets encrypt?
Мы видим эти данные в логах. Колдуя над запросом, сайт падает - неудивительно, ответ должен быть огромным.

У crt есть открытая база данных: psql -h crt.sh -p 5432 -U guest certwatch. И да, у них есть эти таблицы, из которых можно вытащить логи - победа? Permission denied :(

Но откуда у crt вообще эти данные, что происходит?

Пытаясь разобраться, оказывается, что работаем мы с логами Certificate Transparency, которые хранят записи обо всех выданных сертификатах. ИИ говорит, что любой может посмотреть в публичных логах - какие сертификаты и для каких доменов были выданы. Это вызывает сомнения. В конце концов, куда гуглить?

RFC 6962 описывает Certificate Transparency. Из него действительно следует, что центры сертификации обязаны логировать выпущенный сертификат хотя бы в один CT-лог.
А у сервера, который хранит эти логи, должны быть ручки (которые в том числе и описывает RFC), которые публично должны отдавать эти логи.

Пока я все это гуглил, наткнулся на сайт certstream.calidog.io - типа стриминг-монитор CT-логов. GitHub - https://github.com/calidog
Проект - https://github.com/CaliDog/certstream-python

Шесть строчек кода:
Python:
import certstream

def print_callback(message, context):
    if message['message_type'] == "certificate_update":
        domains = message['data']['leaf_cert']['all_domains']
        print("Новые домены:", domains)

certstream.listen_for_events(print_callback, url='wss://certstream.calidog.io/')

Десятки тысяч доменов в минуту.
Не удалось прогрузить медиа


Все, что тут происходит, - лишь подключение по сокету к стриму сервера-ретранслятора. Но как он работает под капотом?
Суть примерно такая: отсюда получаем список журналов серверов - https://www.gstatic.com/ct/log_list/v3/log_list.json
Не удалось прогрузить медиа


Берем, к примеру, https://ct.googleapis.com/logs/us1/argon2025h2 информативное API :)
Превращаем в https://ct.googleapis.com/logs/us1/argon2025h2/ct/v1/get-entries?start=0&end=100
Не удалось прогрузить медиа

Декодируем base64, вытаскиваем x509-сертификат - примерно так.


Спойлер: code
Code:
        import requests

import base64

from cryptography import x509

from cryptography.hazmat.backends import default_backend


def parse_extra_data(extra_data: bytes):

    certs = []

    i = 0

    while i < len(extra_data):

        if i + 3 > len(extra_data):

            break

        cert_len = int.from_bytes(extra_data[i:i+3], "big")

        i += 3

        cert_bytes = extra_data[i:i+cert_len]

        i += cert_len

        certs.append(cert_bytes)

    return certs


url = "https://ct.googleapis.com/logs/us1/argon2025h2/ct/v1/get-entries?start=1000&end=10000"

resp = requests.get(url).json()


for entry in resp["entries"]:

    extra_data = base64.b64decode(entry["extra_data"])

    for cert_bytes in parse_extra_data(extra_data):

        try:

            cert = x509.load_der_x509_certificate(cert_bytes, default_backend())

            domains = []

            try:

                ext = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName)

                domains = ext.value.get_values_for_type(x509.DNSName)

            except Exception:

                pass

            print("CN:", cert.subject.rfc4514_string(), "Domains:", domains)

        except Exception:

            continue

Откуда взялась ручка? ИИ. Но изначально, конечно, она из RFC.

Очень простой способ, и в то же время это не то, что даст Google по запросу получение всех доменов. Обнаружилось методом тыканья :)
👍21 ❤️4 🔥3 🤔2

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