meterSphere(七)接口请求中参数或数据被加密,js逆向操作
需求
登录接口中,增加了x_t和x_v安全校验,且请求数据被加密,
需要能够动态的计算x_t和x_v的值,并且找到data的加密算法,直接明文请求接口是不通的
实现思路
需要找到js加密的算法,在接口请求之前,先调用加密算法将对应数据加密,然后再去请求
实现方式
在chrome中,打开开发者工具,点击到network,先清除所有的记录
点击登录,点击抓到的登录的url,找到需要处理js逆向的字段
不是所有加密的字段都需要单独处理,加密的且每次都会发生变化的字段,才是我们要处理的,可多次请求登录接口,找到这些要单独处理的字段
经过多次请求,我确定x_t、x_v和data是需要单独处理的
点击开发者工具右上角三个点,点击search,搜索字段,找到可能的js文件
点击疑似的js文件,点击大括号将其美化
在可疑的位置打上断点,然后重新请求登录接口,一步步调试,即可找到对应的加密算法
整理加密算法
有些用法,是在浏览器请求中才会有的,直接放到meterSphere中执行会报错,比如以下代码:
function fab(a, b) {
var d;
c = app.session.tk || app.session.token;
a.startsWith("http") ? a = (new URL(a)).pathname: (a.startsWith("/") || (a = "/" + a), a = (new URL("http://x" + a)).pathname);
d = encodeURIComponent(a);
app.isButtonFlash && (a = app.flashButton) && this.joinFlashButton(a);
var test = "hehe" + b + c + d + b + d + c + "haha";
return encodeURIComponent(f(window.btoa("cyber" + b + c + d + b + d + c + "trans")))
}
问题一:app.session不支持
以上算法中有一句是:
c = app.session.tk || app.session.token;
meterspherer中没有这个东西,后来调试发现,这个就是登录接口的token,然后索性将c当成一个参数,调用函数的时候,先在登录接口中,把token提取出来,然后作为参数传给这个函数,代码改为:
function fab(a, b,c) {
var d;
// c = app.session.tk || app.session.token;
// a.startsWith("http") ? a = (new URL(a)).pathname: (a.startsWith("/") || (a = "/" + a), a = (new URL("http://x" + a)).pathname);
d = encodeURIComponent(a);
// app.isButtonFlash && (a = app.flashButton) && this.joinFlashButton(a);
var test = "hehe" + b + c + d + b + d + c + "haha";
return encodeURIComponent(f(window.btoa("hehe" + b + c + d + b + d + c + "haha")))
}
问题二:window.btoa不支持
最开始本来是用的Base64.encode,后来发现,Base64.encode和window.btoa加密出来的结果竟然不一样,百度了很久,找到了btoa的算法
var base64hash = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
function btoa(s) {
if (/([^\u0000-\u00ff])/.test(s)) {
throw new Error('INVALID_CHARACTER_ERR');
}
var i = 0,
prev, ascii, mod, result = [];
while (i < s.length) { ascii = s.charCodeAt(i); mod = i % 3; switch (mod) { case 0: result.push(base64hash.charAt(ascii >> 2));
break;
case 1:
result.push(base64hash.charAt((prev & 3) << 4 | (ascii >> 4)));
break;
case 2:
result.push(base64hash.charAt((prev & 0x0f) << 2 | (ascii >> 6)));
result.push(base64hash.charAt(ascii & 0x3f));
break
}
prev = ascii;
i++
}
if (mod == 0) {
result.push(base64hash.charAt((prev & 3) << 4));
result.push('==')
} else if (mod == 1) {
result.push(base64hash.charAt((prev & 0x0f) << 2));
result.push('=')
}
return result.join('')
}
//atob 方法
// 逆转encode的思路即可
function _atob(s) {
s = s.replace(/\s|=/g, '');
var cur, prev, mod, i = 0,
result = [];
while (i < s.length) {
cur = base64hash.indexOf(s.charAt(i));
mod = i % 4;
switch (mod) {
case 0:
//TODO
break;
case 1:
result.push(String.fromCharCode(prev << 2 | cur >> 4));
break;
case 2:
result.push(String.fromCharCode((prev & 0x0f) << 4 | cur >> 2));
break;
case 3:
result.push(String.fromCharCode((prev & 3) << 6 | cur));
break;
}
prev = cur;
i++;
}
return result.join('');
}
然后在代码中,将
window.btoa("hehe" + b + c + d + b + d + c + "haha")
改为
btoa("hehe" + b + c + d + b + d + c + "haha")
全部整理完的加密算法
点击查看代码
var Base64 = {
// private property
_keyStr: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
// public method for encoding
,
encode: function(input) {
var output = "";
var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
var i = 0;
input = Base64._utf8_encode(input);
while (i < input.length) {
chr1 = input.charCodeAt(i++);
chr2 = input.charCodeAt(i++);
chr3 = input.charCodeAt(i++);
enc1 = chr1 >> 2;
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
enc4 = chr3 & 63;
if (isNaN(chr2)) {
enc3 = enc4 = 64;
} else if (isNaN(chr3)) {
enc4 = 64;
}
output = output + this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) + this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4);
} // Whend
return output;
} // End Function encode
// public method for decoding
,
decode: function(input) {
var output = "";
var chr1, chr2, chr3;
var enc1, enc2, enc3, enc4;
var i = 0;
//input = input.replace(/[^A-Za-z0-9+/ = ] / g,"");
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
while (i < input.length) {
enc1 = this._keyStr.indexOf(input.charAt(i++));
enc2 = this._keyStr.indexOf(input.charAt(i++));
enc3 = this._keyStr.indexOf(input.charAt(i++));
enc4 = this._keyStr.indexOf(input.charAt(i++));
chr1 = (enc1 << 2) | (enc2 >> 4);
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
chr3 = ((enc3 & 3) << 6) | enc4;
output = output + String.fromCharCode(chr1);
if (enc3 != 64) {
output = output + String.fromCharCode(chr2);
}
if (enc4 != 64) {
output = output + String.fromCharCode(chr3);
}
} // Whend
output = Base64._utf8_decode(output);
return output;
} // End Function decode
// private method for UTF-8 encoding
,
_utf8_encode: function(string) {
var utftext = "";
string = String(string).replace(/rn/g, "n");
for (var n = 0; n < string.length; n++) {
var c = string.charCodeAt(n);
if (c < 128) {
utftext += String.fromCharCode(c);
} else if ((c > 127) && (c < 2048)) {
utftext += String.fromCharCode((c >> 6) | 192);
utftext += String.fromCharCode((c & 63) | 128);
} else {
utftext += String.fromCharCode((c >> 12) | 224);
utftext += String.fromCharCode(((c >> 6) & 63) | 128);
utftext += String.fromCharCode((c & 63) | 128);
}
} // Next n
return utftext;
} // End Function _utf8_encode
// private method for UTF-8 decoding
,
_utf8_decode: function(utftext) {
var string = "";
var i = 0;
var c, c1, c2, c3;
c = c1 = c2 = 0;
while (i < utftext.length) {
c = utftext.charCodeAt(i);
if (c < 128) {
string += String.fromCharCode(c);
i++;
} else if ((c > 191) && (c < 224)) {
c2 = utftext.charCodeAt(i + 1);
string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
i += 2;
} else {
c2 = utftext.charCodeAt(i + 1);
c3 = utftext.charCodeAt(i + 2);
string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
i += 3;
}
} // Whend
return string;
} // End Function _utf8_decode
};
function addWords(c, d) {
var a = (c & 65535) + (d & 65535);
return (c >> 16) + (d >> 16) + (a >> 16) << 16 | a & 65535
};
outputTypes = {
Base64: 0,
Hex: 1,
String: 2,
Raw: 3
};
function toBase64(c) {
for (var b = [], a = 0, d = 4 * c.length; a < d; a += 3) for (var f = (c[a >> 2] >> 8 * (3 - a % 4) & 255) << 16 | (c[a + 1 >> 2] >> 8 * (3 - (a + 1) % 4) & 255) << 8 | c[a + 2 >> 2] >> 8 * (3 - (a + 2) % 4) & 255, g = 0; 4 > g; g++) 8 * a + 6 * g > 32 * c.length ? b.push("\x3d") : b.push("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charAt(f >> 6 * (3 - g) & 63));
return b.join("")
}
var d = addWords,
y = [1116352408, 1899447441, 3049323471, 3921009573, 961987163, 1508970993, 2453635748, 2870763221, 3624381080, 310598401, 607225278, 1426881987, 1925078388, 2162078206, 2614888103, 3248222580, 3835390401, 4022224774, 264347078, 604807628, 770255983, 1249150122, 1555081692, 1996064986, 2554220882, 2821834349, 2952996808, 3210313671, 3336571891, 3584528711, 113926993, 338241895, 666307205, 773529912, 1294757372, 1396182291, 1695183700, 1986661051, 2177026350, 2456956037, 2730485921, 2820302411, 3259730800, 3345764771, 3516065817, 3600352804, 4094571909, 275423344, 430227734, 506948616, 659060556, 883997877, 958139571, 1322822218, 1537002063, 1747873779, 1955562222, 2024104815, 2227730452, 2361852424, 2428436474, 2756734187, 3204031479, 3329325298];
function digest(c, b, a, m) {
a = a.slice(0);
var n = Array(64),
g,
q,
v,
p,
t,
u,
k,
w,
e,
h,
l;
c[b >> 5] |= 128 << 24 - b % 32;
c[(b + 64 >> 9 << 4) + 15] = b;
for (w = 0; w < c.length; w += 16) {
b = a[0];
g = a[1];
q = a[2];
v = a[3];
p = a[4];
t = a[5];
u = a[6];
k = a[7];
for (e = 0; 64 > e; e++) {
if (16 > e) n[e] = c[e + w];
else {
h = e;
l = n[e - 2];
l = f(l, 17) ^ f(l, 19) ^ l >>> 10;
l = d(l, n[e - 7]);
var r;
r = n[e - 15];
r = f(r, 7) ^ f(r, 18) ^ r >>> 3;
n[h] = d(d(l, r), n[e - 16])
}
h = p;
h = f(h, 6) ^ f(h, 11) ^ f(h, 25);
h = d(d(d(d(k, h), p & t ^ ~p & u), y[e]), n[e]);
k = b;
k = f(k, 2) ^ f(k, 13) ^ f(k, 22);
l = d(k, b & g ^ b & q ^ g & q);
k = u;
u = t;
t = p;
p = d(v, h);
v = q;
q = g;
g = b;
b = d(h, l)
}
a[0] = d(b, a[0]);
a[1] = d(g, a[1]);
a[2] = d(q, a[2]);
a[3] = d(v, a[3]);
a[4] = d(p, a[4]);
a[5] = d(t, a[5]);
a[6] = d(u, a[6]);
a[7] = d(k, a[7])
}
224 == m && a.pop();
return a
};
function toWord(c) {
for (var b = Array(c.length >> 2), a = 0; a < b.length; a++) b[a] = 0;
for (a = 0; a < 8 * c.length; a += 8) b[a >> 5] |= (c.charCodeAt(a / 8) & 255) << 24 - a % 32;
return b
};
function f(c, b) {
return c >>> b | c << 32 - b
}
//encode,window.btoa和Base64.encode加密的结果不一样,不能直接用Base64.encode
var base64hash = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
function btoa(s) {
if (/([^\u0000-\u00ff])/.test(s)) {
throw new Error('INVALID_CHARACTER_ERR');
}
var i = 0,
prev, ascii, mod, result = [];
while (i < s.length) { ascii = s.charCodeAt(i); mod = i % 3; switch (mod) { case 0: result.push(base64hash.charAt(ascii >> 2));
break;
case 1:
result.push(base64hash.charAt((prev & 3) << 4 | (ascii >> 4)));
break;
case 2:
result.push(base64hash.charAt((prev & 0x0f) << 2 | (ascii >> 6)));
result.push(base64hash.charAt(ascii & 0x3f));
break
}
prev = ascii;
i++
}
if (mod == 0) {
result.push(base64hash.charAt((prev & 3) << 4));
result.push('==')
} else if (mod == 1) {
result.push(base64hash.charAt((prev & 0x0f) << 2));
result.push('=')
}
return result.join('')
}
// 逆转encode的思路即可
function _atob(s) {
s = s.replace(/\s|=/g, '');
var cur, prev, mod, i = 0,
result = [];
while (i < s.length) {
cur = base64hash.indexOf(s.charAt(i));
mod = i % 4;
switch (mod) {
case 0:
//TODO
break;
case 1:
result.push(String.fromCharCode(prev << 2 | cur >> 4));
break;
case 2:
result.push(String.fromCharCode((prev & 0x0f) << 4 | cur >> 2));
break;
case 3:
result.push(String.fromCharCode((prev & 3) << 6 | cur));
break;
}
prev = cur;
i++;
}
return result.join('');
}
function fab(a, b, c) {
var d;
// c = app.session.tk || app.session.token;
// c = '86c43a8bc5bb4daeba896c7dc6cf0822';
//c = '';
// a.startsWith("http") ? a = (new URL(a)).pathname: (a.startsWith("/") || (a = "/" + a), a = (new URL("http://x" + a)).pathname);
d = encodeURIComponent(a);
//app.isButtonFlash && (a = app.flashButton) && this.joinFlashButton(a);
//return encodeURIComponent(f(window.btoa("cyber" + b + c + d + b + d + c + "trans")))
//return window.btoa("cyber" + b + c + d + b + d + c + "trans"))_
var test = "hehe" + b + c + d + b + d + c + "trans";
//e = encodeURIComponent(f(Base64.encode(test)));
// e = Base64.encode(test);
e = btoa(test);
return e;
}
function final(b, c) {
c = c || 0;
h_list = [1779033703, 3144134277, 1013904242, 2773480762, 1359893119, 2600822924, 528734635, 1541459225];
f_num = 256;
b = digest(toWord(b), 8 * b.length, h_list, f_num);
switch (c) {
case outputTypes.Raw:
return b;
case outputTypes.Hex:
return toHex(b);
case outputTypes.String:
return _toString(b);
default:
return toBase64(b)
}
}
function count_final(a, b, c) {
return encodeURIComponent(final(fab(a, b, c)));
}
js代码上传到meterSpere
方式一:直接将以上加密的代码丢到全局前置脚本中
然后得到的加密数据,加到全局变量中,请求接口中,就可以直接用${x_t}和${x_v}调用
a = sampler.getPath();
b = (new Date()).getTime();
c = vars.get("token");
x_v = count_final(a,b,c);
vars.put("x-t",b);
vars.put("x-v",x_v);
- 但是由于之前已经在全局前置脚本中,写了很多java代码,又不想重新用java写一遍加密代码,所以,这种方式,pass
方式二:将js文件上传到服务器在全局前置脚本中,用java执行js脚本
点击查看代码
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.Date;
import java.io.File;
import org.apache.jmeter.protocol.http.control.HeaderManager;
import org.apache.jmeter.protocol.http.control.Header;
HeaderManager headers = sampler.getHeaderManager();
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("javascript");
FileReader reader = new FileReader("/opt/metersphere/data/x_v.js");
engine.eval(reader);
if(engine instanceof Invocable){
Invocable invoke = (Invocable)engine;
// Date date_now = new Date();
String a_url_path = sampler.getPath();
// log.info(url_path);
// 标准API接口不需要计算
if(a_url_path !="/api/ofs/in"){
String b_time = String.valueOf(date.getTime());
String c_token = vars.get("token");
// time = "1642500741868";
String countV = (String)invoke.invokeFunction("count_final",a_url_path,b_time,c_token);
log.info("x_v="+countV);
vars.put("x-v",countV);
vars.put("x-t",b_time);
}
}
reader.close();
-
最开始本来是在【项目设置-》文件管理】中上传了js代码,但是java中一直读不到这个文件,查找文件是在/deployments目录下,但是调用一直报找不到文件,meterSphere官方给出的解释是目前还不支持;
-
然后是直接将js代码上传到后台的/opt/metersphere/data中(此处有一个坑,不能直接上传,要用编辑器编辑上传),调用以上代码,成功执行
其他
之前所有的接口请求头中都没有x_t和x_v,所以本来是在全局前置脚本中添加了以下代码,自动在请求通中添加
点击查看代码
// 其实可以用以下方式自动加请求头,但是不知道为什么,加上之后一直报“服务端异常: null”,因而还是在接口的请求头中加
x_t_header = new Header("x_t",b_time);
x_v_header = new Header("x_v",countV);
x_cbtVersion = new Header("x-cbtVersion",vars.get("x-cbtVersion"));
headers.add(x_t_header);
headers.add(x_v_header);
headers.add(x_cbtVersion);
加完之后,meterSphere的接口请求报文的请求头中,虽然已经有这几个字段,但是实际接口还是没有调通,感觉是接口请求完了之后,才在请求头中加的字段,所以,最后还是手工修改了接口,在请求头中手工加上了这几个字段,
总结
- 首先,要调试代码,找到对应的js文件的加密算法
- 修改不能直接直接运行的代码,比如session、windows之类的
- 调试工具中调试,看算出的结果跟实际请求中是否相同
- 如果meterSphere中,能够直接调用文件管理中上传的js文件,就会简单很多,特别是当搭建meterSphere和使用meterSphere的不是相同的人时,就会减少很多麻烦
附:好用的调试工具,“发条js调试工具”
参考文档:https://blog.52nyg.com/2020/01/494