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

相关