难度:★☆☆☆☆ 1星

一、目标

目标网站: https://www.500d.me/login/
登录的时候提交表单里密码字段enPassword是被加密的:
五百丁登录enPassword参数_enPassword
本次目标就是破解这个参数加密。

二、分析

打开登录页:
https://www.500d.me/login/
打开开发者工具,切换到Network,清空掉无关请求,然后页面里输入账号密码尝试登陆,注意账号密码是随便输的,是故意让它登陆失败观察一下流程的:
五百丁登录enPassword参数_enPassword_02
捕捉到了三个请求,先看下第一个Get请求,链接是:
https://www.500d.me/common/public_key/?_=1605590883257
响应内容:

{
    "modulus": "AM+emhhTb5EOH/ZbDg78dHOw79H4aFQkF4pCFCw9yo8oRigsa0p6bIB3UVjK+S5E1v3OSy1+/4WM10Zb+k+qV/7hK0GuoO2w15s+0nYJLjPC4SO8WmFgNC5aQsdHOPXt9hcK6sbJKh4dWR/U/pyfTOlp1IJqx4ZALyAf5sZN25Np",
    "exponent": "AQAB"
}
乍一看有点懵逼,其实如果之前搞过类似的话看一眼url里面的public_key和响应内容,大概就知道这应该是就是获取加密密码时使用的公钥了,密码也可能就是rsa加密的,我们借助ModHeader来测试一下这个请求是否对cookie或者referer做了检查,可以看到不带cookie和referer也是可以访问的:
五百丁登录enPassword参数_enPassword_03
好的继续看第二个Post请求:
https://www.500d.me/login/submit/
这个是实际提交登录参数的,因此提交了一个表单有用户名密码之类的参数:
五百丁登录enPassword参数_enPassword_04
第三个请求实际上是一个雪碧图,用于在页面上显示图标用的,这里不再详述。
通过观察请求大致捋出来了登录的流程,先是发送一个请求获取公钥,然后再用js加密密码提交登录表单,接下来的重点就在第二个请求发送前的js逻辑,接下就是想办法去定位到那段js代码,复制第二个请求的url,打一个xhr断点:
五百丁登录enPassword参数_enPassword_05
然后在页面上重新尝试登录,就卡在了断点这里,格式化代码,同时在调用栈里往前回溯寻找相关的栈帧:
五百丁登录enPassword参数_enPassword_06
在success方法的栈帧里看到了发出登录请求的代码,这个success是前面那个获取公钥的接口成功时的回调方法:
五百丁登录enPassword参数_enPassword_07
密码字段加密的核心逻辑:
var rsaKey = new RSAKey();
rsaKey.setPublic(b64tohex(data.modulus), b64tohex(data.exponent));
var enPassword = hex2b64(rsaKey.encrypt(form.find("input[name='password']").val()));
然后把鼠标放到RSAKey上悬停一会儿,会弹出弹窗表明出处,单击跟进入:
五百丁登录enPassword参数_enPassword_08
然后定位到了一个叫做rsa.js的文件,把这个文件整个抠出来新建一个文件encrypt.js放进去:
五百丁登录enPassword参数_enPassword_09
然后回到加密的方法,如法炮制找到b64tohex和hex2b64的逻辑:
五百丁登录enPassword参数_enPassword_10
五百丁登录enPassword参数_enPassword_11
这两个都是base64.js文件中,同样跟进去,然后整个抠出来放到encrypt.js,然后在encrypt.js中尝试写一个加密密码的方法为外部提供调用的接口:
/**
 * 向外界暴露加密密码的方法
 *
 * @param passwd
 * @param modulus
 * @param exponent
 * @returns {string|*}
 */
function encryptPasswd(passwd, modulus, exponent) {
    const rsaKey = new RSAKey();
    rsaKey.setPublic(b64tohex(modulus), b64tohex(exponent));
    return hex2b64(rsaKey.encrypt(passwd));
}

console.log(encryptPasswd("cc11001100", "{\n" +
    "    \"modulus\": \"AJbFLrvha10BPOdevQ+cuIDirMylI9srBg3MQe/3jG3FovKT3+/hSHPZbJljaOHnLHskJh1+r8ECwpJEU16xA73D+SbCcK83my+vMH2VLdP9w6eRfqkEfo+W/5yn7ZmNAnGTPlTC29I3b8cyVEuUHHO8HQGpgJXJp7FDbTjxgj6R\"\n" +
    "}", "AQAB"));
同时node运行测试一下是否OK,然后发现报错了,因为扣的js不全,有些依赖没有放到encrypt.js中,回到登录页面,查看源代码,搜索rsa定位到这里,把红色方框内没有扣的js一股脑儿扣了放到encrypt.js中:
五百丁登录enPassword参数_enPassword_12
都扣完了还是报错:
五百丁登录enPassword参数_enPassword_13
然后补环境,把下面的代码插入到encrypt.js的最前面:
// 补环境
const window = {
    navigator: {
        appName: "Netscape"
    }
};
const navigator = window.navigator;
然后再运行一下,能够加密出密码了:
五百丁登录enPassword参数_enPassword_14
把893行的测试代码注释掉,拷到pycharm里,接下来会在python中调用这个js加密密码。
这里还有一个需要注意的问题,就是在登录的时候提交登录表单的接口必须带上token请求头,如果不带的话就会405(方法不被允许: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status/405 ),使用ModHeader手动搞掉token试下:
五百丁登录enPassword参数_enPassword_15
可以看到405了:
五百丁登录enPassword参数_enPassword_16
这个token是用jQuery的ajaxSend方法设置的一个hook里设置到请求头上的,jQuery的ajaxSend可以设置一个函数在ajax发送之前运行,通过正则搜索“header.{0,100}token”,表示搜索出现在header附近的token,因为js设置的话有可能就是这样设置上的:
五百丁登录enPassword参数_enPassword_17
于是就定位到了这个文件:
https://static.500d.me/resources/500d/js/utils.js?v=V7119
文件的这个位置,就是在这里设置的请求头:
五百丁登录enPassword参数_enPassword_18
这个名为token的cookie是在第一次访问登录页的时候设置的cookie:
五百丁登录enPassword参数_enPassword_19
访问一次登录页拿到这个token就好。
流程基本捋清楚了,接下来是编码实现。

 

三、编码实现
#!/usr/bin/env python3
# encoding: utf-8
"""
@author: CC11001100
"""
import functools
import time

import execjs
import requests

session = requests.session()


def login(username, passwd):
    token = get_token()
    print(f"token = {token}")

    public_key = get_public_key()
    print(f"public key = {public_key}")

    data = {
        "username": username,
        "enPassword": load_js_context().call("encryptPasswd", passwd, public_key["modulus"], public_key["exponent"]),
        "service": "",
        "remember": True
    }
    print(data)

    headers = {
        # 这个参数是必须要带的,什么鬼情况,是后端的框架要检测还是手动做的检测,好像有些框架需要这个参数知道是个xhr请求
        "X-Requested-With": "XMLHttpRequest",
        # U-A反倒不是必须的...
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36",
        # token是必须要带的
        "token": token,
    }

    url = "https://www.500d.me/login/submit/"
    r = session.post(url, data=data, headers=headers)
    print(r.status_code)  # 200
    print(r.text)  # {"type":"success","content":""}


@functools.lru_cache(maxsize=1)
def load_js_context():
    with open("./encrypt.js", encoding="UTF-8") as f:
        js_code = f.read()
        return execjs.compile(js_code)


def get_token():
    url = "https://www.500d.me/login/"
    return session.get(url).cookies["token"]


def get_public_key():
    url = f"https://www.500d.me/common/public_key/?_={int(time.time() * 1000)}"
    return session.get(url).json()


if __name__ == "__main__":
    # 注册资料:
    # 邮箱: korowof384@rvemold.com
    # 昵称: cc11001100_test
    # 密码: ccIs0KccIs0K
    login("korowof384@rvemold.com", "ccIs0KccIs0K")

 

仓库:

https://github.com/CC11001100/misc-crawler-public/tree/master/001-anti-crawler-js-re/01-005-www.500d.me

 

请注意爬虫文章具有时效性,本文写于2020-11-17日。