本帖最后由 330wang 于 2023-9-11 10:48 编辑
0x00 从代码失效说起
小米路由器7000开售(2023年5月9日开售)以后入手了一个路由器,但是问题来了,之前的python登录代码失效了,搜遍了全网,找到的都是旧的登录代码(采用sha1运算)。于是乎,就有了这篇文章。
0x01 登录路由前的准备工作
先用电脑通过无线或有线的方式连接上小米路由器,具体怎么连接,这里不多介绍。电脑连接上路由后需要获取网关的地址(也就是说路由器的地址),用下面的命令:ipconfig,如下图所示:
图1
图1 获取电脑的默认网关
打开浏览器的调试模式(即按下键盘的F12,和浏览器无关,现在常用的浏览器都是用F12键打开调试模式),并在浏览器的地址栏中输入网关的地址(192.168.1.1),然后跟踪后续的流量,如图2所示:
图2
图2 地址栏输入网关后续有22个请求,最后定格到当前界面
可以看到,地址栏输入网关后回车,后续有22个请求。最后定格在http://192.168.1.1/cgi-bin/luci/web。在这个界面输入后台管理密码后就可以对路由器进行配置管理、查看相关的信息等操作。在输入密码之前依次点击每一个请求,看看每个请求都是干什么用的。点完后可以看出例如.css是控制样式和布局用的,如图3所示:
3
图3 双击第二个请求,显示的结果
其他的请求就不一一点击了,只需要知道的是,现在还没有输入后台的管理密码。下面重点关注一下图2中划圈的两个请求。.css负责界面和布局,一般不用分析,.js文件是相应的后台代码,某些文件需要着重分析。
4
图4 http://192.168.1.1/cgi-bin/luci/web
5
图5 http://192.168.1.1/cgi-bin/luci/api/xqsystem/init_info
以上是输入网关后得到的一些基本信息,而且图4和图5两个地址可以直接访问,也就是说在地址栏输入相应的地址就能返回如上图所示的内容。这些内容有什么用呢?对于图5来说,只需要在地址栏输入一个固定的请求,路由器就能返回诸如路由器的名称、ID、路由ID、硬件版本、路由器的显示名称、语言包以及是否启用新加密模式(newEncryptMode)等重要信息。这个新加密模式后面再说,先埋个坑。
0x02 输入密码登录路由管理界面
关闭之前的页面,重新打开浏览器并在地址栏输入http://192.168.1.1/cgi-bin/luci/web,同时启用调试模式。输入路由器管理密码后点向右的箭头,登录路由器的后台,同时跟踪后续的各个请求,如下图所示:
6
图6 输入密码后的管理界面
可以看到,从输入密码到登录到当前的管理界面一共有46个请求。先看第一个重要的请求,如图7所示:
7
图7 POST请求
8
图8 请求的表单数据
9
图9 请求后的响应数据
综合图7、图8、图9得知:登录消息头标签告诉我们登录采用的请求:POST登录消息头标签告诉我们登录的网址:http://192.168.1.1/cgi-bin/luci/api/xqsystem/login登录消息头标签告诉我们请求头:可以参考截图上的内容填写登录请求标签的表单数据为:[Asm] 纯文本查看 复制代码 username "admin"
password "6e7ba276d4b12533cb9b08e6013bd1dd012243fa09b094e915342cd0ff2eb5f3"logtype "2"
nonce "0_aa:af:b2:e0:e5:bf_1693890128_7030" 登录成功后的响应数据为:[Asm] 纯文本查看 复制代码 url "/cgi-bin/luci/;stok=286e314fb8233c6ff8d0cb1dbd3b4643/web/home"
token "286e314fb8233c6ff8d0cb1dbd3b4643"
code 0 通过上面的分析知道,小米路由器验证密码后才能登录进管理界面,但是注意一下表单数据,我在登录界面输入的密码是“test1234”,很显然,返给路由器的并不是明文的密码,而是经过变换以后的字符串。再重新输入一个错误的登录密码,重复上面的步骤,例如,密码为“330wang”,这时请求标签的表单数据为:[Asm] 纯文本查看 复制代码 username "admin"
password "2fd4eff94de247b5f59c150ca53293cc69df2c0d59d71cbc862cdd3a969e5d38"
logtype "2"
nonce "0_aa:af:b2:e0:e5:bf_1693896342_3009" 输入错误密码后的响应标签数据为:[Asm] 纯文本查看 复制代码 code 401
msg "not auth" 后续的各个操作需要在地址栏中附加密码正确后响应数据中的token。如图9所示。这个token是正确登录的标志,只要浏览器不关闭,都可以用这个token对路由器进行访问,可以对路由进行配置也可以读取路由器中的参数等。如果密码输入错误,响应数据不会返回token字段,只会返回“not match”的消息,也就没有后面的操作了。
0x03 表单数据的生成
通过上面的分析我们知道,要想对路由器进行后面的操作,例如读取配置WiFi信息、读取配置连网方式、设置IP地址等操作都需要正确的token,而token的产生是和password字段相关的。password字段与输入的路由器管理密码有直接的关系。如果想通过单纯在POST请求登录的话,一定要知道输入的路由器管理密码是怎么生成password的。通过图7我们知道,这个POST请求的上一个网页是http://192.168.1.1/cgi-bin/luci/web,现在就从这个网页入手,看看能不能得到相关的生成规则。在浏览器的地址栏中输入http://192.168.1.1/cgi-bin/luci/web,并按F12,打开调试模式,同时点击查看器标签,查看这个网页的源代码,如图10 所示:
10
图10 查看/cgi-bin/luci/web的源代码
这里是此页面的源代码,包含普通的html代码、相关的css代码以及相应的js代码。可以看出,除了html外,其他代码都是折叠结构,可以在代码中用鼠标定位到相关的元素中,这里不多说了,也不是重点。在这个界面可以很方便地了解网页的布局。下面切换到调试器标签下,在这里详细地调试密码的生成过程。因为根据输入的管理密码生成了表单数据中的password,所以鼠标在调试器的代码位置点一下,使光标位于调试器下的源代码框中,然后按键盘的Ctrl+F弹出搜索框,在搜索框中输入要搜索的字符串“password”,如图11所示,可以看到一共有29个搜索结果:
11
图11 调试器代码中搜索password
按向下的箭头查看每一个可能的数据,直到定位到如图12所示的位置:
12
图12 找到相关的位置
可以看到表单数据中的参数一共有4个,分别是username、password、logtype和nonce。前三个参数比较好理解,但是nonce是干什么用的呢?
Nonce,Number used once或Number once的缩写,在密码学中Nonce是一个只被使用一次的任意或非重复的随机数值,在加密技术中的初始向量和加密散列函数都发挥着重要作用,在各类验证协议的通信应用中确保验证信息不被重复使用以对抗重放攻击(Replay Attack)。在信息安全中,Nonce是一个在加密通信只能使用一次的数字。在认证协议中,它往往是一个随机或伪随机数,以避免重放攻击。Nonce也用于流密码以确保安全。如果需要使用相同的密钥加密一个以上的消息,就需要Nonce来确保不同的消息与该密钥加密的密钥流不同。
-- 以上信息来源于baidu百科:https://baike.baidu.com/item/Nonce/2525414
路由器web页面的中相关函数如下所示:
[Java] 纯文本查看 复制代码 function loginHandle ( e ) {
e.preventDefault();
var formObj = document.rtloginform;
var pwd = $( '#password' ).val(); //取输入的密码,并放到pwd变量中
if ( pwd == '') {
return;
}
var nonce = Encrypt.init(); //nonce:Enctypt.init()函数生成
var oldPwd = Encrypt.oldPwd( pwd ); //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 ) { //执行POST请求
$.pub('loading:stop');
......
具体解释请看每行代码 //后面的注释。通过上面的代码,可以看到最终POST的4个参数的生成方式。
其中username参数为固定值:admin;password参数为输入的管理密码由Encrypt.oldPwd()运算产生;logtype参数为固定值:2;nonce参数由Encrypt.init()产生。下面定位到Encrypt函数,看一看这几个值是怎么产生的,方法是在搜索框中搜索字符串“Encrypt”,如图13所示:
13
图13 Encrypt函数
Encrypt函数如下所示:
[Java] 纯文本查看 复制代码 var Encrypt = {
key: 'a2ffa5c9be07488bbb04a3a47d3c5f6a',
iv: '64175472480004614961023454661220',
nonce: null,
init: function () { //nonce的产生代码
var nonce = this.nonceCreat();
this.nonce = nonce;
return this.nonce;
},
nonceCreat: function () {
var type = 0;
var deviceId = 'aa:af:b2:e0:e5:bf';
var time = Math.floor(new Date().getTime() / 1000);
var random = Math.floor(Math.random() * 10000);
return [type, deviceId, time, random].join('_');
},
oldPwd: function (pwd) { //oldPwd代码
if(newEncryptMode == 1){
return CryptoJS.SHA256(this.nonce + CryptoJS.SHA256(pwd + this.key).toString()).toString();
}else{
return CryptoJS.SHA1(this.nonce + CryptoJS.SHA1(pwd + this.key).toString()).toString();
}
},
newPwd: function (pwd, newpwd) {
var key = CryptoJS.SHA1(pwd + this.key).toString();
var password = CryptoJS.SHA1(newpwd + this.key).toString();
key = CryptoJS.enc.Hex.parse(key).toString();
key = key.substr(0, 32);
key = CryptoJS.enc.Hex.parse(key);
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;
},
newPwd256: function (pwd, newpwd) {
var key = CryptoJS.SHA256(pwd + this.key).toString();
var password = CryptoJS.SHA256(newpwd + this.key).toString();
key = CryptoJS.enc.Hex.parse(key).toString();
key = key.substr(0, 32);
key = CryptoJS.enc.Hex.parse(key);
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;
}
}; nonce的生成规则
先看代码:[Java] 纯文本查看 复制代码 nonceCreat: function () {
var type = 0;
var deviceId = 'aa:af:b2:e0:e5:bf';
var time = Math.floor(new Date().getTime() / 1000);
var random = Math.floor(Math.random() * 10000);
return [type, deviceId, time, random].join('_');
}, 返回type、deviceId、time、random的值并用“_”相连接。如下图,经调试所产生的nonce值:其中,type是固定值,目前为0;deviceId值目前也是明文值,这个值是后台取的设备的ID,这里是“aa:af:b2:e0:e5:bf”;time值是使用Date().getTime()函数得到的当前时间戳(值除以1000,取秒值);random值是调用Math.random()函数得到的值再*10000得到的数值。
图14 nonce示例
password的生成规则
根据loginHandle(),password是由Encrypt().oldPwd(pwd)函数生成的。代码如下:
[Java] 纯文本查看 复制代码 oldPwd: function (pwd) {
if(newEncryptMode == 1){
return CryptoJS.SHA256(this.nonce + CryptoJS.SHA256(pwd + this.key).toString()).toString();
}else{
return CryptoJS.SHA1(this.nonce + CryptoJS.SHA1(pwd + this.key).toString()).toString();
}
}, 如果newEncryptMode的值为1,则调用sha256算法,其他情况则使用sha1算法。一共做两次哈希。
第一次哈希
具体是对用户输入的密码(pwd)和key值做相关的哈希运算。
key值参考代码一开始的常量,如下所示:
[Java] 纯文本查看 复制代码 var Encrypt = {
key: 'a2ffa5c9be07488bbb04a3a47d3c5f6a',
iv: '64175472480004614961023454661220',
nonce: null,
init: function () {
......
在http://192.168.1.1/cgi-bin/luci/api/xqsystem/init_info中我们可以获得newEncryptMode的值,此值为1,所以password的生成采用sha256。
第二次哈希
用上一步产生的nonce加上第一次产生的哈希结果再做一次sha256,最后的结果就是password的值,如图16所示。我们验证一下图8所产生的数据。第一次哈希:密码为“test1234”key值为“a2ffa5c9be07488bbb04a3a47d3c5f6a”,所以第一次sha256的结果为(取小写字母)sha256(‘test1234a2ffa5c9be07488bbb04a3a47d3c5f6a’)如下图所示:
图15 第一次的sha256哈希运行
图16 第二次sha256后的结果
和图8的结果一致。POSThttp://192.168.1.1/cgi-bin/luci/api/xqsystem/login中的表单数据就分析到此。剩下的就是编写登录代码了。
代码
[Python] 纯文本查看 复制代码 # -*- coding: utf-8 -*-
import re
import time
from routers.utils import hash_sha1, hash_sha256
from routers.router_spider import Router
class XiaoMi7000(Router):
def login(self):
url = f'http://{self.ip}/cgi-bin/luci/web'
self.headers = {
"Connection": "keep-alive",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/90.0.4430.72 Safari/537.36"
}
r = self.session.get(url=url, headers=self.headers)
r_data = r.text.replace('\r', '').replace('\n', '').replace('\t', '')
key = re.compile('key:.*?\'(.*?)\',').findall(r_data)[0]
device_id = re.compile('deviceId = \'(.*?)\';').findall(r_data)[0]
init_info_url = f'http://{self.ip}/cgi-bin/luci/api/xqsystem/init_info'
init_info_r = self.session.get(url=init_info_url, headers=self.headers)
init_info_data = init_info_r.json()
# {"code":0,"isSupportMesh":1,"secAcc":1,"inited":1,"connect":0,"modules":{"replacement_assistant":"1"},"hardware":"RC06","support160M":1,"language":"zh_cn","romversion":"1.0.111","countrycode":"CN","routerId":"5dddb********3c7c","id":"44********41","routername":"Xiaomi_C84F","showPrivacy":0,"displayName":"Xiaomi路由器7000","imei":"","moduleVersion":"","maccel":"1","model":"xiaomi.router.rc06","wifi_ap":1,"bound":0,"newEncryptMode":1,"isRedmi":0}
hardware = init_info_data.get("hardware")
rom_version = init_info_data.get("romversion")
serial_number = init_info_data.get("id")
router_name = init_info_data.get("routername")
self.data = {
"hardware": hardware,
"rom_version": rom_version,
"serial_number": serial_number,
"router_name": router_name,
}
new_encrypt_mode = init_info_data.get("newEncryptMode")
pwd = self.password
nonce = f'0_{device_id}_{str(int(time.time()))}_962'
if new_encrypt_mode and int(new_encrypt_mode) == 1:
a = hash_sha256(pwd + key)
pass_word = hash_sha256(nonce + a)
else:
a = hash_sha1(pwd + key)
pass_word = hash_sha1(nonce + a)
log_url = f'http://{self.ip}/cgi-bin/luci/api/xqsystem/login'
data = {
"username": "admin",
"password": pass_word,
"logtype": "2",
"nonce": nonce
}
res = self.session.post(url=log_url, headers=self.headers, data=data)
cookie = res.raw.headers.getlist('Set-Cookie')[0]
self.headers["Cookies"] = cookie.split(';')[0]
self.token = res.json()['token']
self.path = res.json()['url']
self.stok = re.compile(';stok=(.*?)/').findall(self.path)[0]
def get_device_info(self):
d_name = self.session.get(url=f'http://{self.ip}/cgi-bin/luci/;stok={self.stok}/web/home', headers=self.headers)
d_name_html = d_name.text
mapModel = {
'R1D': '小米路由器',
'R2D': '小米路由器2',
'R3D': '小米路由器HD',
'R1CM': '小米路由器MINI',
'R1CL': '小米路由器青春版',
'R3': '小米路由器3',
'R3L': '小米路由器3C',
'R3P': '小米路由器3 Pro',
'R3A': '小米路由器3A',
'R3G': '小米路由器3G',
'R4': '小米路由器4',
'R4C': '小米路由器4Q',
'R4CM': '小米路由器4C',
'D01': '小米路由器Mesh',
'R4AC': '小米路由器4A',
'R4A': '小米路由器4 v2',
'R3Gv2': '小米路由器3G',
'R2600': '小米路由器2600',
'R2100': '小米路由器AC2100',
'R1500': '小米路由器1500',
'R3600': '小米AIoT路由器 AX3600',
'R1800': '小米AIoT路由器 AX1800',
'RA72': '小米路由器 AX6000',
'RA80': '小米路由器 AX3000',
'RA81': 'Redmi路由器 AX3000',
'RB08': 'Xiaomi HomeWiFi',
'RC01': 'Xiaomi万兆路由器',
'RC06': 'Xiaomi路由器7000'
}
d_name_r = ''
d_type = ''
wan_mac = ''
if re.compile('hardwareVersion.*?\'(.*?)\'').findall(d_name_html):
d_name_r = mapModel[re.compile('hardwareVersion.*?\'(.*?)\'').findall(d_name_html)[0]]
d_type = re.compile('hardwareVersion.*?\'(.*?)\'').findall(d_name_html)[0]
if re.compile("""#routermac'\).text\('(.*?)'\);""").findall(d_name_html):
wan_mac = re.compile("""#routermac'\).text\('(.*?)'\);""").findall(d_name_html)[0]
index_url = f'http://{self.ip}/cgi-bin/luci/;stok={self.stok}/api/xqnetwork/pppoe_status'
ip_r = self.session.get(url=index_url, headers=self.headers)
# {'proto': 'dhcp', 'dns': ['192.168.1.3'], 'code': 0, 'status': 0, 'gw': '192.168.1.3', 'ip': {'mask': '255.255.255.0', 'address': '192.168.1.35'}}
ip_r_data = ip_r.json()
name_url = f'http://{self.ip}/cgi-bin/luci/;stok={self.stok}/api/xqnetwork/wifi_detail_all'
name_r = self.session.get(url=name_url, headers=self.headers)
# {'bsd': 0, 'info': [{'ifname': 'wl1', 'channelInfo': {'bandwidth': '0', 'bandList': [], 'channel': 10}, 'encryption': 'mixed-psk', 'bandwidth': '0', 'kickthreshold': '0', 'status': '1', 'mode': 'Master', 'txbf': '0', 'weakthreshold': '0', 'device': 'mt7603e.network1', 'hidden': 0, 'password': '12345678', 'channel': '0', 'txpwr': 'max', 'weakenable': '0', 'ssid': 'Xiaomi_52F5', 'signal': 0}, {'ifname': 'wl0', 'channelInfo': {'bandwidth': '0', 'bandList': [], 'channel': 149}, 'encryption': 'mixed-psk', 'bandwidth': '0', 'kickthreshold': '0', 'status': '1', 'mode': 'Master', 'txbf': '0', 'weakthreshold': '0', 'device': 'mt7612.network1', 'hidden': 0, 'password': '12345678', 'channel': '0', 'txpwr': 'max', 'weakenable': '0', 'ssid': 'Xiaomi_52F5_5G', 'signal': 0}], 'code': 0}
name_r_data = name_r.json()
# 序列号
serial_number = ''
if self.data.get("serial_number"):
serial_number = self.data.get("serial_number")
# 设备名称
product_number = name_r_data["info"][0]["ssid"]
run_time = ''
# WAN网口
wan_port = ''
# 链接方式
link_method = ip_r_data["proto"]
pppoe_user = ''
pppoe_pwd = ''
if 'dhcp' not in link_method:
pppoe_status = self.session.get(url=f'http://{self.ip}/cgi-bin/luci/;stok={self.stok}/api/xqnetwork/pppoe_status', headers=self.headers)
pppoe_status_data = pppoe_status.json()
pppoe_user = pppoe_status_data['pppoename']
pppoe_pwd = pppoe_status_data['password']
ip = ip_r_data["ip"]["address"]
# 子网掩码
mask = ip_r_data["ip"]["mask"]
# 网关地址
gateway = ip_r_data["gw"]
# 主DNS地址
main_dns = ip_r_data["dns"][0] if ip_r_data["dns"] else ''
slave_dns = ''
cfg_url = f'http://{self.ip}/cgi-bin/luci/;stok={self.stok}/api/misystem/status'
cfg_res = self.session.get(url=cfg_url, headers=self.headers)
# {'dev': [{'mac': '18:****:87', 'maxdownloadspeed': '30351', 'upload': '137281', 'upspeed': '0', 'downspeed': '0', 'online': '298', 'devname': 'DESKTOP-D6GLNBH', 'maxuploadspeed': '12451', 'download': '266417'}], 'code': 0, 'mem': {'usage': 0.38, 'total': '128MB', 'hz': '1200MHz', 'type': 'DDR3'}, 'temperature': 0, 'count': {'all': 1, 'online': 1}, 'hardware': {'mac': '64:64****:52:F5', 'platform': 'R4A', 'version': '2.28.65', 'channel': 'release', 'sn': '21894/22358114'}, 'upTime': '408.31', 'cpu': {'core': 4, 'hz': '880MHz', 'load': 0.0045}, 'wan': {'downspeed': '571', 'maxdownloadspeed': '34500', 'history': '1003,838,711,1146,206,732,1568,5676,0,195,154,154,725,121,537,321,1423,181,202,195,0,1691,364,346,0,1134,1331,1462,0,1137,365,730,228,569,154,0,1187,0,0,487,338,1261,627,617,1163,229,7392,235,142,918', 'devname': 'eth1', 'upload': '166582', 'upspeed': '347', 'maxuploadspeed': '12322', 'download': '484827'}}
cfg_data = cfg_res.json()
mac = cfg_data["hardware"]["mac"]
device_info_base_data = {
"device_name": d_name_r,
"device_type": d_type,
"serial_number": serial_number,
"run_time": run_time,
"connection_type": link_method,
"wan_ip": "",
"wan_mac": wan_mac,
"lan_ip": "",
"lan_mac": "",
"ipv4_wan_ip": "",
"ipv4_lan_ip": "",
"ipv4_gateway": "",
"ipv4_dns_server": "",
"ipv6_wan_ip": "",
"ipv6_lan_ip": "",
"ipv6_gateway": "",
"ipv6_dns_server": "",
"ip": ip,
"mac": mac,
"gateway": gateway,
"mask": mask,
"dns_server": main_dns,
"pppoe_user": pppoe_user,
"pppoe_pwd": pppoe_pwd,
"wifi2_ssid": name_r_data['info'][0]['ssid'],
"wifi2_key": name_r_data['info'][0]['password'],
"wifi5_ssid": name_r_data['info'][1]['ssid'],
"wifi5_key": name_r_data['info'][1]['password'],
"pin": "",
"encryption_mode": name_r_data['info'][0]['encryption'],
}
return device_info_base_data
def get_device_host_info(self):
device_ulr = f'http://{self.ip}/cgi-bin/luci/;stok={self.stok}/api/misystem/devicelist'
r = self.session.post(url=device_ulr, headers=self.headers)
data = r.json()
device_list = []
for one_device in data["list"]:
status = 1
device_info = {
"ip": one_device['ip'][0]['ip'],
"mac": one_device['mac'],
"name": one_device['name'],
"on_line_status": status,
}
device_list.append(device_info)
return device_list
|