Bboysoul's Blog

首页 公告 RSS

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

September 23, 2020 本文有 2246 个字 需要花费 5 分钟阅读

简介

之前买了一个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


Tags:

本站总访问量 本站总访客数