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

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

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

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

галерея

🚀 500 МБ в 50? Реально? Да — ловите бомбовый…
Магазин Andon Market, светлый интерьер, номер помещения 2102, уютная атмосфера, витрина.
Завод Tesla с солнечными панелями и ветряками, экологически чистое производство автомобилей.
Текст на экране: исходные данные для дневника питания и активности для расчёта калорий.
dummy-img
Логотип Booking.com на синем фоне с цветами на переднем плане.
Лектор объясняет материал студентам в университете, классная аудитория.
Диаграмма процесса планирования и верификации решений с участием агентов AI.
Археологические раскопки: вид сверху каменных руин древнего сооружения.
Image Not Found
Логотип Booking.com на синем фоне с цветами на переднем плане.

Компания Booking.com подтвердила, что хакеры получили доступ к данным клиентов.

Источник изображения: Шон Галлап / Getty Images В понедельник компания Booking.com подтвердила, что хакеры могли получить доступ к личным данным клиентов, включая имена, электронные адреса, физические адреса, номера телефонов и детали бронирования. По данным нескольких сообщений в…

Апр 13, 2026
Лектор объясняет материал студентам в университете, классная аудитория.

Философия труда

В качестве научного сотрудника программы «Этика технологий» в Северной Каролине Михал Масны занимается развитием диалога, преподаванием и исследованиями социальных и этических аспектов новых вычислительных технологий. «Я хочу, чтобы этот курс стал важным событием в расписании студента», —…

Апр 13, 2026
Диаграмма процесса планирования и верификации решений с участием агентов AI.

DS-STAR: Современный универсальный агент для анализа данных.

DS-STAR — это передовой агент для обработки данных, универсальность которого демонстрируется его способностью автоматизировать целый ряд задач — от статистического анализа до визуализации и обработки данных — для различных типов данных, что в конечном итоге приводит к…

Апр 13, 2026
Археологические раскопки: вид сверху каменных руин древнего сооружения.

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

Возможно, они были частью придорожной гостиницы Специалисты из Национального института охранных археологических исследований (Inrap)…

Апр 13, 2026

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