抓包校友邦小程序实现自动签到

7 min read

由于 Android 7.0 之后不在信任用户证书,无法抓取 HTTPS 请求,根据网上的教程配置了一番无法配置成功,干脆使用了 Android 5.0 ,同时微信版本不要太高,貌似最新版本不兼容 Android 5.0,一直崩溃。

所用工具

工具除 Burpsuite 文末均会附上下载地址

  • 夜神模拟器 我比较常用国际版6.5.0.3,国内最新版也行,注意最新版默认是 Android 7 需要在 多开器 中新建 Android 5
  • 微信 (旧版8.0.0)
  • Fiddler (汉化版5.0.2)
  • Burpsuite

配置环境

选择网卡

使用 ipconfig 查询本机网卡信息,网卡的选择随意即可,我这里选择了 Vmnet8 网卡,

image-20221215151352444

Burpsuite

新增一个 监听器监听地址 使用我们上面选择的 192.168.8.1,端口的话就默认 8080 即可

image-20221215151248441

Fiddler

在菜单栏 工具选项 中勾选配置如下图即可。

image-20221215151219960

连接 这里可以自定义 Fiddler 监听端口,我们保持默认 8888 即可

image-20221215151909746

这里配置 网关 的目的,是让流量可以被 Burpsuite 接收到,这里代理配置填写 Burpsuite 监听器中的配置。

image-20221215151225025

夜神模拟器

配置代理

WLAN 长按 修改网络 ,勾选 高级选项,然后在配置如下图所示

代理服务器主机名:填写你选择网卡IP
代理服务器端口:填写Fiddler监听的端口,默认8888

image-20221215150036716

安装证书

修改完毕后前往浏览器访问 代理IP:端口,点击 FiddlerRoot certficate下载证书

image-20221215150025007

然后在手机 设置安全 里选择 从SD卡中安装

image-20221215150345935

我们依次选择 内部存储空间Download,会看到 FidderRoot.crt,点击进行安装操作即可,名称随意。

image-20221215150345935

开始抓包

此时我们在模拟器中,访问一个页面,可以看到 FiddlerBurpsuite 均接收到了流量

image-20221215152200619

打开 校友邦 小程序,默认只能 微信登录 ,登录后在 设置退出登录 ,即可 手机号登录,可以通过 忘记密码 功能来重设密码。

image-20221225172318576

模拟登录

我们在账号密码登录的时候直接抓包,得到如下请求(已删除无用参数进行精简)

POST /login/login.action HTTP/1.1
Host: xcx.xybsyw.com
Connection: close
Content-Length: 39
charset: utf-8
content-type: application/x-www-form-urlencoded

username=手机号&password=md5(密码)

image-20221225173048459

响应如下,对我们有用的目前只发现 sessionIdloginId

{
  "code": "200",
  "data": {
    "activate": true,
    "sessionId": "******",
    "needComplete": false,
    "loginerId": ******,
    "phone": "手机号",
    "loginKey": "手机号"
  },
  "msg": "登录成功",
  "mstv": {
    "t": 时间戳,
    "m": "*****",
    "s": "*****",
  }
}

直接编写代码,实现登录功能(为方便演示代码使用的 Python ,但是感觉写起来没 PHP 顺手)

globalData = {'username' : 13888888888, 'password' : '密码'}

def md5(s):
    m = hashlib.md5()
    m.update(s.encode('utf-8'))
    return m.hexdigest()

def userLogin(u, p):
    url = 'https://xcx.xybsyw.com/login/login.action'
    data = {'username': u, 'password': md5(p)}
    r = requests.post(url, data=data)
    if r.json()['code'] == '200':
        globalData['sessionId'] = r.json()['data']['sessionId']
        globalData['loginerId'] = r.json()['data']['loginerId']
        return(globalData)
    else:
        return(r.json()['msg'])

获取信息

继续抓包,得到一个包含 个人信息 的请求

POST /account/LoadAccountInfo.action HTTP/1.1
Host: xcx.xybsyw.com
Connection: close
cookie: JSESSIONID=*******

image-20221225175855340

继续编写代码,请求中的 Cookie 来自 登录成功 返回的 sessionId,取出 姓名学校ID,后面会用到

def getUserInfo(c):
    url = 'https://xcx.xybsyw.com/account/LoadAccountInfo.action'
    headers = {'Cookie': 'JSESSIONID=' + c}
    r = requests.get(url, headers=headers)
    if r.json()['code'] == '200':
        globalData['username'] = r.json()['data']['loginer']
        globalData['schoolId'] = r.json()['data']['schoolId']
        return globalData
    else:
        return None

健康上报

image-20221227122206457

其中 healthCodeImg 为健康码照片,留空则为不提交,如果提交的话,直接填写图片url地址即可。

def postHealthData(c, img):
    url = 'https://xcx.xybsyw.com/student/clock/saveEpidemicSituation.action'
    data = {'healthCodeStatus': 0, 'locationRiskLevel': 0, 'healthCodeImg': img}
    headers = {'Cookie': 'JSESSIONID=' + c}
    r = requests.post(url, data=data, headers=headers)
    if r.json()['code'] == '200':
        return True
    else:
        return None

进行签到

进入签到页面,发现请求了一个接口,返回如下(已删除无用参数,只展示一下我们用得到的),发现提供了经纬度(由于自己签了一次,不知道经纬度是最初报名时的还是最后一次签到时的),不过这个接口也可以用作签到判断。

image-20221227123753129

{
  "code": "200",
  "data": {
    "clockInfo": {
      "inAddress": "签到地址",
      "inTime": "00:15:02", // 签到时间
      "outTime": "",        // 签退时间
    },
    "postInfo": {
    "address": "好像是报名时自行填写的地址",
    "lat": '维度',
    "lng": '经度'
    }
  }
}

请求该接口的时候提交了 traineeId 参数,翻一下历史流量找到了 traineeId 参数值的来源

image-20221227125702526

直接编写代码取出 traineeId

def getSignState(c, traineeId):
    url = 'https://xcx.xybsyw.com/student/clock/GetPlan!detail.action'
    data = {'traineeId': traineeId}
    headers = {'Cookie': 'JSESSIONID=' + c}
    r = requests.post(url, data=data, headers=headers)
    if r.json()['code'] == '200':
        return r.json()['data']
    else:
        return None

在从 实习成长 页面一步步进入 签到页面 中,每请起一次都会附带请求 behavior/Duration.action 接口,暂不知道用途,先模拟出来

def getUserIP():
    url = 'https://xcx.xybsyw.com/behavior/Duration!getIp.action'
    r = requests.get(url)
    if r.json()['code'] == '200':
        return r.json()['data']['ip']
    else:
        return None

def postSignData(d):
    url = 'https://app.xybsyw.com/behavior/Duration.action'
    data = {
        'app': 'wx_student',
        'appVersion': '1.6.36',
        'userId': d['loginerId'],
        'deviceToken': '',
        'userName': d['username'],
        'country': d['location']['country'],
        'province': d['location']['province'],
        'city': d['location']['city'],
        'deviceModel': 'microsoft',
        'operatingSystem': 'android',
        'operatingSystemVersion': '11',
        'screenHeight': '800',
        'screenWidth': '450',
        'eventTime': int(time.time()),
        'pageId': '2',
        'pageName': '成长',
        'pageUrl': 'pages/growup/growup',
        'eventType': 'click',
        'eventName': 'clickSignEvent',
        'clientIP': getUserIP(),
        'reportSrc': '2',
        'login': '1',
        'netType': 'WIFI',
        'itemID': 'none',
        'itemType': '其他'
    }
    headers = {'Cookie': 'JSESSIONID=' + d['sessionId']}
    r = requests.post(url, data=data, headers=headers)
    if r.json()['code'] == '200':
        return r.json()
    else:
        return None

至此,该获取的东西基本就获取到了,直接开始进行签到操作

def sign(c, traineeId, d):
    url = 'https://xcx.xybsyw.com/student/clock/Post!autoClock.action'
    data = {
        'traineeId': traineeId,
        'adcode': d['location']['adcode'],
        'lat': d['location']['lat'],
        'lng': d['location']['lng'],
        'address': d['location']['address'],
        'deviceName': 'microsoft',
        'punchInStatus': '1',
        'clockStatus': '2',
        'imgUrl': '',
        'reason': ''
    }
    headers = {'Cookie': 'JSESSIONID=' + c}
    r = requests.post(url, data=data, headers=headers)
    if r.json()['code'] == '200':
        return True
    else:
        return None

写在后面

2023/3/22 更新:关于加密逻辑可以前往 校友邦小程序签到加密逻辑解析