Login to Huawei WS5200 Router Using Java Program

The reason I did this is because I realized that I have very poor time management skills. Often, when I’m halfway through reading a book and want to look up something for 10 minutes, I get distracted by news about a certain country dumping nuclear waste. Then I continue reading and see some unruly tourists picking fruits at a certain mountainous scenic area, so I click to take a look. After that, I discover various news articles, and by the time I’m satisfied with reading, it’s already been half an hour or even an hour. I only spend a few minutes looking up information, and the rest of the time is spent reading news. So I wondered if it would be possible to control the router through a program, so that during my reading time, 10 minutes would be exactly 10 minutes, and the internet would be disconnected when the time is up.(English version Translated by GPT-3.5, 返回中文)

Introduction

Enough talk, let’s get to work. Since I only have the WS5200 router at hand, and my computer is connected to this router, I’ll start with this router.

Exploring the Algorithm

Initial Login

First, open the router, press F12 to login, and then look at the “ALL” section. I noticed the following requests for login:

  1. The router first requests the user_login_nonce interface.

    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. Then it immediately requests 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"
    }

Simple Analysis

We can see that the Huawei router makes two requests, the second of which is the actual login request. From the requests, I can roughly guess that the username for the router is “admin”, although this information is not really useful. In the second request, the response contains “user_pass_err”, which indicates that I entered a random password. Here are the main things I noticed:

  1. Every time the router logs in, it carries a “csrf_param” and “csrf_token”. From the two requests mentioned above, it can be inferred that the login request includes the two parameters mentioned earlier. So, I can guess that whenever a new csrf is received, the current csrf parameter needs to be updated. But why is the csrf parameter already present in the first request for user_login_nonce?
  2. I couldn’t find any passwords or numbers in the requests. It’s not a simple encryption like MD5, because I tried logging in with the password “123456”, and the MD5 hash of “123456” starts with “e10”. However, there was no related content in the requests, so the password must have been processed.
  3. Looking at the parameters returned by the login, “count” and “maxfailtimes” tell us not to exceed the maximum number of login failures, or we’ll be blocked for a minute.
  4. There’s also the “Cookie” parameter, which plays a crucial role in HTTP requests. It’s important to see where this parameter is set, as there is usually a “Set-Cookie” response header for setting cookies.

Obtaining the Initial csrf Parameter

Since the first user_login_nonce already includes the csrf parameter, this means that there must be a place where this parameter is given before entering the login page, or it is generated automatically.

Because the token I used for user_login_nonce is “bFOBG8SSFQ4ZqD92j0rOSZXrQnWluYQs”, I just need to search in the Chrome Network for any page or JS that mentions this value. And, indeed, I found it!

On the initial index.html page:

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>

I found that this token is exactly the same, down to every letter, just like twins!

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

So, it can be concluded that the initial token is obtained from the response of index.html.

Finding the Encryption Part

This encryption has definitely been processed. But usually, when you open the Chrome webpage debugger’s Network tab and filter for JavaScript, you can find the encryption there. Luckily, there aren’t many JS files in this case.
js-files
I searched one by one and searched for the keyword ‘user_login_proof’. I found it in the seventh file I checked.
find-user-login-proof
Click on the {} button below the search bar to format the JS text and extract the login JS code as follows:

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)
}
})
}
}

The basic logic can be seen below:

  1. First, request user_login_nonce with the parameters “username” and “firstNonce”.

    1
    其中firstNonce是从scram.nonce().toString()算出来的。
  2. Then, take the “salt” parameter and “serverNonce” from the response mentioned above. Use them as login parameters and perform the following code calculation, converting the “clientKey” to hexadecimal, and send it in the request.

    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]
    }

Organizing the Encryption Part

  1. Nonce Part

    In the above process, the nonce part seems like a random value. When I manually ran this code, it always returned a random string of length 64 composed of [0-9, a-f]. Although I confirmed later that it is indeed a random number.

    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. User Login Encryption Part

    This part is more complicated because it involves multiple algorithms. For now, let’s separate it first. It calls the cat_enc.js file, so after extracting it, create a new HTML page, import the cat_enc.js file, and write the following JS code:

    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())

    After executing it in the browser, the result is as follows, which is exactly the same as when making the request.

    1
    2
    3
    4
    5
    6
    firstNonce: aa0591fd5a971da559b37eb6369eb2d2b25252e4a115e3ab31f6cde19df1eb72
    saltPassword: d051d0ce9b09584087012c6a36c78732cd7b919b5d4d02d35fb1827331044ca0
    clientKey: 9df927a1d72d2e77fdf0c59b5c34b02e1c50801b4b193789ce2dbbfb9671574f
    storeKey: 5d461845ea9ea0311c1ed476314eb2357c4a32b11b332695e6e23f5982d900b8
    clientsignature: c6d2b14463890f53e3f50dda9fb3ccde2134bc2ba49a6feb442233de2b2e1887
    clientProof: 5b2b96e5b4a421241e05c841c3877cf03d643c30ef8358628a0f8825bd5f4fc8

Step by Step Decomposition, and Implementing and Calculating the Encryption in Java

  1. First, “firstNonce”. I chose a randomly generated 64-bit hashed string.

    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. Then, try to write the line “scram.saltedPassword(password, salt, iter)”.

    First, go to cat_enc.js:2403 and see the following content:

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

    It can be seen that PBKDF2 encryption algorithm is used here. After searching in Java, the corresponding algorithm name is PBKDF2WithHmacSHA256. After googling, write the following code. As for why the key length is 256 below, I’m not entirely sure either. I always thought it was 32 because this.cfg.keySize returns 32, but I found that the encrypted length was different from the JS code. When I changed it to 256, the lengths matched. At the same time, I found that the length of the calculated saltedPassword is exactly 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. Implementing scram.clientKey(CryptoJS.enc.Hex.parse(saltPassword)).toString(). This piece of JS code is exactly the same as generating the ClientKey, just replacing “Client Key” with “authMsg” here. And authMsg is a combination of “firstNonce + “,” + finalNonce + “,” + finalNonce”. So, this code can also be replaced with getHmac, but with the key parameter changed to authMsg.

  4. Implementing scram.storedKey(CryptoJS.enc.Hex.parse(clientKey)). This line is equivalent to calculating the digest of the clientKey generated above using SHA256. The code is only one line.

    1
    2
    3
    private byte[] getStoreKey(byte[] clientKey) throws NoSuchAlgorithmException {
    return MessageDigest.getInstance("SHA-256").digest(clientKey);
    }
  5. Implementing scram.signature(CryptoJS.enc.Hex.parse(storekey), authMsg). This piece of JS code is the same as generating the ClientKey, except that it replaces “Client Key” with authMsg. The authMsg here is a combination of “firstNonce + “,” + finalNonce + “,” + finalNonce”. So, this code can also use getHmac, but with the key parameter changed to authMsg.

  6. The last step is to perform XOR. Because I’m not very familiar with CryptoJS, but looking at its loop logic, I guess it XORs each byte of clientKey with each byte of clientSignature, and coincidentally, they are the same length.

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

    The corresponding Java code is much easier to understand.

    1
    2
    3
    for (int i = 0; i < clientKey.length; i++) {
    clientKey[i] = (byte) (clientKey[i] ^ clientSignature[i]);
    }
  7. The core login code mentioned above has all been written, now let’s test it.

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

    After running it, the output is as follows, and the result is exactly the same.

    1
    2
    3
    4
    5
    6
    7
    SaltedPassword: d051d0ce9b09584087012c6a36c78732cd7b919b5d4d02d35fb1827331044ca0
    ClientKey: 9df927a1d72d2e77fdf0c59b5c34b02e1c50801b4b193789ce2dbbfb9671574f
    StoreKey: 5d461845ea9ea0311c1ed476314eb2357c4a32b11b332695e6e23f5982d900b8
    ClientSignature: c6d2b14463890f53e3f50dda9fb3ccde2134bc2ba49a6feb442233de2b2e1887
    Final ClientProof: 5b2b96e5b4a421241e05c841c3877cf03d643c30ef8358628a0f8825bd5f4fc8

    Process finished with exit code 0

Trying to Get All Hosts

In fact, at this point, the entire content is finished. Now let’s simply get all the hosts.

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
}
]

Let’s start writing the request.

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

Alright, we’re done. Here’s the complete Java test code.

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