Иконки меню приложения: дневник, успеваемость, профиль, техподдержка, расписание.

000xpda или как я реверсил электронный дневник и нашел ключи в логах

Дисклеймер

Данная статья написана исключительно в ознакомительных и образовательных целях. Автор не поощряет взлом информационных систем. Исследование проводилось в рамках личного аккаунта для изучения протоколов передачи данных и автоматизации доступа к собственным персональным данным.Все имена, ссылки, ID изменены/удалены.

Предыстория: зачем?

Все началось с того, что в нашей школе внедрили новый электронный дневник. Дизайн в стиле «привет из 2015-го», медленная загрузка — типичный набор для государственного софта. Глядя на всё это, я подумал: если визуальная часть сделана так лениво, то что же там под капотом?

дизайн приложения
дизайн приложения

Погружение в .apk

Декомпилировав APK через JADX(я решил сразу заглянуть в код, а не снифить, чтобы не возиться с сертификатами), первым делом отправился искать механизм авторизации. Моё внимание привлек класс ESIAActivity. Как понятно из названия, он отвечает за вход через Госуслуги (ЕСИА) внутри встроенного WebView.

При анализе метода onPageFinished я обнаружил то, что заставило меня улыбнуться. Разработчики оставили в коде отладочный вывод:

@Override public void onPageFinished(WebView webView2, String str) { String str2; String cookie = CookieManager.getInstance().getCookie(str); if (cookie == null) { str2 = «»; } else { str2 = «»; for (String str3 : cookie.split(«;»)) { String[] strArrSplit = str3.split(«=»); if (strArrSplit.length > 1 && strArrSplit[0].equals(» X1_SSO»)) { str2 = strArrSplit[1]; } } } if (str2.equals(«») || ESIAActivity.this.logged) { return; } Log.d(«X1_SSO», str2); // ! Очень важно ESIAActivity.this.logged = true; ESIAActivity.this.makeLoginRequest(str2); } }); } public void makeLoginRequest(String str) { ApiService.instance.loginEsia(str, Base64.encodeToString(Crypt.crypt_xor(str.substring(0, str.length() / 2)), 2), new Response.Listener<JSONObject>() { @Override // com.android.volley.Response.Listener public void onResponse(JSONObject jSONObject) { User.setLoginEntity(jSONObject); if (User.getLoginEntity().isSuccess()) { ESIAActivity.this.loginSuccess(User.getLoginEntity().getData()); return; } ToastUtils.show(ESIAActivity.this, «Ошибка! » + User.getLoginEntity().getMessage()); ApplicationData.clearCookies(ESIAActivity.this); ApplicationData.clearCache(ESIAActivity.this.ctx, 2); } }, new Response.ErrorListener() { @Override // com.android.volley.Response.ErrorListener public void onErrorResponse(VolleyError volleyError) { ToastUtils.show(ESIAActivity.this, «Ошибка! Неверный логин или пароль.»); }8592dcad444eb77b398661a83ac0bb34

Log.d(«X1_SSO», str2); Cookie X1_SSO, через которую мы входим, логируется в системе!
Тут же вызывается функция loginEsia из ApiService

public void loginEsia(String str, String str2, Response.Listener<JSONObject> listener, Response.ErrorListener errorListener) { HashMap map = new HashMap(); map.put(«api_key», str2); map.put(«sid», str); createRequest(this.journalsLoginUrl, map, listener, errorListener); }26aeb2d2e8ab6b71deddfd1e0c70078d

А при анализе класса Crypt я обнаружил забавный момент: в коде присутствуют следы AES-шифрования, но по факту используется обычный XOR. Более того, ключ ru.vendor.schoolapp (название пакета) лежит прямо здесь, метод replaceChars, который должен переставлять символы для запутывания, работает вхолостую, не меняя итоговую строку:

public class Crypt { private static Cipher cipher = null; public static String encryptedGUID = «»; private static SecretKeySpec key = null; private static String transformation = «AES/ECB/PKCS5Padding»; public static byte[] crypt_xor(String str) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < str.length(); i++) { sb.append((char) (str.charAt(i) ^ «ru.vendor.schoolapp».charAt(i % 25))); } return sb.toString().getBytes(); } private static StringBuilder replaceChars(StringBuilder sb, int[] iArr, int i) { StringBuilder sb2 = new StringBuilder(); if (sb.length() < i) { i = sb.length(); } for (int i2 = 0; i2 < i; i2++) { int i3 = iArr[i2]; if (i3 < i) { sb2.append(sb.charAt(i3)); } } return sb2; } public static byte[] hashString(String str) { replaceChars(new StringBuilder(str), new int[]{2, 5, 1, 3, 10, 7, 8, 4, 9, 0, 14, 12, 11, 13, 6, 15}, 16); StringBuilder sb = new StringBuilder(); for (int i = 0; i < str.length(); i++) { sb.append((char) (str.charAt(i) ^ «ru.vendor.schoolapp».charAt(i % 25))); } return sb.toString().getBytes(); } }5fcdb936915e48e859984271b85cef4e

API ключ создается так:

  • Берется первая половина X1_SSO.

  • Прогоняется через тот самый crypt_xor с ключом ru.vendor.schoolapp

  • Результат кодируется в Base64.

Получение куки и имитация логина

Я подключил свой телефон к пк, с включенной отладкой по usb, запустил adb logcat -s X1_SSO , перезашёл в свой аккаунт через Госуслуги, так как разработчики забыли убрать отладочный вывод в релизной сборке, сессионная кука просто выплевывалась в консоль.
Нашел в resources.arsc строку с api_url: https://mp.example.ru и эндпоинт «login_journals_url» : journals/login
Написал скрипт на python, имитирующий логин:

import base64 import requests import json # Данные, которые мы добыли из реверса BASE_URL = «https://mp.example.ru» LOGIN_ENDPOINT = «/journals/login» XOR_KEY = «ru.vendor.mobileschool» def crypt_xor(data_str): «»»Реализация метода Crypt.crypt_xor из исходников»»» sb = «» for i in range(len(data_str)): sb += chr(ord(data_str[i]) ^ ord(XOR_KEY[i % 25])) return sb.encode() def login_with_cookie(x1_sso_cookie): «»»Имитация метода ESIAActivity.makeLoginRequest»»» # 1. Берем половину куки для создания api_key half_sid = x1_sso_cookie[:len(x1_sso_cookie) // 2] # 2. XOR + Base64 encrypted_part = crypt_xor(half_sid) api_key = base64.b64encode(encrypted_part).decode(‘utf-8’) # 3. Формируем JSON-запрос payload = { «sid»: x1_sso_cookie, «api_key»: api_key } url = BASE_URL + LOGIN_ENDPOINT headers = {‘Content-Type’: ‘application/json’} print(f»— Отправка запроса —«) print(f»URL: {url}») print(f»Payload: {json.dumps(payload, indent=2)}») response = requests.post(url, json=payload, headers=headers) if response.status_code == 200: data = response.json() print(«— Ответ сервера —«) print(json.dumps(data, indent=2, ensure_ascii=False)) return data else: print(f»Ошибка! Статус: {response.status_code}») print(response.text) return Nonefe8b34709607332ea8a5ff2f333df91e

После имитации логина сервер возвращает массив данных о пользователе. Самое важное для нас здесь — SYS_GUID (уникальный идентификатор ученика) и SCHOOL_ID. Без них мы не сможем запросить расписание.

Скрытый текст

{
«success»: true,
«system»: false,
«message»: «ok»,
«data»: {
«LOGIN»: «СНИЛС»,
«SURNAME»: «фамилия»,
«NAME»: «имя»,
«SECONDNAME»: «отчество»,
«EMAIL»: «почта»,
«CONFIRMATION»: «NONE»,
«CONFIRM_EXPIRATION»: 0,
«SESSION_ID»: null,
«SCHOOLS»: [
{
«ROLES»: [
«participant»
],
«SCHOOL»: {
«SYS_GUID»: «»,
«ID»: «»,
«NAME»: «школа»,
«SHORT_NAME»: «школа»
},
«GOVERNMENT»: null,
«TEACHER»: null,
«PARENT»: null,
«PARTICIPANT»: {
«SYS_GUID»: «»,
«SURNAME»: «фамилия»,
«NAME»: «имя»,
«SECONDNAME»: «отчество»,
«GRADE»: {
«SYS_GUID»: «»,
«NAME»: «класс»,
«SCHOOL»: {
«SYS_GUID»: «»,
«ID»: «»,
«NAME»: «школа»,
«SHORT_NAME»: «школа»
},
и остальное

Из важного тут SYS_GUID, который понадобится далее.

Получение расписания

В приложении за это отвечает getSchoolDayRequest:

public void getSchoolDayRequest(String str, String str2, String str3, String str4, Response.Listener<JSONObject> listener, Response.ErrorListener errorListener) { HashMap map = new HashMap(); map.put(«pdakey», str4); map.put(«apikey», str3); map.put(«guid», str); map.put(«date», str2); createRequest(this.journalsDiaryDayUrl, map, listener, errorListener); }c6254471438a1801393a80817439367b

У нас уже есть apikey, guid, дату сделать не проблема.
У меня ушло достаточно времени на поиск pdakey, я пытался его задать по эндпоинту url_set_pda: https://mp.example.ru/pda/setpdakey, где мне его благополучно не дали. Он также хранится в SharedPreferences, куда я не могу попасть без root доступа на телефоне. Самый смешной момент:

public void checkPda() { if (User.getUser_pda().equals(«») && ApplicationData.active_session) { User.setUser_pda(«000xpda»); } }581a8c005ec61da13841c4ea7f21a487

Вишенкой на торте стало то, что если приложение не смогло получить pda из SharedPreferences, оно ставит захардкоженную заглушку 000xpda, так я получил валидный pdakey.
Начал писать python скрипт для получения оценок:

import base64 import requests import json XOR_KEY = «ru.vendor.mobileschool» def generate_apikey(source_str): «»»Генератор ключа по формуле из DiaryFragment»»» # Берем половину строки half = source_str[:len(source_str) // 2] res_chars = «».join(chr(ord(c) ^ ord(XOR_KEY[i % 25])) for i, c in enumerate(half)) return base64.b64encode(res_chars.encode(‘utf-8’)).decode(‘utf-8’) def get_my_diary(guid, cookie): url = «https://mp.example.ru/journals/school-day» api_key = generate_apikey(guid) headers = { ‘User-Agent’: ‘Dalvik/2.1.0 (Linux; U; Android 13)’, # маскируемся под приложение на телефоне ‘Cookie’: f’X1_SSO={cookie}’, ‘Content-Type’: ‘application/json’, ‘X-Requested-With’: ‘ru.vendor.mobileschool’ } payload = { «guid»: guid, «date»: «2026-02-04», # дата на сегодня «apikey»: api_key, «pdakey»: «000xpda» # найденная заглушка } print(f»— Запрос дневника —«) r = requests.post(url, json=payload, headers=headers) if r.status_code == 200: data = r.json() print(«ДАННЫЕ ПОЛУЧЕНЫ:») print(json.dumps(data, indent=2, ensure_ascii=False)) return data else: print(f» Ошибка {r.status_code}: {r.text}»)781cf4e7aa02823774723e3910533a75

Формула apikey из DiaryFragment: Crypt.encryptedGUID = Base64.encodeToString(Crypt.crypt_xor(User.getUser_sys_guid().substring(0, User.getUser_sys_guid().length() / 2)), 2);

Во время написания этого скрипта сервер часто меня не принимал и перенаправлял на страницу входа. В конце сервер все же сдался, и я получил расписание на сегодня в виде JSON.

Конечный продукт

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

Расписание на 04.02.2026

  1. Алгебра ?? [ПР]
    ? 12:35 — 13:15
    ? Тема не указана
    ? ДЗ: №636(а,б)638(а)

  2. Обществознание ?? [ПР]
    ? 13:45 — 14:25
    ? Повторительно-обобщающий урок по теме «Человек в экономических отношениях»
    ? ДЗ: п. 19

  3. География ?? (5)
    ? 14:35 — 15:15
    ? . Контрольная работа по разделу «Природа России». Обобщающее повторение по темам: «Геологическое строение, рельеф и полезные ископаемые», «Климат и климатические ресурсы», «Моря России и внутренние воды»
    ? ДЗ: нет

    ab2c4d7981864dec1cca7ce421521eb4

Апдейт 1:

Как я стал учителем: найден IDOR

Продолжив исследование, я тыкнул в эндпоинт journals/get-journal с заданным guid журнала, полученным с домашкой. Думал, что сервер даст мне, с правами ученика, по рукам — а он открыл дверь и пригласил войти — мне выдали журнал класса со всеми guid и ФИО.

полученный журнал класса
полученный журнал класса

Далее я постучался в /journals/get-teacher-journals c guid учителя и, бинго, получаю все журналы учителя:

7dd4f4484f0fd11e5d28df4904e03df7

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

у моего друга 3 двойки за день)
у моего друга 3 двойки за день)

А где авторизация?

Решил проверить теорию: а что будет, если вообще не передавать куку авторизации?
Результат? Серверу абсолютно всё равно, кто я. Выяснилось, что защита всей системы держится не на авторизации пользователя, а на статичном 25-символьном XOR-ключе, зашитом в коде приложения. Если ты смог его вычислить (а мы это сделали в первой части), то для системы ты — свой.

Это Broken Access Control (BAC) и IDOR в одном флаконе. BAC, потому что сервер не проверяет роли (ученик может быть учителем).

  • IDOR, потому что доступ к данным идет через прямой перебор ID (GUID).

  • No Auth, потому что куки не нужны.

b4ece97bd97885195ca3aaf2d4791db7

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

Итоги и выводы

Проведенное исследование API мобильного приложения показало, что безопасность системы строится на концепции «безопасность через неясность», которая в 2026 году уже не является эффективной защитой.

1. Технические итоги

В ходе реверса были выявлены следующие критические недоработки:

  • Хардкод в продакшене: Наличие заглушек для отладки (вроде pdakey: 000xpda), которые не были вырезаны перед релизом. Это позволило обойти проверку легитимности устройства.

  • Слабая криптография: Использование XOR с фиксированной солью, зашитой в коде. Любой декомпилятор вскрывает такую защиту за 5 минут.

  • Мертвый код: Наличие в классе Crypt неиспользуемых переменных для AES-шифрования и функций, работающих вхолостую (replaceChars). Это говорит о низком качестве контроля кода.

  • Отсутствие привязки сессии: Токен авторизации (X1_SSO) не привязан к «отпечатку» устройства (Fingerprint) или IP-адресу, что позволяет использовать его в сторонних скриптах.

  • Полный провал авторизации (IDOR/BAC): На стороне бэкенда отсутствует проверка прав доступа. Сервер отдает данные учителей и других учеников любому пользователю, знающему apikey.

  • Zero-Auth доступ: Обнаружено, что для получения персональных данных и журналов серверу не требуется сессионная кука (X1_SSO). Вся безопасность держится на статичном XOR-ключе, что делает систему уязвимой для автоматизированного сбора данных извне.

2. Практический результат

  • Создан легковесный Telegram-бот, который работает быстрее официального клиента.

  • Реализован парсинг вложенных структур JSON.

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

  • Потенциальный Write-Access: Выявлены эндпоинты для записи данных (выставление оценок), которые с высокой долей вероятности имеют те же проблемы с безопасностью, что и методы чтения.

3. Резюме

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

Источник: habr.com

Источник: ai-news.ru

✅ Найденные теги: 000xpda, Ключи, Логи, новости, Реверс, Электронный Дневник

ОСТАВЬТЕ СВОЙ КОММЕНТАРИЙ

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Каталог бесплатных опенсорс-решений, которые можно развернуть локально и забыть о подписках

галерея

Звёздное небо с галактиками и туманностями, космос, Вселенная, астрофотография.
Женщина с длинными тёмными волосами в синем свете, нейтральный фон.
Спутник исследует черную дыру в космосе, испускающий световой луч.
Пикачу использует электрический разряд на фоне неба.
Черный углеродное волокно с текстурой плетения, отражающий свет.
Круглый экран с изображением замка и горы, рядом электронная плата.
Код на экране компьютера, программирование, интерфейс разработчика.
Статистика использования видеокарт NVIDIA RTX, показывающая изменения за октябрь-февраль.
Макросъемка клетки под микроскопом, текстура и форма на голубом фоне.
Image Not Found
Звёздное небо с галактиками и туманностями, космос, Вселенная, астрофотография.

Система оповещения обсерватории Рубина отправила 800 000 сигналов в первую ночь наблюдений.

Астрономы будут получать оповещения о небесных явлениях в течение нескольких минут после их обнаружения. Теренс О'Брайен, редактор раздела «Выходные». Публикации этого автора будут добавляться в вашу ежедневную рассылку по электронной почте и в ленту новостей на главной…

Мар 2, 2026
Женщина с длинными тёмными волосами в синем свете, нейтральный фон.

Расследование в отношении 61-фунтовой машины, которая «пожирает» пластик и выплевывает кирпичи.

Обзор компактного пресса для мягкого пластика Clear Drop — и что будет дальше. Шон Холлистер, старший редактор Публикации этого автора будут добавляться в вашу ежедневную рассылку по электронной почте и в ленту новостей на главной странице вашего…

Мар 2, 2026
Черный углеродное волокно с текстурой плетения, отражающий свет.

Материал будущего: как работает «бессмертный» композит

Учёные из Университета штата Северная Каролина представили композит нового поколения, способный самостоятельно восстанавливаться после серьёзных повреждений.  Речь идёт о модифицированном армированном волокном полимере (FRP), который не просто сохраняет прочность при малом весе, но и способен «залечивать» внутренние…

Мар 2, 2026
Круглый экран с изображением замка и горы, рядом электронная плата.

Круглый дисплей Waveshare для креативных проектов

Круглый 7-дюймовый сенсорный дисплей от Waveshare создан для разработчиков и дизайнеров, которым нужен нестандартный экран.  Это IPS-панель с разрешением 1 080×1 080 пикселей, поддержкой 10-точечного ёмкостного сенсора, оптической склейкой и защитным закалённым стеклом, выполненная в круглом форм-факторе.…

Мар 2, 2026

Впишите свой почтовый адрес и мы будем присылать вам на почту самые свежие новости в числе самых первых