子墨的博客

总得让实力配上野心


  • 首页

  • 标签71

  • 分类16

  • 归档29

  • 关于

  • 搜索

超详细i至诚app打卡流程分析与自动打卡实现

置顶 发表于 2021-07-19 分类于 爬虫 , 抓包 阅读次数:
本文字数: 11k 阅读时长 ≈ 10 分钟

背景

受人之托,分析了一下i至诚app(是由树维公司出的一款套壳app)的打卡流程,并实现了自动化,app版本是v1.1.2,记录一下

开始

app下载地址

http://www.fdzcxy.edu.cn/ueditor/asp/upload/file/20200305/zcxy_v1_1_2.apk

抓包

我使用的工具是fiddler,这个app本质上是一个weex构建的基于vue的套壳app项目,登陆时使用的是okhttp框架 并有一定程度的证书校验,因此抓包时用老办法(hook okhttp)绕过一下,然后局域网下设置好代理之后,就可以抓包了,过程不再赘述

报文分析

登陆

i至诚使用的是jwt token认证

请求报文如下

1
2
3
4
5
6
POST https://superapp.fdzcxy.edu.cn/auth-server/jwt/token/login?username=用户名&password=密码 HTTP/1.1
Content-Length: 0
Host: superapp.fdzcxy.edu.cn
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/3.12.1

响应报文如下,登陆成功会返回jwt token,失败会直接403

1
2
3
4
5
6
7
8
9
10
11
12
13
14
HTTP/1.1 200
Server: nginx/1.16.1
Date: Mon, 19 Jul 2021 07:27:01 GMT
Content-Type: text/plain;charset=UTF-8
Connection: keep-alive
Vary: Accept-Encoding
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY

eyJhbGciOiJSUzUxMiJ9eyJzdWIiOiLlrablj7ciLCJST0xFUyI6IlJPTEVfQURNSU4sYWRtaW5pc3RyYXRvcix1c2VyIiwiY3JlYXRlZCI6MTYyNjY5NjA1NTkzMiwiZXhwIjoxNjI5Mjg4MDU1fS5zaWduYXR1cmU=

这里简单介绍一下jwt(是一种基于token的鉴权机制),jwt token通常由三部分构成,格式是这样的:header.payload.signature

所以上面的响应可以直接解析为:

header:声明类型和加密的算法

1
{"alg":"RS512"}

payload:存放有效信息

1
2
3
4
5
6
{
"sub": "学号",#subject,标识实体
"ROLES": "ROLE_ADMIN,administrator,user",
"created": 1626696055932,#创建时间
"exp": 1629288055,#expire,过期时间
}

signature:可以理解为签名

1
是一串RSA私钥加密之后的密文,由于泄露个人信息,已打码处理

分析登陆时其实可以不用抓包处理,因为树维公司其实提供了文档,链接:https://superapp.fdzcxy.edu.cn/auth-server/doc/index#%E7%99%BB%E5%BD%95

然后文档中还顺带把公钥给我们了,基于此,顺带写了一个验签工具,并把之前抓包的jwt token解密出来了

该工具需要的maven依赖

1
2
3
4
5
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>

工具源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;

import java.security.Key;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

public class Main {
public static void main(String[] args) throws InvalidKeySpecException, NoSuchAlgorithmException {
getClaimsFromToken("你抓到的jwt token");
}

public static RSAPublicKey stringToPublicKey(String publicKeyPem) throws NoSuchAlgorithmException, InvalidKeySpecException {
//System.out.println(publicKeyPem);
if (publicKeyPem.startsWith("-----BEGIN PUBLIC KEY-----")) {
publicKeyPem = publicKeyPem.replaceAll("-----BEGIN PUBLIC KEY-----", "");
}
if (publicKeyPem.endsWith("-----END PUBLIC KEY-----")) {
publicKeyPem = publicKeyPem.replaceAll("-----END PUBLIC KEY-----", "");
}
publicKeyPem = publicKeyPem.replaceAll("\n", "");

//System.out.println(publicKeyPem);
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(Base64.getMimeDecoder().decode(publicKeyPem));
KeyFactory x509KeyFactory = KeyFactory.getInstance("RSA");
RSAPublicKey x509PublicKey = (RSAPublicKey) x509KeyFactory.generatePublic(x509KeySpec);

return x509PublicKey;
}

public static Key getPublicKey() throws InvalidKeySpecException, NoSuchAlgorithmException {

String publicKeyPem = "-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDBQw6TmvJ+nOuRaLoHsZJGIBzRg/wbskNv6UevL3/nQioYooptPfdIHVzPiKRVT5+DW5+nqzav3DOxY+HYKjO9nFjYdj0sgvRae6iVpa5Ji1wbDKOvwIDNukgnKbqvFXX2Isfl0RxeN3uEKdjeFGGFdr38I3ADCNKFNxtbmfqvjQIDAQAB -----END PUBLIC KEY-----";

Key publicKey = stringToPublicKey(publicKeyPem);
return publicKey;
}

static Claims getClaimsFromToken(String token) throws InvalidKeySpecException, NoSuchAlgorithmException {
Key publicKey = getPublicKey();

Claims claims;
try {
claims = Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token).getBody();
System.out.println(claims);
} catch (Exception e) {
e.printStackTrace();
claims = null;
}
return claims;
}
}

解密出来的明文如下:其实就是把payload加密了一下

1
{sub=学号, ROLES=ROLE_ADMIN,administrator,user, created=1626696055932, exp=1629288055}

每日健康上报

在i至诚app中,除了登陆,几乎其他所有的功能都是用webview加载的vue项目,每日健康上报功能也是这样,而在这些应用中,几乎都没有证书验证,因此可以直接抓包,或者在浏览器中访问,所以这里最简单的方式,当然是无脑的使用自动化测试框架啦(虽然慢是慢点,因为浏览器渲染是一个比较耗时的操作),我并没有采用这种方式,我选择的是直接分析整个流程,然后模拟提交,不依赖浏览器的渲染

大体上有这么几个过程:请求每日健康上报 -> 重定向并获取sessionID -> 激活sessionid -> 获取打卡信息,jsConfId和callbackConfId -> 提交打卡信息

自动化

经过上面的分析,最终的成品如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# -*- coding: utf-8 -*-
from urllib.parse import quote
import requests
import time
import re

username = '学号'
password = '密码'
name = '姓名'

pattern = re.compile(r'[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}')


def login():
url = 'https://superapp.fdzcxy.edu.cn/auth-server/jwt/token/login'
headers = {
'Host': 'superapp.fdzcxy.edu.cn',
'Connection': 'Keep-Alive',
'Accept-Encoding': 'gzip',
'User-Agent': 'okhttp/3.12.1',
}
data = {
'username': username,
'password': password,
}
res = requests.post(url=url, headers=headers, data=data)
# print(res.text)
return res.text


def daka(token):
# 请求每日健康上报,获取必要的信息
url = 'http://dw10.fdzcxy.edu.cn/datawarn/home/handle.action?redirectUrl=app/yibao.frm'
headers = {
'Host': 'dw10.fdzcxy.edu.cn',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1',
'User-Agent': 'Mozilla/5.0 (Linux; Android 11) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/90.0.4430.210 Mobile Safari/537.36 SuperApp',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'authorization': 'JWTToken ' + token,
'usertoken': token,
'jwttoken': token,
'X-Requested-With': 'com.lantu.MobileCampus.zcxy',
'Accept-Encoding': 'gzip, deflate',
'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7',
'Cookie': 'userToken=' + token
}
res = requests.get(url, headers=headers, allow_redirects=False)
url = 'http://dw10.fdzcxy.edu.cn/' + res.headers['location']
reffer = url
# print(url)

# 获取sessionID
res = requests.get(url=url, headers=headers)
sessionID = pattern.search(res.text)[0]
# print(sessionID)

# 更新JSESSIONID
url = 'http://dw10.fdzcxy.edu.cn/datawarn/decision/resources?path=/com/fr/web/core/js/vancharts-all.js&deviceType=iPhone&buildVersion=2020.12.02.10.19.36.340'
headers = {
'Host': 'dw10.fdzcxy.edu.cn',
'Connection': 'keep-alive',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Accept-Encoding': 'gzip, deflate',
'Accept-Language': 'zh-CN,zh;q=0.9',
}
res = requests.get(url=url, headers=headers)
cookie = 'JSESSIONID=' + requests.utils.dict_from_cookiejar(res.cookies)['JSESSIONID']
# print(cookie)

# 激活sessionid的必要步骤
url = 'http://dw10.fdzcxy.edu.cn/datawarn/decision/url/mobile/view/firstdata?op=h5&cmd=firstdata&userno=' + username + '&token=' + token + '&__parameters__={}&sessionID=' + sessionID
# print(url)
headers = {
'Host': 'dw10.fdzcxy.edu.cn',
'Connection': 'keep-alive',
'responseType': 'json',
'terminal': 'H5',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'__device__': 'unknown',
'Accept': 'application/json, text/plain, */*',
'Cache-Control': 'no-cache',
'clientType': 'mobile/h5_5.0',
'deviceType': 'unknown',
'Accept-Encoding': 'gzip, deflate',
'Accept-Language': 'zh-CN,zh;q=0.9',
}
res = requests.get(url=url, headers=headers)
# print(res.text)

# 获取打卡信息,jsConfId和callbackConfId
url = 'http://dw10.fdzcxy.edu.cn/datawarn/decision/view/form?sessionID=' + sessionID + '&op=fr_form&cmd=load_content&toVanCharts=true&fine_api_v_json=3&widgetVersion=1'
res = requests.get(url=url, headers=headers)
# print(res.text)
# 不嫌麻烦的话,可以写一个逻辑构建打卡所需要提交的表单,这里我就偷懒了
items = res.json()['items'][0]['el']['items']
for i in items:
if i['widgetName'] == 'SUBMIT':
submit = i['listeners'][0]['action']
break
# print(submit)
jsConfId = pattern.findall(submit)[0]
callbackConfId = pattern.findall(submit)[1]
# print(jsConfId, callbackConfId)

# 提交打卡信息
url = 'http://dw10.fdzcxy.edu.cn/datawarn/decision/view/form'
headers = {
'Host': 'dw10.fdzcxy.edu.cn',
'Connection': 'keep-alive',
'terminal': 'H5',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'__device__': 'unknown',
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json, text/plain, */*',
'Cache-Control': 'no-cache',
'sessionID': sessionID,
'clientType': 'mobile/h5_5.0',
'deviceType': 'unknown',
'Origin': 'http://dw10.fdzcxy.edu.cn',
'Referer': reffer,
'Accept-Encoding': 'gzip, deflate',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Cookie': cookie,
}
data = {
'op': 'dbcommit',
'__parameters__': quote(
'{"jsConfId":"' + jsConfId + '","callbackConfId":"' + callbackConfId + '","LABEL2":" 每日健康上报","XH":"' + username + '","XM":"' + name + '","LABEL12":"","LABEL0":"1. 目前所在位置:","SHENG":"350000","SHI":"福州市","QU":"鼓楼区","LABEL11":"2.填报时间:","SJ":"' + time.strftime(
"%Y-%m-%d %H:%M:%S",
time.localtime()) + '","LABEL1":"3. 今日体温是否正常?(体温小于37.3为正常)","TWZC":"正常","LABEL6":"目前体温为:","TW":"0","TXWZ":"350000福州市鼓楼区","LABEL9":"4. 昨日午检体温:","WUJ":"36.4","LABEL8":"5. 昨日晚检体温:","WJ":"36.5","LABEL10":"6. 今日晨检体温:","CJ":"36.4","LABEL3":"7. 今日健康状况?","JK":["健康"],"JKZK":"","QTB":"请输入具体症状:","QT":" ","LABEL4":"8. 近14日你和你的共同居住者(包括家庭成员、共同租住的人员)是否存在确诊、疑似、无症状新冠感染者?","WTSQK":["无以下特殊情况"],"SFXG":"","LABEL5":"9. 今日隔离情况?","GLQK":"无需隔离","LABEL7":"* 本人承诺以上所填报的内容全部真实,并愿意承担相应责任。","CHECK":true,"DWWZ":{},"SUBMIT":"提交信息"}'),
}
# print(data)
res = requests.post(url=url, headers=headers, data=data)
if res.text:
return True


def main_handler(event, context):
token = login()
# print(token)
if daka(token):
return 'success'


if __name__ == '__main__':
print(main_handler({}, {}))

依赖

1
2
3
4
5
certifi==2021.5.30
charset-normalizer==2.0.1
idna==3.2
requests==2.26.0
urllib3==1.26.6

挂云函数配置触发器定时执行或者放云服务器设置定时执行就好了

配置触发器参考文档:https://cloud.tencent.com/document/product/583/9708

其他,比如打卡成功后发封邮件提示之类的,就自己发挥了(我懒

觉得不错,打赏一下
子墨 微信支付

微信支付

子墨 支付宝

支付宝

  • 本文作者: 子墨
  • 本文链接: https://blog.zimo.wiki/posts/5a29fa14/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
爬虫 自动打卡 i至诚 python3
一份不专业的树梅派400使用指南
  • 文章目录
  • 站点概览
子墨

子墨

子墨的博客
29 日志
16 分类
71 标签
RSS
GitHub E-Mail CSDN QQ Gitee
友情链接
  • 高正杰的博客

Tag Cloud

  • 8076
  • HttpCanary1
  • JavaScript2
  • Jupyter Notebook1
  • c++1
  • centos1
  • cuda1
  • c语言6
  • deepin1
  • dns2tcp1
  • fiddler1
  • hexo2
  • html1
  • i至诚1
  • jar1
  • java3
  • jetbrains1
  • linux3
  • linux server1
  • markdown1
  • nginx1
  • nodejs1
  • python2
  • python31
  • pytorch1
  • tesseract-ocr1
  • ubantu1
  • virtualenvwrapper-win1
  • war1
  • windows2
  • windows server1
  • 个人博客2
  • 代理1
  • 代码托管1
  • 代码雨1
  • 伪装位置1
  • 使用指南1
  • 刷recovery1
  • 力扣1
  • 劫持1
  • 双系统1
  • 小爱课程表1
  • 小米61
  • 常识1
  • 快捷键冲突1
  • 抓包1
  • 折腾1
  • 挖矿木马1
  • 服务器1
  • 机器学习2
  • 极客1
  • 树梅派4001
  • 油猴脚本1
  • 爬虫1
  • 环境搭建1
  • 直播服务器1
  • 科普1
  • 程序综合设计6
  • 算法1
  • 终端1
  • 编译1
  • 考研7
  • 自动打卡1
  • 蓝桥杯1
  • 解锁bl1
  • 运维1
  • 部署1
  • 钉子户1
  • 题解1
  • 黑客帝国1
  • 黑苹果1
  1. 1. 背景
  2. 2. 开始
    1. 2.1. app下载地址
    2. 2.2. 抓包
    3. 2.3. 报文分析
      1. 2.3.1. 登陆
      2. 2.3.2. 每日健康上报
    4. 2.4. 自动化
蜀ICP备18029083号 © 2019 – 2022 子墨 | 站点总字数: 167k | 站点阅读时长 ≈ 2:32
由 Hexo 强力驱动 v3.9.0
|
主题 – NexT.Pisces v7.3.0
载入网站运行时间中...
|
0%