【JavaScript 逆向】最新某米商城登录逆向,user,hash 参数解密

前言

        现在一些网站对 JavaScript 代码采取了一定的保护措施,比如变量名混淆、执行逻辑混淆、反调试、核心逻辑加密等,有的还对数据接口进行了加密,这次的案例就是对一种 AES 加密方式的破解。

        AES 是对称加密,对称加密是指加密和解密时使用同一个密钥,这种加密方式加密速度非常快,适合经常发送数据的场合,缺点是密钥的传输比较麻烦。

AES 相关资料可参考:对称加密及 AES 加密算法

很推荐阅读这篇博客:一文彻底搞懂加密、数字签名和数字证书

声明

        本文章中所有内容仅供学习交流,相关链接做了脱敏处理,若有侵权,请联系我立即删除!

案例目标

网址:aHR0cHM6Ly9hY2NvdW50LnhpYW9taS5jb20vZmUvc2VydmljZS9sb2dpbi9wYXNzd29yZD9fbG9jYWxlPXpoX0NO

登录接口:aHR0cHM6Ly9hY2NvdW50LnhpYW9taS5jb20vcGFzcy9zZXJ2aWNlTG9naW5BdXRoMg==

以上均做了脱敏处理,Base64 编码及解码方式:

import base64
# 编码
# result = base64.b64encode('待编码字符串'.encode('utf-8'))
# 解码
result = base64.b64decode('待解码字符串'.encode('utf-8'))
print(result)

常规 JavaScript 逆向思路

一般情况下,JavaScript 逆向分为三步:

  1. 寻找入口:逆向在大部分情况下就是找一些加密参数到底是怎么来的,关键逻辑可能写在某个关键的方法或者隐藏在某个关键的变量里,一个网站可能加载了很多 JavaScript 文件,如何从这么多的 JavaScript 文件的代码行中找到关键的位置,很重要
  2. 调试分析:找到入口后,我们定位到某个参数可能是在某个方法中执行的了,那么里面的逻辑是怎么样的,调用了多少加密算法,经过了多少赋值变换,需要把整体思路整理清楚,以便于断点或反混淆工具等进行调试分析
  3. 模拟执行:经过调试分析后,差不多弄清了逻辑,就需要对加密过程进行逻辑复现,以拿到最后我们想要的数据

接下来开始正式进行案例分析:

寻找入口 

        进入到某米商城的登录页面,F12 打开开发者人员工具,切换到 network 准备查看网络抓包请求,随便输入一个账号、密码,查看网络抓包请求情况,如下图,可以看到抓包到的这条数据,请求方式为 POST,Form Data 中存在一些表单 json 格式提交的参数信息,同时在 Preview 响应预览中可以看到验证失败提示,证明这里就是点击登录返回响应的位置,即找到了登录的入口:

接下来进一步分析响应头中的信息,可以看到请求方式是 POST,响应状态码为 200,不存在以前的 302 重定向情况:

        那么我们直接观察 Headers 中响应的参数情况(Form Data),直接观察得不出结论,我们可以再输入一个账号、密码,将新抓包到的接口数据来与之前的作对比: 

        由上图可知,hash 和 user 的值是变化的,其他的都是定值,可以初步推断这两个值是用户名和密码经过加密后得到的结果,接下来我们就需要寻找这两个参数的加密位置,先来定位 hash 的位置,CTRL+SHIFT+F 全局搜索 hash + 冒号,会出现好几个结果,点击 DHome~DSNS.ddb02494.chunk.js 进入,再 CTRL+F 局部搜索 hash + 冒号,可以看到只有一条结果:

调试分析

hash 参数

        以上 2865 行 hash: 后 j()(a.password).toUpperCase() 这个方法看起来就很像字符串经过加密之后转换成了大写,我们在这打断点进一步调试分析看看,在这行打下断点后,再次点击登录按钮,会发现成功断住了,证明这里就是登录响应的位置:

        鼠标悬停在 a.password 上会出现一串明文结果:123456,这就是我们输入的密码内容,至此为密码加密的位置,a.password 处是明文密码内容,那么加密过程肯定是在 j() 函数中进行的,整体选中 j() 进入到其的构造位置 DHome~DSNS.ddb02494.chunk.js:formatted:3821:

在 3852 行 return 处打下断点进行调试,F8 或点击以下按钮(resume script execution)执行到下一个断点位置,可以看到成功断住,e 为明文密码:

        return 中的值存在着一定的混淆,这里是个三目运算方法,由上图可知,n 是未定义的,所以前面的值皆为 false,最后 return 的值为 t.bytesToHex(r),鼠标悬停在它上面,可以看到是密码加密后的值:

鼠标悬停在 t.bytesToHex 上,进入到这个方法的构造位置,DHome~DSNS.ddb02494.chunk.js:formatted:921:

        这里先调用 wordsToBytes() 方法将明文密码字符串转为 byte 数组,无论密码的长度如何,最后得到的 byte 数组都是 16 位的,然后调用 bytesToHex() 方法,循环遍历生成的 byte 类型数组,让其生成 32 位字符串,无论密码长度如何,最终得到的密文都是 32 位的,而且都由字母和数字组成,这里可以推测为 MD5 加密,接下来进行验证:

123456 加密后的值为:e10adc3949ba59abbe56e057f20f883e,以下用 MD5 对 123456 进行加密测试,可以看到结果是一样的,验证了我们的猜想:

# python 复现
import hashlib
password = '123456'
encrypted_password = hashlib.md5(password.encode(encoding='utf-8')).hexdigest().upper()
print(encrypted_password)
# e10adc3949ba59abbe56e057f20f883e

以上思路参考:K哥爬虫 

user 参数

之前某米商城的登录参数中 user 的值是明文显示的,现在对其进行了加密处理,现在对这个参数的加密方式进行调试分析:

在刚刚打断点的地方上面 2863 行,鼠标悬停后可以看到,user 的值在这里已经被加密过了:

一步步跟踪 v 到 f 到 u 的构造位置,可以看到 u = l.encryptAes,‘AES’?这是暗示还是明示呢,我们进一步跟踪进去看看,鼠标悬停跟踪进:

在函数末尾打断点调试看看,可以看到在 2982 行,user 已经被加密完毕了:

从 2975 行开始,这是个很明显的 AES 加密结构:

  • iv:偏移量或初始向量,与密钥结合使用,作为加密数据的手段,它是一个固定长度的值,iv 的长度取决于加密方法,通常与使用的加密密钥或密码块的长度相当,一般在使用过程中会要求它是随机数或拟随机数

  • padding:填充方式,块密码只能对确定长度的数据块进行处理,而消息的长度通常是可变的,因此部分模式最后一块数据在加密前需要进行填充

  • 更多相关知识可参考:爬虫常见加密解密算法

        底下这一部分,看起来很像 RSA 加密算法中的公钥内容,但是仔细观察会发现,只有 EUI 对象调用了 h 这个变量,而且通过逐行调试会发现 user 的结果在 g[e] = i 的时候就加密生成了,所以可以忽略这部分的内容:        

接下来可以通过 JavaScript 对其加密过程进行复现: 

var CryptoJS = require('crypto-js');

function Pt(t) {
  t = t || {};
  var i = function(t) {
      for (var e = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*", r = "", i = 0; i < t; i++) {
          var n = Math.floor(Math.random() * e.length);
          r += e.substring(n, n + 1)
      }
      return r
  }(16)

  var u = CryptoJS.enc.Utf8.parse("0102030405060708")
    , f = CryptoJS.enc.Utf8.parse(i);
    var result;
    Object.keys(t).forEach(function(e) {
      var r = t[e]
        , i = CryptoJS.AES.encrypt(r, f, {
          iv: u,
          padding: CryptoJS.pad.Pkcs7
      });
      result = i.toString()
  });
  return result
}

console.log(Pt({user: "123333"}));

以下及成功复现: 

        我们可以进一步对其进行校验,比如将断点打在 2966 行,就会看到 i 的值在加密之前的值,再将这个值写入到 f = CryptoJS.enc.Utf8.parse(i); 这行代码中,即将密钥 f 的值写死,看最后生成的加密结果是否和调试过程中显示的一样,改写后的代码如下:

var CryptoJS = require('crypto-js');

function Pt(t) {
  t = t || {};
  var i = function(t) {
      for (var e = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*", r = "", i = 0; i < t; i++) {
          var n = Math.floor(Math.random() * e.length);
          r += e.substring(n, n + 1)
      }
      return r
  }(16)

  var u = CryptoJS.enc.Utf8.parse("0102030405060708")
    , f = CryptoJS.enc.Utf8.parse("%9eGpDwU*VkBQTBV");
    var result;
    Object.keys(t).forEach(function(e) {
      var r = t[e]
        , i = CryptoJS.AES.encrypt(r, f, {
          iv: u,
          padding: CryptoJS.pad.Pkcs7
      });
      result = i.toString()
  });
  return result
}

console.log(Pt({user: "123456"}));

然后在 2967 行开始逐行调试,直到得出 i 加密后的值,对比验证可以发现加密结果值一样,逻辑复现正确,user 参数逆向解决: 

完整代码

import json
import hashlib
import urllib.parse
import execjs
import requests

login_url = 'aHR0cHM6Ly9hY2NvdW50LnhpYW9taS5jb20vcGFzcy9zZXJ2aWNlTG9naW5BdXRoMg=='
headers = {
    'Host': '去 login_url 的请求头中复制即可',
    'Origin': '去 login_url 的请求头中复制即可',
    'Referer': '去 login_url 的请求头中复制即可',
    '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'
}
session = requests.session()


def get_encrypted_password(password):
    encrypted_password = hashlib.md5(password.encode(encoding='utf-8')).hexdigest().upper()
    return encrypted_password


def get_encrypted_uesr(username):
    with open('js_xiaomi.js', 'r', encoding='utf-8') as f:
        js_xm = f.read()
    user_param = execjs.compile(js_xm).call('Pt', username)
    return user_param


def get_parameter():
    referer_url = 'Referer 的值'
    urlparse = urllib.parse.urlparse(referer_url)
    query_dict = urllib.parse.parse_qs(urlparse.query)
    return query_dict


def login(username, encrypted_password, query_dict):
    data = {
        'bizDeviceType': '',
        'needTheme': query_dict['needTheme'][0],
        'theme': '',
        'showActiveX': query_dict['showActiveX'][0],
        'serviceParam': query_dict['serviceParam'][0],
        'callback': query_dict['callback'][0],
        'qs': query_dict['qs'][0],
        'sid': query_dict['sid'][0],
        '_sign': query_dict['_sign'][0],
        'user': username,
        'cc': '+86',
        'hash': encrypted_password,
        '_json': True,
        'policyName': 'miaccount',
        'captCode': ''
    }
    response = session.post(url=login_url, data=data, headers=headers)
    response_json = json.loads(response.text.replace('&&&START&&&', ''))
    print(response_json)
    return response_json


def main():
    username = '你的用户名'
    password = '你的密码'
    encrypted_password = get_encrypted_password(password)
    encrypted_username = get_encrypted_uesr(username)
    parameter = get_parameter()
    login(encrypted_username, encrypted_password, parameter)


if __name__ == '__main__':
    main()

迷惑现象

        在结果验证的时候,发现了一个神奇的现象,user 参数的值明显经过加密,我们刚刚也测试了代码的加密逻辑是正确的,但是我们将 user 加密后的值传入到参数字典中,运行结果显示验证失败:

而直接将明文 user 传递到参数字典中,则能验证成功,以下链接部分点进去是安全验证,这里可以通过对发送验证码过程的抓包,过短信验证码实现登陆:

 

好的,接下来更神奇的事情出现了,我写了个循环登录:

for _ in range(3):
    login(username, encrypted_password, parameter)

按道理每次都应该返回的是安全验证页面,但是经过多次尝试,循环登录三次只有第一次会出现安全验证,后面几次都能登陆成功,点击链接能成功跳转到个人账号信息页面:

总结

        以上是对某米商城最新加密参数的逆向分析,最后实验结果很令人迷惑,明明是加密的,却只需要传输明文即可,而且循环几次即可过安全验证 

        如有任何见解欢迎评论区或私信指正交流~

 

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 15
    评论
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值