简单逆向XX写作平台的注册过程

感谢 @Chieri 提供的网站

话不多说我们直接开始!

首先看入口界面,顺手一不小心打开F12界面。

标准的手机号验证码框框。

那我们就随便乱填一个号看看请求

屏幕截图 2025-01-05 161528.png

哎呀,负载(我们发出的内容)居然是加密的,那就转到发起程序去看看。

屏幕截图 2025-01-05 161457.png

一眼定真,三段一长选最长,我们直接看与众不同的那行内容。

提示,调用堆栈是从下往上看,最底下那行是最早执行的内容。

哎呀是混淆过的内容,也正常,我们来看看名字叫C函数的内容吧

注意我打断点的位置,await E函数,几个单词好眼熟啊。mobile移动电话,scene场景,country_code国家代码。看不懂没关系,我们有断点了,直接跟进E函数看看。

吐槽(哇咔,captchaVerifyParam验证码验证参数居然直接为空,看起来是为后续开发准mai备lei啊。)

仔细观察发现文件开头有一行

import {d as I, o as i, f as y, w as v, j as k, h as e, a as S, r as $, m, b as U, aP as F, at as L, g as u, av as H, aw as D, br as P, i as V, N as T, au as j, a4 as q, q as f, eh as E} from "./index-19dffaa2.js";

其中内容规定eh as E 

所以我们去另一个js文件找eh

嗯?居然改名了,q7 as eh,那就从断点跟进到函数q7

function q7(e) {
return $e({
url: "/sms/send",
data: e
})
}

此时浏览器开发工具提示// e={"mobile": "133号码略","captchaVerifyParam": "","scene": "register","country_code": 86}

那么说明没找错地方, $e 函数 就是一个打包的发送函数,混淆过程将每一次请求都包裹起来了,这里也能从字面上看到url的接口位置,和网络那边的标头一致。

请求 URL:https://xingyuexiezuo.com/api/sms/send

现在再来看下一个函数

function $e({url: e, data: t, method: r="POST", headers: o, onDownloadProgress: n, signal: i, beforeRequest: s, afterRequest: a}) {
return Vy({
url: e,
method: r,
data: t,
headers: o,
onDownloadProgress: n,
signal: i,
beforeRequest: s,
afterRequest: a
})
}

好像就是解释一下而已的嘛,继续追踪!

Vy很长,后面我们会反复用到它


function Vy({url: e, data: t, method: r, headers: o, onDownloadProgress: n, signal: i, beforeRequest: s, afterRequest: a}, l=5) {
zn.defaults.baseURL || (zn.defaults.baseURL = Jd().url);
const c = d => {
const p = oc()
, h = Tf();
if (d.data.status === "Unauthorized" && (p.removeToken(),
h.resetUserInfo(),
Uy()),
d.data.code !== 200)
return Promise.reject(d.data);
const g = d.data.data
, x = Wd(g.encoded);// 注意到3
return x && (d.data.data = x),
d.data.status === "Success" || typeof d.data == "string" ? d.data : Promise.reject(d.data)
}
, u = d => {
throw a == null || a(),
new Error((d == null ? void 0 : d.message) || "Error")
}
, f = d => {
s == null || s();
let p = typeof t == "function" ? t() : t ?? {};
const h = {
headers: o,
signal: i,
onDownloadProgress: n
};
return r === "POST" && (p = {// 注意到1
data: zd(p)
}),// 注意到2
(r === "GET" ? zn.get(e, {
params: p,
...h
}) : zn.post(e, p, h)).then(c).catch(x => x.code > 0 ? u(x) : d < l ? (console.error(`Request failed, retrying... (${l - d} retries left)`),
f(d + 1)) : u(x))
}
;
return f(0)
}

观察注意到1的位置,我们看到熟悉的单词POST,发出验证码的请求就是POST类型,其中data:内容显然就是请求的负载。

zd函数内是什么呢,熟悉加密的朋友应该看出来了,AES.encrypt单词还不懂吗?看我断点处。

同样的,在注意到3的位置 Wd函数 是解码函数。

刚巧我又用过加密库,其原型格式是

CryptoJS.AES.encrypt(message, key, cfg).toString();

因此规律就破解了,输入信息e是一个对象,其值为e={"mobile": "13344445578","captchaVerifyParam": "","scene": "register","country_code": 86},钥匙key的值是VC,配置的cfg初始化按对象格式填写。

刚开始,在这里我还卡了一下。眼瞎没看到就在上两行明确定义了

const fo = NI(xF)
, vF = "chloefuckityoall"
, bF = "9311019310287172"
, VC = fo.enc.Utf8.parse(vF)
, qC = fo.enc.Utf8.parse(bF);

我最初傻傻的直接通过调试器看VC和qC对象的值(变量名肯定是混淆程序重命名了,没意义)。

分别是VC = {"words": [1667787887,1701213539,1802073209,1868655724],"sigBytes": 16}

和qC = {"words": [959656241,808532275,825242168,925972274],"sigBytes": 16}

因为AES 算法通常使用 128 位(16 字节)的密钥,显然4个数字是wordArray 类型,正好合适。

所以我直接在外部const VC = CryptoJS.lib.WordArray.create([1667787887, 1701213539, 1802073209, 1868655724], 16);新建一个值去测试。

我另外自己写了一个程序,然后JSON.stringify(e);序列化一个对象为字符,传值进去看看。

很好,输出的内容和网站的一致(我试了几个号码,所以不同于最上面截图,但是相同电话号码加密后的文本都是对的。)

如果用网页上的硬编码也一样enc.Utf8.parse从字符串转换密钥WordArray对象,比从数字创建省事一点。

那么,整个请求过程就破解了,整理格式削减多余的步骤得到如下

// 假设 CryptoJS 已额外加载
// 定义密钥和IV
const vF = "chloefuckityoall";
const bF = "9311019310287172";
const VC = CryptoJS.enc.Utf8.parse(vF);
const qC = CryptoJS.enc.Utf8.parse(bF);

// 定义配置参数
const cfg = {
iv: qC,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
};

// 定义加密方法
function zd(e) {
const t = JSON.stringify(e);
return CryptoJS.AES.encrypt(t, VC, cfg).toString();
}

// 定义发送请求的方法
async function sendSmsRequest(phone) {
const msgObj = {
"mobile": ""+phone,
"captchaVerifyParam": "",
"scene": "register",
"country_code": 86
};
const encryptedData = zd(msgObj);// 编码后的内容
const body = `{ "data": "${encryptedData}" }`;

try {
const response = await fetch("https://xingyuexiezuo.com/api/sms/send", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "*/*",
"Referer": "https://xingyuexiezuo.com/"
},
body: body
});

const result = await response.json();
if (result.status === "Success") {
console.log("验证码已发送", result);
} else {
console.error("发送验证码失败", result);
}
} catch (error) {
console.error("请求失败", error);
}
}

// 示例调用
sendSmsRequest("13344445555");

最新回复 (2)
  • 红红火火恍恍惚惚
    刑满释放 好人卡 风纪委员
    10月前 3
    0 举报

    我们再来看提交信息的函数吧,这次加速快点

    function yM(e) {
    return $e({
    url: "/user/register",
    data: e
    })
    }

    可见地址是url: "/user/register",其中的

    e = {
    "captcha": "2345",
    "mobile": "13344441234",
    "password": "1234abcd",
    "confirm_password": "1234abcd",
    "inviter": "19957",
    "cd_key": "",
    "country_code": 86
    }

    后面的内容就都一样了,都是去 Vy()函数 处理,然后CryptoJS.AES.encrypt得到值,再往请求体body的data:填充编码后的字符对象,发送即可。

    e的内容分别是,四位数验证号码,手机号,自设密码,确认密码,5位数用户id作邀请码,cd_key本来就是空的我不知道有什么样,国家电话代码

  • 红红火火恍恍惚惚
    刑满释放 好人卡 风纪委员
    10月前 2
    0 举报
    缩进格式全无好难受啊,辣鸡

    还有-8条回复,登录后查看更多!

返回
红红火火恍恍惚惚

刑满释放 好人卡 风纪委员