使用Java编写程序来登录华为WS5200路由器
弄这个的初衷是我发现自己真的太没有时间观念了,经常我看书看一半想资料查10分钟,然而,百度一开,分分钟被某国要倒核废水瞬间吸引过去了,看完又看到某门山景区没素质的游客摘水果,又点开看看,接着看到了各种各样的新闻,等我看爽看满足后,已经差不多半个小时甚至1个小时没了。资料查了几分钟,剩下都在看新闻。所以才想到能否通过程序方式控制下路由器,至少在我看书期间,10分钟就是10分钟,时间到断网。
(English version translate by GPT-3.5)
前言
说干就干,因为我手头只有WS5200的路由器,而我电脑连接的是这台路由器,所以从这台路由器下手。
算法初探
登录初看
先打开路由器,按F12进行登录,然后看到ALL部分,看到登录出现了以下请求
路由器会先请求 user_login_nonce 接口
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
51POST /api/system/user_login_nonce HTTP/1.1
Host: 10.0.0.10
Connection: keep-alive
Content-Length: 214
Pragma: no-cache
Cache-Control: no-cache
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
_ResponseFormat: JSON
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.128 Safari/537.36
Content-Type: application/json; charset=UTF-8
Origin: http://10.0.0.10
Referer: http://10.0.0.10/html/index.html
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: SessionID_R3=4Xj1P0SvG5u5274ClvmYz0g7NoEBIeagDK8UokAbbWfu0zfaqdQYJOyhPEJpCOwAAZpX9LvR38hJaFIegMuSe5FG6eiSpO1zRlZyRaCTZ2qqf3a5os2heaMYcY9jpajW
Response Header
HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Content-Type: application/javascript
X-Download-Options: noopen
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
Content-Security-Policy: default-src 'self' 'unsafe-inline' 'unsafe-eval'
X-Content-Type-Options: nosniff
Content-Length: 337
Request Payload
{
"data": {
"username": "admin",
"firstnonce": "aa0591fd5a971da559b37eb6369eb2d2b25252e4a115e3ab31f6cde19df1eb72"
},
"csrf": {
"csrf_param": "U7FMqd5SX0twxRA0kaKqLiPCfWqkCjLJ",
"csrf_token": "tUcxR4fDHHrUSNMU650Mp0rd5UXt0iJ7"
}
}
Response Payload
{
"csrf_token": "rTtnLfQMHAuGJItKd7MlxaL4WNXmP6EE",
"salt": "07f0b7e87c2ad06ebd2938c326909307ac1da81460b27fa6d84a8b240ac795fe",
"csrf_param": "LWFNHzU7dSG0paH0vlUZ70GJG9PItZAL",
"err": 0,
"modeselected": 1,
"servernonce": "aa0591fd5a971da559b37eb6369eb2d2b25252e4a115e3ab31f6cde19df1eb72JREAGMZitcXiwQp2tsnUhLFViZev60z0",
"isopen": 0,
"iterations": 100
}然后会紧接着请求 user_login_proof
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
63POST /api/system/user_login_proof HTTP/1.1
Host: 10.0.0.10
Connection: keep-alive
Content-Length: 308
Pragma: no-cache
Cache-Control: no-cache
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
_ResponseFormat: JSON
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.128 Safari/537.36
Content-Type: application/json; charset=UTF-8
Origin: http://10.0.0.10
Referer: http://10.0.0.10/html/index.html
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: SessionID_R3=4Xj1P0SvG5u5274ClvmYz0g7NoEBIeagDK8UokAbbWfu0zfaqdQYJOyhPEJpCOwAAZpX9LvR38hJaFIegMuSe5FG6eiSpO1zRlZyRaCTZ2qqf3a5os2heaMYcY9jpajW
Response Header
HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Content-Type: application/javascript
X-Download-Options: noopen
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
Content-Security-Policy: default-src 'self' 'unsafe-inline' 'unsafe-eval'
X-Content-Type-Options: nosniff
Content-Length: 183
Request Payload
{
"data": {
"clientproof": "5b2b96e5b4a421241e05c841c3877cf03d643c30ef8358628a0f8825bd5f4fc8",
"finalnonce": "aa0591fd5a971da559b37eb6369eb2d2b25252e4a115e3ab31f6cde19df1eb72JREAGMZitcXiwQp2tsnUhLFViZev60z0"
},
"csrf": {
"csrf_param": "LWFNHzU7dSG0paH0vlUZ70GJG9PItZAL",
"csrf_token": "rTtnLfQMHAuGJItKd7MlxaL4WNXmP6EE"
}
}
Response Payload
{
"ishilink": 0,
"errorCategory": "user_pass_err",
"err": 4784230,
"csrf_param": "XzvfGOA3bx41wFc3aKrgKw8bEVT0KZ4d",
"count": 1,
"maxfailtimes": 3,
"csrf_token": "E8KE4pwzHUN350Lpf1KzrDDgIMtETpdn"
}
这是登录成功的响应
{
"level": 2,
"rsapubkeysignature": "xxxxxxxxx",
"rsae": "010001",
"ishilink": 0,
"rsan": "yyyyyyyyyyy",
"err": 0,
"serversignature": "zzzzzzzzz",
"csrf_param": "eaXn0d7oYBdOe1HWpe0JrFvpqqt2AGyB", // 这里不一定和上面一致,因为我重新刷新过页面了
"csrf_token": "iuhI91oH1YoSH90XVVnjhITrpxcMA4Mg"
}
简单分析
可以看到,华为路由器进行了2次请求,其中第二次是正式登录的一次,而且也大致猜测,路由器用户名是admin,虽然这个信息的确没啥用,第二段请求中返回了user_pass_err,当然我是随便输入了个密码。其中下面是我主要看到的。
- 路由器每次登录的时候,会携带一个csrf_param,以及csrf_token,从上面2个请求看出,下面的登录请求2个参数,正好是上面返回的2个参数,那我可以这么猜测,是不是每次收到新的csrf时,都要更新当前的csrf参数?为什么在第一次请求user_login_nonce的时候就已经就有csrf参数?
- 请求中没有看到任何密码和数字,也不是类似的MD5的简单加密,因为我在试上面之前试过用123456登录,123456的MD5时e10开头的,但是上面并没有任何相关的内容,因此密码肯定被处理过。
- 看登录返回的参数,count和maxfailtimes告诉我们,别超过了maxfailtimes,不然封你1分钟没商量。。
- 还有上面的Cookie参数,这个在HTTP请求中作用极大,一定要看这个是在哪里被设置的,一般设置Cookie会有 Set-Cookie响应头。
获得最开始的csrf参数
因为在第一次user_login_nonce就有了csrf参数,就表示在进入登录页面前就肯定有一个地方给了这个参数,或者就是自动生成的。
因为我user_login_nonce登录时使用的token是bFOBG8SSFQ4ZqD92j0rOSZXrQnWluYQs
,所以我只需要在Chrome Network中寻找,是否有任何页面或js提到了这个值,结果还真找到了
在最开始的index.html页面
1 | GET /html/index.html HTTP/1.1 |
哎呀,这token简直是一摸一样一字不差,比双胞胎还像双胞胎。
1 | <meta name="csrf_param" content="U7FMqd5SX0twxRA0kaKqLiPCfWqkCjLJ"/> |
基本可以确定,最初始的token就是index.html返回的。
寻找加密部分
这个加密绝对被处理过的,但是一般处理,将chrome网页调试Network选项卡,只过滤Javascript,一般加密就在这里。。还好人家的js文件不多
我就一个一个搜索了,就搜索关键字user_login_proof
吧。。结果刚找到第七个文件就有了
点击搜索框正下方的{}
按钮,来格式化js文本,拿出登录的js代码如下
1 | login: function(context, data) { |
可以看到基本逻辑如下
首先,请求user_login_nonce,参数是username和firstNonce
1
其中firstNonce是从scram.nonce().toString()算出来的。
接着,取得上述返回中的salt参数,serverNonce以及iterations并作为登录参数,进行如下代码运算后,将clientKey转为16进制,请求过去。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16var salt = CryptoJS.enc.Hex.parse(res['salt']);
var iter = res['iterations'];
var finalNonce = res['servernonce'];
var authMsg = firstNonce + "," + finalNonce + "," + finalNonce;
var saltPassword = scram.saltedPassword(password, salt, iter).toString();
var serverKey = scram.serverKey(CryptoJS.enc.Hex.parse(saltPassword));
var clientKey = scram.clientKey(CryptoJS.enc.Hex.parse(saltPassword)).toString();
var storekey = scram.storedKey(CryptoJS.enc.Hex.parse(clientKey));
storekey = storekey.toString();
var clientsignature = scram.signature(CryptoJS.enc.Hex.parse(storekey), authMsg);
clientsignature = clientsignature.toString();
clientsignature = CryptoJS.enc.Hex.parse(clientsignature);
clientKey = CryptoJS.enc.Hex.parse(clientKey);
for (var i = 0; i < clientKey.sigBytes / 4; i++) {
clientKey.words[i] = clientKey.words[i] ^ clientsignature.words[i]
}
整理加密部分
Nonce部分
上述操作中,nonce部分看上去就像是随机值,因为我手动运行这段代码,均返回如下内容,即长度为64且由[0-9,a-f]组成的随机数字。虽然我最后确认过,它的确是随机数。
1
2
3
4
5
6
7
8
9
10
11
12> CryptoJS.SCRAM({keySize: 8}).nonce().toString()
"4f5e4baeb43914052e5ad5236fae7ad4d29d90d46cfab72f6e0919722df7b4a4"
> CryptoJS.SCRAM({keySize: 8}).nonce().toString()
"2f914d316187ae65c63d83603569d960a6c0bae0451858a22da60e549c088170"
> CryptoJS.SCRAM({keySize: 8}).nonce().toString()
"3a6cd54f571ef42bb3751ce09c0888da3b705ead93e294a4676d140930020438"
> CryptoJS.SCRAM({keySize: 8}).nonce().toString()
"af03afe7b13f0cdf70d2f70c4dc71e5b91df0ca144069c563bed6c49c71c51a5"
> CryptoJS.SCRAM({keySize: 8}).nonce().toString()
"6844a2c4984b34b698291dffb0cc93ce2e1775b4ce061ddacd52c9d55512fccb"
> CryptoJS.SCRAM({keySize: 8}).nonce().toString()
"ada7c66cbe74b9efb9e1d9d32ea424423c4de0552be62ce4a975c32ac58bc1bb"用户登录加密部分
这一部分其实比较麻烦的,因为涉及成堆的算法,先暂时把他分离出来,里面的方法调用到了cat_enc.js,将其提取出来后,新建一个html页面,引入cat_enc.js文件,编写如下的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
25var scram = CryptoJS.SCRAM({keySize: 8})
var firstNonce = "aa0591fd5a971da559b37eb6369eb2d2b25252e4a115e3ab31f6cde19df1eb72";
console.log("firstNonce: " + firstNonce)
var password = "123456";
var salt = CryptoJS.enc.Hex.parse("07f0b7e87c2ad06ebd2938c326909307ac1da81460b27fa6d84a8b240ac795fe");
var iter = 100;
var finalNonce = "aa0591fd5a971da559b37eb6369eb2d2b25252e4a115e3ab31f6cde19df1eb72JREAGMZitcXiwQp2tsnUhLFViZev60z0";
var authMsg = firstNonce + "," + finalNonce + "," + finalNonce;
var saltPassword = scram.saltedPassword(password, salt, iter).toString();
console.log("saltPassword: " + saltPassword)
var serverKey = scram.serverKey(CryptoJS.enc.Hex.parse(saltPassword));
var clientKey = scram.clientKey(CryptoJS.enc.Hex.parse(saltPassword)).toString();
console.log("clientKey: " + clientKey)
var storekey = scram.storedKey(CryptoJS.enc.Hex.parse(clientKey));
storekey = storekey.toString();
console.log("storeKey: " + storekey)
var clientsignature = scram.signature(CryptoJS.enc.Hex.parse(storekey), authMsg);
console.log("clientsignature: " + clientsignature)
clientsignature = clientsignature.toString();
clientsignature = CryptoJS.enc.Hex.parse(clientsignature);
clientKey = CryptoJS.enc.Hex.parse(clientKey);
for (var i = 0; i < clientKey.sigBytes / 4; i++) {
clientKey.words[i] = clientKey.words[i] ^ clientsignature.words[i]
}
console.log("clientProof: " + clientKey.toString())浏览器执行后,效果如下,可以看到和上面请求时的一摸一样
1
2
3
4
5
6firstNonce: aa0591fd5a971da559b37eb6369eb2d2b25252e4a115e3ab31f6cde19df1eb72
saltPassword: d051d0ce9b09584087012c6a36c78732cd7b919b5d4d02d35fb1827331044ca0
clientKey: 9df927a1d72d2e77fdf0c59b5c34b02e1c50801b4b193789ce2dbbfb9671574f
storeKey: 5d461845ea9ea0311c1ed476314eb2357c4a32b11b332695e6e23f5982d900b8
clientsignature: c6d2b14463890f53e3f50dda9fb3ccde2134bc2ba49a6feb442233de2b2e1887
clientProof: 5b2b96e5b4a421241e05c841c3877cf03d643c30ef8358628a0f8825bd5f4fc8
一步步分解,并用Java实现和计算加密部分
首先 firstNonce 我选择用随机生成的64位的hash字符串
1
2
3
4
5
6
7
8
9
10private String randomNonce() {
String rand = "abcdef1234567890";
Random random = new Random();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 64; i++) {
int number = random.nextInt(rand.length());
sb.append(rand.charAt(number));
}
return sb.toString();
}然后,尝试编写
scram.saltedPassword(password, salt, iter)
这句话首先进入
cat_enc.js:2403
看到如下内容1
2
3
4
5
6
7saltedPassword: function(password, salt, iterations) {
return CryptoJS.PBKDF2(password, salt, {
keySize: this.cfg.keySize,
iterations:iterations,
hasher: this.cfg.hasher
});
}可以看到,人家用了PBKDF2的加密算法,java中找了下,对应的算法名为
PBKDF2WithHmacSHA256
,所以各种google后,编写代码如下,至于下面的key长度为什么是256,我也不是特别清楚,我一直以为是32的,因为this.cfg.keySize
它返回的就是32,结果发现加密后的长度和js不一样,调到256后,长度就一致了,同时也发现计算出来的saltedPassword
的长度正好是32。1
2
3
4
5private byte[] getSaltedPassword(String password, byte[] salt, int iterations) throws NoSuchAlgorithmException, InvalidKeySpecException {
KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, iterations, 256);
SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
return f.generateSecret(spec).getEncoded();
}实现
scram.clientKey(CryptoJS.enc.Hex.parse(saltPassword)).toString()
这一段,这段js代码如下,因为serverKey对全程登录没有啥帮助。。1
2
3clientKey: function(saltPwd) {
return this.cfg.hmac(saltPwd, "Client Key");
}这段代码看上去就是用
HmacSHA256
对 key为Client Key
进行加密一下的事情,之前这里踩坑了,以为key是前面的saltedPassword
结果算出来的clientKey就是和js不一样。后来发现,java中Key指的就是Client Key
这个字符串。所以,编写代码如下,因为下面getStoreKey也用到了这个方法,因此这里命名就用getHmac了,其中input就是上面的saltedPassword
1
2
3
4
5
6
7
8private byte[] getHmac(String key, byte[] input) throws NoSuchAlgorithmException, InvalidKeyException {
String hmacName = "HmacSHA256";
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.US_ASCII), hmacName);
Mac mac = Mac.getInstance(hmacName);
mac.init(secretKeySpec);
mac.update(input);
return mac.doFinal();
}实现
scram.storedKey(CryptoJS.enc.Hex.parse(clientKey))
,这句话就相当于用SHA256对上面计算出来的clientKey生成摘要计算,代码就一行1
2
3private byte[] getStoreKey(byte[] clientKey) throws NoSuchAlgorithmException {
return MessageDigest.getInstance("SHA-256").digest(clientKey);
}实现
scram.signature(CryptoJS.enc.Hex.parse(storekey), authMsg)
这里它的js代码和生成ClientKey一样的,只不过把Client Key换成了这里的authMsg,而这里的authMsg是firstNonce + "," + finalNonce + "," + finalNonce
的组合,所以这段代码其实也可以用getHmac
,只不过把key的参数换成authMsg即可最后一段就是亦或的这一段,这段因为我对CryptoJS不是很熟,但是我看它的循环逻辑,盲猜是对clientKey的每一个byte与clientSignature的每一个byte进行亦或,而且恰好他们长度都是一样的
1
2
3for (var i = 0; i < clientKey.sigBytes / 4; i++) {
clientKey.words[i] = clientKey.words[i] ^ clientsignature.words[i]
}对应的java代码就好理解多了
1
2
3for (int i = 0; i < clientKey.length; i++) {
clientKey[i] = (byte) (clientKey[i] ^ clientSignature[i]);
}上面的核心登录代码都已经写完了,然后就进行测试下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public static void main(String[] args) throws InvalidKeySpecException, NoSuchAlgorithmException, InvalidKeyException {
String firstNonce = "aa0591fd5a971da559b37eb6369eb2d2b25252e4a115e3ab31f6cde19df1eb72";
String salt = "07f0b7e87c2ad06ebd2938c326909307ac1da81460b27fa6d84a8b240ac795fe";
int iterations = 100;
String finalNonce = "aa0591fd5a971da559b37eb6369eb2d2b25252e4a115e3ab31f6cde19df1eb72JREAGMZitcXiwQp2tsnUhLFViZev60z0";
HuaweiWS5200AccessUtils huaweiWS5200AccessUtils = new HuaweiWS5200AccessUtils();
byte[] saltedPassword = huaweiWS5200AccessUtils.getSaltedPassword(password, hexToByteArray(salt), iterations);
System.out.println("SaltedPassword: " + bytesToHex(saltedPassword));
byte[] clientKey = huaweiWS5200AccessUtils.getHmac("Client Key", saltedPassword);
System.out.println("ClientKey: " + bytesToHex(clientKey));
byte[] storeKey = huaweiWS5200AccessUtils.getStoreKey(clientKey);
System.out.println("StoreKey: " + bytesToHex(storeKey));
String authMsg = firstNonce + "," + finalNonce + "," + finalNonce;
byte[] clientSignature = huaweiWS5200AccessUtils.getHmac(authMsg, storeKey);
System.out.println("ClientSignature: " + bytesToHex(clientSignature));
for (int i = 0; i < clientKey.length; i++) {
clientKey[i] = (byte) (clientKey[i] ^ clientSignature[i]);
}
System.out.println("Final ClientProof: " + bytesToHex(clientKey));
}运行后输出如下,可以看到结果完全一样。
1
2
3
4
5
6
7SaltedPassword: d051d0ce9b09584087012c6a36c78732cd7b919b5d4d02d35fb1827331044ca0
ClientKey: 9df927a1d72d2e77fdf0c59b5c34b02e1c50801b4b193789ce2dbbfb9671574f
StoreKey: 5d461845ea9ea0311c1ed476314eb2357c4a32b11b332695e6e23f5982d900b8
ClientSignature: c6d2b14463890f53e3f50dda9fb3ccde2134bc2ba49a6feb442233de2b2e1887
Final ClientProof: 5b2b96e5b4a421241e05c841c3877cf03d643c30ef8358628a0f8825bd5f4fc8
Process finished with exit code 0
尝试获取所有的主机
其实到这一步,整篇内容就结束了。接下来简单获取下所有主机。
1 | GET /api/system/HostInfo HTTP/1.1 |
开始写请求
1 | public static void main(String[] args) throws InvalidKeySpecException, NoSuchAlgorithmException, InvalidKeyException { |
好了,结束,附上完整Java测试代码
1 | package com.ruterfu.test; |