使用Java编写程序来登录华为WS5200路由器

弄这个的初衷是我发现自己真的太没有时间观念了,经常我看书看一半想资料查10分钟,然而,百度一开,分分钟被某国要倒核废水瞬间吸引过去了,看完又看到某门山景区没素质的游客摘水果,又点开看看,接着看到了各种各样的新闻,等我看爽看满足后,已经差不多半个小时甚至1个小时没了。资料查了几分钟,剩下都在看新闻。所以才想到能否通过程序方式控制下路由器,至少在我看书期间,10分钟就是10分钟,时间到断网。

(English version translate by GPT-3.5)

前言

说干就干,因为我手头只有WS5200的路由器,而我电脑连接的是这台路由器,所以从这台路由器下手。

算法初探

登录初看

先打开路由器,按F12进行登录,然后看到ALL部分,看到登录出现了以下请求

  1. 路由器会先请求 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
    51
    POST /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
    }
  2. 然后会紧接着请求 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
    63
    POST /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,当然我是随便输入了个密码。其中下面是我主要看到的。

  1. 路由器每次登录的时候,会携带一个csrf_param,以及csrf_token,从上面2个请求看出,下面的登录请求2个参数,正好是上面返回的2个参数,那我可以这么猜测,是不是每次收到新的csrf时,都要更新当前的csrf参数?为什么在第一次请求user_login_nonce的时候就已经就有csrf参数?
  2. 请求中没有看到任何密码和数字,也不是类似的MD5的简单加密,因为我在试上面之前试过用123456登录,123456的MD5时e10开头的,但是上面并没有任何相关的内容,因此密码肯定被处理过。
  3. 看登录返回的参数,count和maxfailtimes告诉我们,别超过了maxfailtimes,不然封你1分钟没商量。。
  4. 还有上面的Cookie参数,这个在HTTP请求中作用极大,一定要看这个是在哪里被设置的,一般设置Cookie会有 Set-Cookie响应头。

获得最开始的csrf参数

因为在第一次user_login_nonce就有了csrf参数,就表示在进入登录页面前就肯定有一个地方给了这个参数,或者就是自动生成的。

因为我user_login_nonce登录时使用的token是bFOBG8SSFQ4ZqD92j0rOSZXrQnWluYQs,所以我只需要在Chrome Network中寻找,是否有任何页面或js提到了这个值,结果还真找到了

在最开始的index.html页面

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
GET /html/index.html HTTP/1.1
Host: 10.0.0.10
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
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
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://10.0.0.10/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9

Response Header
HTTP/1.1 200 OK
Set-Cookie: SessionID_R3=4Xj1P0SvG5u5274ClvmYz0g7NoEBIeagDK8UokAbbWfu0zfaqdQYJOyhPEJpCOwAAZpX9LvR38hJaFIegMuSe5FG6eiSpO1zRlZyRaCTZ2qqf3a5os2heaMYcY9jpajW; path=/; HttpOnly;
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Content-Type: text/html; charset=utf-8
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
Date: Mon, 19 Apr 2021 12:20:11 GMT
Connection: Keep-Alive
Content-Language: en
Content-Length: 5837

Response Body
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
....
<meta name="csrf_param" content="U7FMqd5SX0twxRA0kaKqLiPCfWqkCjLJ"/>
<meta name="csrf_token" content="tUcxR4fDHHrUSNMU650Mp0rd5UXt0iJ7"/>

<meta name="n" content="d3027a62ef512087a147d1526371667fcdc8bd78af9cf350b36f6ee650daa1e04e49ea9d663595e8c40e298cdb70a7975f1aeedbebe7180d269ae4b2bf02cd6c128d2610ee239c6a58d129d51d27b2d5725e1805925e24bf639a1cf964157a4014211d725b05a98dbdc2011ab02da2cd48da83b6e51f18b0ea018e98af4c1f356e478bff8d1ba640a0e84d673118b9f53c339287f309761c58540b546dd99bef508570524aa892733d331140594ae6558e7295f817b2a2475a395409827cf9c9bb32ccb8b158f27dcc38f87cbcf332d37daf997271f1e9cbd5fe583db87734847dd51de9660cb5d08e193f957d161cbd920095c6e8b9593faca4a818bbbc23ad"/>
<meta name="e" content="010001"/>
<title></title>
.....
</body>
</html>

哎呀,这token简直是一摸一样一字不差,比双胞胎还像双胞胎。

1
2
<meta name="csrf_param" content="U7FMqd5SX0twxRA0kaKqLiPCfWqkCjLJ"/>
<meta name="csrf_token" content="tUcxR4fDHHrUSNMU650Mp0rd5UXt0iJ7"/>

基本可以确定,最初始的token就是index.html返回的。

寻找加密部分

这个加密绝对被处理过的,但是一般处理,将chrome网页调试Network选项卡,只过滤Javascript,一般加密就在这里。。还好人家的js文件不多
js-files
我就一个一个搜索了,就搜索关键字user_login_proof吧。。结果刚找到第七个文件就有了
find-user-login-proof
点击搜索框正下方的{}按钮,来格式化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
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
login: function(context, data) {
loginTimes++;
if (g_userScram == 1) {
var scram = CryptoJS.SCRAM({
keySize: 8
});
var firstNonce = scram.nonce().toString();
var password = data.password;
var param = {
name: 'user_login_nonce',
data: {
username: data.username,
firstnonce: firstNonce
}
};
return context.dispatch('commonPost', param).then(function(res) {
if (!res || res.errcode) {
if (loginTimes >= 5) {
loginTimes = 0;
return Promise.reject(res)
} else {
return context.dispatch('login', data)
}
}
if (res.err == 0) {
var 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]
}
var param1 = {
name: 'user_login_proof',
data: {
clientproof: clientKey.toString(),
finalnonce: finalNonce
}
};
return context.dispatch('commonPost', param1).then(function(result) {
if (result.err == 0) {
var serverProof = scram.serverProof(password, salt, iter, authMsg);
serverProof = serverProof.toString();
if (result.serversignature == serverProof) {
var publicKey = result.rsan;
var publicKeySignature = scram.signature(CryptoJS.enc.Hex.parse(publicKey), serverKey);
publicKeySignature = publicKeySignature.toString();
if (result.rsapubkeysignature == publicKeySignature) {
g_userLevel = result.level;
loginTimes = 0;
context.state.Data.login.ence = result.rsan
context.state.Data.login.encn = result.rsae
return Promise.resolve(result);
}
} else {
loginTimes = 0;
return Promise.reject(result)
}
} else {
loginTimes = 0;
return Promise.reject(result)
}
})
} else {
loginTimes = 0;
return Promise.reject(res)
}
})
} else {
var csrf_obj = context.state.csrf_obj;
var post_data = {
UserName: data.username,
Password: data.password
};
var plaintPwd = data.username + base64Encode(SHA256(data.password)) + csrf_obj.csrf_param + csrf_obj.csrf_token;
post_data["Password"] = SHA256(plaintPwd);
post_data["LoginFlag"] = 1;
return context.dispatch('commonPost', {
name: 'user_login',
data: post_data
}).then(function(result) {
loginTimes = 0;
if ('ok' == result['errorCategory']) {
g_userLevel = result['level'];
return Promise.resolve(result)
} else if (1 != result.errcode) {
return Promise.reject(result)
}
})
}
}

可以看到基本逻辑如下

  1. 首先,请求user_login_nonce,参数是username和firstNonce

    1
    其中firstNonce是从scram.nonce().toString()算出来的。
  2. 接着,取得上述返回中的salt参数,serverNonce以及iterations并作为登录参数,进行如下代码运算后,将clientKey转为16进制,请求过去。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    var 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]
    }

整理加密部分

  1. 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"
  2. 用户登录加密部分

    这一部分其实比较麻烦的,因为涉及成堆的算法,先暂时把他分离出来,里面的方法调用到了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
    25
    var 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
    6
    firstNonce: aa0591fd5a971da559b37eb6369eb2d2b25252e4a115e3ab31f6cde19df1eb72
    saltPassword: d051d0ce9b09584087012c6a36c78732cd7b919b5d4d02d35fb1827331044ca0
    clientKey: 9df927a1d72d2e77fdf0c59b5c34b02e1c50801b4b193789ce2dbbfb9671574f
    storeKey: 5d461845ea9ea0311c1ed476314eb2357c4a32b11b332695e6e23f5982d900b8
    clientsignature: c6d2b14463890f53e3f50dda9fb3ccde2134bc2ba49a6feb442233de2b2e1887
    clientProof: 5b2b96e5b4a421241e05c841c3877cf03d643c30ef8358628a0f8825bd5f4fc8

一步步分解,并用Java实现和计算加密部分

  1. 首先 firstNonce 我选择用随机生成的64位的hash字符串

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    private 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();
    }
  2. 然后,尝试编写scram.saltedPassword(password, salt, iter)这句话

    首先进入cat_enc.js:2403看到如下内容

    1
    2
    3
    4
    5
    6
    7
    saltedPassword: 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
    5
    private 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();
    }
  3. 实现scram.clientKey(CryptoJS.enc.Hex.parse(saltPassword)).toString()这一段,这段js代码如下,因为serverKey对全程登录没有啥帮助。。

    1
    2
    3
    clientKey: 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
    8
    private 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();
    }
  4. 实现scram.storedKey(CryptoJS.enc.Hex.parse(clientKey)),这句话就相当于用SHA256对上面计算出来的clientKey生成摘要计算,代码就一行

    1
    2
    3
    private byte[] getStoreKey(byte[] clientKey) throws NoSuchAlgorithmException {
    return MessageDigest.getInstance("SHA-256").digest(clientKey);
    }
  5. 实现scram.signature(CryptoJS.enc.Hex.parse(storekey), authMsg)这里它的js代码和生成ClientKey一样的,只不过把Client Key换成了这里的authMsg,而这里的authMsg是firstNonce + "," + finalNonce + "," + finalNonce的组合,所以这段代码其实也可以用getHmac,只不过把key的参数换成authMsg即可

  6. 最后一段就是亦或的这一段,这段因为我对CryptoJS不是很熟,但是我看它的循环逻辑,盲猜是对clientKey的每一个byte与clientSignature的每一个byte进行亦或,而且恰好他们长度都是一样的

    1
    2
    3
    for (var i = 0; i < clientKey.sigBytes / 4; i++) {
    clientKey.words[i] = clientKey.words[i] ^ clientsignature.words[i]
    }

    对应的java代码就好理解多了

    1
    2
    3
    for (int i = 0; i < clientKey.length; i++) {
    clientKey[i] = (byte) (clientKey[i] ^ clientSignature[i]);
    }
  7. 上面的核心登录代码都已经写完了,然后就进行测试下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public 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
    7
    SaltedPassword: d051d0ce9b09584087012c6a36c78732cd7b919b5d4d02d35fb1827331044ca0
    ClientKey: 9df927a1d72d2e77fdf0c59b5c34b02e1c50801b4b193789ce2dbbfb9671574f
    StoreKey: 5d461845ea9ea0311c1ed476314eb2357c4a32b11b332695e6e23f5982d900b8
    ClientSignature: c6d2b14463890f53e3f50dda9fb3ccde2134bc2ba49a6feb442233de2b2e1887
    Final ClientProof: 5b2b96e5b4a421241e05c841c3877cf03d643c30ef8358628a0f8825bd5f4fc8

    Process finished with exit code 0

尝试获取所有的主机

其实到这一步,整篇内容就结束了。接下来简单获取下所有主机。

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
GET /api/system/HostInfo HTTP/1.1
Host: 10.0.0.10
Connection: keep-alive
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
Referer: http://10.0.0.10/html/index.html
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7,ja;q=0.6,fr;q=0.5,nl;q=0.4,ru;q=0.3,th;q=0.2
Cookie: SessionID_R3=7ANFhXv0pI8P1AIyxBcuFmMZPQDkdc0yarQPrg1xYU1kFCyZ8Ua7BBFTVs3rd7Kn1O0xQR2f6VZdsat4iXUYSLRvl96sRECOKB1qlsFIEIQ4NwnwMlKYZZjBApIPYFwc

Response Header
HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
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
Date: Mon, 19 Apr 2021 13:32:11 GMT
Connection: Keep-Alive
Content-Language: en
Content-Length: 16283
Content-Type: application/javascript; charset=utf-8

Response Payload
[
{
"ID": "InternetGatewayDevice.LANDevice.1.Hosts.Host.2.",
"MACAddress": "**:**:**:**:00",
"IPAddress": "10.0.0.100",
"HostName": "Ruter-PC",
"Active": true,
......
"TelOperator": 0,
"ActualManu": "",
"DeviceMaxDownLoadRate": 7372
},
.....
{
"ID": "InternetGatewayDevice.LANDevice.1.Hosts.Host.4.",
"MACAddress": "**:**:**:**:00",
"HiLinkDevIsDelFromList": false,
"HiLinkDevHide": false,
"IPAddress": "10.0.0.125",
.....
"ProdId": "",
"DeviceMaxUpLoadRate": 0,
"DeviceMaxDownLoadRate": 0
}
]

开始写请求

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) throws InvalidKeySpecException, NoSuchAlgorithmException, InvalidKeyException {
HuaweiWS5200AccessUtils router = new HuaweiWS5200AccessUtils();
if(router.init() && router.login()) {
// 获得主机
String data = router.accessRouter("/api/system/HostInfo");
System.out.println(data);
}
}

返回
[{"ID":"InternetGatewayDevice.LANDevice.1.Hosts.Host.2.","MACAddress":"**:**:**.....wnLoadRate":0}]

Process finished with exit code 0

好了,结束,附上完整Java测试代码

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
package com.ruterfu.test;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import okhttp3.*;

import javax.crypto.Mac;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Random;

public class HuaweiWS5200AccessUtils {
private String csrfParam;
private String csrfToken;
private String cookie;
private final OkHttpClient okHttpClient = new OkHttpClient();

private static final String URL = "http://10.0.0.10";
private static final String password = "123456";

public static void main(String[] args) {
HuaweiWS5200AccessUtils router = new HuaweiWS5200AccessUtils();
if(router.init() && router.login()) {
// 获得主机
String data = router.accessRouter("/api/system/HostInfo");
System.out.println(data);
}
}
public boolean login() {
JSONObject data = new JSONObject();
String firstNonce = randomNonce();
data.put("username", "admin");
data.put("firstnonce", firstNonce);
JSONObject loginNonce = accessRouter("/api/system/user_login_nonce", data, null);
if(loginNonce != null) {
try {
int iterations = loginNonce.getIntValue("iterations");
String salt = loginNonce.getString("salt");
String serverNonce = loginNonce.getString("servernonce");
byte[] saltedPassword = getSaltedPassword(password, hexToByteArray(salt), iterations);
byte[] clientKey = getHmac("Client Key", saltedPassword);
byte[] storeKey = getStoreKey(clientKey);
String authMsg = firstNonce + "," + serverNonce + "," + serverNonce;
byte[] clientSignature = getHmac(authMsg, storeKey);
for (int i = 0; i < clientKey.length; i++) {
clientKey[i] = (byte) (clientKey[i] ^ clientSignature[i]);
}
String clientProof = bytesToHex(clientKey);
JSONObject login = new JSONObject();
login.put("clientproof", clientProof);
login.put("finalnonce", serverNonce);
JSONObject loginDone = accessRouter("/api/system/user_login_proof", login, null);
return loginDone.containsKey("err") && loginDone.getIntValue("err") == 0;
} catch (Exception e) {
e.printStackTrace();
}
}
return false;
}

private 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();
}

private 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();
}

private byte[] getStoreKey(byte[] clientKey) throws NoSuchAlgorithmException {
return MessageDigest.getInstance("SHA-256").digest(clientKey);
}

/**
* from https://blog.csdn.net/qq_34763699/article/details/78650272
*/
public static byte[] hexToByteArray(String inHex){
int hexLength = inHex.length();
byte[] result;
if (hexLength % 2 == 1){
hexLength++;
result = new byte[(hexLength / 2)];
inHex = "0" + inHex;
}else {
result = new byte[(hexLength / 2)];
}
int j=0;
for (int i = 0; i < hexLength; i += 2){
result[j]=(byte)Integer.parseInt(inHex.substring(i, i + 2),16);
j++;
}
return result;
}
/**
* from https://blog.csdn.net/qq_34763699/article/details/78650272
*/
public static String bytesToHex(byte[] bytes) {
StringBuffer sb = new StringBuffer();
for(int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(bytes[i] & 0xFF);
if(hex.length() < 2){
sb.append(0);
}
sb.append(hex);
}
return sb.toString();
}

public boolean init() {
Request request = new Request.Builder().url(URL + "/html/index.html").get().build();
try(Response response = okHttpClient.newCall(request).execute(); ResponseBody responseBody = response.body() != null ? response.body() : null) {
if(response.code() == 200) {
String data = responseBody.string();
int csrfStart = data.indexOf("csrf_param");
data = data.substring(csrfStart);
int csrfParamStart = data.indexOf("=\"");
int csrfParamEnd = data.indexOf("\"/>");
csrfParam = data.substring(csrfParamStart + 2, csrfParamEnd).trim();
int csrfTokenStart = data.indexOf("csrf_token");
data = data.substring(csrfTokenStart);
int csrfTokenParamStart = data.indexOf("=\"");
int csrfTokenParamEnd = data.indexOf("\"/>");
csrfToken = data.substring(csrfTokenParamStart + 2, csrfTokenParamEnd).trim();
String cookiePath = response.header("Set-Cookie");
int cookieSplit = cookiePath.indexOf(";");
cookie = cookiePath.substring(0, cookieSplit);
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}

private JSONObject accessRouter(String url, JSONObject data, String action) {
JSONObject jsonObject = new JSONObject();
JSONObject csrf = new JSONObject();
csrf.put("csrf_param", csrfParam);
csrf.put("csrf_token", csrfToken);
jsonObject.put("csrf", csrf);
jsonObject.put("data", data);
if(action != null && action.length() > 0) {
jsonObject.put("action", action);
}

Request request = new Request.Builder().url(URL + url).header("Cookie", cookie).post(RequestBody.create(jsonObject.toJSONString(), MediaType.parse("application/json"))).build();
try(Response response = okHttpClient.newCall(request).execute(); ResponseBody responseBody = response.body() != null ? response.body() : null) {
if(response.code() == 200) {
JSONObject repsJson = JSON.parseObject(responseBody.string());
if(repsJson.containsKey("csrf_token")) {
this.csrfToken = repsJson.getString("csrf_token");
repsJson.remove("csrf_token");
}
if(repsJson.containsKey("csrf_param")) {
this.csrfParam = repsJson.getString("csrf_param");
repsJson.remove("csrf_param");
}
String newHeader = response.header("Set-Cookie");
if(newHeader != null) {
cookie = newHeader;
}
return repsJson;
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private String accessRouter(String url) {
Request request = new Request.Builder().url(URL + url).header("Cookie", cookie).get().build();
try(Response response = okHttpClient.newCall(request).execute(); ResponseBody responseBody = response.body() != null ? response.body() : null) {
if(response.code() == 200) {
String body = responseBody.string();
String newHeader = response.header("Set-Cookie");
if(newHeader != null) {
cookie = newHeader;
}
return body;
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

private 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();
}
}