Skip to content

Instantly share code, notes, and snippets.

@ariankordi
Last active December 13, 2020 22:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ariankordi/a3dbed15e5da0cea0bfd22b6308a93ce to your computer and use it in GitHub Desktop.
Save ariankordi/a3dbed15e5da0cea0bfd22b6308a93ce to your computer and use it in GitHub Desktop.
thi this mitmproxy script kinda make me go thonking
# mitmproxy ctx for logging and http for making responses
from mitmproxy import http, ctx
# status code constants
from mitmproxy.net.http import status_codes
# http requests are made for getting and sending back user config
import urllib.request, urllib.parse
# used for making cookie expire date
import datetime
# response en/decoding
import json
from os import getenv
# names of hosts for this app
app_host = 'incite.educationincites.com'
streamservice_host = 'streamservice.educationincites.com'
# domain name that cookies are set on
cookie_domain = '.educationincites.com'
# user agent used for user config requests
user_agent = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36'
# prefix for authorization header
authorization_prefix = 'Bearer {}'
# config template that target user will be placed into
# most of these are probably not necessary as well as some localstorage keys
# todo compare this to the original in some way and see what else is manually added
user_config = {
# required so that we are not immediately redirected to sso login
"bypassedSso": True,
"token": " ",
# basically forever
"tokenExpire": 2000000000000,
"tokenValability": 8,
"email": "",
"userId": "",
"username": "",
"userLoggedInstaffId": "",
"userLoggedIn": " ",
"userLoggedInFirstName": "",
"userLoggedInLastName": "",
"roles": [
{
"_id": "57ae11cc4b39615c060000ec",
"role": "district administrator",
"active": True
}
],
"role": "district administrator",
"allRoles": [
"district administrator"
],
# not necessary either apparently
"defaultCutScores": [],
# only present in student response apparently
"clientId": "gacobb",
"_id": "",
"SideBarOn": True,
"middleName": "",
"inciteAppButton": " ",
"inciteAppUrl": " ",
"announcements": [],
"messages": [],
"impersonate": False,
# added here, idk if this is actually functional
"allowImpersonation": True,
"userWhoImpersonatesFirstName": None,
"userWhoImpersonatesLastName": None,
# LMAO
"userWhoImpersonatesFullname": "null null",
"userWhoImpersonatesUsername": None,
"userWhoImpersonatesstaffId": None,
"userWhoImpersonatesId": None,
"userWhoImpersonatesRoles": None,
"userWhoImpersonatesAllRoles": None,
"userWhoImpersonatesRole": None,
"userWhoImpersonateshasNoOverallRestriction": True,
# added here and enables admin button apparently idk what else it does
"hasNoOverallRestriction": True,
"districtRestrict": False,
"regions": [],
"regionsRestrict": True,
"schools": [],
"schoolsRestrict": True,
"classes": [],
"classRestrict": True,
"targetClassesOnly": True,
"links": {
"linksTestManagerToPrint": "0",
"linksTestManagerToBubblesheets": "0",
"linksTestManagerToProctor": "0",
"linksTestManagerToManualScoring": "0",
"linksTestManagerToEdit": "0",
"linksTestManagerToTestScoreEntry": "0",
"linksRubricScoringToDashboard": "0"
},
"clientInformation": {
"clientId": "gacobb",
"name": "Cobb County School District",
"dbname": "gaCobb",
"state": "Georgia",
"environment": "production",
"isActive": True,
"clientStartDate": "2018-06-01T11:51:39.276Z",
"services": {
"cbt": "https://cbtservice.educationincites.com/api",
"proctor": "https://proctorservice.educationincites.com/api",
"incite": "https://incitesservice.educationincites.com/api",
"inciteClient": "https://incite.educationincites.com/#/client/gacobb"
},
"jaspersoftSettings": {
"password": "gacobb1*",
"server": "https://jaspersoft.education-insights.com/jasperserver-pro",
"userName": "incites-gacobb-prod",
"brandingImage": "repo:/brandingImages/cobbbubble"
},
"currentSchoolYear": "20-21",
"schoolStartDate": "Sun Aug 02 2020 04:00:00 GMT+0000 (UTC)",
"schoolEndDate": "Thu Jul 01 2021 03:59:59 GMT+0000 (UTC)",
"allowSchoolPriorityStandards": True,
"partialCreditDefault": {
"dropdown": False,
"textEntry": False,
"fillInTable": False,
"hotspot": False,
"matching": False,
"selectText": False,
"numberline": False,
"graphingPoint": False,
"graphingLine": False,
"classification": False,
"multiPart": False
},
"isProgramsVisible": True,
"externalLinks": {
"accountButton": {
"url": "",
"label": ""
}
}
},
"reportSettings": {
"teacherReportDataAccess": "teacherSeeOtherTeacherData",
"contentTypeRestrictions": [
{
"contentType": "testScoreEntry",
"restrictedReports": [
"rpt_assmt_strands_analysis",
"rpt_standard_analysis",
"rpt_new_item_analysis",
"rpt_stud_item_responses",
"rpt_student_feedback"
]
}
],
"teacherRestrictedTestTypes": [
"SGM"
]
},
"menuAccess": {
"Standards": True,
"Dashboard": True,
"Reports": True,
"More": True,
"Items": True,
"Assessments": True,
"GlobalLeftNav": True,
"ItemsCreateItem": True,
"AssessmentsTestManager": True,
"MoreCourseCatalog": True,
"ItemsCreateRubric": True,
"MoreStudents": True,
"ItemsSearchRubric": True,
"ItemsSearch": True,
"AssessmentsCreate": True,
"AssessmentsSearch": True,
"ReportsTeacher": True,
"ItemsCreateResource": True,
"MoreClasses": True,
"ItemsSearchResource": True,
"ReportsReports": True,
"ReportsInciteAnalytics": True,
"MoreStudentGroups": True,
"AssessmentsPrograms": True,
"MoreGroups": True,
"GlobalLeftNavTeach": True,
"GlobalLeftNavAssess": True,
"GlobalLeftNavHome": True,
"GlobalLeftNavAnalyze": True,
"GlobalLeftNavProfessionalLearning": True,
"GlobalLeftNavResourceLibrary": True,
"GlobalLeftNavParent": True,
"AssessmentsFPLiteracy": True,
"GlobalLeftNavTeachCatalog": True,
"GlobalLeftNavTeachResourceLibrary": True,
"GlobalLeftNavProfessionalLearningPLCourses": True,
"GlobalLeftNavProfessionalLearningMyGrades": True,
"GlobalLeftNavTeachMyCourses": True,
"GlobalLeftNavTeachResourceLibraryLOR": True,
"GlobalLeftNavProfessionalLearningAdminCourses": True
},
"hasReportAccess": {
"SchoolReports": True,
"PerformanceLevelDistribution_SchoolReports": True,
"AssessmentComparison_SchoolReports": True,
"ItemAnalysis_SchoolReports": True,
"StandardAnalysis_SchoolReports": True,
"SubgroupComparison_SchoolReports": True,
"AssessmentSummary_SchoolReports": True,
"AssessmentStrandsAnalysis_SchoolReports": True,
"TeacherReports": True,
"AdministrationAnalysis_TeacherReports": True,
"AssessmentStrandsAnalysis_TeacherReports": True,
"PerformanceLevelDistribution_TeacherReports": True,
"AssessmentComparison_TeacherReports": True,
"AssessmentSummary_TeacherReports": True,
"StandardAnalysis_TeacherReports": True,
"DistractorAnalysis_TeacherReports": True,
"ItemAnalysis_TeacherReports": True,
"SubgroupComparison_TeacherReports": True,
"AssessmentTrends_TeacherReports": True,
"StudentFeedback_TeacherReports": True,
"StudentReports": True,
"CurrentYearAssessments_StudentReports": True,
"CurrentYearSummary_StudentReports": True,
"AssessmentSummary_StudentReports": True,
"CurrentYearStandards_StudentReports": True
},
"isPrintAssessmentActive": False,
"isPrintAnswerActive": False,
"isPrintBubbleSheetActive": False,
"rolesPrintAssessments": [],
"userPrintAssessments": [],
"rolesPrintAnswerKey": [],
"usersPrintAnswerKey": [],
"rolesPrintBubbleSheet": [],
"resetStudentScoreRolePrivileges": [],
"resetStudentScoreUserPrivileges": [],
"continueAssessmentRolePrivileges": [],
"continueAssessmentUserPrivileges": [],
"usersPrintBubbleSheet": [],
"studentScoreResponse": "",
"isShowStudentResponse": False,
"isHideStudentResponse": False,
"isShowScoreAfterTest": False,
"rolesReportAssessment": [],
"RolesResetStudentScore": [],
"reportOnAssessmentUserPrivileges": [],
"availableAssessmentContentBanks": [
"Preparation/Practice Test",
"Professional Learning",
"Controlled Assessment/Exam",
"Local School Assessment"
],
"availableItemContentBanksForRead": [],
"availableItemContentBanksForEdit": [],
"availableItemContentBanksForAdd": [],
"reportControls": {
"activeReport": "",
"reportDir": "",
"brandingImg": "",
"activeReportUrl": "",
"reportType": "",
"schoolYear": "20-21",
"assessment": None,
"studentAssessment": None,
"region": None,
"school": None,
"teacher": None,
"class": None,
"classes": None,
"student": None,
"grade": None,
"subject": None,
"bank": None,
"region_noTest": None,
"school_noTest": None,
"teacher_noTest": None,
"multi": {
"assessments": None,
"assessmentsFilter": None,
"assessmentIds": None,
"region": None,
"school": None,
"teacher": None
}
},
"passages": [],
"notRestrictedByAdministrationWindow": False,
"allowMultipleGradeSubject": False,
"recommendationEngineOn": True,
"administrationTypes": [
{
"text": "Pre Learning",
"id": 1
},
{
"text": "During Learning",
"id": 2
},
{
"text": "Post Learning",
"id": 3
}
],
"priorityStds": [],
"editPerformanceBand": True,
"isCurriculumModuleAccessible": False,
"showTagsItemSelection": False,
"signedCookies": {},
"signedCookiesForFiles": {},
"availableContentTypes": [
{
"value": "itemBank",
"label": "Item Bank",
"selected": True
},
{
"value": "external",
"label": "External",
"selected": False
},
{
"value": "testScoreEntry",
"label": "Test Score Entry",
"selected": False
}
],
"restrictions": {
"grade": [],
"subject": [],
"contentArea": []
},
"resetFiltersTestManagerWhenImpersonate": False,
"resetFiltersClassesWhenImpersonate": False,
"resetFiltersCourseCatalogWhenImpersonate": False,
"resetFiltersSchoolsWhenImpersonate": False,
"resetFiltersStaffWhenImpersonate": False,
"resetFiltersStudentsWhenImpersonate": False,
"resetFiltersReportsAnalyticsWhenImpersonate": False,
"leftSidebarOn": True,
"studentPortalSidebarOn": True,
"navState": None,
"teacherStaffSchools": [],
"availableItemContentBanksForReadAndCopy": [
"Shared Teacher Bank",
"Touchstones"
],
"shareWithTeacherGroups": True,
"shareWithMySchool": True,
"allowCreateStudentGroup": True,
"userIcon": "img/user-icons/u-teacher.png"
}
# request handlers to instantly respond to certain requests
# also most of these handlers don't capture hostname except for index page
# hopefully there isn't toooooo much? confusion
def request(flow):
global user_config
# hack to get token because the app itself won't do it LMAO
if flow.request.path == '/_/token':
if not user_config['token']:
ctx.log.warn('/_/token requested but no token is set, just returning nothing for now')
flow.response = http.HTTPResponse.make(
status_codes.OK,
'',
)
return
flow.response = http.HTTPResponse.make(
status_codes.OK,
user_config['token']
)
# /checkSession?undefined GET
elif '/checkSession' in flow.request.path:
ctx.log.info('checkSession, returning status:true')
# always return status true json
flow.response = http.HTTPResponse.make(
status_codes.OK,
'{"status":true}',
{'Content-Type': 'application/json'}
)
# handle requesting user config by just returning the current user config
# note: student portal requests this path without the api part
elif 'getUserSessionSettings' in flow.request.path and flow.request.method == 'POST':
ctx.log.info('getUserSessionSettings')
# warn if token appears to be blank for some reason
if len(user_config['token']) < 2:
ctx.log.warn('user config token appears to be blank while requesting user session settings did you mean to do this?')
user_config_response = json.dumps(user_config)
flow.response = http.HTTPResponse.make(
status_codes.OK,
user_config_response,
{
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
)
# index handler which will first request user config for cookies that will be set in response handler
elif flow.request.host == app_host and flow.request.path == '/':
ctx.log.info('requested app index path!')
# get user session settings
ctx.log.info('requesting getUserSessionSettings w token')
# construct urllib request to get settings
settings_get_url = 'https://{}/api/getUserSessionSettings'.format(streamservice_host)
req = urllib.request.Request(settings_get_url, method='POST')
req.add_header('Authorization', authorization_prefix.format(user_config['token']))
req.add_header('User-Agent', user_agent)
resp = urllib.request.urlopen(req)
# current user config to take cookie attributes from
resp_content = resp.read()
# if response content is blank
if not resp_content:
# usually this happens when token is invalid
ctx.log.warn('token is invalid..??? please try using a new token')
return
user_config_current = json.loads(resp_content)
ctx.log.info('getUserSessionSettings retrieved and decoded! token is ' + user_config_current['token'])
# copy this current token and (valid) signed cookies
user_config['token'] = user_config_current['token']
# cooki
user_config['signedCookies'] = user_config_current['signedCookies']
user_config['signedCookiesForFiles'] = user_config_current['signedCookies']
# finally, in order for the app to work correctly we need to SET settings
ctx.log.info('now sending back user config to setUserSessionSettings')
# begin by making the url from the last url. i am so, so sorry for this line
settings_set_url = settings_get_url.replace('get', 'set')
req_data = urllib.parse.urlencode({'userSettings': json.dumps(user_config)}).encode()
req = urllib.request.Request(settings_set_url, req_data)
req.add_header('Authorization', authorization_prefix.format(user_config['token']))
req.add_header('User-Agent', user_agent)
#print(req.__dict__)
# response content isn't required
urllib.request.urlopen(req)
# by the way user config doesn't actually need to be returned this time
# because it is requested later by the app ok
return
# add user token to every request if it's not empty sorry that this is lazy
# not actually necessary anymore because client will just start sending it now
# i will uncomment it anyway as a funny fallover
if user_config['token'] and len(user_config['token']):
flow.request.headers['Authorization'] = authorization_prefix.format(user_config['token'])
def response(flow):
global user_config
# scrape student portal session settings for token
#elif
if (flow.request.path == '/getUserSessionSettings' or flow.request.path == '/loginPsswd') and flow.request.method == 'POST':
ctx.log.info('hello! scraping getUserSessionSettings (or loginpsswd, same thing) from student portal for user config')
if funny_token:
ctx.log.info('nvm skipping this for student portal because funny token is defined so that will alwayhs be usee')
return
if not flow.response.status_code == status_codes.OK:
ctx.log.info('getUserSessionSettings response skipped because status code is not 200 uh oh')
return
if flow.response.content:
user_config_current = json.loads(flow.response.content)
# copy token AND cookies
user_config['token'] = user_config_current['token']
user_config['signedCookies'] = user_config_current['signedCookies']
user_config['signedCookiesForFiles'] = user_config_current['signedCookies']
ctx.log.info('stored student portal token hi ' + user_config['token'])
else:
ctx.log.warn('wtf no response content for student portal session? hmmm...')
# index page
elif flow.request.path == '/':
# replace js on index page that adds some fixes to the app
# sets localstorage items which should make it work when not logged in, and
# sets token in localstorage and headers so that it's correct
flow.response.text = flow.response.text.replace('var _rollbarConfig', '''
// required to function and log in i think (some may not be necessary)
localStorage.setItem("id", " ");
localStorage.setItem("userId", " ");
localStorage.setItem("school", "");
localStorage.setItem("sso", \'{"enabled":false}\');
localStorage.setItem("userLoggedIn", " ");
localStorage.setItem("headers", \'{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8"}\');
// request token externally outside of the app so we can set it here
// this is a bad line it assumes that this request will logically load before everything else
// which may actually be fine for the specific use case but this is supposed to
// be a solution to something that the developers of the app could fix anyway
fetch('/_/token').then(response => response.text()).then(text => {
localStorage.setItem("token", text);
localStorage.setItem("headers", \'{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8","Authorization":"Bearer \'+text+\'"}\');
});
var _rollbarConfig''')
# assign cookies that have been previously retrieved in request handler
cookies = []
# set cookie expiry date for two days from now
cookies_expiry = (datetime.datetime.utcnow() + datetime.timedelta(days=2)).strftime('%a, %d %b %Y %H:%M:%S GMT')
for k in user_config['signedCookies'].keys():
cookies.append(
(k,
(user_config['signedCookies'][k], [
('Domain', cookie_domain),
('Path', '/'),
('Expires', cookies_expiry)
# httponly appears to not be settable here
]))
)
#print(cookies)
flow.response.cookies = cookies
# match main js to patch out checksession and idle log out
elif '/js/main' in flow.request.path:
#print('this IS JS MAIN HHHHFJFBDH')
# there could be many ways to go about doing this and i just picked one of them
# i wish i could chain these methods on multiple lines but idk how to do that so. sorry for this
flow.response.text = flow.response.text.replace('$rootScope.startCheckSessionInterval();', '/*$rootScope.startCheckSessionInterval();*/')
flow.response.text = flow.response.text.replace('triggerCheckSession = true', 'triggerCheckSession = false')
# replace idletimeout handler with nothing, which does much more than stopping logouts
flow.response.text = flow.response.text.replace('IdleTimeout\', ', 'idleTimeout\', function(){return});(function foo(){//')
#flow.response.text = 'hi'
# main function, set arguments from environment i guess
# FUNNY_TOKEN expected to be token and FUNNY_USERNAME is supposed to be username
# nvm no more funny_token because i just realized we can get it earlier
funny_token = getenv('FUNNY_TOKEN')
funny_username = getenv('FUNNY_USERNAME')
if not funny_token:
print('FUNNY_TOKEN is not set meaning that you will have to go to the student portal so we can see the token thanks (if that sentence just made ANY sense....)')
if not funny_username:
print('please set FUNNY_USERNAME environment variable (and FUNNY_TOKEN too if you want)')
# copy token which is then used in request handler
user_config['token'] = funny_token
# copy username to various places
user_config['username'] = funny_username
user_config['userId'] = funny_username
# change full name to username so that it appears in app
user_config['userLoggedIn'] = funny_username
# probably not necessary
user_config['_id'] = funny_username
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment