├── .env.example ├── .gitignore ├── README.md ├── axios.config.js ├── ef.js ├── package.json ├── pnpm-lock.yaml ├── screenshots └── screenshot1.png └── utils.js /.env.example: -------------------------------------------------------------------------------- 1 | AUTHORIZATION="" 2 | COOKIE="" 3 | X_EF_ACCESS="" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EF-Exploit 2 | ### How to use 3 | 1. Install the requirements 4 | ```bash 5 | pnpm install # or npm install or yarn 6 | ``` 7 | 2. Create a `.env` file with the following content 8 | ```ini 9 | AUTHORIZATION="" 10 | COOKIE="" 11 | X_EF_ACCESS="" 12 | ``` 13 | You'll get the values from the browser's network tab. Check the requests that end with `troop` and copy the values from the headers. 14 | 15 | ![image](./screenshots/screenshot1.png) 16 | 17 | 3. Run the script 18 | ```bash 19 | node ef.js 20 | ``` 21 | 22 | The number of lessons is optional, if not provided it will default to 1. 23 | 24 | ### Optional step 25 | Click on the button below: 26 | 27 | [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/ivainqueur) 28 | 29 | -------------------------------------------------------------------------------- /axios.config.js: -------------------------------------------------------------------------------- 1 | const URLS = { 2 | COMPLETION: 'https://learn.corporate.ef.com/api/school-proxy/v1/commands/activity/complete', 3 | CURRENT_LESSON: 'https://learn.corporate.ef.com/api/school-proxy/v1/troop?c=countrycode%3Dus%7Cculturecode%3Den%7Cpartnercode%3DCorp%7Cstudentcountrycode%3Dus%7Clanguagecode%3Den%7Csiteversion%3D18-1%7Cdevicetypeid%3D1%7Cproductid%3D100', 4 | LESSON_PROGRESS: 'https://learn.corporate.ef.com/api/school-proxy/v1/troop?c=countrycode%3Dus%7Cculturecode%3Den%7Cpartnercode%3DCorp%7Cstudentcountrycode%3Dus%7Clanguagecode%3Den%7Csiteversion%3D18-1%7Cdevicetypeid%3D1%7Cproductid%3D100' 5 | } 6 | 7 | const QUERIES = { 8 | CURRENT_LESSON: 'school_context!current.user|student_course_enrollment!current|student_platform_version!current', 9 | LESSON_PROGRESS_FAKE: 'student_lesson!$lessonId', 10 | LESSON_PROGRESS: 'student_lesson!$lessonId.children.progress' 11 | } 12 | 13 | export { 14 | URLS, 15 | QUERIES, 16 | } -------------------------------------------------------------------------------- /ef.js: -------------------------------------------------------------------------------- 1 | import { configDotenv } from 'dotenv' 2 | configDotenv() 3 | import axios from 'axios' 4 | import { errorWrapper, replaceInString } from './utils.js' 5 | import { QUERIES, URLS } from './axios.config.js' 6 | 7 | const HEADERS = { 8 | 'Content-Type': 'multipart/form-data', 9 | 'Authorization': process.env.AUTHORIZATION, 10 | 'Cookie': process.env.COOKIE, 11 | 'X-Ef-Access': process.env.X_EF_ACCESS 12 | } 13 | 14 | const completeStep = errorWrapper(async (stepId) => { 15 | const { data } = await axios.request({ 16 | method: 'post', 17 | url: URLS.COMPLETION, 18 | data: { 19 | minutesSpent: Math.floor(Math.random() * (20 - 4 + 1)) + 4, 20 | studentActivityId: stepId, 21 | score: 100, 22 | studyMode: 0 23 | }, 24 | headers: { 25 | ...HEADERS, 26 | 'Content-Type': 'application/json' 27 | } 28 | }) 29 | 30 | console.log(stepId, !data.length ? 'completed' : 'failed') 31 | }, { exitOnError: false }) 32 | 33 | const getCurrentLesson = errorWrapper(async () => { 34 | const formData = new FormData(); 35 | formData.append('q', QUERIES.CURRENT_LESSON) 36 | const { data } = await axios({ 37 | method: 'post', 38 | url: URLS.CURRENT_LESSON, 39 | data: formData, 40 | headers: { 41 | ...HEADERS, 42 | 'Content-Type': 'multipart/form-data' 43 | } 44 | }) 45 | const containingStudentLesson = data.filter(el => el.studentLessonId) 46 | if (!containingStudentLesson.length) throw new Error("Couldn't find next lesson id"); 47 | 48 | const nextLessonId = containingStudentLesson[0].studentLessonId 49 | const tmpInfo = await getLessonProgress(replaceInString(QUERIES.LESSON_PROGRESS_FAKE, { lessonId: nextLessonId })) 50 | if (!tmpInfo.templateLessonId) throw new Error("Couldn't find next lesson template lesson id"); 51 | console.log("Solving", `"${tmpInfo.lessonName}"`) 52 | 53 | const templateLessonInfo = await getTemplateLessonInfo(tmpInfo.templateLessonId) 54 | if (!templateLessonInfo.lesson.id) throw new Error("Couldn't find next lesson id"); 55 | 56 | // get actual lesson progress 57 | const lessonProgress = await getLessonProgress(templateLessonInfo.lesson.id + '.children.progress', true) 58 | 59 | const stepIds = lessonProgress.filter(el => el.stepName).map(el => el.children).flat().map(el => el.id.match(/student_activity!(.*)/)[1]) 60 | 61 | for (const stepId of stepIds) { 62 | await completeStep(stepId) 63 | } 64 | }, { exitOnError: false }) 65 | 66 | const getLessonProgress = errorWrapper(async (lessonId, returnALl = false) => { 67 | const formData = new FormData(); 68 | formData.append('q', lessonId) 69 | const { data } = await axios({ 70 | method: 'post', 71 | url: URLS.LESSON_PROGRESS, 72 | data: formData, 73 | headers: { 74 | ...HEADERS, 75 | 'Content-Type': 'multipart/form-data' 76 | } 77 | }) 78 | if (!data.length) throw new Error("Couldn't find lesson progress data"); 79 | 80 | return returnALl ? data : data[0] 81 | }) 82 | 83 | const getTemplateLessonInfo = errorWrapper(async (templateLessonId) => { 84 | const formData = new FormData(); 85 | formData.append('q', 'pc_student_lesson_map!' + templateLessonId) 86 | const { data } = await axios({ 87 | method: 'post', 88 | url: URLS.LESSON_PROGRESS, 89 | data: formData, 90 | headers: { 91 | ...HEADERS, 92 | 'Content-Type': 'multipart/form-data' 93 | } 94 | }) 95 | if (!data.length) throw new Error("Couldn't find template lesson progress data"); 96 | return data[0] 97 | }) 98 | 99 | // get lesson number from arguments 100 | const lessonNumber = process.argv[2] ?? 1 101 | console.log('Going to complete', lessonNumber, 'lesson(s)') 102 | 103 | const main = async () => { 104 | for (let i = 0; i < lessonNumber; i++) { 105 | await getCurrentLesson() 106 | } 107 | } 108 | 109 | main() -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tmp_iv", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "type": "module", 6 | "license": "MIT", 7 | "dependencies": { 8 | "axios": "^1.5.1", 9 | "dotenv": "^16.4.5" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | dependencies: 8 | axios: 9 | specifier: ^1.5.1 10 | version: 1.6.8 11 | dotenv: 12 | specifier: ^16.4.5 13 | version: 16.4.5 14 | 15 | packages: 16 | 17 | /asynckit@0.4.0: 18 | resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} 19 | dev: false 20 | 21 | /axios@1.6.8: 22 | resolution: {integrity: sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==} 23 | dependencies: 24 | follow-redirects: 1.15.6 25 | form-data: 4.0.0 26 | proxy-from-env: 1.1.0 27 | transitivePeerDependencies: 28 | - debug 29 | dev: false 30 | 31 | /combined-stream@1.0.8: 32 | resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} 33 | engines: {node: '>= 0.8'} 34 | dependencies: 35 | delayed-stream: 1.0.0 36 | dev: false 37 | 38 | /delayed-stream@1.0.0: 39 | resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} 40 | engines: {node: '>=0.4.0'} 41 | dev: false 42 | 43 | /dotenv@16.4.5: 44 | resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} 45 | engines: {node: '>=12'} 46 | dev: false 47 | 48 | /follow-redirects@1.15.6: 49 | resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==} 50 | engines: {node: '>=4.0'} 51 | peerDependencies: 52 | debug: '*' 53 | peerDependenciesMeta: 54 | debug: 55 | optional: true 56 | dev: false 57 | 58 | /form-data@4.0.0: 59 | resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} 60 | engines: {node: '>= 6'} 61 | dependencies: 62 | asynckit: 0.4.0 63 | combined-stream: 1.0.8 64 | mime-types: 2.1.35 65 | dev: false 66 | 67 | /mime-db@1.52.0: 68 | resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} 69 | engines: {node: '>= 0.6'} 70 | dev: false 71 | 72 | /mime-types@2.1.35: 73 | resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} 74 | engines: {node: '>= 0.6'} 75 | dependencies: 76 | mime-db: 1.52.0 77 | dev: false 78 | 79 | /proxy-from-env@1.1.0: 80 | resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} 81 | dev: false 82 | -------------------------------------------------------------------------------- /screenshots/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IVainqueur/ef-exploit/ef089dedf172e02b659ca05374c90d651644af11/screenshots/screenshot1.png -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | export function replaceInString(str, obj) { 2 | let result = String(str); 3 | for (let key in obj) { 4 | result = result.replace(new RegExp(`\\$${key}`, "g"), obj[key]); 5 | } 6 | return result; 7 | } 8 | 9 | export function errorWrapper(callback, { exitOnError = true } = {}) { 10 | return async function (...args) { 11 | try { 12 | return await callback(...args); 13 | } catch (error) { 14 | if (error.response) { 15 | // The request was made and the server responded with a status code 16 | // that falls out of the range of 2xx 17 | console.log(error.response.data); 18 | console.log(error.response.status); 19 | } else if (error.request) { 20 | // The request was made but no response was received 21 | // `error.request` is an instance of XMLHttpRequest in the browser and an instance of 22 | // http.ClientRequest in node.js 23 | console.log(error.request); 24 | } else { 25 | // Something happened in setting up the request that triggered an Error 26 | console.log('Error', error.message); 27 | } 28 | if (exitOnError) process.exit(1); 29 | } 30 | } 31 | } --------------------------------------------------------------------------------