Как я стал учителем за 5 минут: BAC в электронном дневнике

от автора

Дисклеймер

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

Предыстория: как всё начиналось

В предыдущей части я рассказывал о том, как медлительность и «привет из 2015-го» в дизайне нового школьного дневника заставили меня заглянуть под капот его .apk.

Если кратко напомнить содержание:

  • Реверс-инжиниринг: Через JADX я обнаружил, что разработчики оставили отладочный вывод сессионных кук X1_SSO прямо в logcat.

  • Криптография «на коленке»: Вместо обещанного в коде AES, вся защита данных держалась на обычном XOR с захардкоженным ключом ru.vendor.schoolapp(имя пакета)

  • Костыли и заглушки: Выяснилось, что если приложение не может получить ключ устройства, оно использует универсальную заглушку 000xpda.

Тогда моей целью была простая автоматизация: я написал Python-скрипт и Telegram-бота, чтобы просто по-человечески смотреть своё расписание и оценки, не дожидаясь, пока WebView соизволит прогрузиться.

Но чем дальше я копал API, тем больше понимал: возможность видеть свои оценки — это не фича, это лишь верхушка айсберга.

Я думал, что сервер проверяет мои права доступа, но стоило мне подставить в запрос случайный guid из полученного JSON и убрать X1_SSO из заголовков, как «дверь» открылась настежь. Оказалось, что система не просто плохо написана — в ней фактически отсутствует проверка полномочий.

Прежде чем мы перейдем к коду, стоит прояснить один момент: это приложение — не уникальная разработка одного города. Это настоящий «серийный» продукт. Исследование показало, что абсолютно идентичный интерфейс, логика и (что самое печальное) дыры в безопасности «радуют» пользователей как минимум в трех субъектах.

Бюрократический пинг-понг (Минцифры vs РКН vs Разработчики)

Я решил пойти путем Responsible Disclosure (ответственного разглашения). Казалось бы, данные миллионов детей под угрозой — реакция должна быть мгновенной. Но реальность оказалась куда хуже.

Раунд 1: Минцифры Первым делом улетело обращение в Минцифры. Суть проста: «Ребята, у вас в региональном приложении авторизация — это одно название, поправьте».

Результат: Минцифры, недолго думая, включили режим «моя хата с краю» и перенаправили запрос

Я → Минцифры → Минобр N-нного города → РЦИТ (Региональный центр информационных технологий, который указан как разработчик в одном из регионов).

Тогда РЦИТ официально отрапортовал: «Информация принята для анализа и устранения выявленных замечаний». Казалось бы, победа? Как бы не так.

Их «работа над ошибками» выглядела как идеальный анекдот:

  • Вместо рефакторинга — макияж: Они убрали простейший XOR-шифр, который смущал всех, кто хоть раз открывал декомпилятор.

  • Вместо реальной криптографии — «имитация»: На его место водрузили AES. Казалось бы, прогресс! Но стоило заглянуть в класс Crypt.java, как всё встало на свои места: секретный ключ шифрования и API-ключи они заботливо положили прямо в код приложения, как записку с паролем под клавиатурой.

  • Бездействие в остальных регионах: проблему «исправили» только в одном из 3 приложений

package ru.vendor.schoolapp2.common;import android.util.Base64;import javax.crypto.Cipher;import javax.crypto.spec.SecretKeySpec;/* JADX INFO: loaded from: classes2.dex */public class Crypt {    public static String alt_api_key = "gqhy671nmn3478fhsdjf4f09f45sf4fg4lf842d1";    private static Cipher cipher = null;    public static String encryptedTOKEN = "";    private static SecretKeySpec key = null;    public static String session_apikey = "0xt25240s9s12xv767v1ll17757e32e34x12ppix332vdi2i";    private static String transformation = "AES/ECB/PKCS5Padding";    private static byte[] encrypt(SecretKeySpec secretKeySpec, byte[] bArr) {        try {            Cipher cipher2 = Cipher.getInstance(transformation);            cipher = cipher2;            cipher2.init(1, secretKeySpec);            return cipher.doFinal(bArr);        } catch (Exception unused) {            return null;        }    }    private static byte[] decrypt(SecretKeySpec secretKeySpec, byte[] bArr) {        try {            Cipher cipher2 = Cipher.getInstance(transformation);            cipher = cipher2;            cipher2.init(2, secretKeySpec);            return cipher.doFinal(bArr);        } catch (Exception unused) {            return null;        }    }    public static String encryptString(String str) {        return Base64.encodeToString(encrypt(key, str.getBytes()), 2);    }    public static String decryptString(String str) {        byte[] bArrDecrypt = decrypt(key, Base64.decode(str, 2));        if (bArrDecrypt != null) {            return new String(bArrDecrypt);        }        return "";    }    public static void initKey() {        key = createSecretKey();    }    private static SecretKeySpec createSecretKey() {        return new SecretKeySpec(new byte[]{10, 20, 30, 40, 50, 23, 19, 31, 0, 0, 0, 0, 0, 0, 0, 0}, 0, 16, "AES");    }}

Раунд 2: Второе обращение, Роскомнадзор

  • Я → Минцифры : «Ребята, ваш предыдущий патч — это декорация. Данные миллионов детей всё еще в открытом доступе».

  • Минцифры → Роскомнадзор: «Это по части защиты ПДн, разбирайтесь».

  • РКН → Управление РКН по ЦФО: «Спустите на региональный уровень, пусть проверят».

    В официальном ответе РКН было сказано, что обращение рассмотрено и перенаправлено по компетенции. Но пока «компетенция» раскачивалась, я решил проверить гипотезу о Client-Side Trust.

Точка входа — Shared Preferences и доверие на слово

Изучив исходники в JADX, я наткнулся на интересную особенность архитектуры. Разработчики решили, что хранить все данные о текущей сессии пользователя в обычном XML-файле shared_prefs — это отличная идея. Но дьявол, как обычно, крылся в переменных.

Моё внимание привлекли поля:

  • user_sys_guid — уникальный ID пользователя.

  • rolename — строка, определяющая, кто ты: «Ученик» или «Учитель».

Я задался вопросом: а что, если приложение само решает, какой интерфейс мне показывать, основываясь только на этой строчке? Проверяет ли сервер мою роль при каждом запросе, или он просто верит тому, что прислал клиент?

Эксперимент: Для проверки гипотезы мне понадобилась полноценная android среда с root. На моем Arch Linux выбор пал на Waydroid — контейнеризированный Android, который позволяет копаться в потрохах приложений без лишних костылей.

  1. Запуск: Ставим приложение, логинимся под обычной учеткой ученика.

  2. Проникновение: Пока приложение «спит», ныряем в терминал:

    sudo waydroid shellcd /data/data/ru.vendor.schoolapp/shared_prefs/
  3. Момент истины: Открываем заветный XML. Кодировка встретила меня «кракозябрами» (привет, UTF-8 в ASCII-терминале), но структуру не скрыть. Находим строки: <string name="rolename">Ученик</string> и <string name="user_sys_guid">123abc</string>

  4. Ставим учительский GUID, который нам вернул сервер при запросе дневника, роль учителя и даже имя

Приложение открылось. Но вместо привычного скучного дневника меня встретил… полноценный интерфейс учителя. Появились кнопки «СОХРАНИТЬ», «ДОБАВИТЬ КОЛОНКУ» и выбор любого класса.

Это был официальный диагноз системе: Broken Access Control. Серверная часть оказалась настолько доверчивой, что просто отдала мне функции управления журналом, потому что я сам себя назвал учителем в текстовом файле на телефоне.

Заключение: Безопасность как декорация

Подводя итоги моего «сериала» с обращениями в Минцифры и РКН:

  • Имитация патчей: Замена XOR на AES с ключом в коде — это не защита, а попытка пустить пыль в глаза.

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

  • Масштаб: Проблема затрагивает как минимум три региона, а значит, под угрозой данные сотен тысяч учеников.

Дисклеймер (Повторно)

Все данные в ходе исследования остались в сохранности. Ни одна оценка не была исправлена (хотя искушение было велико), ни один прогул не был удален. Цель статьи — не навредить, а заставить ответственных лиц наконец-то заняться делом.

А я пока жду исправлений

ссылка на оригинал статьи https://habr.com/ru/articles/1025016/