简介
之前买了一个Redmi路由器AX5,个人感觉总体的性价比还是可以的,稳定性也还ok,连续运行了一个多月也没有关机重启过什么的,家里设备也是比较偏多的那种。但是一直看不到路由器的负载,也不能采集上传下载的带宽信息,这个就很烦,因为这样就不知道家里什么时候网络带宽高什么时候网络带宽低,所以就想看看这个后台能不能有接口可以采集,一看没想到还真的有
问题分析
首先f12看了一下登陆的接口
接口地址http://10.10.100.1/cgi-bin/luci/api/xqsystem/login
请求的参数
- username: admin
- password: e427b4ff49d2d16a185e632be8aa3ecb12edab92
- logtype: 2
- nonce: 0_50:7b:9d:3f:f8:47_1600657503_8385
关键就是分析请求的参数
用户名没什么好说的,密码加了一下密,logtype看了一下就是固定的,这个nonce就很有意思了0_
这里是固定的 50:7b:9d:3f:f8:47
这一段明显是mac地址,看了一下本机的mac地址发现就是本地电脑的mac,不是路由器的mac _1600657503_8385
这一段感觉是时间加后面4个不知道是什么的数字,看了一下unix时间发现1600657503这个的确是时间
那么现在的问题就是获取password和nonce的生成方式就ok了,password看了一下每次加密之后的结果都是不一样的,那么就不是普通的加密,推断里面包含了时间再去加密的。
看js代码
因为是路由器,所以判断肯定是直接js加密的,所以直接看js代码就好了,看密码的输入框代码
<input id="password" class="ipt-text" type="password" name="router_password" autocomplete="off" placeholder="请输入路由器管理密码" reqmsg="请输入路由器管理密码">
之后找id="password"
的js函数,找到了下满这段
$(function(){
var pwdErrorCount = 0;
$( '#password' ).focus();
$( '#password' ).on( 'keypress', function( e ) {
$('#rtloginform .form-item' ).removeClass( 'form-item-err' );
$('#rtloginform .form-item .t' ).hide();
});
function buildUrl( s, token ){
if (!window.location.origin){
window.location.origin = window.location.protocol+"//"+window.location.host;
}
return window.location.origin + '/cgi-bin/luci/;stok=' + token+ '/web/setting/' + s;
}
function loginHandle ( e ) {
e.preventDefault();
var formObj = document.rtloginform;
var pwd = $( '#password' ).val();
if ( pwd == '') {
return;
}
var nonce = Encrypt.init();
var oldPwd = Encrypt.oldPwd( pwd );
var param = {
username: 'admin',
password: oldPwd,
logtype: 2,
nonce: nonce
};
$.pub('loading:start');
var url = '/cgi-bin/luci/api/xqsystem/login';
$.post( url, param, function( rsp ) {
$.pub('loading:stop');
var rsp = $.parseJSON( rsp );
if ( rsp.code == 0 ) {
var redirect,
token = rsp.token;
if ( /action=wan/.test(location.href) ) {
redirect = buildUrl('wan', token);
} else if ( /action=lannetset/.test(location.href) ) {
redirect = buildUrl('lannetset', token);
} else {
redirect = rsp.url;
}
window.location.href = redirect;
} else if ( rsp.code == 403 ) {
window.location.reload();
} else {
pwdErrorCount ++;
var errMsg = '密码错误';
if (pwdErrorCount >= 4) {
errMsg = '多次密码错误,将禁止继续尝试';
}
Valid.fail( document.getElementById('password'), errMsg, false);
$( formObj )
.addClass( 'shake animated' )
.one( 'webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', function(){
$('#password').focus();
$( this ).removeClass('shake animated');
} );
}
});
}
在loginHandle
里面可以看到下面几句
var nonce = Encrypt.init();
var oldPwd = Encrypt.oldPwd( pwd );
这里就是生成密码和nonce的关键了,但是不明白的是为什么把password叫做oldPwd,那之后就是找到Encrypt这个对象的init和oldPwd这两个方法处理了什么东西就好了
var Encrypt = {
key: 'a2ffa5c9be07488bbb04a3a47d3c5f6a',
iv: '64175472480004614961023454661220',
nonce: null,
init: function(){
var nonce = this.nonceCreat();
this.nonce = nonce;
return this.nonce;
},
nonceCreat: function(){
var type = 0;
var deviceId = '50:7b:9d:3f:f8:47';
var time = Math.floor(new Date().getTime() / 1000);
var random = Math.floor(Math.random() * 10000);
return [type, deviceId, time, random].join('_');
},
oldPwd : function(pwd){
return CryptoJS.SHA1(this.nonce + CryptoJS.SHA1(pwd + this.key).toString()).toString();
},
newPwd: function(pwd, newpwd){
var key = CryptoJS.SHA1(pwd + this.key).toString();
key = CryptoJS.enc.Hex.parse(key).toString();
key = key.substr(0, 32);
key = CryptoJS.enc.Hex.parse(key);
var password = CryptoJS.SHA1(newpwd + this.key).toString();
var iv = CryptoJS.enc.Hex.parse(this.iv);
var aes = CryptoJS.AES.encrypt(
password,
key,
{iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }
).toString();
return aes;
}
};
可以看到init里面执行了nonceCreat这个方法,nonceCreat就是生成nonce的方法,接下来仔细看nonce的结构
0_50:7b:9d:3f:f8:47_1600657503_8385
0 就是已经定义好了的type
50:7b:9d:3f:f8:47 这里是访问设备的mac地址
1600657503 这个就是unix时间
8385 这个是10000以内的随机数
那么到这里为止,我们的nonce就应该很好生成了
之后就是密码,oldPwd这个方法直接return了下面东西
CryptoJS.SHA1(this.nonce + CryptoJS.SHA1(pwd + this.key).toString()).toString
nonce 这个是刚才生成好的
pwd 就是没有加密的密码
key 就是这个对象的属性开头就存在了a2ffa5c9be07488bbb04a3a47d3c5f6a
这个貌似就是盐,不会变的
之后把pwd和key合在一起做一次sha1,然后再和nonce合在一起做一次sha1就是密码了
这两个关键的数据出来之后就是一次post请求解决的事情了
使用python模拟登陆
这里有两种方法,一种是使用python直接执行js脚本,这样你就可以省很多事情了,你也不需要管js里面发生了什么,还有一种就是老老实实去做模拟
使用python执行js的方法
直接看代码,这里我使用的是execjs库,这个库可以直接执行js函数,但是这里我们要修改下js函数
oldPwd改为
oldPwd : function(pwdkey, nonce){
return CryptoJS.SHA1(nonce + CryptoJS.SHA1(pwdkey).toString()).toString();
},
nonceCreat改为
nonceCreat: function(deviceId){
var type = 0;
var time = Math.floor(new Date().getTime() / 1000);
var random = Math.floor(Math.random() * 10000);
return [type, deviceId, time, random].join('_');
},
还有其他的依赖库也要添加上
最后总的代码可以看
https://gist.githubusercontent.com/bboysoulcn/e9b2f284dcdb78f4ed35461a09cc234f/raw/eada59d62e24a2ed2202182037a206b222e13a30/gistfile1.txt
这里
修改完js代码之后就是写python代码了,给个示例
import execjs
import os
import requests
# 定义变量
deviceId = ""
route_url = ""
key = ""
password = ""
os.environ["EXECJS_RUNTIME"] = "Node"
# 打开js文件
with open('xiaomi.js', 'r') as f:
js = f.read()
ctx = execjs.compile(js)
# 生成nonce
nonce = ctx.call("Encrypt.nonceCreat",deviceId)
# 加密密码
pwdkey = password + key
password = ctx.call("Encrypt.oldPwd", pwdkey, nonce)
url = "http://"+route_url+"/cgi-bin/luci/api/xqsystem/login"
data = {
"logtype": 2,
"nonce": nonce,
"password": password,
"username": "admin"
}
#发送请求
re = requests.post(url, data)
print(re.text)
执行脚本之后返回是
{"url":"/cgi-bin/luci/;stok=38fb9b975354ea6535dc5078ad18c735/web/home","token":"38fb9b975354ea6535dc5078ad18c735","code":0}
直接使用python
直接使用python个人感觉还是比较简单的主要还是生成nonce和密码的两段代码
生成nonce
req = s.get(route_url + '/cgi-bin/luci/web', timeout=timeout)
key = re.findall(r'key: \'(.*)\',', req.text)[0]
mac_addr = re.findall(r'deviceId = \'(.*)\';', req.text)[0]
nonce = "0_" + mac_addr + "_" + str(int(time.time())) + "_" + str(random.randint(1000, 10000))
因为mac地址每一台机器都不一样,所以最好用请求去获取
# 第一次加密 对应CryptoJS.SHA1(pwd + this.key)
password_encrypt1 = SHA.new()
password_encrypt1.update((password + key).encode('utf-8'))
# 第二次加密对应 CryptoJS.SHA1(this.nonce + CryptoJS.SHA1(pwd + this.key).toString()).toString();
password_encrypt2 = SHA.new()
password_encrypt2.update((nonce + password_encrypt1.hexdigest()).encode('utf-8'))
hexpwd = password_encrypt2.hexdigest()
密码的话就是两次加密了,没什么好说的
获取到token之后就可以直接使用token去请求各种接口了
最后为了可以有一个完美的图表展示,我就直接做成了一个exporter,然后使用prometheus加grafana就可以了
exporter 的地址
https://github.com/bboysoulcn/miwifi-exporter
当然也做了容器化
容器的地址
https://hub.docker.com/repository/docker/bboysoul/miwifi-exporter
注意这个容器是arm架构的
最后在grafana中的样子,其中cpu貌似因为固件的原因一直数值是0
喜欢的可以给个star,欢迎反馈bug
欢迎关注我的博客www.bboy.app
Have Fun