华为路由器后台登录协议实现
实际使用路由器为荣耀路由Pro
下面所述过程,很大部分参考了下面两篇博客
https://blog.csdn.net/Touale/article/details/113571862
https://www.ruterfu.com/2021/04/19/20210419-login-by-codes-huawei-router-WS5200/
其中第一篇博客稍显不好,只是给出简略的过程,若是对于http, js(javascript)不熟悉的人很难上手,实际实现的代码也没有,但还是给出的登录过程的关键数据。第二篇博客就很好的了,给出了整个的加密过程还有最终实现的代码。由于本人多用python,对javascript及java不是很熟悉,稍做尝试,还是用回python。感谢两位博主,特此写上自己的python实现,希望有需要的用得上。
实现过程
导入库
import sys
import requests
from lxml import etree
import json
import hashlib
import hmac
from Crypto import Random
其中requests用于http的交互,etree用于解析网页源码html,并获取相应字段,hashlib和hmac用于实现加密算法,Crypto也是加密算法库,但这里用的不多,仅是用来生成随机字符串
Crypto的安装为pip3 install pycryptodome
获取第一次post中需要的csrf_param,csrf_token,username,firstnonce数据
可查看第二篇博客,有关于post的抓包,这里就不放我的抓包的,用wireshark即可。
重点说明这几个数据来源:
csrf_param,csrf_token为上一次访问回复中的数据,所以第一次post时,应先访问首页来获得这两个数据
username则默认为admin,我的路由器是只有密码输入的。
firstnonce则32位长的随机字节流,在表现上可能是64位的hex码,如在post上,抓包也是,64位(只含0-9,a-f的)
firstNonce = Random.get_random_bytes(32)
result = sess.get("http://192.168.3.1/html/index.html")
update_Information(result)
request_header = {
'Connection': 'keep-alive',
'Cache-Control': 'max-age=0',
'Origin': 'http://192.168.3.1',
'Content-Type': 'application/json;charset=UTF-8',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36',
'Accept': 'application/json, text/javascript, */*; q=0.01',
'X-Requested-With': 'XMLHttpRequest',
'_ResponseFormat': 'JSON',
'Referer': 'http://192.168.3.1/html/index.html',
'Accept-Encoding': 'gzip, deflate',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
}
firstNonce_str = Random.get_random_bytes(32).hex()
firstNonce = bytes(firstNonce_str, encoding='utf-8')
login_post_data = {
'csrf': {'csrf_param': gl_csrf_param, 'csrf_token': gl_csrf_token},
'data': {'username': 'admin', 'firstnonce': firstNonce_str},
}
url_routerlogin = 'http://192.168.3.1/api/system/user_login_nonce' #/api/system/user_login_nonce
deviceLogin = sess.post(url_routerlogin,
headers=request_header, # headers http 头部信息
data=json.dumps(login_post_data, ensure_ascii=True),
# params = {'_':int(time.time())}, #params 参数
cookies = gl_cookies, #cookies cookie
# allow_redirects = False, #allow_redirects 禁用跳转
# timeout=0.5
)
login_respon = json.loads(deviceLogin.text)
save_html(deviceLogin)
# print(deviceLogin.url)
if(login_respon['err'] == 0):
print('first post success')
else:
print('first post failed')
获取第二次post中需要的csrf_param,csrf_token,finalnonce,clientproof数据
其中csrf_param,csrf_token由上一次的post的回复可获得,finalnonce在上一次的post的回复的servernonce字段,clientproof则是密码加密后的数据,也是不同编程语言下的最大差异地方。
如第二篇博客提到的,这里实际使用的加密算法为PBKDF2,对应在python中,则为hashlib.pbkdf2_hmac
def salt_password(password, salt, iter_times):
return hashlib.pbkdf2_hmac('sha256', password, salt, iter_times)
其过程为,
gl_csrf_param = login_respon['csrf_param'] #字符串 特别信息,包含所有字符,不是hex表示
gl_csrf_token = login_respon['csrf_token'] #字符串 特别信息,包含所有字符,不是hex表示
salt = bytes.fromhex(login_respon['salt']) #只含0-9,a-f为hex表示,str转为bytes #firstnonce也是
finalNonce = login_respon['servernonce']
authMsg = firstNonce_str + ',' + finalNonce + ',' + finalNonce
iterations_rece = int(login_respon['iterations'])
passwd = passwd.encode() #str to bytes
saltPassword = salt_password(passwd, salt, iterations_rece) # 加盐算法不同语言命名有差异hashlib.pbkdf2_hmac
mac = hmac.new(b'Client Key', saltPassword, hashlib.sha256) #b'Client Key'作key,msg加密
clientKey = mac.digest()
storeKey = hashlib.sha256(clientKey).digest()
mac = hmac.new(bytes(authMsg, encoding='utf-8'), storeKey, hashlib.sha256)
clientSignature = mac.digest()
clientKey = bytearray(clientKey)
for i in range(len(clientKey)):
clientKey[i] = clientKey[i] ^ clientSignature[i]
clientProof = bytes(clientKey)
salt+passwd+iterations_rece(迭代次数)---PBKDF2加密--->saltPassword
'Client Key'+saltPassword----哈希算法?hmac, sha256--->clientKey
clientKey+clientSignature----做位或运算---->clientProof
这里的关键要理清每一步操作的对象的要求,是bytes还是string,是hex的64位,还是string的32位
login_post_data = { #要求为字符串
'csrf': {'csrf_param': gl_csrf_param, 'csrf_token': gl_csrf_token},
'data': {'finalnonce': finalNonce, 'clientproof': clientProof.hex()}, #传输时为hex码,不会出现异常码,出现不是ascii的情况
}
url_routerlogin = 'http://192.168.3.1/api/system/user_login_proof' # /api/system/user_login_nonce
deviceLogin = sess.post(url_routerlogin,
headers=request_header, # headers http 头部信息
data=json.dumps(login_post_data),
# params = {'_':int(time.time())}, #params 参数
# cookies=gl_cookies, # cookies cookie
# allow_redirects = False, #allow_redirects 禁用跳转
# timeout=0.5
)
login_respon = json.loads(deviceLogin.text)
if(login_respon['err']==0):
print('login success')
else:
print('login failed')
return
获取路由器网页信息
在网页中,不同页面,如连接设备,设置等,其url是
http://192.168.3.1/html/advance.html#home
http://192.168.3.1/html/advance.html#devicecontrol
其中#号是标签,实际上使用是的同一个url,http://192.168.3.1/html/advance。
在chrome浏览器,按F12在Elements选项中,可以看到很复杂的html信息。但是在requests.get()及右键的"查看网页源代码"可看到实际传入的html是很简单,缺失很多信息的。
其原因在于浏览器执行了许多js脚本,在一开始获得的静态网页中,以修改的形式加载了众多的内容。找到对应加载的内容,可以在F12中network选项找,主要是XHR选项中的内容。
对于连接设备页面,js下访问的页面为http://192.168.3.1/api/system/HostInfo
完整python代码
#!/usr/bin/python
# coding=utf-8
import sys
import requests
from lxml import etree
import json
import hashlib
import hmac
from Crypto import Random
gl_cookies = {}
gl_cookie = ''
gl_cookies_path = "cookies"
gl_csrf_param = ''
gl_csrf_token = ''
gl_count = 0
firstNonce = ''
finalNonce = ''
salt = ''
#由首页获取信息
def update_Information(html):
# html = requests.get("http://192.168.3.1/html/index.html")
global gl_cookies
global gl_csrf_param
global gl_csrf_token
gl_cookies = html.cookies
url_tree = etree.HTML(html.text)
gl_csrf_param = url_tree.xpath("//meta[@name='csrf_param']/@content")[0]
gl_csrf_token = url_tree.xpath("//meta[@name='csrf_token']/@content")[0]
def save_html(html):
global gl_count
gl_count += 1
with open("H:%d.html" % gl_count, "wb") as f:
f.write(bytes(html.text, encoding='utf-8'))
def salt_password(password, salt, iter_times):
return hashlib.pbkdf2_hmac('sha256', password, salt, iter_times)
def Main_Login(sess, username, passwd):
global gl_cookie
global gl_cookies
global gl_csrf_param
global gl_csrf_token
firstNonce = Random.get_random_bytes(32)
result = sess.get("http://192.168.3.1/html/index.html")
update_Information(result)
request_header = {
'Connection': 'keep-alive',
'Cache-Control': 'max-age=0',
'Origin': 'http://192.168.3.1',
'Content-Type': 'application/json;charset=UTF-8',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36',
'Accept': 'application/json, text/javascript, */*; q=0.01',
'X-Requested-With': 'XMLHttpRequest',
'_ResponseFormat': 'JSON',
'Referer': 'http://192.168.3.1/html/index.html',
'Accept-Encoding': 'gzip, deflate',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
}
firstNonce_str = Random.get_random_bytes(32).hex()
firstNonce = bytes(firstNonce_str, encoding='utf-8')
login_post_data = {
'csrf': {'csrf_param': gl_csrf_param, 'csrf_token': gl_csrf_token},
'data': {'username': 'admin', 'firstnonce': firstNonce_str},
}
url_routerlogin = 'http://192.168.3.1/api/system/user_login_nonce' #/api/system/user_login_nonce
deviceLogin = sess.post(url_routerlogin,
headers=request_header, # headers http 头部信息
data=json.dumps(login_post_data, ensure_ascii=True),
# params = {'_':int(time.time())}, #params 参数
cookies = gl_cookies, #cookies cookie
# allow_redirects = False, #allow_redirects 禁用跳转
# timeout=0.5
)
login_respon = json.loads(deviceLogin.text)
save_html(deviceLogin)
# print(deviceLogin.url)
if(login_respon['err'] == 0):
print('first post success')
else:
print('first post failed')
# 第二次post
gl_csrf_param = login_respon['csrf_param'] #字符串 特别信息,包含所有字符,不是hex表示
gl_csrf_token = login_respon['csrf_token'] #字符串 特别信息,包含所有字符,不是hex表示
salt = bytes.fromhex(login_respon['salt']) #只含0-9,a-f为hex表示,str转为bytes #firstnonce也是
finalNonce = login_respon['servernonce']
authMsg = firstNonce_str + ',' + finalNonce + ',' + finalNonce
iterations_rece = int(login_respon['iterations'])
passwd = passwd.encode() #str to bytes
saltPassword = salt_password(passwd, salt, iterations_rece) # 加盐算法不同语言命名有差异hashlib.pbkdf2_hmac
mac = hmac.new(b'Client Key', saltPassword, hashlib.sha256) #b'Client Key'作key,msg加密
clientKey = mac.digest()
storeKey = hashlib.sha256(clientKey).digest()
mac = hmac.new(bytes(authMsg, encoding='utf-8'), storeKey, hashlib.sha256)
clientSignature = mac.digest()
clientKey = bytearray(clientKey)
for i in range(len(clientKey)):
clientKey[i] = clientKey[i] ^ clientSignature[i]
clientProof = bytes(clientKey)
login_post_data = { #要求为字符串
'csrf': {'csrf_param': gl_csrf_param, 'csrf_token': gl_csrf_token},
'data': {'finalnonce': finalNonce, 'clientproof': clientProof.hex()}, #传输时为hex码,不会出现异常码,出现不是ascii的情况
}
url_routerlogin = 'http://192.168.3.1/api/system/user_login_proof' # /api/system/user_login_nonce
deviceLogin = sess.post(url_routerlogin,
headers=request_header, # headers http 头部信息
data=json.dumps(login_post_data),
# params = {'_':int(time.time())}, #params 参数
# cookies=gl_cookies, # cookies cookie
# allow_redirects = False, #allow_redirects 禁用跳转
# timeout=0.5
)
login_respon = json.loads(deviceLogin.text)
if(login_respon['err']==0):
print('login success')
else:
print('login failed')
return
if __name__ == "__main__":
sess = requests.Session()
username = 'admin' # str(input('帐号:'))
passwd = '123456' # str(input('密码:'))
Main_Login(sess, username, passwd)
result = sess.get('http://192.168.3.1/api/system/HostInfo')
false = False
true = True
result = eval(result.text)
# save_html(result)
for device in result:
if(device['Active']):
print(device['HostName'])
# print(result.text)
sys.exit(0)
上一篇: VRRP-虚拟路由器冗余协议
下一篇: [LeetCode]53. 最大子序和