模拟登陆小米路由器获取小米路由器的监控信息

简介

之前买了一个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代码就好了,看密码的输入框代码

1
<input id="password" class="ipt-text" type="password" name="router_password" autocomplete="off" placeholder="请输入路由器管理密码" reqmsg="请输入路由器管理密码">

之后找id="password"的js函数,找到了下满这段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
$(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里面可以看到下面几句

1
2
var nonce = Encrypt.init();
var oldPwd = Encrypt.oldPwd( pwd );

这里就是生成密码和nonce的关键了,但是不明白的是为什么把password叫做oldPwd,那之后就是找到Encrypt这个对象的init和oldPwd这两个方法处理了什么东西就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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改为

1
2
3
4
5
oldPwd : function(pwdkey, nonce){

return CryptoJS.SHA1(nonce + CryptoJS.SHA1(pwdkey).toString()).toString();

},

nonceCreat改为

1
2
3
4
5
6
7
8
9
10
11
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代码了,给个示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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

1
2
3
4
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地址每一台机器都不一样,所以最好用请求去获取

1
2
3
4
5
6
7
8
# 第一次加密 对应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

欢迎关注Bboysoul的博客www.bboy.app

Have Fun

欢迎关注我的其它发布渠道