编写Python脚本拉取优学院答案

上个月审计了云班课和优学院的前端代码,实现了强制修改视频进度和获取选择题答案的功能。但是单个发包效率太低了,昨晚开始着手写了一个脚本,早上修改了一下,基本功能写出来了。考虑到风险,这个脚本是获取答案,不自动答题。

4.29 追加:应要求,对脚本进行了更新。


0x00

脚本的核心部分就是一个获取答案的接口,通过传递题目的questionid,可以查询到对应的答案。细节部分主要是维持会话,保留站点的cookie,解析json,过滤html代码。

其中最花费时间和精力的是分析请求包,分析json数据,从大量json出甄选有效值(章节信息,课程信息,题目信息等)。

0x01 获取题目ID

上次审计时顺便找到了获取答案的API,但是要实现批量获取答案,必然要批量获取题目信息

打开答题页面,审查元素,发现每个题目都有一个唯一的questionid。

批量获取题目信息(4.29 追加,此接口新增了parentId参数)

但是获取题目信息要首先获取到章节信息

但是要获取以上信息,还需要进行登录,还有一个原因就是:获取个人相关信息时(课程,班级),需要一个UA-AUTHORIZATION头。后面讲解登录以及这个HTTP头的作用

0x02 登录过程信息

抓登录包时,发现了两个登录包,但是后来发现只用发一个包就OK。

这个包不知道有什么用,传入了账户名,不管账户是否存在,返回值都一样

第一个登录包
登陆成功时会Set一个UMOOC_SESSION,并返回1

登陆失败时也会Set一个UMOOC_SESSION,但返回0

但是上面那个登录包其实没这一个重要

这个包在登陆成功时,会Set大量的Cookie

0x03 其他请求包

除了之上请求,我还筛选出来了两个可能有用的请求

获取全校班级和班级ID(这个苹果画的不错)

获取个人信息,可以用来显示欢迎语或者判断登录是否成功

0x04 编写主要功能代码

登录

HTTP请求全部用requests库处理
登录成功后,Cookie中会保存一个token,可以作为判断登录是否成功的条件
持久保存cookie,维持会话,可以用requests.Session()

def login():
	auth = {}
	auth["name"] = input("[*] 用户名:")
	auth["passwd"] = getpass.getpass("[*] 密码(输入时不可见):")
	session.post(login_url, auth)
	if "token" in session.cookies.keys():
		global token
		token = session.cookies["token"]
		print("[+] 登录成功")
		return True
	else:
		print("[-] 登录失败")

如何获取JSON数据

要获取数据需要至少两个参数,classID和courseID,分别对应班级和课程
这两个参数可以从URL中获取

def parse_que_id(url):
	course_id = re.search(r"courseId=\d+", url).group()
	course_id = course_id.replace("courseId=", "")
	class_id = re.search(r"classId=\d+", url).group()
	class_id = class_id.replace("classId=", "")
	return course_id, class_id

处理JSON数据

JSON数据量庞大,分析结果和筛选有效属性,花了大量时间
下面列出章节信息的关键结构

其中通过nodeid可以获取每一小节的JSON数据

每小节中的title、questionid分别对应题目内容和题目ID

def parse_chap_info(course_id, class_id):
	url = outline_url.replace("cseid", course_id)
	url = url.replace("clsid", class_id)
	chap_info = session.get(url, headers={"UA-AUTHORIZATION": token})
	chap_info = chap_info.text.strip()
	chap_info = json.loads(chap_info)
	for x in range(len(chap_info["chapters"])):
		global chapters
		chap = dict()
		chap["title"] = chap_info["chapters"][x]["nodetitle"]
		chap["id"] = chap_info["chapters"][x]["nodeid"]
[y]["title"])
		chapters.append(chap)
	print_all_answers()

获取答案

4.29 追加:新增了parentId参数

def print_answer(que_id,par_id):
	res = session.get(api_url + str(que_id) + "?parentId=" + str(par_id))
	data = res.text.strip().replace("\n", "")
	data = json.loads(data)
	print("答案:" + str(data["correctAnswerList"]))

0x05 完整代码

因为安全问题,这里上传了压缩文件(4.29 更新)


果然,最麻烦的过程还是分析HTTP数据包,梳理程序逻辑。